parent
7410c4c6ab
commit
bc2eeee497
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::ProController < Api::BaseController
|
||||
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_pro_statuses
|
||||
end
|
||||
|
||||
def cached_pro_statuses
|
||||
cache_collection pro_statuses, Status
|
||||
end
|
||||
|
||||
def pro_statuses
|
||||
statuses = pro_timeline_statuses.paginate_by_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
|
||||
if truthy_param?(:only_media)
|
||||
# `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids.
|
||||
status_ids = statuses.joins(:media_attachments).distinct(:id).pluck(:id)
|
||||
statuses.where(id: status_ids)
|
||||
else
|
||||
statuses
|
||||
end
|
||||
end
|
||||
|
||||
def pro_timeline_statuses
|
||||
Status.as_pro_timeline(current_account)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit, :only_media).permit(:limit, :only_media).merge(core_params)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_timelines_pro_url pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_timelines_pro_url pagination_params(min_id: pagination_since_id)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
end
|
|
@ -51,6 +51,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
|||
}
|
||||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user');
|
||||
export const connectProStream = () => connectTimelineStream('pro', 'pro');
|
||||
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
|
||||
|
|
|
@ -166,6 +166,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
};
|
||||
|
||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||
export const expandProTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('pro', '/api/v1/timelines/pro', { max_id: maxId }, done);
|
||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies, commentsOnly } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${commentsOnly ? ':comments_only' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { only_comments: commentsOnly, exclude_replies: (!withReplies && !commentsOnly), max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||
|
|
|
@ -164,6 +164,11 @@ class Sidebar extends ImmutablePureComponent {
|
|||
]
|
||||
|
||||
const exploreItems = [
|
||||
{
|
||||
title: 'Pro Feed',
|
||||
icon: 'circle',
|
||||
to: '/timeline/pro',
|
||||
},
|
||||
{
|
||||
title: 'Chat',
|
||||
icon: 'chat',
|
||||
|
|
|
@ -39,6 +39,7 @@ const messages = defineMessages({
|
|||
chat: { id: 'tabs_bar.chat', defaultMessage: 'Chat' },
|
||||
help: { id: 'getting_started.help', defaultMessage: 'Help' },
|
||||
display: { id: 'display_options', defaultMessage: 'Display Options' },
|
||||
pro: { id: 'pro_feed', defaultMessage: 'Pro Feed' },
|
||||
})
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
@ -94,6 +95,12 @@ class SidebarXS extends ImmutablePureComponent {
|
|||
onClick: this.handleSidebarClose,
|
||||
title: intl.formatMessage(messages.profile),
|
||||
},
|
||||
{
|
||||
icon: 'list',
|
||||
to: '/lists',
|
||||
onClick: this.handleSidebarClose,
|
||||
title: intl.formatMessage(messages.lists),
|
||||
},
|
||||
{
|
||||
icon: 'pro',
|
||||
href: 'https://pro.gab.com',
|
||||
|
@ -124,18 +131,18 @@ class SidebarXS extends ImmutablePureComponent {
|
|||
onClick: this.handleSidebarClose,
|
||||
title: intl.formatMessage(messages.search),
|
||||
},
|
||||
{
|
||||
icon: 'circle',
|
||||
to: '/timeline/pro',
|
||||
onClick: this.handleSidebarClose,
|
||||
title: intl.formatMessage(messages.pro),
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
href: '/settings/preferences',
|
||||
onClick: this.handleSidebarClose,
|
||||
title: intl.formatMessage(messages.preferences),
|
||||
},
|
||||
{
|
||||
icon: 'list',
|
||||
to: '/lists',
|
||||
onClick: this.handleSidebarClose,
|
||||
title: intl.formatMessage(messages.lists),
|
||||
},
|
||||
// {
|
||||
// icon: 'group',
|
||||
// to: '/follow_requests',
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import { expandProTimeline } from '../actions/timelines'
|
||||
import { connectProStream } from '../actions/streaming'
|
||||
import StatusList from '../components/status_list'
|
||||
|
||||
const messages = defineMessages({
|
||||
empty: { id: 'empty_column.pro', defaultMessage: 'The pro timeline is empty.' },
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
@connect(null)
|
||||
class ProTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props
|
||||
|
||||
dispatch(expandProTimeline())
|
||||
|
||||
this.disconnect = dispatch(connectProStream())
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.disconnect) {
|
||||
this.disconnect()
|
||||
this.disconnect = null
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = (maxId) => {
|
||||
const { dispatch } = this.props
|
||||
|
||||
dispatch(expandProTimeline({ maxId }))
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl } = this.props
|
||||
|
||||
const emptyMessage = intl.formatMessage(messages.empty)
|
||||
|
||||
return (
|
||||
<StatusList
|
||||
scrollKey='pro_timeline'
|
||||
timelineId='pro'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -36,6 +36,7 @@ import ListsPage from '../../pages/lists_page'
|
|||
import BasicPage from '../../pages/basic_page'
|
||||
import ModalPage from '../../pages/modal_page'
|
||||
import SettingsPage from '../../pages/settings_page'
|
||||
import ProPage from '../../pages/pro_page'
|
||||
|
||||
import {
|
||||
AccountGallery,
|
||||
|
@ -64,6 +65,7 @@ import {
|
|||
ListTimeline,
|
||||
Mutes,
|
||||
Notifications,
|
||||
ProTimeline,
|
||||
Search,
|
||||
// Shortcuts,
|
||||
StatusFeature,
|
||||
|
@ -151,6 +153,7 @@ class SwitchingArea extends PureComponent {
|
|||
<WrappedRoute path='/compose' exact page={BasicPage} component={Compose} content={children} componentParams={{ title: 'Compose' }} />
|
||||
|
||||
<WrappedRoute path='/timeline/all' exact page={CommunityPage} component={CommunityTimeline} content={children} componentParams={{ title: 'Community Feed' }} />
|
||||
<WrappedRoute path='/timeline/pro' exact page={ProPage} component={ProTimeline} content={children} componentParams={{ title: 'Pro Feed' }} />
|
||||
|
||||
<WrappedRoute path='/groups' exact page={GroupsPage} component={GroupsCollection} content={children} componentParams={{ activeTab: 'featured' }} />
|
||||
<WrappedRoute path='/groups/new' exact page={GroupsPage} component={GroupsCollection} content={children} componentParams={{ activeTab: 'new' }} />
|
||||
|
|
|
@ -55,6 +55,7 @@ export function Mutes() { return import(/* webpackChunkName: "features/mutes" */
|
|||
export function MuteModal() { return import(/* webpackChunkName: "modals/mute_modal" */'../../../components/modal/mute_modal') }
|
||||
export function NavSettingsPopover() { return import(/* webpackChunkName: "modals/nav_settings_popover" */'../../../components/popover/nav_settings_popover') }
|
||||
export function Notifications() { return import(/* webpackChunkName: "features/notifications" */'../../notifications') }
|
||||
export function ProTimeline() { return import(/* webpackChunkName: "features/pro_timeline" */'../../pro_timeline') }
|
||||
export function ProfileOptionsPopover() { return import(/* webpackChunkName: "components/profile_options_popover" */'../../../components/popover/profile_options_popover') }
|
||||
export function ProUpgradeModal() { return import(/* webpackChunkName: "components/pro_upgrade_modal" */'../../../components/modal/pro_upgrade_modal') }
|
||||
export function ReportModal() { return import(/* webpackChunkName: "modals/report_modal" */'../../../components/modal/report_modal') }
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { Fragment } from 'react'
|
||||
import { defineMessages, injectIntl } from 'react-intl'
|
||||
import PageTitle from '../features/ui/util/page_title'
|
||||
import LinkFooter from '../components/link_footer'
|
||||
import VerifiedAccountsPanel from '../components/panel/verified_accounts_panel'
|
||||
import ProgressPanel from '../components/panel/progress_panel'
|
||||
import DefaultLayout from '../layouts/default_layout'
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { 'id': 'column.pro', 'defaultMessage': 'Pro feed' },
|
||||
})
|
||||
|
||||
export default
|
||||
@injectIntl
|
||||
class ProPage extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, children } = this.props
|
||||
|
||||
const title = intl.formatMessage(messages.title)
|
||||
|
||||
return (
|
||||
<DefaultLayout
|
||||
title={title}
|
||||
layout={(
|
||||
<Fragment>
|
||||
<ProgressPanel />
|
||||
<VerifiedAccountsPanel />
|
||||
<LinkFooter />
|
||||
</Fragment>
|
||||
)}
|
||||
>
|
||||
<PageTitle path={title} />
|
||||
{children}
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -96,6 +96,7 @@ class Status < ApplicationRecord
|
|||
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
|
||||
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
|
||||
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
|
||||
scope :popular_accounts, -> { left_outer_joins(:account).where('accounts.is_investor=true OR accounts.is_donor=true OR accounts.is_verified=true OR accounts.is_pro=true') }
|
||||
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
|
||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||
scope :tagged_with_all, ->(tags) {
|
||||
|
@ -341,8 +342,13 @@ class Status < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def as_pro_timeline(account = nil)
|
||||
query = timeline_scope.without_replies.popular_accounts.where('statuses.created_at > ?', 2.days.ago)
|
||||
apply_timeline_filters(query, account)
|
||||
end
|
||||
|
||||
def as_public_timeline(account = nil)
|
||||
query = timeline_scope.without_replies
|
||||
query = timeline_scope.without_replies.where('statuses.created_at > ?', 15.minutes.ago)
|
||||
apply_timeline_filters(query, account)
|
||||
end
|
||||
|
||||
|
|
|
@ -87,6 +87,10 @@ class BatchedRemoveStatusService < BaseService
|
|||
payload = @json_payloads[status.id]
|
||||
|
||||
redis.pipelined do
|
||||
if status.account.is_pro || status.account.is_donor || status.account.is_investor || status.account.is_verified
|
||||
redis.publish('timeline:pro', payload)
|
||||
end
|
||||
|
||||
@tags[status.id].each do |hashtag|
|
||||
redis.publish("timeline:hashtag:#{hashtag}", payload)
|
||||
redis.publish("timeline:hashtag:#{hashtag}:local", payload) if status.local?
|
||||
|
|
|
@ -24,6 +24,11 @@ class FanOutOnWriteService < BaseService
|
|||
deliver_to_hashtags(status)
|
||||
|
||||
return if status.reply? && status.in_reply_to_account_id != status.account_id
|
||||
|
||||
|
||||
if status.account.is_pro || status.account.is_donor || status.account.is_investor || status.account.is_verified
|
||||
deliver_to_pro(status)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -95,6 +100,12 @@ class FanOutOnWriteService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def deliver_to_pro(status)
|
||||
Rails.logger.debug "Delivering status #{status.id} to pro timeline"
|
||||
|
||||
Redis.current.publish('timeline:pro', @payload)
|
||||
end
|
||||
|
||||
def deliver_to_own_conversation(status)
|
||||
AccountConversation.add_status(status.account, status)
|
||||
end
|
||||
|
|
|
@ -22,6 +22,7 @@ class RemoveStatusService < BaseService
|
|||
remove_from_affected
|
||||
remove_reblogs
|
||||
remove_from_hashtags
|
||||
remove_from_pro
|
||||
|
||||
@status.destroy!
|
||||
else
|
||||
|
@ -143,6 +144,12 @@ class RemoveStatusService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def remove_from_pro
|
||||
if @account.is_pro || @account.is_donor || @account.is_investor || @account.is_verified
|
||||
redis.publish('timeline:pro', @payload)
|
||||
end
|
||||
end
|
||||
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "distribute:#{@status.id}" }
|
||||
end
|
||||
|
|
|
@ -321,6 +321,7 @@ Rails.application.routes.draw do
|
|||
resource :direct, only: :show, controller: :direct
|
||||
resource :home, only: :show, controller: :home
|
||||
resource :public, only: :show, controller: :public
|
||||
resource :pro, only: :show, controller: :pro
|
||||
resources :tag, only: :show
|
||||
resources :list, only: :show
|
||||
resources :group, only: :show
|
||||
|
|
|
@ -296,8 +296,6 @@ const startWorker = (workerId) => {
|
|||
};
|
||||
|
||||
const PUBLIC_ENDPOINTS = [
|
||||
'/api/v1/streaming/public',
|
||||
'/api/v1/streaming/public/local',
|
||||
'/api/v1/streaming/hashtag',
|
||||
'/api/v1/streaming/hashtag/local',
|
||||
];
|
||||
|
@ -585,6 +583,10 @@ const startWorker = (workerId) => {
|
|||
});
|
||||
});
|
||||
|
||||
app.get('/api/v1/streaming/pro', (req, res) => {
|
||||
streamFrom('timeline:pro', req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)));
|
||||
});
|
||||
|
||||
const wss = new WebSocketServer({ server, verifyClient: wsVerifyClient });
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
|
@ -595,6 +597,9 @@ const startWorker = (workerId) => {
|
|||
let channel;
|
||||
|
||||
switch (location.query.stream) {
|
||||
case 'pro':
|
||||
streamFrom('timeline:pro', req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)));
|
||||
break;
|
||||
case 'statuscard':
|
||||
channel = `statuscard:${req.accountId}`;
|
||||
streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)));
|
||||
|
|
Loading…
Reference in New Issue