Skip to content

Commit

Permalink
Add featured hashtags to profiles (mastodon#9755)
Browse files Browse the repository at this point in the history
* Add hashtag filter to profiles

GET /@:username/tagged/:hashtag
GET /api/v1/accounts/:id/statuses?tagged=:hashtag

* Display featured hashtags on public profile

* Use separate model for featured tags

* Update featured hashtag counters on-write

* Limit featured tags to 10
  • Loading branch information
Gargron authored and hiyuki2578 committed Oct 2, 2019
1 parent a9c697c commit f1206fc
Show file tree
Hide file tree
Showing 24 changed files with 238 additions and 8 deletions.
14 changes: 12 additions & 2 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
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)
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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 @@ -111,6 +113,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit f1206fc

Please sign in to comment.