Initial bubble timeline support
This commit is contained in:
parent
7374678aa7
commit
55b25a777b
16 changed files with 275 additions and 34 deletions
|
@ -3,7 +3,7 @@
|
||||||
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
|
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
|
||||||
before_action :require_user!, only: [:show], if: :require_auth?
|
before_action :require_user!, only: [:show], if: :require_auth?
|
||||||
|
|
||||||
PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze
|
PERMITTED_PARAMS = %i(local remote bubble limit only_media allow_local_only).freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cache_if_unauthenticated!
|
cache_if_unauthenticated!
|
||||||
|
@ -38,6 +38,7 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
|
||||||
PublicFeed.new(
|
PublicFeed.new(
|
||||||
current_account,
|
current_account,
|
||||||
local: truthy_param?(:local),
|
local: truthy_param?(:local),
|
||||||
|
bubble: truthy_param?(:bubble),
|
||||||
remote: truthy_param?(:remote),
|
remote: truthy_param?(:remote),
|
||||||
only_media: truthy_param?(:only_media),
|
only_media: truthy_param?(:only_media),
|
||||||
allow_local_only: truthy_param?(:allow_local_only),
|
allow_local_only: truthy_param?(:allow_local_only),
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
fillPublicTimelineGaps,
|
fillPublicTimelineGaps,
|
||||||
fillCommunityTimelineGaps,
|
fillCommunityTimelineGaps,
|
||||||
fillListTimelineGaps,
|
fillListTimelineGaps,
|
||||||
|
fillBubbleTimelineGaps,
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -150,6 +151,14 @@ export const connectUserStream = () =>
|
||||||
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
|
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {boolean} [options.onlyMedia]
|
||||||
|
* @returns {function(): void}
|
||||||
|
*/
|
||||||
|
export const connectBubbleStream = ({ onlyMedia } = {}) =>
|
||||||
|
connectTimelineStream(`bubble${onlyMedia ? ':media' : ''}`, `public:bubble${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillBubbleTimelineGaps({ onlyMedia })) });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {boolean} [options.onlyMedia]
|
* @param {boolean} [options.onlyMedia]
|
||||||
|
|
|
@ -156,6 +156,7 @@ export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
|
||||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
|
export const expandBubbleTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`bubble${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { bubble: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||||
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
|
||||||
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
|
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
|
||||||
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
||||||
|
@ -174,6 +175,7 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } =
|
||||||
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
|
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
|
||||||
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
|
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done);
|
||||||
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
|
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
|
||||||
|
export const fillBubbleTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`bubble${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { bubble: true, only_media: !!onlyMedia }, done);
|
||||||
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
|
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
|
||||||
|
|
||||||
export function expandTimelineRequest(timeline, isLoadingMore) {
|
export function expandTimelineRequest(timeline, isLoadingMore) {
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { addColumn } from 'flavours/glitch/actions/columns';
|
import { addColumn } from 'flavours/glitch/actions/columns';
|
||||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||||
import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming';
|
import { connectPublicStream, connectCommunityStream, connectBubbleStream } from 'flavours/glitch/actions/streaming';
|
||||||
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandPublicTimeline, expandCommunityTimeline, expandBubbleTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
|
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
|
||||||
import SettingText from 'flavours/glitch/components/setting_text';
|
import SettingText from 'flavours/glitch/components/setting_text';
|
||||||
import initialState, { domain } from 'flavours/glitch/initial_state';
|
import initialState, { domain } from 'flavours/glitch/initial_state';
|
||||||
|
@ -91,6 +91,9 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||||
case 'public':
|
case 'public':
|
||||||
dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } }));
|
dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } }));
|
||||||
break;
|
break;
|
||||||
|
case 'bubble':
|
||||||
|
dispatch(addColumn('BUBBLE', { other: { onlyMedia }, regex: { body: regex } }));
|
||||||
|
break;
|
||||||
case 'public:remote':
|
case 'public:remote':
|
||||||
dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex } }));
|
dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex } }));
|
||||||
break;
|
break;
|
||||||
|
@ -105,6 +108,9 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||||
case 'community':
|
case 'community':
|
||||||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||||
break;
|
break;
|
||||||
|
case 'bubble':
|
||||||
|
dispatch(expandBubbleTimeline({ maxId, onlyMedia }));
|
||||||
|
break;
|
||||||
case 'public':
|
case 'public':
|
||||||
dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly }));
|
dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly }));
|
||||||
break;
|
break;
|
||||||
|
@ -128,6 +134,12 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||||
disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'bubble':
|
||||||
|
dispatch(expandBubbleTimeline({ onlyMedia }));
|
||||||
|
if (signedIn) {
|
||||||
|
disconnect = dispatch(connectBubbleStream({ onlyMedia }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'public':
|
case 'public':
|
||||||
dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly }));
|
dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly }));
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
|
@ -145,35 +157,58 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||||
return () => disconnect?.();
|
return () => disconnect?.();
|
||||||
}, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]);
|
}, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]);
|
||||||
|
|
||||||
const prependBanner = feedType === 'community' ? (
|
let prependBanner;
|
||||||
<DismissableBanner id='community_timeline'>
|
let emptyMessage;
|
||||||
<FormattedMessage
|
|
||||||
id='dismissable_banner.community_timeline'
|
|
||||||
defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
|
|
||||||
values={{ domain }}
|
|
||||||
/>
|
|
||||||
</DismissableBanner>
|
|
||||||
) : (
|
|
||||||
<DismissableBanner id='public_timeline'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='dismissable_banner.public_timeline'
|
|
||||||
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
|
|
||||||
values={{ domain }}
|
|
||||||
/>
|
|
||||||
</DismissableBanner>
|
|
||||||
);
|
|
||||||
|
|
||||||
const emptyMessage = feedType === 'community' ? (
|
if (feedType === 'community') {
|
||||||
<FormattedMessage
|
prependBanner = (
|
||||||
id='empty_column.community'
|
<DismissableBanner id='community_timeline'>
|
||||||
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
|
<FormattedMessage
|
||||||
/>
|
id='dismissable_banner.community_timeline'
|
||||||
) : (
|
defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.'
|
||||||
<FormattedMessage
|
values={{ domain }}
|
||||||
id='empty_column.public'
|
/>
|
||||||
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
|
</DismissableBanner>
|
||||||
/>
|
);
|
||||||
);
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.community'
|
||||||
|
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (feedType === 'bubble') {
|
||||||
|
prependBanner = (
|
||||||
|
<DismissableBanner id='bubble_timeline'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='dismissable_banner.bubble_timeline'
|
||||||
|
defaultMessage='These are the most recent public posts from people on the social web whose accounts are on other servers selected by {domain}.'
|
||||||
|
values={{ domain }}
|
||||||
|
/>
|
||||||
|
</DismissableBanner>
|
||||||
|
);
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.bubble'
|
||||||
|
defaultMessage='The bubble timeline is currently empty, but something might show up here soon!'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
prependBanner = (
|
||||||
|
<DismissableBanner id='public_timeline'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='dismissable_banner.public_timeline'
|
||||||
|
defaultMessage='These are the most recent public posts from people on the social web that people on {domain} follow.'
|
||||||
|
values={{ domain }}
|
||||||
|
/>
|
||||||
|
</DismissableBanner>
|
||||||
|
);
|
||||||
|
emptyMessage = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.public'
|
||||||
|
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
||||||
|
@ -193,6 +228,10 @@ const Firehose = ({ feedType, multiColumn }) => {
|
||||||
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
|
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink exact to='/public/bubble'>
|
||||||
|
<FormattedMessage tagName='div' id='firehose.bubble' defaultMessage='Bubble servers' />
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
<NavLink exact to='/public/remote'>
|
<NavLink exact to='/public/remote'>
|
||||||
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
|
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
|
@ -208,6 +208,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
<Redirect from='/timelines/public/local' to='/public/local' exact />
|
||||||
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
<WrappedRoute path='/public' exact component={Firehose} componentParams={{ feedType: 'public' }} content={children} />
|
||||||
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
<WrappedRoute path='/public/local' exact component={Firehose} componentParams={{ feedType: 'community' }} content={children} />
|
||||||
|
<WrappedRoute path='/public/bubble' exact component={Firehose} componentParams={{ feedType: 'bubble' }} content={children} />
|
||||||
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
<WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
|
|
|
@ -43,8 +43,11 @@
|
||||||
"confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}",
|
"confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}",
|
||||||
"content-type.change": "Content type",
|
"content-type.change": "Content type",
|
||||||
"direct.group_by_conversations": "Group by conversation",
|
"direct.group_by_conversations": "Group by conversation",
|
||||||
|
"dismissable_banner.bubble_timeline": "These are the most recent public posts from people on the social web whose accounts are on other servers selected by {domain}.",
|
||||||
|
"empty_column.bubble": "The bubble timeline is currently empty, but something might show up here soon!",
|
||||||
"endorsed_accounts_editor.endorsed_accounts": "Featured accounts",
|
"endorsed_accounts_editor.endorsed_accounts": "Featured accounts",
|
||||||
"favourite_modal.combo": "You can press {combo} to skip this next time",
|
"favourite_modal.combo": "You can press {combo} to skip this next time",
|
||||||
|
"firehose.bubble": "Bubble servers",
|
||||||
"firehose.column_settings.allow_local_only": "Show local-only posts in \"All\"",
|
"firehose.column_settings.allow_local_only": "Show local-only posts in \"All\"",
|
||||||
"home.column_settings.advanced": "Advanced",
|
"home.column_settings.advanced": "Advanced",
|
||||||
"home.column_settings.filter_regex": "Filter out by regular expressions",
|
"home.column_settings.filter_regex": "Filter out by regular expressions",
|
||||||
|
|
43
app/models/bubble_domain.rb
Normal file
43
app/models/bubble_domain.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: bubble_domains
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# domain :string default(""), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
|
||||||
|
class BubbleDomain < ApplicationRecord
|
||||||
|
include Paginable
|
||||||
|
include DomainNormalizable
|
||||||
|
include DomainMaterializable
|
||||||
|
|
||||||
|
validates :domain, presence: true, uniqueness: true, domain: true
|
||||||
|
|
||||||
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
|
|
||||||
|
def to_log_human_identifier
|
||||||
|
domain
|
||||||
|
end
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def in_bubble?(domain)
|
||||||
|
!rule_for(domain).nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def bubble_domains
|
||||||
|
select(:domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def rule_for(domain)
|
||||||
|
return if domain.blank?
|
||||||
|
|
||||||
|
uri = Addressable::URI.new.tap { |u| u.host = domain.delete('/') }
|
||||||
|
|
||||||
|
find_by(domain: uri.normalized_host)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,6 +6,7 @@ class PublicFeed
|
||||||
# @option [Boolean] :with_replies
|
# @option [Boolean] :with_replies
|
||||||
# @option [Boolean] :with_reblogs
|
# @option [Boolean] :with_reblogs
|
||||||
# @option [Boolean] :local
|
# @option [Boolean] :local
|
||||||
|
# @option [Boolean] :bubble
|
||||||
# @option [Boolean] :remote
|
# @option [Boolean] :remote
|
||||||
# @option [Boolean] :only_media
|
# @option [Boolean] :only_media
|
||||||
# @option [Boolean] :allow_local_only
|
# @option [Boolean] :allow_local_only
|
||||||
|
@ -26,6 +27,7 @@ class PublicFeed
|
||||||
scope.merge!(without_replies_scope) unless with_replies?
|
scope.merge!(without_replies_scope) unless with_replies?
|
||||||
scope.merge!(without_reblogs_scope) unless with_reblogs?
|
scope.merge!(without_reblogs_scope) unless with_reblogs?
|
||||||
scope.merge!(local_only_scope) if local_only?
|
scope.merge!(local_only_scope) if local_only?
|
||||||
|
scope.merge!(bubble_only_scope) if bubble_only?
|
||||||
scope.merge!(remote_only_scope) if remote_only?
|
scope.merge!(remote_only_scope) if remote_only?
|
||||||
scope.merge!(account_filters_scope) if account?
|
scope.merge!(account_filters_scope) if account?
|
||||||
scope.merge!(media_only_scope) if media_only?
|
scope.merge!(media_only_scope) if media_only?
|
||||||
|
@ -51,11 +53,15 @@ class PublicFeed
|
||||||
end
|
end
|
||||||
|
|
||||||
def local_only?
|
def local_only?
|
||||||
options[:local] && !options[:remote]
|
options[:local] && !options[:remote] && !options[:bubble]
|
||||||
|
end
|
||||||
|
|
||||||
|
def bubble_only?
|
||||||
|
options[:bubble] && !options[:local] && !options[:remote]
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_only?
|
def remote_only?
|
||||||
options[:remote] && !options[:local]
|
options[:remote] && !options[:local] && !options[:bubble]
|
||||||
end
|
end
|
||||||
|
|
||||||
def account?
|
def account?
|
||||||
|
@ -78,6 +84,10 @@ class PublicFeed
|
||||||
Status.local
|
Status.local
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bubble_only_scope
|
||||||
|
Status.bubble
|
||||||
|
end
|
||||||
|
|
||||||
def remote_only_scope
|
def remote_only_scope
|
||||||
Status.remote
|
Status.remote
|
||||||
end
|
end
|
||||||
|
|
|
@ -127,6 +127,8 @@ class Status < ApplicationRecord
|
||||||
|
|
||||||
scope :not_local_only, -> { where(local_only: [false, nil]) }
|
scope :not_local_only, -> { where(local_only: [false, nil]) }
|
||||||
|
|
||||||
|
scope :bubble, -> { left_outer_joins(:account).where(accounts: { domain: BubbleDomain.bubble_domains }) }
|
||||||
|
|
||||||
after_create_commit :trigger_create_webhooks
|
after_create_commit :trigger_create_webhooks
|
||||||
after_update_commit :trigger_update_webhooks
|
after_update_commit :trigger_update_webhooks
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ class TagFeed < PublicFeed
|
||||||
scope.merge!(tagged_with_all_scope)
|
scope.merge!(tagged_with_all_scope)
|
||||||
scope.merge!(tagged_with_none_scope)
|
scope.merge!(tagged_with_none_scope)
|
||||||
scope.merge!(local_only_scope) if local_only?
|
scope.merge!(local_only_scope) if local_only?
|
||||||
|
scope.merge!(bubble_only_scope) if bubble_only?
|
||||||
scope.merge!(remote_only_scope) if remote_only?
|
scope.merge!(remote_only_scope) if remote_only?
|
||||||
scope.merge!(account_filters_scope) if account?
|
scope.merge!(account_filters_scope) if account?
|
||||||
scope.merge!(media_only_scope) if media_only?
|
scope.merge!(media_only_scope) if media_only?
|
||||||
|
|
|
@ -25,6 +25,7 @@ Rails.application.routes.draw do
|
||||||
/home
|
/home
|
||||||
/public
|
/public
|
||||||
/public/local
|
/public/local
|
||||||
|
/public/bubble
|
||||||
/public/remote
|
/public/remote
|
||||||
/conversations
|
/conversations
|
||||||
/lists/(*any)
|
/lists/(*any)
|
||||||
|
|
13
db/migrate/20240114042123_create_bubble_domains.rb
Normal file
13
db/migrate/20240114042123_create_bubble_domains.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateBubbleDomains < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
create_table :bubble_domains do |t|
|
||||||
|
t.string :domain, default: '', null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :bubble_domains, :domain, name: :index_bubble_domains_on_domain, unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do
|
ActiveRecord::Schema[7.1].define(version: 2024_01_14_042123) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -296,6 +296,13 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_09_103012) do
|
||||||
t.index ["status_id"], name: "index_bookmarks_on_status_id"
|
t.index ["status_id"], name: "index_bookmarks_on_status_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "bubble_domains", force: :cascade do |t|
|
||||||
|
t.string "domain", default: "", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["domain"], name: "index_bubble_domains_on_domain", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "bulk_import_rows", force: :cascade do |t|
|
create_table "bulk_import_rows", force: :cascade do |t|
|
||||||
t.bigint "bulk_import_id", null: false
|
t.bigint "bulk_import_id", null: false
|
||||||
t.jsonb "data"
|
t.jsonb "data"
|
||||||
|
|
87
lib/mastodon/cli/bubble_domains.rb
Normal file
87
lib/mastodon/cli/bubble_domains.rb
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'concurrent'
|
||||||
|
require_relative 'base'
|
||||||
|
|
||||||
|
module Mastodon::CLI
|
||||||
|
class BubbleDomains < Base
|
||||||
|
desc 'list', 'List domains in the bubble'
|
||||||
|
def list
|
||||||
|
BubbleDomain.find_each do |entry|
|
||||||
|
say(entry.domain.to_s, :white)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'add [DOMAIN...]', 'Add domains to the bubble'
|
||||||
|
def add(*domains)
|
||||||
|
if domains.empty?
|
||||||
|
say('No domain(s) given', :red)
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
domains = domains.map { |domain| TagManager.instance.normalize_domain(domain) }
|
||||||
|
|
||||||
|
skipped = 0
|
||||||
|
processed = 0
|
||||||
|
|
||||||
|
domains.each do |domain|
|
||||||
|
if BubbleDomain.exists?(domain: domain)
|
||||||
|
say("#{domain} is already in the bubble.", :yellow)
|
||||||
|
skipped += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
bubble_domain = BubbleDomain.new(domain: domain)
|
||||||
|
bubble_domain.save!
|
||||||
|
processed += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
say("Added #{processed}, skipped #{skipped}", color(processed, 0))
|
||||||
|
end
|
||||||
|
|
||||||
|
desc 'remove DOMAIN...', 'Remove domain from the bubble'
|
||||||
|
def remove(*domains)
|
||||||
|
if domains.empty?
|
||||||
|
say('No domain(s) given', :red)
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
skipped = 0
|
||||||
|
processed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
domains.each do |domain|
|
||||||
|
entry = BubbleDomain.find_by(domain: domain)
|
||||||
|
|
||||||
|
if entry.nil?
|
||||||
|
say("#{domain} is not in the bubble.", :yellow)
|
||||||
|
skipped += 1
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
result = entry.destroy
|
||||||
|
|
||||||
|
if result
|
||||||
|
processed += 1
|
||||||
|
else
|
||||||
|
say("#{domain} could not be removed.", :red)
|
||||||
|
failed += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
say("Removed #{processed}, skipped #{skipped}, failed #{failed}", color(processed, failed))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def color(processed, failed)
|
||||||
|
if !processed.zero? && failed.zero?
|
||||||
|
:green
|
||||||
|
elsif failed.zero?
|
||||||
|
:yellow
|
||||||
|
else
|
||||||
|
:red
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,6 +3,7 @@
|
||||||
require_relative 'base'
|
require_relative 'base'
|
||||||
|
|
||||||
require_relative 'accounts'
|
require_relative 'accounts'
|
||||||
|
require_relative 'bubble_domains'
|
||||||
require_relative 'cache'
|
require_relative 'cache'
|
||||||
require_relative 'canonical_email_blocks'
|
require_relative 'canonical_email_blocks'
|
||||||
require_relative 'domains'
|
require_relative 'domains'
|
||||||
|
@ -63,6 +64,9 @@ module Mastodon::CLI
|
||||||
desc 'canonical_email_blocks SUBCOMMAND ...ARGS', 'Manage canonical e-mail blocks'
|
desc 'canonical_email_blocks SUBCOMMAND ...ARGS', 'Manage canonical e-mail blocks'
|
||||||
subcommand 'canonical_email_blocks', CanonicalEmailBlocks
|
subcommand 'canonical_email_blocks', CanonicalEmailBlocks
|
||||||
|
|
||||||
|
desc 'bubble_domains SUBCOMMAND ...ARGS', 'Manage bubble domains'
|
||||||
|
subcommand 'bubble_domains', BubbleDomains
|
||||||
|
|
||||||
desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities'
|
desc 'maintenance SUBCOMMAND ...ARGS', 'Various maintenance utilities'
|
||||||
subcommand 'maintenance', Maintenance
|
subcommand 'maintenance', Maintenance
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,8 @@ const PUBLIC_CHANNELS = [
|
||||||
'public:media',
|
'public:media',
|
||||||
'public:local',
|
'public:local',
|
||||||
'public:local:media',
|
'public:local:media',
|
||||||
|
'public:bubble',
|
||||||
|
'public:bubble:media',
|
||||||
'public:remote',
|
'public:remote',
|
||||||
'public:remote:media',
|
'public:remote:media',
|
||||||
'hashtag',
|
'hashtag',
|
||||||
|
@ -438,6 +440,8 @@ const startServer = async () => {
|
||||||
return onlyMedia ? 'public:media' : 'public';
|
return onlyMedia ? 'public:media' : 'public';
|
||||||
case '/api/v1/streaming/public/local':
|
case '/api/v1/streaming/public/local':
|
||||||
return onlyMedia ? 'public:local:media' : 'public:local';
|
return onlyMedia ? 'public:local:media' : 'public:local';
|
||||||
|
case '/api/v1/streaming/public/bubble':
|
||||||
|
return onlyMedia ? 'public:bubble:media' : 'public:bubble';
|
||||||
case '/api/v1/streaming/public/remote':
|
case '/api/v1/streaming/public/remote':
|
||||||
return onlyMedia ? 'public:remote:media' : 'public:remote';
|
return onlyMedia ? 'public:remote:media' : 'public:remote';
|
||||||
case '/api/v1/streaming/hashtag':
|
case '/api/v1/streaming/hashtag':
|
||||||
|
@ -1096,6 +1100,13 @@ const startServer = async () => {
|
||||||
options: { needsFiltering: true, allowLocalOnly: true },
|
options: { needsFiltering: true, allowLocalOnly: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'public:bubble':
|
||||||
|
resolve({
|
||||||
|
channelIds: ['timeline:public:bubble'],
|
||||||
|
options: { needsFiltering: true, allowLocalOnly: false },
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'public:remote':
|
case 'public:remote':
|
||||||
resolve({
|
resolve({
|
||||||
|
@ -1124,6 +1135,13 @@ const startServer = async () => {
|
||||||
options: { needsFiltering: true, allowLocalOnly: true },
|
options: { needsFiltering: true, allowLocalOnly: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'public:bubble:media':
|
||||||
|
resolve({
|
||||||
|
channelIds: ['timeline:public:bubble:media'],
|
||||||
|
options: { needsFiltering: true, allowLocalOnly: false },
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'public:remote:media':
|
case 'public:remote:media':
|
||||||
resolve({
|
resolve({
|
||||||
|
|
Loading…
Reference in a new issue