Skip to content

Commit

Permalink
Add bubble timeline
Browse files Browse the repository at this point in the history
  • Loading branch information
TheEssem committed Dec 2, 2024
1 parent 5550f53 commit 5ff87a8
Show file tree
Hide file tree
Showing 25 changed files with 369 additions and 30 deletions.
49 changes: 49 additions & 0 deletions app/controllers/admin/bubble_domains_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module Admin
class BubbleDomainsController < BaseController
before_action :set_bubble_domain, except: [:index, :new, :create]

def index
authorize :bubble_domain, :update?
@bubble_domains = BubbleDomain.all
end

def new
authorize :bubble_domain, :update?
@bubble_domain = BubbleDomain.new(domain: params[:_domain])
end

def create
authorize :bubble_domain, :update?

domain = TagManager.instance.normalize_domain(resource_params[:domain])

@bubble_domain = BubbleDomain.new(domain: domain)

if @bubble_domain.save!
log_action :create, @bubble_domain
redirect_to admin_bubble_domains_path, notice: I18n.t('admin.bubble_domains.created_msg')
else
render :new
end
end

def destroy
authorize :bubble_domain, :update?
@bubble_domain.destroy
log_action :destroy, @bubble_domain
redirect_to admin_bubble_domains_path
end

private

def set_bubble_domain
@bubble_domain = BubbleDomain.find(params[:id])
end

def resource_params
params.require(:bubble_domain).permit(:domain)
end
end
end
3 changes: 2 additions & 1 deletion app/controllers/api/v1/timelines/public_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
before_action -> { authorize_if_got_token! :read, :'read:statuses' }

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
cache_if_unauthenticated!
Expand Down Expand Up @@ -34,6 +34,7 @@ def public_feed
PublicFeed.new(
current_account,
local: truthy_param?(:local),
bubble: truthy_param?(:bubble),
remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media),
allow_local_only: truthy_param?(:allow_local_only),
Expand Down
9 changes: 9 additions & 0 deletions app/javascript/flavours/glitch/actions/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
fillPublicTimelineGaps,
fillCommunityTimelineGaps,
fillListTimelineGaps,
fillBubbleTimelineGaps,
} from './timelines';

/**
Expand Down Expand Up @@ -164,6 +165,14 @@ export const connectUserStream = () =>
export const connectCommunityStream = ({ 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 {boolean} [options.onlyMedia]
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/flavours/glitch/actions/timelines.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export function fillTimelineGaps(timelineId, path, params = {}) {
export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}) => 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 });
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia });
export const expandBubbleTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`bubble${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { bubble: true, max_id: maxId, only_media: !!onlyMedia });
export const expandDirectTimeline = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { 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 });
Expand All @@ -178,6 +179,7 @@ export const expandHashtagTimeline = (hashtag, { maxId, tags, local } =
export const fillHomeTimelineGaps = () => fillTimelineGaps('home', '/api/v1/timelines/home', {});
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly });
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia });
export const fillBubbleTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`bubble${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { bubble: true, only_media: !!onlyMedia });
export const fillListTimelineGaps = (id) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {});

export function expandTimelineRequest(timeline, isLoadingMore) {
Expand Down
93 changes: 66 additions & 27 deletions app/javascript/flavours/glitch/features/firehose/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { useIdentity } from '@/flavours/glitch/identity_context';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import { addColumn } from 'flavours/glitch/actions/columns';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming';
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
import { connectPublicStream, connectCommunityStream, connectBubbleStream } from 'flavours/glitch/actions/streaming';
import { expandPublicTimeline, expandCommunityTimeline, expandBubbleTimeline } from 'flavours/glitch/actions/timelines';
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import SettingText from 'flavours/glitch/components/setting_text';
import { domain } from 'flavours/glitch/initial_state';
Expand Down Expand Up @@ -93,6 +93,9 @@ const Firehose = ({ feedType, multiColumn }) => {
case 'public':
dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } }));
break;
case 'bubble':
dispatch(addColumn('BUBBLE', { other: { onlyMedia }, regex: { body: regex } }));
break;
case 'public:remote':
dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex } }));
break;
Expand All @@ -107,6 +110,9 @@ const Firehose = ({ feedType, multiColumn }) => {
case 'community':
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
break;
case 'bubble':
dispatch(expandBubbleTimeline({ maxId, onlyMedia }));
break;
case 'public':
dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly }));
break;
Expand All @@ -130,6 +136,12 @@ const Firehose = ({ feedType, multiColumn }) => {
disconnect = dispatch(connectCommunityStream({ onlyMedia }));
}
break;
case 'bubble':
dispatch(expandBubbleTimeline({ onlyMedia }));
if (signedIn) {
disconnect = dispatch(connectBubbleStream({ onlyMedia }));
}
break;
case 'public':
dispatch(expandPublicTimeline({ onlyMedia, allowLocalOnly }));
if (signedIn) {
Expand All @@ -147,35 +159,58 @@ const Firehose = ({ feedType, multiColumn }) => {
return () => disconnect?.();
}, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]);

const prependBanner = feedType === 'community' ? (
<DismissableBanner id='community_timeline'>
let prependBanner;
let emptyMessage;

if (feedType === 'community') {
prependBanner = (
<DismissableBanner id='community_timeline'>
<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>
);
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 }}
id='empty_column.community'
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
/>
</DismissableBanner>
) : (
<DismissableBanner id='public_timeline'>
);
} 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 fediverse whose accounts are on other servers selected by {domain}.'
values={{ domain }}
/>
</DismissableBanner>
);
emptyMessage = (
<FormattedMessage
id='dismissable_banner.public_timeline'
defaultMessage='These are the most recent public posts from people on the fediverse that people on {domain} follow.'
values={{ domain }}
id='empty_column.bubble'
defaultMessage='The bubble timeline is currently empty, but something might show up here soon!'
/>
</DismissableBanner>
);

const emptyMessage = feedType === 'community' ? (
<FormattedMessage
id='empty_column.community'
defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!'
/>
) : (
<FormattedMessage
id='empty_column.public'
defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up'
/>
);
);
} else {
prependBanner = (
<DismissableBanner id='public_timeline'>
<FormattedMessage
id='dismissable_banner.public_timeline'
defaultMessage='These are the most recent public posts from people on the fediverse 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 (
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
Expand All @@ -196,6 +231,10 @@ const Firehose = ({ feedType, multiColumn }) => {
<FormattedMessage tagName='div' id='firehose.local' defaultMessage='This server' />
</NavLink>

<NavLink exact to='/public/bubble'>
<FormattedMessage tagName='div' id='firehose.bubble' defaultMessage='Bubble servers' />
</NavLink>

<NavLink exact to='/public/remote'>
<FormattedMessage tagName='div' id='firehose.remote' defaultMessage='Other servers' />
</NavLink>
Expand Down
1 change: 1 addition & 0 deletions app/javascript/flavours/glitch/features/ui/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ class SwitchingColumnsArea extends PureComponent {
<Redirect from='/timelines/public/local' to='/public/local' exact />
<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/bubble' exact component={Firehose} componentParams={{ feedType: 'bubble' }} 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='/tags/:id' component={HashtagTimeline} content={children} />
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/flavours/glitch/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@
"confirmations.missing_media_description.edit": "Edit media",
"confirmations.missing_media_description.message": "At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.",
"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!",
"favourite_modal.favourite": "Favourite post?",
"federation.federated.long": "Allow this post to reach other servers",
"federation.federated.short": "Federated",
"federation.local_only.long": "Prevent this post from reaching other servers",
"federation.local_only.short": "Local-only",
"firehose.bubble": "Bubble servers",
"firehose.column_settings.allow_local_only": "Show local-only posts in \"All\"",
"home.column_settings.advanced": "Advanced",
"home.column_settings.filter_regex": "Filter out by regular expressions",
Expand Down
1 change: 1 addition & 0 deletions app/javascript/material-icons/400-24px/bubble_chart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions app/models/bubble_domain.rb
Original file line number Diff line number Diff line change
@@ -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
pluck(: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
14 changes: 12 additions & 2 deletions app/models/public_feed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class PublicFeed
# @option [Boolean] :with_replies
# @option [Boolean] :with_reblogs
# @option [Boolean] :local
# @option [Boolean] :bubble
# @option [Boolean] :remote
# @option [Boolean] :only_media
# @option [Boolean] :allow_local_only
Expand All @@ -26,6 +27,7 @@ def get(limit, max_id = nil, since_id = nil, min_id = nil)
scope.merge!(without_replies_scope) unless with_replies?
scope.merge!(without_reblogs_scope) unless with_reblogs?
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!(account_filters_scope) if account?
scope.merge!(media_only_scope) if media_only?
Expand All @@ -51,11 +53,15 @@ def with_replies?
end

def local_only?
options[:local] && !options[:remote]
options[:local] && !options[:remote] && !options[:bubble]
end

def bubble_only?
options[:bubble] && !options[:local] && !options[:remote]
end

def remote_only?
options[:remote] && !options[:local]
options[:remote] && !options[:local] && !options[:bubble]
end

def account?
Expand All @@ -78,6 +84,10 @@ def local_only_scope
Status.local
end

def bubble_only_scope
Status.bubble
end

def remote_only_scope
Status.remote
end
Expand Down
2 changes: 2 additions & 0 deletions app/models/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ class Status < ApplicationRecord

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_update_commit :trigger_update_webhooks

Expand Down
1 change: 1 addition & 0 deletions app/models/tag_feed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def get(limit, max_id = nil, since_id = nil, min_id = nil)
scope.merge!(tagged_with_all_scope)
scope.merge!(tagged_with_none_scope)
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!(account_filters_scope) if account?
scope.merge!(media_only_scope) if media_only?
Expand Down
7 changes: 7 additions & 0 deletions app/policies/bubble_domain_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class BubbleDomainPolicy < ApplicationPolicy
def update?
role.can?(:manage_federation)
end
end
5 changes: 5 additions & 0 deletions app/views/admin/bubble_domains/_bubble_domain.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
%tr
%td
%samp= bubble_domain.domain
%td
= table_link_to 'close', t('admin.bubble_domains.delete'), admin_bubble_domain_path(bubble_domain), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
Loading

0 comments on commit 5ff87a8

Please sign in to comment.