From db8e5524db15e19c16b63943ada83b3ac7b75ec6 Mon Sep 17 00:00:00 2001 From: Fire Demon Date: Sat, 15 Aug 2020 03:26:55 -0500 Subject: [Profiles] Add category filters --- app/controllers/accounts_controller.rb | 51 +++++++---- .../api/v1/accounts/statuses_controller.rb | 91 +++++++++++--------- .../flavours/glitch/actions/timelines.js | 13 ++- .../features/account_timeline/components/header.js | 4 +- .../glitch/features/account_timeline/index.js | 22 ++--- .../glitch/features/ui/components/report_modal.js | 6 +- .../flavours/glitch/features/ui/index.js | 6 +- app/javascript/mastodon/locales/en-MP.json | 9 +- app/models/status.rb | 98 ++++++++++++++++++---- app/views/accounts/_header.html.haml | 2 +- app/views/accounts/show.html.haml | 4 + config/locales/en-MP.yml | 7 +- config/routes.rb | 3 + 13 files changed, 219 insertions(+), 97 deletions(-) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index e26b6395f..8059e0e5d 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -13,7 +13,7 @@ class AccountsController < ApplicationController before_action :require_authenticated!, if: -> { @account.require_auth? || @account.private? } skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } - skip_before_action :require_functional! #, unless: :whitelist_mode? + skip_before_action :require_functional! # , unless: :whitelist_mode? def show respond_to do |format| @@ -66,19 +66,26 @@ class AccountsController < ApplicationController end def show_pinned_statuses? - [replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? + [threads_requested?, replies_requested?, reblogs_requested?, mentions_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none? end def filtered_statuses + return mentions_scope if mentions_requested? + default_statuses.tap do |statuses| - statuses.merge!(hashtag_scope) if tag_requested? statuses.merge!(only_media_scope) if media_requested? - statuses.merge!(no_replies_scope) unless (current_account&.id == @account.id || @account.show_replies?) && replies_requested? end end def default_statuses - @account.statuses.permitted_for(@account, current_account, user_signed_in: user_signed_in?).not_local_only + @account.statuses.permitted_for( + @account, + current_account, + include_reblogs: !(threads_requested? || replies_requested?), + only_reblogs: reblogs_requested?, + only_replies: replies_requested?, + tag: tag_requested? ? params[:tag] : nil + ) end def only_media_scope @@ -89,18 +96,10 @@ class AccountsController < ApplicationController @account.media_attachments.attached.reorder(nil).select(:status_id).distinct end - def no_replies_scope - Status.without_replies - end - - def hashtag_scope - tag = Tag.find_normalized(params[:tag]) + def mentions_scope + return Status.none unless current_account? - if tag - Status.tagged_with(tag.id) - else - Status.none - end + Status.mentions_between(@account, current_account) end def username_param @@ -128,8 +127,14 @@ class AccountsController < ApplicationController short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id) elsif media_requested? short_account_media_url(@account, max_id: max_id, min_id: min_id) + elsif threads_requested? + short_account_threads_url(@account, max_id: max_id, min_id: min_id) elsif replies_requested? short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) + elsif reblogs_requested? + short_account_reblogs_url(@account, max_id: max_id, min_id: min_id) + elsif mentions_requested? + short_account_mentions_url(@account, max_id: max_id, min_id: min_id) else short_account_url(@account, max_id: max_id, min_id: min_id) end @@ -139,7 +144,13 @@ class AccountsController < ApplicationController request.path.split('.').first.ends_with?('/media') && !tag_requested? end + def threads_requested? + request.path.split('.').first.ends_with?('/threads') && !tag_requested? + end + def replies_requested? + return false unless current_account&.id == @account.id || @account.show_replies? + request.path.split('.').first.ends_with?('/with_replies') && !tag_requested? end @@ -147,6 +158,14 @@ class AccountsController < ApplicationController request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end + def reblogs_requested? + request.path.split('.').first.ends_with?('/reblogs') && !tag_requested? + end + + def mentions_requested? + request.path.split('.').first.ends_with?('/mentions') && !tag_requested? + end + def filtered_status_page filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id)) end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 3e0feed7e..61633ce36 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -17,6 +17,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController @account = Account.find(params[:account_id]) end + def owner? + @account.id == current_account&.id + end + def load_statuses cached_account_statuses end @@ -26,62 +30,75 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def account_statuses - return [] if (@account.private && !following?(@account)) || (@account.require_auth && !current_account?) - statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses - statuses.merge!(only_media_scope) if truthy_param?(:only_media) - statuses.merge!(no_replies_scope) if (current_account&.id != @account.id && !@account.show_replies?) || truthy_param?(:exclude_replies) - statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) - statuses.merge!(hashtag_scope) if params[:tagged].present? - statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id)) end def permitted_account_statuses - @account.statuses.permitted_for(@account, current_account, user_signed_in: authenticated_or_following?(@account)) + return mentions_scope if truthy_param?(:mentions) + return Status.none if unauthorized? + + @account.statuses.permitted_for( + @account, + current_account, + include_reblogs: include_reblogs?, + include_replies: include_replies?, + only_reblogs: only_reblogs?, + only_replies: only_replies?, + include_unpublished: owner?, + tag: params[:tagged] + ) end - def only_media_scope - Status.where(id: account_media_status_ids) + def unauthorized? + (@account.private && !following?(@account)) || (@account.require_auth && !current_account?) end - def account_media_status_ids - # `SELECT DISTINCT id, updated_at` is too slow, so pluck ids at first, and then select id, updated_at with ids. - # Also, Avoid getting slow by not narrowing down by `statuses.account_id`. - # When narrowing down by `statuses.account_id`, `index_statuses_20180106` will be used - # and the table will be joined by `Merge Semi Join`, so the query will be slow. - @account.statuses.joins(:media_attachments).merge(@account.media_attachments).permitted_for(@account, current_account, user_signed_in: authenticated_or_following?(@account)) - .paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id]) - .reorder(id: :desc).distinct(:id).pluck(:id) + def include_reblogs? + params[:include_reblogs].present? ? truthy_param?(:include_reblogs) : !truthy_param?(:exclude_reblogs) end - def pinned_scope - return Status.none if @account.blocking?(current_account) + def include_replies? + return false unless owner? || @account.show_replies? - @account.pinned_statuses + params[:include_replies].present? ? truthy_param?(:include_replies) : !truthy_param?(:exclude_replies) + end + + def only_reblogs? + truthy_param?(:only_reblogs).presence || false end - def no_replies_scope - Status.without_replies + def only_replies? + return false unless owner? || @account.show_replies? + + truthy_param?(:only_replies).presence || false end - def no_reblogs_scope - Status.without_reblogs + def mentions_scope + return Status.none unless current_account? + + Status.mentions_between(@account, current_account) end - def hashtag_scope - tag = Tag.find_normalized(params[:tagged]) + def only_media_scope + Status.where(id: account_media_status_ids) + end - if tag - Status.tagged_with(tag.id) - else - Status.none - end + def account_media_status_ids + @account.media_attachments.attached.reorder(nil).select(:status_id).distinct + end + + def pinned_scope + return Status.none if @account.blocking?(current_account) + + @account.pinned_statuses end def pagination_params(core_params) - params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) + params.slice(:limit, :only_media, :include_replies, :exclude_replies, :only_replies, :include_reblogs, :exclude_reblogs, :only_relogs, :mentions) + .permit(:limit, :only_media, :include_replies, :exclude_replies, :only_replies, :include_reblogs, :exclude_reblogs, :only_relogs, :mentions) + .merge(core_params) end def insert_pagination_headers @@ -89,15 +106,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def next_path - if records_continue? - api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @statuses.empty? - api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) - end + api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) unless @statuses.empty? end def records_continue? diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index b19666e62..f6cb2d989 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -133,7 +133,18 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex 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 expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); -export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandAccountTimeline = (accountId, { maxId, filter } = {}) => { + const params = { + include_replies: filter === ':replies', + include_reblogs: filter === ':reblogs', + only_replies: filter === ':replies', + only_reblogs: filter === ':reblogs', + mentions: filter === ':mentions', + max_id: maxId, + }; + + return expandTimeline(`account:${accountId}${filter}`, `/api/v1/accounts/${accountId}/statuses`, params); +}; export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index 527352497..938d854eb 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -124,10 +124,12 @@ export default class Header extends ImmutablePureComponent { {!hideTabs && (
- + + { (account.get('id') === me || account.get('show_replies')) && () } +
)} diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 5558ba2a3..66bf55ec4 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -17,15 +17,15 @@ import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import TimelineHint from 'flavours/glitch/components/timeline_hint'; -const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { - const path = withReplies ? `${accountId}:with_replies` : accountId; +const mapStateToProps = (state, { params: { accountId }, filter = '' }) => { + const path = `${accountId}${filter}`; return { remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), remoteUrl: state.getIn(['accounts', accountId, 'url']), isAccount: !!state.getIn(['accounts', accountId]), statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), - featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), + featuredStatusIds: !filter ? state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()) : ImmutableList(), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), }; @@ -49,7 +49,7 @@ class AccountTimeline extends ImmutablePureComponent { featuredStatusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, hasMore: PropTypes.bool, - withReplies: PropTypes.bool, + filter: PropTypes.string, isAccount: PropTypes.bool, remote: PropTypes.bool, remoteUrl: PropTypes.string, @@ -57,24 +57,24 @@ class AccountTimeline extends ImmutablePureComponent { }; componentWillMount () { - const { params: { accountId }, withReplies } = this.props; + const { params: { accountId }, filter } = this.props; this.props.dispatch(fetchAccount(accountId)); this.props.dispatch(fetchAccountIdentityProofs(accountId)); - if (!withReplies) { + if (!filter) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } - this.props.dispatch(expandAccountTimeline(accountId, { withReplies })); + this.props.dispatch(expandAccountTimeline(accountId, { filter })); } componentWillReceiveProps (nextProps) { - if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { + if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.filter !== this.props.filter) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); - if (!nextProps.withReplies) { + if (!nextProps.filter) { this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } - this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); + this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { filter: nextProps.params.filter })); } } @@ -83,7 +83,7 @@ class AccountTimeline extends ImmutablePureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); + this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, filter: this.props.filter })); } setRef = c => { diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index 9016b08d7..7473cfbe0 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -30,7 +30,7 @@ const makeMapStateToProps = () => { account: getAccount(state, accountId), comment: state.getIn(['reports', 'new', 'comment']), forward: state.getIn(['reports', 'new', 'forward']), - statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), + statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), }; }; @@ -70,12 +70,12 @@ class ReportModal extends ImmutablePureComponent { } componentDidMount () { - this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true })); + this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { filter: ':replies' })); } componentWillReceiveProps (nextProps) { if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true })); + this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { filter: ':replies' })); } } diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index bf76c0e57..ee1d898bb 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -214,8 +214,10 @@ class SwitchingColumnsArea extends React.PureComponent { - - + + + + diff --git a/app/javascript/mastodon/locales/en-MP.json b/app/javascript/mastodon/locales/en-MP.json index 7490fca60..2e1fe2393 100644 --- a/app/javascript/mastodon/locales/en-MP.json +++ b/app/javascript/mastodon/locales/en-MP.json @@ -4,8 +4,12 @@ "account.follows.empty": "This creature doesn't follow anyone yet.", "account.follows": "Follows", "account.locked_info": "This creature manually reviews who can follow them.", - "account.posts_with_replies": "Roars & Growls", - "account.posts": "Roars", + "account.media": "Media", + "account.mentions": "Mentions", + "account.posts_with_replies": "Replies", + "account.posts": "Blog", + "account.reblogs": "Boosts", + "account.threads": "Threads", "account.statuses_counter": "{count, plural, one {{counter} Roar} other {{counter} Roars}}", "advanced_options.local-only.long": "Do not post to other servers", "column_header.profile": "Creature", @@ -151,7 +155,6 @@ "upload_form.edit": "Add description text", "upload_modal.edit_media": "Add description text", "video.expand": "Open video", - "column.community": "Monsterpit", "community.column_settings.local_only": "Monsterpit only", "directory.local": "From Monsterpit", diff --git a/app/models/status.rb b/app/models/status.rb index e568bfed7..d31e5668e 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -135,6 +135,10 @@ class Status < ApplicationRecord scope :without_semiprivate, -> { where(semiprivate: false) } scope :reblogs, -> { where('statuses.reblog_of_id IS NOT NULL') } scope :locally_reblogged, -> { where(id: Status.unscoped.local.reblogs.select(:reblog_of_id)) } + scope :public_conversations, -> { joins(:conversation).where(conversations: { public: true }) } + scope :conversations_by, ->(account) { joins(:conversation).where(conversations: { account: account }) } + scope :mentioning_account, ->(account) { joins(:mentions).where(mentions: { account: account }) } + scope :replies, -> { where(reply: true).where('statuses.in_reply_to_account_id != statuses.account_id') } scope :not_hidden_by_account, ->(account) do left_outer_joins(:mutes, :conversation_mute).where('(status_mutes.account_id IS NULL OR status_mutes.account_id != ?) AND (conversation_mutes.account_id IS NULL OR (conversation_mutes.account_id != ? AND conversation_mutes.hidden = TRUE))', account.id, account.id) @@ -504,27 +508,27 @@ class Status < ApplicationRecord end end - def permitted_for(target_account, account, user_signed_in: false) - visibility = user_signed_in || target_account.show_unlisted? ? [:public, :unlisted] : :public + def permitted_for(target_account, account, **options) + visibility = [:public, :unlisted] - if account.nil? - where(visibility: visibility).not_local_only.published - elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps - none - elsif account.id == target_account.id # author can see own stuff - all - else - # followers can see followers-only stuff, but also things they are mentioned in. - # non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in. - visibility.push(:private) if account.following?(target_account) && user_signed_in - scope = left_outer_joins(:reblog) - - scope = scope.where(visibility: visibility) - .or(scope.where(id: account.mentions.select(:status_id))) - .merge(scope.where(reblog_of_id: nil).or(scope.where.not(reblogs_statuses: { account_id: account.excluded_from_timeline_account_ids }))) + if account.present? + return none if target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) + return apply_category_filters(all, target_account, account, **options) if account.id == target_account.id - apply_timeline_filters(scope.published, account, false) + visibility.push(:private) if account.following?(target_account) + elsif !target_account.show_unlisted? + visibility = :public end + + scope = where(visibility: visibility) + scope = apply_account_filters(scope, account, **options) + apply_category_filters(scope, target_account, account, **options) + end + + def mentions_between(account, target_account) + return none if account.blank? || target_account.blank? + + account.statuses.mentioning_account(target_account).or(target_account.statuses.mentioning_account(account)) end def from_text(text) @@ -544,6 +548,64 @@ class Status < ApplicationRecord private + # TODO: Cast cleanup spell. + # rubocop:disable Metrics/PerceivedComplexity + def apply_category_filters(query, target_account, account, **options) + return query if options[:without_category_filters] + + query = query.published unless options[:include_unpublished] + + if options[:only_reblogs] + query = query.joins(:reblog) + if account.present? && account.excluded_from_timeline_account_ids.present? + query = query.where.not( + reblogs_statuses: { account_id: account.excluded_from_timeline_account_ids } + ) + end + elsif target_account.id == account&.id + query = query.without_replies unless options[:include_replies] || options[:only_replies] + query = query.without_reblogs unless options[:include_reblogs] || options[:only_reblogs] + query = query.reblogs if options[:only_reblogs] + query = query.replies if options[:only_replies] + else + if options[:include_reblogs] && account.present? && account.excluded_from_timeline_account_ids.present? + query = query.left_outer_joins(:reblog).where( + '(statuses.reblog_of_id IS NULL OR reblogs_statuses.account_id NOT IN (?))', + account.excluded_from_timeline_account_ids + ) + elsif !options[:include_reblogs] + query = query.without_reblogs + end + + query = if options[:only_replies] + query.replies + elsif options[:include_replies] + if target_account.present? + query.public_conversations.or(query.conversations_by(target_account)) + else + query.public_conversations + end + else + query.without_replies + end + end + + return query if options[:tag].blank? + + (tag = Tag.find_normalized(options[:tag])) ? query.merge(Status.tagged_with(tag.id)) : none + end + # rubocop:enable Metrics/PerceivedComplexity + + def apply_account_filters(query, account, **options) + return query.not_local_only if account.blank? + return (account.local? ? query : query.not_local_only) if options[:without_account_filters] + + query = query.not_local_only unless account.local? + query = query.not_hidden_by_account(account) + query = query.in_chosen_languages(account) if account.chosen_languages.present? + query + end + def timeline_scope(scope = false, include_unlisted: false) starting_scope = case scope when :local, true diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index 33a144ed2..27a29c061 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -13,7 +13,7 @@ = fa_icon('lock') if account.locked? .public-account-header__tabs__tabs .details-counters - .counter{ class: active_nav_class(short_account_url(account), short_account_with_replies_url(account), short_account_media_url(account)) } + .counter{ class: active_nav_class(short_account_url(account), short_account_threads_url(account), short_account_with_replies_url(account), short_account_reblogs_url(account), short_account_mentions_url(account), short_account_media_url(account)) } = link_to short_account_url(account), class: 'u-url u-uid', title: number_with_delimiter(account.statuses_count) do %span.counter-label= t('accounts.posts', count: account.statuses_count) diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index 9eda36f1e..461262f60 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -29,9 +29,13 @@ .account__section-headline = active_link_to t('accounts.posts_tab_heading'), short_account_url(@account) + = active_link_to t('accounts.threads'), short_account_threads_url(@account) + - if @account.present? + = active_link_to t('accounts.mentions'), short_account_mentions_url(@account) - if @account.show_replies? = active_link_to t('accounts.posts_with_replies'), short_account_with_replies_url(@account) = active_link_to t('accounts.media'), short_account_media_url(@account) + = active_link_to t('accounts.reblogs'), short_account_reblogs_url(@account) - if user_signed_in? && @account.blocking?(current_account) .nothing-here.nothing-here--under-tabs= t('accounts.unavailable') diff --git a/config/locales/en-MP.yml b/config/locales/en-MP.yml index be4be53e2..481c299e5 100644 --- a/config/locales/en-MP.yml +++ b/config/locales/en-MP.yml @@ -18,8 +18,11 @@ en-MP: posts: one: Roar other: Roars - posts_tab_heading: Roars - posts_with_replies: Roars & Growls + posts_tab_heading: Blog + posts_with_replies: Replies + reblogs: Boosts + threads: Threads + mentions: Mentions admin: accounts: search_same_email_domain: Other creatures with the same e-mail domain diff --git a/config/routes.rb b/config/routes.rb index 4702e766b..cdfb4a77a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,8 +89,11 @@ Rails.application.routes.draw do resource :inbox, only: [:create], module: :activitypub get '/@:username', to: 'accounts#show', as: :short_account + get '/@:username/threads', to: 'accounts#show', as: :short_account_threads get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies get '/@:username/media', to: 'accounts#show', as: :short_account_media + get '/@:username/reblogs', to: 'accounts#show', as: :short_account_reblogs + get '/@:username/mentions', to: 'accounts#show', as: :short_account_mentions get '/@:username/tagged/:tag', to: 'accounts#show', as: :short_account_tag get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status -- cgit