Skip to content

Commit

Permalink
HTL media only timeline.
Browse files Browse the repository at this point in the history
  • Loading branch information
aquarla committed Jul 28, 2020
1 parent 8fa4c18 commit 4f743f2
Show file tree
Hide file tree
Showing 15 changed files with 141 additions and 45 deletions.
7 changes: 6 additions & 1 deletion app/controllers/api/v1/timelines/home_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def cached_home_statuses
end

def home_statuses
account_home_feed.get(
feed = truthy_param?(:only_media) ? account_home_media_feed : account_home_feed
feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id],
Expand All @@ -37,6 +38,10 @@ def account_home_feed
HomeFeed.new(current_account)
end

def account_home_media_feed
HomeMediaFeed.new(current_account)
end

def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/actions/streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
dispatch(fetchAnnouncements(done))))));
};

export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectUserStream = ({ onlyMedia } = {}) => connectTimelineStream(`home${onlyMedia ? ':media' : ''}`, `user${onlyMedia ? ':media' : ':all'}`);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
export const connectHashtagStream = (id, tag, local, accept) => connectTimelineStream(`hashtag:${id}${local ? ':local' : ''}`, `hashtag${local ? ':local' : ''}&tag=${tag}`, null, accept);
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/actions/timelines.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,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 expandHomeTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`home${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/home', { max_id: maxId, only_media: !!onlyMedia }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, 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 expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
Expand Down
33 changes: 22 additions & 11 deletions app/javascript/mastodon/containers/mastodon.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,38 @@ store.dispatch(hydrateAction);
store.dispatch(fetchCustomEmojis());

const mapStateToProps = state => ({
onlyMedia: state.getIn(['settings', 'home', 'other', 'onlyMedia']),
showIntroduction: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
});

@connect(mapStateToProps)
class MastodonMount extends React.PureComponent {

static propTypes = {
onlyMedia: PropTypes.bool,
showIntroduction: PropTypes.bool,
};

componentDidMount() {
const { onlyMedia } = this.props;
this.disconnect = store.dispatch(connectUserStream({ onlyMedia }));
}

componentDidUpdate (prevProps) {
const { onlyMedia } = this.props;
if (prevProps.onlyMedia !== onlyMedia) {
this.disconnect();
this.disconnect = store.dispatch(connectUserStream({ onlyMedia }));
}
}

componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

shouldUpdateScroll (_, { location }) {
return location.state !== previewMediaState && location.state !== previewVideoState;
}
Expand Down Expand Up @@ -65,17 +87,6 @@ export default class Mastodon extends React.PureComponent {
locale: PropTypes.string.isRequired,
};

componentDidMount() {
this.disconnect = store.dispatch(connectUserStream());
}

componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}

render () {
const { locale } = this.props;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
</div>

<div className='column-settings__row'>
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='home.column_settings.media_only' defaultMessage='Media only' />} />
</div>
</div>
);
}
Expand Down
51 changes: 34 additions & 17 deletions app/javascript/mastodon/features/home_timeline/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,26 @@ const messages = defineMessages({
hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' },
});

const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
showAnnouncements: state.getIn(['announcements', 'show']),
});
const mapStateToProps = state => {
const onlyMedia = state.getIn(['settings', 'home', 'other', 'onlyMedia']);
return {
hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
isPartial: state.getIn(['timelines', 'home', 'isPartial']),
hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(),
unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')),
showAnnouncements: state.getIn(['announcements', 'show']),
onlyMedia,
};
};

export default @connect(mapStateToProps)
@injectIntl
class HomeTimeline extends React.PureComponent {

static defaultProps = {
onlyMedia: false,
};

static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
Expand All @@ -43,15 +51,16 @@ class HomeTimeline extends React.PureComponent {
hasAnnouncements: PropTypes.bool,
unreadAnnouncements: PropTypes.number,
showAnnouncements: PropTypes.bool,
onlyMedia: PropTypes.bool,
};

handlePin = () => {
const { columnId, dispatch } = this.props;
const { columnId, dispatch, onlyMedia } = this.props;

if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('HOME', {}));
dispatch(addColumn('HOME', {other: { onlyMedia }}));
}
}

Expand All @@ -69,30 +78,38 @@ class HomeTimeline extends React.PureComponent {
}

handleLoadMore = maxId => {
this.props.dispatch(expandHomeTimeline({ maxId }));
const { onlyMedia } = this.props;
this.props.dispatch(expandHomeTimeline({ maxId, onlyMedia }));
}

componentDidMount () {
this.props.dispatch(fetchAnnouncements());
this._checkIfReloadNeeded(false, this.props.isPartial);
const { dispatch, onlyMedia, isPartial } = this.props;
dispatch(fetchAnnouncements());
dispatch(expandHomeTimeline({ onlyMedia }));
this._checkIfReloadNeeded(false, isPartial);
}

componentDidUpdate (prevProps) {
this._checkIfReloadNeeded(prevProps.isPartial, this.props.isPartial);
const { dispatch, onlyMedia, isPartial } = this.props;
if (prevProps.onlyMedia !== onlyMedia) {
dispatch(expandHomeTimeline({ onlyMedia }));
} else {
this._checkIfReloadNeeded(prevProps.isPartial, isPartial);
}
}

componentWillUnmount () {
this._stopPolling();
}

_checkIfReloadNeeded (wasPartial, isPartial) {
const { dispatch } = this.props;
const { dispatch, onlyMedia } = this.props;

if (wasPartial === isPartial) {
return;
} else if (!wasPartial && isPartial) {
this.polling = setInterval(() => {
dispatch(expandHomeTimeline());
dispatch(expandHomeTimeline({ onlyMedia }));
}, 3000);
} else if (wasPartial && !isPartial) {
this._stopPolling();
Expand All @@ -112,7 +129,7 @@ class HomeTimeline extends React.PureComponent {
}

render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements, onlyMedia } = this.props;
const pinned = !!columnId;

let announcementsButton = null;
Expand Down Expand Up @@ -152,7 +169,7 @@ class HomeTimeline extends React.PureComponent {
trackScroll={!pinned}
scrollKey={`home_timeline-${columnId}`}
onLoadMore={this.handleLoadMore}
timelineId='home'
timelineId={`home${onlyMedia ? ':media' : ''}`}
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Visit {public} or use search to get started and meet other users.' values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
Expand Down
1 change: 1 addition & 0 deletions app/javascript/mastodon/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.column_settings.media_only": "Media only",
"home.hide_announcements": "Hide announcements",
"home.show_announcements": "Show announcements",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
Expand Down
1 change: 1 addition & 0 deletions app/javascript/mastodon/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
"home.column_settings.basic": "基本設定",
"home.column_settings.show_reblogs": "ブースト表示",
"home.column_settings.show_replies": "返信表示",
"home.column_settings.media_only": "メディアのみ表示",
"home.hide_announcements": "お知らせを隠す",
"home.show_announcements": "お知らせを表示",
"intervals.full.days": "{number}日",
Expand Down
4 changes: 1 addition & 3 deletions app/javascript/mastodon/reducers/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,7 @@ export default function notifications(state = initialState, action) {
case TIMELINE_DELETE:
return deleteByStatus(state, action.id);
case TIMELINE_DISCONNECT:
return action.timeline === 'home' ?
state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
state;
return state;
case NOTIFICATIONS_MOUNT:
return state.set('mounted', true);
case NOTIFICATIONS_UNMOUNT:
Expand Down
13 changes: 11 additions & 2 deletions app/lib/feed_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ def push_to_home(account, status)
return false unless add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?)

trim(:home, account.id)

PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}:media") if push_update_required?("timeline:#{account.id}", true) && status.proper.media_attachments.any?
true
end

def unpush_from_home(account, status)
return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)

redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
redis.publish("timeline:#{account.id}:media", Oj.dump(event: :delete, payload: status.id.to_s)) if status.proper.media_attachments.any?
true
end

Expand Down Expand Up @@ -70,9 +73,11 @@ def unpush_from_list(list, status)
def trim(type, account_id)
timeline_key = key(type, account_id)
reblog_key = key(type, account_id, 'reblogs')
media_key = key(type, account_id, 'media')

# Remove any items past the MAX_ITEMS'th entry in our feed
redis.zremrangebyrank(timeline_key, 0, -(FeedManager::MAX_ITEMS + 1))
redis.zremrangebyrank(media_key, 0, -(FeedManager::MAX_ITEMS + 1))

# Get the score of the REBLOG_FALLOFF'th item in our feed, and stop
# tracking anything after it for deduplication purposes.
Expand Down Expand Up @@ -174,8 +179,9 @@ def populate_feed(account)

private

def push_update_required?(timeline_id)
redis.exists?("subscribed:#{timeline_id}")
def push_update_required?(timeline_id, only_media = false)
channel = only_media ? "subscribed:#{timeline_id}:media" : "subscribed:#{timeline_id}"
redis.exists?(channel)
end

def blocks_or_mutes?(receiver_id, account_ids, context)
Expand Down Expand Up @@ -269,6 +275,7 @@ def phrase_filtered?(status, receiver_id, context)
def add_to_feed(timeline_type, account_id, status, aggregate_reblogs = true)
timeline_key = key(timeline_type, account_id)
reblog_key = key(timeline_type, account_id, 'reblogs')
media_key = key(timeline_type, account_id, 'media')

if status.reblog? && (aggregate_reblogs.nil? || aggregate_reblogs)
# If the original status or a reblog of it is within
Expand All @@ -286,6 +293,7 @@ def add_to_feed(timeline_type, account_id, status, aggregate_reblogs = true)
# reblogging it).
redis.zadd(timeline_key, status.id, status.id)
redis.zadd(reblog_key, status.id, status.reblog_of_id)
redis.zadd(media_key, status.id, status.id) if status.proper.media_attachments.any?
else
# Another reblog of the same status was already in the
# REBLOG_FALLOFF most recent statuses, so we note that this
Expand All @@ -304,6 +312,7 @@ def add_to_feed(timeline_type, account_id, status, aggregate_reblogs = true)
return false unless rank.nil?

redis.zadd(timeline_key, status.id, status.id)
redis.zadd(media_key, status.id, status.id) if status.proper.media_attachments.any?
end

true
Expand Down
1 change: 1 addition & 0 deletions app/models/custom_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def clean_up_contexts
def remove_cache
Rails.cache.delete("filters:#{account_id}")
Redis.current.publish("timeline:#{account_id}", Oj.dump(event: :filters_changed))
Redis.current.publish("timeline:#{account_id}:media", Oj.dump(event: :filters_changed))
end

def context_must_be_valid
Expand Down
12 changes: 12 additions & 0 deletions app/models/home_media_feed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class HomeMediaFeed < HomeFeed
def initialize(account)
@subtype = 'media'
super account
end

def key
FeedManager.instance.key(@type, @id, @subtype)
end
end
1 change: 1 addition & 0 deletions app/services/notify_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def push_notification!
return if @notification.activity.nil?

Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
Redis.current.publish("timeline:#{@recipient.id}:media", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
send_push_notifications!
end

Expand Down
1 change: 1 addition & 0 deletions app/services/remove_status_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def remove_from_lists
def remove_from_affected
@mentions.map(&:account).select(&:local?).each do |account|
redis.publish("timeline:#{account.id}", @payload)
redis.publish("timeline:#{account.id}:media", @payload) if status.media_attachments.any?
end
end

Expand Down

0 comments on commit 4f743f2

Please sign in to comment.