Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hashtag filter to profiles #9755

Merged
merged 5 commits into from Feb 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 12 additions & 2 deletions app/controllers/accounts_controller.rb
Expand Up @@ -57,6 +57,7 @@ def show_pinned_statuses?

def filtered_statuses
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 replies_requested?
end
Expand All @@ -78,12 +79,15 @@ def no_replies_scope
Status.without_replies
end

def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the project's preference is, but I've been seeing a lot of people move from #[] to #fetch() since it tells the reader a lot more about the intent. For example, in this method we EXPECT :tag to be a key, thus params.fetch(:tag), but if we didn't params.fetch(:tag, nil) and of course we get all the niceness of #fetch's other default api: fetch(:key) {dynamicvalue}.

Either way, seems like what you want here could also be expressed as: Status.tagged_with(Tag.where(name: params[:tag]).pluck(:id).first), though if tagged_with allows an array you can just drop .first.

end

def set_account
@account = Account.find_local!(params[:username])
end

def older_url
::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
pagination_url(max_id: @statuses.last.id)
end

Expand All @@ -92,7 +96,9 @@ def newer_url
end

def pagination_url(max_id: nil, min_id: nil)
if media_requested?
if tag_requested?
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 replies_requested?
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
Expand All @@ -109,6 +115,10 @@ def replies_requested?
request.path.ends_with?('/with_replies')
end

def tag_requested?
request.path.ends_with?("/tagged/#{params[:tag]}")
end

def filtered_status_page(params)
if params[:min_id].present?
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/api/v1/accounts/statuses_controller.rb
Expand Up @@ -33,6 +33,7 @@ def account_statuses
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?

statuses
end
Expand Down Expand Up @@ -67,6 +68,10 @@ def no_reblogs_scope
Status.without_reblogs
end

def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tagged])&.id)
end

def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end
Expand Down
51 changes: 51 additions & 0 deletions app/controllers/settings/featured_tags_controller.rb
@@ -0,0 +1,51 @@
# frozen_string_literal: true

class Settings::FeaturedTagsController < Settings::BaseController
layout 'admin'

before_action :authenticate_user!
before_action :set_featured_tags, only: :index
before_action :set_featured_tag, except: [:index, :create]
before_action :set_most_used_tags, only: :index

def index
@featured_tag = FeaturedTag.new
end

def create
@featured_tag = current_account.featured_tags.new(featured_tag_params)
@featured_tag.reset_data

if @featured_tag.save
redirect_to settings_featured_tags_path
else
set_featured_tags
set_most_used_tags

render :index
end
end

def destroy
@featured_tag.destroy!
redirect_to settings_featured_tags_path
end

private

def set_featured_tag
@featured_tag = current_account.featured_tags.find(params[:id])
end

def set_featured_tags
@featured_tags = current_account.featured_tags.reject(&:new_record?)
end

def set_most_used_tags
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
end

def featured_tag_params
params.require(:featured_tag).permit(:name)
end
end
2 changes: 1 addition & 1 deletion app/controllers/settings/profiles_controller.rb
Expand Up @@ -32,6 +32,6 @@ def account_params
end

def set_account
@account = current_user.account
@account = current_account
end
end
1 change: 1 addition & 0 deletions app/controllers/settings/sessions_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true

class Settings::SessionsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_session, only: :destroy

def destroy
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/styles/mastodon/accounts.scss
Expand Up @@ -288,3 +288,7 @@
border-bottom: 0;
}
}

.directory__tag .trends__item__current {
width: auto;
}
7 changes: 6 additions & 1 deletion app/javascript/styles/mastodon/admin.scss
Expand Up @@ -153,10 +153,15 @@ $content-width: 840px;
font-weight: 500;
}

.directory__tag a {
.directory__tag > a,
.directory__tag > div {
box-shadow: none;
}

.directory__tag .table-action-link .fa {
color: inherit;
}

.directory__tag h4 {
font-size: 18px;
font-weight: 700;
Expand Down
7 changes: 5 additions & 2 deletions app/javascript/styles/mastodon/widgets.scss
Expand Up @@ -269,7 +269,8 @@
box-sizing: border-box;
margin-bottom: 10px;

a {
& > a,
& > div {
display: flex;
align-items: center;
justify-content: space-between;
Expand All @@ -279,15 +280,17 @@
text-decoration: none;
color: inherit;
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
}

& > a {
&:hover,
&:active,
&:focus {
background: lighten($ui-base-color, 8%);
}
}

&.active a {
&.active > a {
background: $ui-highlight-color;
cursor: default;
}
Expand Down
1 change: 1 addition & 0 deletions app/models/concerns/account_associations.rb
Expand Up @@ -55,5 +55,6 @@ module AccountAssociations

# Hashtags
has_and_belongs_to_many :tags
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
end
end
46 changes: 46 additions & 0 deletions app/models/featured_tag.rb
@@ -0,0 +1,46 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: featured_tags
#
# id :bigint(8) not null, primary key
# account_id :bigint(8)
# tag_id :bigint(8)
# statuses_count :bigint(8) default(0), not null
# last_status_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#

class FeaturedTag < ApplicationRecord
belongs_to :account, inverse_of: :featured_tags, required: true
belongs_to :tag, inverse_of: :featured_tags, required: true

delegate :name, to: :tag, allow_nil: true

validates :name, presence: true
validate :validate_featured_tags_limit, on: :create

def name=(str)
self.tag = Tag.find_or_initialize_by(name: str.delete('#').mb_chars.downcase.to_s)
end

def increment(timestamp)
update(statuses_count: statuses_count + 1, last_status_at: timestamp)
end

def decrement(deleted_status_id)
update(statuses_count: [0, statuses_count - 1].max, last_status_at: account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).where.not(id: deleted_status_id).select(:created_at).first&.created_at)
end

def reset_data
self.statuses_count = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).count
self.last_status_at = account.statuses.where(visibility: %i(public unlisted)).tagged_with(tag).select(:created_at).first&.created_at
end

private

def validate_featured_tags_limit
errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= 10
end
end
2 changes: 2 additions & 0 deletions app/models/tag.rb
Expand Up @@ -14,6 +14,7 @@ class Tag < ApplicationRecord
has_and_belongs_to_many :accounts
has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'

has_many :featured_tags, dependent: :destroy, inverse_of: :tag
has_one :account_tag_stat, dependent: :destroy

HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
Expand All @@ -23,6 +24,7 @@ class Tag < ApplicationRecord

scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) }
scope :hidden, -> { where(account_tag_stats: { hidden: true }) }
scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) }

delegate :accounts_count,
:accounts_count=,
Expand Down
12 changes: 11 additions & 1 deletion app/services/process_hashtags_service.rb
Expand Up @@ -2,12 +2,22 @@

class ProcessHashtagsService < BaseService
def call(status, tags = [])
tags = Extractor.extract_hashtags(status.text) if status.local?
tags = Extractor.extract_hashtags(status.text) if status.local?
records = []

tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name|
tag = Tag.where(name: name).first_or_create(name: name)

status.tags << tag
records << tag

TrendingTags.record_use!(tag, status.account, status.created_at) if status.public_visibility?
end

return unless status.public_visibility? || status.unlisted_visibility?

status.account.featured_tags.where(tag_id: records.map(&:id)).each do |featured_tag|
featured_tag.increment(status.created_at)
end
end
end
4 changes: 4 additions & 0 deletions app/services/remove_status_service.rb
Expand Up @@ -131,6 +131,10 @@ def remove_reblogs
end

def remove_from_hashtags
@account.featured_tags.where(tag_id: @status.tags.pluck(:id)).each do |featured_tag|
featured_tag.decrement(@status.id)
end

return unless @status.public_visibility?

@tags.each do |hashtag|
Expand Down
13 changes: 13 additions & 0 deletions app/views/accounts/show.html.haml
Expand Up @@ -63,4 +63,17 @@
- @endorsed_accounts.each do |account|
= account_link_to account

- @account.featured_tags.each do |featured_tag|
.directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
= link_to short_account_tag_path(@account, featured_tag.tag) do
%h4
= fa_icon 'hashtag'
= featured_tag.name
%small
- if featured_tag.last_status_at.nil?
= t('accounts.nothing_here')
- else
%time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
.trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true

= render 'application/sidebar'
27 changes: 27 additions & 0 deletions app/views/settings/featured_tags/index.html.haml
@@ -0,0 +1,27 @@
- content_for :page_title do
= t('settings.featured_tags')

= simple_form_for @featured_tag, url: settings_featured_tags_path do |f|
= render 'shared/error_messages', object: @featured_tag

.fields-group
= f.input :name, wrapper: :with_block_label, hint: safe_join([t('simple_form.hints.featured_tag.name'), safe_join(@most_used_tags.map { |tag| link_to("##{tag.name}", settings_featured_tags_path(featured_tag: { name: tag.name }), method: :post) }, ', ')], ' ')

.actions
= f.button :button, t('featured_tags.add_new'), type: :submit

%hr.spacer/

- @featured_tags.each do |featured_tag|
.directory__tag{ class: params[:tag] == featured_tag.name ? 'active' : nil }
%div
%h4
= fa_icon 'hashtag'
= featured_tag.name
%small
- if featured_tag.last_status_at.nil?
= t('accounts.nothing_here')
- else
%time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
= table_link_to 'trash', t('filters.index.delete'), settings_featured_tag_path(featured_tag), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
.trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
5 changes: 5 additions & 0 deletions config/locales/en.yml
Expand Up @@ -588,6 +588,10 @@ en:
lists: Lists
mutes: You mute
storage: Media storage
featured_tags:
add_new: Add new
errors:
limit: You have already featured the maximum amount of hashtags
filters:
contexts:
home: Home timeline
Expand Down Expand Up @@ -807,6 +811,7 @@ en:
development: Development
edit_profile: Edit profile
export: Data export
featured_tags: Featured hashtags
followers: Authorized followers
import: Import
migrate: Account migration
Expand Down
4 changes: 4 additions & 0 deletions config/locales/simple_form.en.yml
Expand Up @@ -37,6 +37,8 @@ en:
setting_theme: Affects how Mastodon looks when you're logged in from any device.
username: Your username will be unique on %{domain}
whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word
featured_tag:
name: 'You might want to use one of these:'
imports:
data: CSV file exported from another Mastodon instance
sessions:
Expand Down Expand Up @@ -110,6 +112,8 @@ en:
username: Username
username_or_email: Username or Email
whole_word: Whole word
featured_tag:
name: Hashtag
interactions:
must_be_follower: Block notifications from non-followers
must_be_following: Block notifications from people you don't follow
Expand Down
1 change: 1 addition & 0 deletions config/navigation.rb
Expand Up @@ -6,6 +6,7 @@

primary.item :settings, safe_join([fa_icon('cog fw'), t('settings.settings')]), settings_profile_url do |settings|
settings.item :profile, safe_join([fa_icon('user fw'), t('settings.edit_profile')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration}
settings.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url
settings.item :preferences, safe_join([fa_icon('sliders fw'), t('settings.preferences')]), settings_preferences_url
settings.item :notifications, safe_join([fa_icon('bell fw'), t('settings.notifications')]), settings_notifications_url
settings.item :password, safe_join([fa_icon('lock fw'), t('auth.security')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete}
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Expand Up @@ -74,6 +74,7 @@
get '/@:username', to: 'accounts#show', as: :short_account
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
get '/@:username/media', to: 'accounts#show', as: :short_account_media
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

Expand Down Expand Up @@ -116,6 +117,7 @@
resource :migration, only: [:show, :update]

resources :sessions, only: [:destroy]
resources :featured_tags, only: [:index, :create, :destroy]
end

resources :media, only: [:show] do
Expand Down