From 73be8f38c115c279e3d3961b98bd2b82b9706b05 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 6 Dec 2018 17:36:11 +0100 Subject: [PATCH 01/14] Add profile directory (#9427) Fix #5578 --- app/controllers/admin/tags_controller.rb | 44 +++++ .../api/v1/accounts/credentials_controller.rb | 2 +- app/controllers/directories_controller.rb | 48 +++++ .../settings/profiles_controller.rb | 2 +- app/helpers/admin/filter_helper.rb | 3 +- app/javascript/styles/mastodon/accounts.scss | 5 + app/javascript/styles/mastodon/widgets.scss | 165 ++++++++++++++++++ app/models/account.rb | 40 +++++ app/models/account_stat.rb | 12 +- app/models/account_tag_stat.rb | 24 +++ app/models/concerns/account_associations.rb | 3 + app/models/concerns/account_counters.rb | 1 + app/models/tag.rb | 26 +++ app/policies/tag_policy.rb | 15 ++ app/services/update_account_service.rb | 5 + app/views/admin/tags/_tag.html.haml | 12 ++ app/views/admin/tags/index.html.haml | 19 ++ app/views/directories/index.html.haml | 59 +++++++ app/views/layouts/public.html.haml | 4 + app/views/settings/profiles/show.html.haml | 4 +- config/locales/en.yml | 19 ++ config/locales/simple_form.en.yml | 2 + config/navigation.rb | 1 + config/routes.rb | 12 ++ ...3003808_create_accounts_tags_join_table.rb | 8 + ...1203021853_add_discoverable_to_accounts.rb | 5 + ...439_add_last_status_at_to_account_stats.rb | 5 + ...20181204215309_create_account_tag_stats.rb | 11 ++ db/schema.rb | 21 ++- .../account_tag_stat_fabricator.rb | 3 + spec/models/account_tag_stat_spec.rb | 5 + 31 files changed, 578 insertions(+), 7 deletions(-) create mode 100644 app/controllers/admin/tags_controller.rb create mode 100644 app/controllers/directories_controller.rb create mode 100644 app/models/account_tag_stat.rb create mode 100644 app/policies/tag_policy.rb create mode 100644 app/views/admin/tags/_tag.html.haml create mode 100644 app/views/admin/tags/index.html.haml create mode 100644 app/views/directories/index.html.haml create mode 100644 db/migrate/20181203003808_create_accounts_tags_join_table.rb create mode 100644 db/migrate/20181203021853_add_discoverable_to_accounts.rb create mode 100644 db/migrate/20181204193439_add_last_status_at_to_account_stats.rb create mode 100644 db/migrate/20181204215309_create_account_tag_stats.rb create mode 100644 spec/fabricators/account_tag_stat_fabricator.rb create mode 100644 spec/models/account_tag_stat_spec.rb diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb new file mode 100644 index 0000000000..3f22565664 --- /dev/null +++ b/app/controllers/admin/tags_controller.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Admin + class TagsController < BaseController + before_action :set_tags, only: :index + before_action :set_tag, except: :index + before_action :set_filter_params + + def index + authorize :tag, :index? + end + + def hide + authorize @tag, :hide? + @tag.account_tag_stat.update!(hidden: true) + redirect_to admin_tags_path(@filter_params) + end + + def unhide + authorize @tag, :unhide? + @tag.account_tag_stat.update!(hidden: true) + redirect_to admin_tags_path(@filter_params) + end + + private + + def set_tags + @tags = Tag.discoverable + @tags.merge!(Tag.hidden) if filter_params[:hidden] + end + + def set_tag + @tag = Tag.find(params[:id]) + end + + def set_filter_params + @filter_params = filter_params.to_hash.symbolize_keys + end + + def filter_params + params.permit(:hidden) + end + end +end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index dcd41b35c1..e77f57910b 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController private def account_params - params.permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) + params.permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) end def user_settings_params diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb new file mode 100644 index 0000000000..265fd5fab2 --- /dev/null +++ b/app/controllers/directories_controller.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +class DirectoriesController < ApplicationController + layout 'public' + + before_action :set_instance_presenter + before_action :set_tag, only: :show + before_action :set_tags + before_action :set_accounts + + def index + render :index + end + + def show + render :index + end + + private + + def set_tag + @tag = Tag.discoverable.find_by!(name: params[:id].downcase) + end + + def set_tags + @tags = Tag.discoverable.limit(30) + end + + def set_accounts + @accounts = Account.searchable.discoverable.page(params[:page]).per(50).tap do |query| + query.merge!(Account.tagged_with(@tag.id)) if @tag + + if popular_requested? + query.merge!(Account.popular) + else + query.merge!(Account.by_recent_status) + end + end + end + + def set_instance_presenter + @instance_presenter = InstancePresenter.new + end + + def popular_requested? + request.path.ends_with?('/popular') + end +end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 5b3bfd71fe..20a55785c5 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -29,7 +29,7 @@ class Settings::ProfilesController < ApplicationController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value]) end def set_account diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 9a663051c6..8807cc7846 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -5,8 +5,9 @@ module Admin::FilterHelper REPORT_FILTERS = %i(resolved account_id target_account_id).freeze INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze + TAGS_FILTERS = %i(hidden).freeze - FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS def filter_link_to(text, link_to_params, link_class_params = link_to_params) new_url = filtered_url_for(link_to_params) diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 06effbdb2e..63a5c61b8b 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -189,6 +189,11 @@ &--under-tabs { border-radius: 0 0 4px 4px; } + + &--flexible { + box-sizing: border-box; + min-height: 100%; + } } .account-role { diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index f843f0b42b..a838ca778b 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -240,3 +240,168 @@ border-radius: 0; } } + +.page-header { + background: lighten($ui-base-color, 8%); + box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + border-radius: 4px; + padding: 60px 15px; + text-align: center; + margin: 10px 0; + + h1 { + color: $primary-text-color; + font-size: 36px; + line-height: 1.1; + font-weight: 700; + margin-bottom: 10px; + } + + p { + font-size: 15px; + color: $darker-text-color; + } +} + +.directory { + background: $ui-base-color; + border-radius: 0 0 4px 4px; + box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + + &__tag { + box-sizing: border-box; + margin-bottom: 10px; + + a { + display: flex; + align-items: center; + justify-content: space-between; + background: $ui-base-color; + border-radius: 4px; + padding: 15px; + text-decoration: none; + color: inherit; + box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + + &:hover, + &:active, + &:focus { + background: lighten($ui-base-color, 8%); + } + } + + &.active a { + background: $ui-highlight-color; + cursor: default; + } + + h4 { + flex: 1 1 auto; + font-size: 18px; + font-weight: 700; + color: $primary-text-color; + + .fa { + color: $darker-text-color; + } + + small { + display: block; + font-weight: 400; + font-size: 15px; + margin-top: 8px; + color: $darker-text-color; + } + } + + &.active h4 { + &, + .fa, + small { + color: $primary-text-color; + } + } + + .avatar-stack { + flex: 0 0 auto; + width: (36px + 4px) * 3; + } + + &.active .avatar-stack .account__avatar { + border-color: $ui-highlight-color; + } + } +} + +.avatar-stack { + display: flex; + justify-content: flex-end; + + .account__avatar { + flex: 0 0 auto; + width: 36px; + height: 36px; + border-radius: 50%; + position: relative; + margin-left: -10px; + border: 2px solid $ui-base-color; + + &:nth-child(1) { + z-index: 1; + } + + &:nth-child(2) { + z-index: 2; + } + + &:nth-child(3) { + z-index: 3; + } + } +} + +.accounts-table { + width: 100%; + + .account { + padding: 0; + border: 0; + } + + thead th { + text-align: center; + text-transform: uppercase; + color: $darker-text-color; + font-weight: 700; + padding: 10px; + + &:first-child { + text-align: left; + } + } + + tbody td { + padding: 15px 0; + vertical-align: middle; + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + + tbody tr:last-child td { + border-bottom: 0; + } + + &__count { + width: 120px; + text-align: center; + font-size: 15px; + font-weight: 500; + color: $primary-text-color; + + small { + display: block; + color: $darker-text-color; + font-weight: 400; + font-size: 14px; + } + } +} diff --git a/app/models/account.rb b/app/models/account.rb index fb089de90e..20b0b72391 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -43,11 +43,13 @@ # featured_collection_url :string # fields :jsonb # actor_type :string +# discoverable :boolean # class Account < ApplicationRecord USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i + MIN_FOLLOWERS_DISCOVERY = 10 include AccountAssociations include AccountAvatar @@ -89,6 +91,10 @@ class Account < ApplicationRecord scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) } + scope :discoverable, -> { where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) } + scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } + scope :popular, -> { order('account_stats.followers_count desc') } + scope :by_recent_status, -> { order('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc') } delegate :email, :unconfirmed_email, @@ -174,6 +180,40 @@ class Account < ApplicationRecord @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) end + def tags_as_strings=(tag_names) + tag_names.map! { |name| name.mb_chars.downcase } + tag_names.uniq!(&:to_s) + + # Existing hashtags + hashtags_map = Tag.where(name: tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag } + + # Initialize not yet existing hashtags + tag_names.each do |name| + next if hashtags_map.key?(name) + hashtags_map[name.downcase] = Tag.new(name: name) + end + + # Remove hashtags that are to be deleted + tags.each do |tag| + if hashtags_map.key?(tag.name) + hashtags_map.delete(tag.name) + else + transaction do + tags.delete(tag) + tag.decrement_count!(:accounts_count) + end + end + end + + # Add hashtags that were so far missing + hashtags_map.each_value do |tag| + transaction do + tags << tag + tag.increment_count!(:accounts_count) + end + end + end + def fields (self[:fields] || []).map { |f| Field.new(self, f) } end diff --git a/app/models/account_stat.rb b/app/models/account_stat.rb index d5715268eb..9813aa84ff 100644 --- a/app/models/account_stat.rb +++ b/app/models/account_stat.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true - # == Schema Information # # Table name: account_stats @@ -11,16 +10,25 @@ # followers_count :bigint(8) default(0), not null # created_at :datetime not null # updated_at :datetime not null +# last_status_at :datetime # class AccountStat < ApplicationRecord belongs_to :account, inverse_of: :account_stat def increment_count!(key) - update(key => public_send(key) + 1) + update(attributes_for_increment(key)) end def decrement_count!(key) update(key => [public_send(key) - 1, 0].max) end + + private + + def attributes_for_increment(key) + attrs = { key => public_send(key) + 1 } + attrs[:last_status_at] = Time.now.utc if key == :statuses_count + attrs + end end diff --git a/app/models/account_tag_stat.rb b/app/models/account_tag_stat.rb new file mode 100644 index 0000000000..3c36c155ab --- /dev/null +++ b/app/models/account_tag_stat.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: account_tag_stats +# +# id :bigint(8) not null, primary key +# tag_id :bigint(8) not null +# accounts_count :bigint(8) default(0), not null +# hidden :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class AccountTagStat < ApplicationRecord + belongs_to :tag, inverse_of: :account_tag_stat + + def increment_count!(key) + update(key => public_send(key) + 1) + end + + def decrement_count!(key) + update(key => [public_send(key) - 1, 0].max) + end +end diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 0f7482fa6a..ae50860eda 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -49,5 +49,8 @@ module AccountAssociations # Account migrations belongs_to :moved_to_account, class_name: 'Account', optional: true + + # Hashtags + has_and_belongs_to_many :tags end end diff --git a/app/models/concerns/account_counters.rb b/app/models/concerns/account_counters.rb index fa3ec9a3da..3581df8dd8 100644 --- a/app/models/concerns/account_counters.rb +++ b/app/models/concerns/account_counters.rb @@ -16,6 +16,7 @@ module AccountCounters :followers_count=, :increment_count!, :decrement_count!, + :last_status_at, to: :account_stat def account_stat diff --git a/app/models/tag.rb b/app/models/tag.rb index 4f31f796e6..b28e2cc185 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -11,12 +11,31 @@ class Tag < ApplicationRecord has_and_belongs_to_many :statuses + has_and_belongs_to_many :accounts + + has_one :account_tag_stat, dependent: :destroy HASHTAG_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*' HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i } + scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(name: :asc) } + scope :hidden, -> { where(account_tag_stats: { hidden: true }) } + + delegate :accounts_count, + :accounts_count=, + :increment_count!, + :decrement_count!, + :hidden?, + to: :account_tag_stat + + after_save :save_account_tag_stat + + def account_tag_stat + super || build_account_tag_stat + end + def to_param name end @@ -43,4 +62,11 @@ class Tag < ApplicationRecord Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit) end end + + private + + def save_account_tag_stat + return unless account_tag_stat&.changed? + account_tag_stat.save + end end diff --git a/app/policies/tag_policy.rb b/app/policies/tag_policy.rb new file mode 100644 index 0000000000..c63de01dbe --- /dev/null +++ b/app/policies/tag_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class TagPolicy < ApplicationPolicy + def index? + staff? + end + + def hide? + staff? + end + + def unhide? + staff? + end +end diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb index ec69d944a3..36665177db 100644 --- a/app/services/update_account_service.rb +++ b/app/services/update_account_service.rb @@ -10,6 +10,7 @@ class UpdateAccountService < BaseService authorize_all_follow_requests(account) if was_locked && !account.locked check_links(account) + process_hashtags(account) end end @@ -24,4 +25,8 @@ class UpdateAccountService < BaseService def check_links(account) VerifyAccountLinksWorker.perform_async(account.id) end + + def process_hashtags(account) + account.tags_as_strings = Extractor.extract_hashtags(account.note) + end end diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml new file mode 100644 index 0000000000..961b83f93c --- /dev/null +++ b/app/views/admin/tags/_tag.html.haml @@ -0,0 +1,12 @@ +%tr + %td + = link_to explore_hashtag_path(tag) do + = fa_icon 'hashtag' + = tag.name + %td + = t('directories.people', count: tag.accounts_count) + %td + - if tag.hidden? + = table_link_to 'eye', t('admin.tags.unhide'), unhide_admin_tag_path(tag.id, **@filter_params), method: :post + - else + = table_link_to 'eye-slash', t('admin.tags.hide'), hide_admin_tag_path(tag.id, **@filter_params), method: :post diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml new file mode 100644 index 0000000000..4ba3958605 --- /dev/null +++ b/app/views/admin/tags/index.html.haml @@ -0,0 +1,19 @@ +- content_for :page_title do + = t('admin.tags.title') + +.filters + .filter-subset + %strong= t('admin.reports.status') + %ul + %li= filter_link_to t('admin.tags.visible'), hidden: nil + %li= filter_link_to t('admin.tags.hidden'), hidden: '1' + +.table-wrapper + %table.table + %thead + %tr + %th= t('admin.tags.name') + %th= t('admin.tags.accounts') + %th + %tbody + = render @tags diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml new file mode 100644 index 0000000000..7cd6b50d43 --- /dev/null +++ b/app/views/directories/index.html.haml @@ -0,0 +1,59 @@ +- content_for :page_title do + = t('directories.explore_mastodon') + +- content_for :header_tags do + %meta{ name: 'description', content: t('directories.explanation') } + + = opengraph 'og:site_name', site_title + = opengraph 'og:title', t('directories.explore_mastodon', title: site_title) + = opengraph 'og:description', t('directories.explanation') + +.page-header + %h1= t('directories.explore_mastodon', title: site_title) + %p= t('directories.explanation') + +.grid + .column-0 + .account__section-headline + = active_link_to t('directories.most_recently_active'), @tag ? explore_hashtag_path(@tag) : explore_path + = active_link_to t('directories.most_popular'), @tag ? explore_hashtag_popular_path(@tag) : explore_popular_path + + - if @accounts.empty? + = nothing_here + - else + .directory + %table.accounts-table + %tbody + - @accounts.each do |account| + %tr + %td= account_link_to account + %td.accounts-table__count + = number_to_human account.statuses_count, strip_insignificant_zeros: true + %small= t('accounts.posts', count: account.statuses_count) + %td.accounts-table__count + = number_to_human account.followers_count, strip_insignificant_zeros: true + %small= t('accounts.followers', count: account.followers_count) + %td.accounts-table__count + - if account.last_status_at.present? + %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at + - else + \- + %small= t('accounts.last_active') + + = paginate @accounts + + .column-1 + - if @tags.empty? + .nothing-here.nothing-here--flexible + - else + - @tags.each do |tag| + .directory__tag{ class: tag.id == @tag&.id ? 'active' : nil } + = link_to explore_hashtag_path(tag) do + %h4 + = fa_icon 'hashtag' + = tag.name + %small= t('directories.people', count: tag.accounts_count) + + .avatar-stack + - tag.accounts.limit(3).each do |account| + = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar' diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index e17c777d05..831c7f012e 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -8,6 +8,10 @@ .nav-left = link_to root_url, class: 'brand' do = image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' + + = link_to t('directories.directory'), explore_path, class: 'nav-link' + = link_to t('about.about_this'), about_more_path, class: 'nav-link' + = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link' .nav-center .nav-right - if user_signed_in? diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 4530ffae27..fa3869f6f9 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -18,7 +18,6 @@ = f.input :avatar, wrapper: :with_label, input_html: { accept: AccountAvatar::IMAGE_MIME_TYPES.join(',') }, hint: t('simple_form.hints.defaults.avatar', dimensions: '400x400', size: number_to_human_size(AccountAvatar::LIMIT)) - %hr.spacer/ .fields-group @@ -27,6 +26,9 @@ .fields-group = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') + .fields-group + = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable_html', min_followers: Account::MIN_FOLLOWERS_DISCOVERY, path: explore_path) + %hr.spacer/ .fields-row diff --git a/config/locales/en.yml b/config/locales/en.yml index 2d27a4ac7c..243b513fd6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,6 +48,7 @@ en: other: Followers following: Following joined: Joined %{date} + last_active: last active link_verified_on: Ownership of this link was checked on %{date} media: Media moved_html: "%{name} has moved to %{new_profile_link}:" @@ -114,6 +115,7 @@ en: media_attachments: Media attachments memorialize: Turn into memoriam moderation: + active: Active all: All silenced: Silenced suspended: Suspended @@ -439,6 +441,14 @@ en: proceed: Proceed title: Suspend %{acct} warning_html: 'Suspending this account will irreversibly delete data from this account, which includes:' + tags: + accounts: Accounts + hidden: Hidden + hide: Hide from directory + name: Hashtag + title: Hashtags + unhide: Show in directory + visible: Visible title: Administration admin_mailer: new_report: @@ -517,6 +527,15 @@ en: success_msg: Your account was successfully deleted warning_html: Only deletion of content from this particular instance is guaranteed. Content that has been widely shared is likely to leave traces. Offline servers and servers that have unsubscribed from your updates will not update their databases. warning_title: Disseminated content availability + directories: + directory: Profile directory + explanation: Discover users based on their interests + explore_mastodon: Explore %{title} + most_popular: Most popular + most_recently_active: Most recently active + people: + one: "%{count} person" + other: "%{count} people" errors: '403': You don't have permission to view this page. '404': The page you were looking for doesn't exist. diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index d34ec79cc6..e24d8f4e6a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -8,6 +8,7 @@ en: bot: This account mainly performs automated actions and might not be monitored context: One or multiple contexts where the filter should apply digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence + discoverable_html: The directory lets people find accounts based on interests and activity. Requires at least %{min_followers} followers email: You will be sent a confirmation e-mail fields: You can have up to 4 items displayed as a table on your profile header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px @@ -48,6 +49,7 @@ en: context: Filter contexts current_password: Current password data: Data + discoverable: List this account on the directory display_name: Display name email: E-mail address expires_in: Expire after diff --git a/config/navigation.rb b/config/navigation.rb index 99d227f111..1b3c05ef7a 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -28,6 +28,7 @@ SimpleNavigation::Configuration.run do |navigation| admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts} admin.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path + admin.item :tags, safe_join([fa_icon('tag fw'), t('admin.tags.title')]), admin_tags_path admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? } admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? } admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } diff --git a/config/routes.rb b/config/routes.rb index b203e13294..2628684137 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,6 +80,11 @@ Rails.application.routes.draw do get '/interact/:id', to: 'remote_interaction#new', as: :remote_interaction post '/interact/:id', to: 'remote_interaction#create' + get '/explore', to: 'directories#index', as: :explore + get '/explore/popular', to: 'directories#index', as: :explore_popular + get '/explore/:id', to: 'directories#show', as: :explore_hashtag + get '/explore/:id/popular', to: 'directories#show', as: :explore_hashtag_popular + namespace :settings do resource :profile, only: [:show, :update] resource :preferences, only: [:show, :update] @@ -207,6 +212,13 @@ Rails.application.routes.draw do end resources :account_moderation_notes, only: [:create, :destroy] + + resources :tags, only: [:index] do + member do + post :hide + post :unhide + end + end end get '/admin', to: redirect('/admin/dashboard', status: 302) diff --git a/db/migrate/20181203003808_create_accounts_tags_join_table.rb b/db/migrate/20181203003808_create_accounts_tags_join_table.rb new file mode 100644 index 0000000000..3c275c2b78 --- /dev/null +++ b/db/migrate/20181203003808_create_accounts_tags_join_table.rb @@ -0,0 +1,8 @@ +class CreateAccountsTagsJoinTable < ActiveRecord::Migration[5.2] + def change + create_join_table :accounts, :tags do |t| + t.index [:account_id, :tag_id] + t.index [:tag_id, :account_id], unique: true + end + end +end diff --git a/db/migrate/20181203021853_add_discoverable_to_accounts.rb b/db/migrate/20181203021853_add_discoverable_to_accounts.rb new file mode 100644 index 0000000000..5bbae2203e --- /dev/null +++ b/db/migrate/20181203021853_add_discoverable_to_accounts.rb @@ -0,0 +1,5 @@ +class AddDiscoverableToAccounts < ActiveRecord::Migration[5.2] + def change + add_column :accounts, :discoverable, :boolean + end +end diff --git a/db/migrate/20181204193439_add_last_status_at_to_account_stats.rb b/db/migrate/20181204193439_add_last_status_at_to_account_stats.rb new file mode 100644 index 0000000000..9466627071 --- /dev/null +++ b/db/migrate/20181204193439_add_last_status_at_to_account_stats.rb @@ -0,0 +1,5 @@ +class AddLastStatusAtToAccountStats < ActiveRecord::Migration[5.2] + def change + add_column :account_stats, :last_status_at, :datetime + end +end diff --git a/db/migrate/20181204215309_create_account_tag_stats.rb b/db/migrate/20181204215309_create_account_tag_stats.rb new file mode 100644 index 0000000000..15ed8587e3 --- /dev/null +++ b/db/migrate/20181204215309_create_account_tag_stats.rb @@ -0,0 +1,11 @@ +class CreateAccountTagStats < ActiveRecord::Migration[5.2] + def change + create_table :account_tag_stats do |t| + t.belongs_to :tag, null: false, foreign_key: { on_delete: :cascade }, index: { unique: true } + t.bigint :accounts_count, default: 0, null: false + t.boolean :hidden, default: false, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index d675214425..6d643c27ca 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2018_11_27_130500) do +ActiveRecord::Schema.define(version: 2018_12_04_215309) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -63,9 +63,19 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do t.bigint "followers_count", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "last_status_at" t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true end + create_table "account_tag_stats", force: :cascade do |t| + t.bigint "tag_id", null: false + t.bigint "accounts_count", default: 0, null: false + t.boolean "hidden", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["tag_id"], name: "index_account_tag_stats_on_tag_id", unique: true + end + create_table "accounts", force: :cascade do |t| t.string "username", default: "", null: false t.string "domain" @@ -106,6 +116,7 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do t.string "featured_collection_url" t.jsonb "fields" t.string "actor_type" + t.boolean "discoverable" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id" @@ -113,6 +124,13 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do t.index ["url"], name: "index_accounts_on_url" end + create_table "accounts_tags", id: false, force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "tag_id", null: false + t.index ["account_id", "tag_id"], name: "index_accounts_tags_on_account_id_and_tag_id" + t.index ["tag_id", "account_id"], name: "index_accounts_tags_on_tag_id_and_account_id", unique: true + end + create_table "admin_action_logs", force: :cascade do |t| t.bigint "account_id" t.string "action", default: "", null: false @@ -637,6 +655,7 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade add_foreign_key "account_pins", "accounts", on_delete: :cascade add_foreign_key "account_stats", "accounts", on_delete: :cascade + add_foreign_key "account_tag_stats", "tags", on_delete: :cascade add_foreign_key "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade add_foreign_key "backups", "users", on_delete: :nullify diff --git a/spec/fabricators/account_tag_stat_fabricator.rb b/spec/fabricators/account_tag_stat_fabricator.rb new file mode 100644 index 0000000000..9edb550bec --- /dev/null +++ b/spec/fabricators/account_tag_stat_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:account_tag_stat) do + accounts_count "" +end diff --git a/spec/models/account_tag_stat_spec.rb b/spec/models/account_tag_stat_spec.rb new file mode 100644 index 0000000000..f1ebc55783 --- /dev/null +++ b/spec/models/account_tag_stat_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AccountTagStat, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From c1c0f7c5161452ded4373e6aa1cc9f852f8836c1 Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Fri, 7 Dec 2018 12:18:37 +0900 Subject: [PATCH 02/14] Fix tag mb_chars comparison of profile directory (#9448) --- app/models/account.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index 20b0b72391..c22836afe5 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -181,8 +181,8 @@ class Account < ApplicationRecord end def tags_as_strings=(tag_names) - tag_names.map! { |name| name.mb_chars.downcase } - tag_names.uniq!(&:to_s) + tag_names.map! { |name| name.mb_chars.downcase.to_s } + tag_names.uniq! # Existing hashtags hashtags_map = Tag.where(name: tag_names).each_with_object({}) { |tag, h| h[tag.name] = tag } @@ -190,7 +190,7 @@ class Account < ApplicationRecord # Initialize not yet existing hashtags tag_names.each do |name| next if hashtags_map.key?(name) - hashtags_map[name.downcase] = Tag.new(name: name) + hashtags_map[name] = Tag.new(name: name) end # Remove hashtags that are to be deleted From ecd303c097bf20aa971628ba8420a9f17f3dd1f7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 7 Dec 2018 16:37:32 +0100 Subject: [PATCH 03/14] Fix various things in the directory (#9449) * Fix missing variable in directory page title * Order hashtags by number of people instead of alphabetically * Add icon to OpenGraph preview of directory page * Prevent line breaks in hashtags and ensure lowercase in the table --- app/javascript/styles/mastodon/widgets.scss | 3 +++ app/models/tag.rb | 2 +- app/views/directories/index.html.haml | 10 ++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index a838ca778b..c863e3b4fe 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -300,6 +300,9 @@ font-size: 18px; font-weight: 700; color: $primary-text-color; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; .fa { color: $darker-text-color; diff --git a/app/models/tag.rb b/app/models/tag.rb index b28e2cc185..41e58e3ca1 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -20,7 +20,7 @@ class Tag < ApplicationRecord validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i } - scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(name: :asc) } + scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order('account_tag_stats.accounts_count desc') } scope :hidden, -> { where(account_tag_stats: { hidden: true }) } delegate :accounts_count, diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml index 7cd6b50d43..219950a518 100644 --- a/app/views/directories/index.html.haml +++ b/app/views/directories/index.html.haml @@ -1,12 +1,14 @@ - content_for :page_title do - = t('directories.explore_mastodon') + = t('directories.explore_mastodon', title: site_title) - content_for :header_tags do %meta{ name: 'description', content: t('directories.explanation') } - = opengraph 'og:site_name', site_title + = opengraph 'og:site_name', t('about.hosted_on', domain: site_hostname) + = opengraph 'og:type', 'website' = opengraph 'og:title', t('directories.explore_mastodon', title: site_title) = opengraph 'og:description', t('directories.explanation') + = opengraph 'og:image', File.join(root_url, 'android-chrome-192x192.png') .page-header %h1= t('directories.explore_mastodon', title: site_title) @@ -29,10 +31,10 @@ %td= account_link_to account %td.accounts-table__count = number_to_human account.statuses_count, strip_insignificant_zeros: true - %small= t('accounts.posts', count: account.statuses_count) + %small= t('accounts.posts', count: account.statuses_count).downcase %td.accounts-table__count = number_to_human account.followers_count, strip_insignificant_zeros: true - %small= t('accounts.followers', count: account.followers_count) + %small= t('accounts.followers', count: account.followers_count).downcase %td.accounts-table__count - if account.last_status_at.present? %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at From 51cbd045dab531725b448311ea52d0aec215a8f8 Mon Sep 17 00:00:00 2001 From: ysksn Date: Sat, 8 Dec 2018 00:37:56 +0900 Subject: [PATCH 04/14] Add specs for AccountTagStat model (#9452) --- spec/models/account_tag_stat_spec.rb | 35 +++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/spec/models/account_tag_stat_spec.rb b/spec/models/account_tag_stat_spec.rb index f1ebc55783..6d3057f355 100644 --- a/spec/models/account_tag_stat_spec.rb +++ b/spec/models/account_tag_stat_spec.rb @@ -1,5 +1,38 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe AccountTagStat, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + key = 'accounts_count' + let(:account_tag_stat) { Fabricate(:tag).account_tag_stat } + + describe '#increment_count!' do + it 'calls #update' do + args = { key => account_tag_stat.public_send(key) + 1 } + expect(account_tag_stat).to receive(:update).with(args) + account_tag_stat.increment_count!(key) + end + + it 'increments value by 1' do + expect do + account_tag_stat.increment_count!(key) + end.to change { account_tag_stat.accounts_count }.by(1) + end + end + + describe '#decrement_count!' do + it 'calls #update' do + args = { key => [account_tag_stat.public_send(key) - 1, 0].max } + expect(account_tag_stat).to receive(:update).with(args) + account_tag_stat.decrement_count!(key) + end + + it 'decrements value by 1' do + account_tag_stat.update(key => 1) + + expect do + account_tag_stat.decrement_count!(key) + end.to change { account_tag_stat.accounts_count }.by(-1) + end + end end From 57bb62d5cfe1b269ccfc82c0d2fd73fbf1252a3d Mon Sep 17 00:00:00 2001 From: ysksn Date: Sat, 8 Dec 2018 00:38:50 +0900 Subject: [PATCH 05/14] Remove pending spec (#9454) Since dots are not allowed in username, this spec is no longer needed. --- spec/validators/unique_username_validator_spec.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/spec/validators/unique_username_validator_spec.rb b/spec/validators/unique_username_validator_spec.rb index b9d773bed3..c2e2eedf41 100644 --- a/spec/validators/unique_username_validator_spec.rb +++ b/spec/validators/unique_username_validator_spec.rb @@ -15,14 +15,6 @@ describe UniqueUsernameValidator do expect(account).to be_valid end - it 'adds an error when the username is already used with ignoring dots' do - pending 'allowing dots in username is still in development' - Fabricate(:account, username: 'abcd.ef') - account = double(username: 'ab.cdef', persisted?: false, errors: double(add: nil)) - subject.validate(account) - expect(account.errors).to have_received(:add) - end - it 'adds an error when the username is already used with ignoring cases' do Fabricate(:account, username: 'ABCdef') account = double(username: 'abcDEF', persisted?: false, errors: double(add: nil)) From 88b3eed16f9ecabfc81b81d1af3a2e10b0f68517 Mon Sep 17 00:00:00 2001 From: ysksn Date: Sat, 8 Dec 2018 00:39:20 +0900 Subject: [PATCH 06/14] Add specs for Admin::AccountModerationNotesHelper (#9455) --- .../account_moderation_notes_helper_spec.rb | 62 +++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb index 01b60c8516..c07f6c4b88 100644 --- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -1,15 +1,55 @@ +# frozen_string_literal: true + require 'rails_helper' -# Specs in this file have access to a helper object that includes -# the Admin::AccountModerationNotesHelper. For example: -# -# describe Admin::AccountModerationNotesHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" + include StreamEntriesHelper + + describe '#admin_account_link_to' do + context 'account is nil' do + let(:account) { nil } + + it 'returns nil' do + expect(helper.admin_account_link_to(account)).to be_nil + end + end + + context 'with account' do + let(:account) { Fabricate(:account) } + + it 'calls #link_to' do + expect(helper).to receive(:link_to).with( + admin_account_path(account.id), + class: name_tag_classes(account), + title: account.acct + ) + + helper.admin_account_link_to(account) + end + end + end + + describe '#admin_account_inline_link_to' do + context 'account is nil' do + let(:account) { nil } + + it 'returns nil' do + expect(helper.admin_account_inline_link_to(account)).to be_nil + end + end + + context 'with account' do + let(:account) { Fabricate(:account) } + + it 'calls #link_to' do + expect(helper).to receive(:link_to).with( + admin_account_path(account.id), + class: name_tag_classes(account, true), + title: account.acct + ) + + helper.admin_account_inline_link_to(account) + end + end + end end From d3547fa00580a03d1687316d56c32f407c0d9fe6 Mon Sep 17 00:00:00 2001 From: ysksn Date: Sat, 8 Dec 2018 00:40:01 +0900 Subject: [PATCH 07/14] Add specs for ActivityPub::InboxesController (#9456) --- .../activitypub/inboxes_controller_spec.rb | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb index 5c12fea7df..4055d93424 100644 --- a/spec/controllers/activitypub/inboxes_controller_spec.rb +++ b/spec/controllers/activitypub/inboxes_controller_spec.rb @@ -1,7 +1,29 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe ActivityPub::InboxesController, type: :controller do describe 'POST #create' do - pending + context 'if signed_request_account' do + it 'returns 202' do + allow(controller).to receive(:signed_request_account) do + Fabricate(:account) + end + + post :create + expect(response).to have_http_status(202) + end + end + + context 'not signed_request_account' do + it 'returns 401' do + allow(controller).to receive(:signed_request_account) do + false + end + + post :create + expect(response).to have_http_status(401) + end + end end end From 5c7f641565e8022c3d8d704e49b510a79e5f16ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Ngei?= Date: Fri, 7 Dec 2018 16:42:22 +0100 Subject: [PATCH 08/14] Escape HTML in profile name preview in profile settings (#9446) * fix non-escaped html in the profile settings * provide a default profile text in case if there's no custom one * update haml syntax * simplify default profile name to username * sanitize user-input html but display emojified icons --- app/javascript/packs/public.js | 8 ++++++-- app/views/application/_card.html.haml | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 36b1fd26b8..6ba37c049f 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -1,3 +1,4 @@ +import escapeTextContentForBrowser from 'escape-html'; import loadPolyfills from '../mastodon/load_polyfills'; import ready from '../mastodon/ready'; import { start } from '../mastodon/common'; @@ -133,9 +134,12 @@ function main() { delegate(document, '#account_display_name', 'input', ({ target }) => { const name = document.querySelector('.card .display-name strong'); - if (name) { - name.innerHTML = emojify(target.value); + if (target.value) { + name.innerHTML = emojify(escapeTextContentForBrowser(target.value)); + } else { + name.textContent = document.querySelector('#default_account_display_name').textContent; + } } }); diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index 9cf8f8ff2b..e6059b0350 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -9,6 +9,7 @@ = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' .display-name + %span{id: "default_account_display_name", style: "display:none;"}= account.username %bdi %strong.emojify.p-name= display_name(account, custom_emojify: true) %span From 1c7061fb90403bac96922ae35ecc0182c01e2db5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Fri, 7 Dec 2018 16:42:50 +0100 Subject: [PATCH 09/14] Bump rubocop from 0.61.0 to 0.61.1 (#9451) Bumps [rubocop](https://github.com/rubocop-hq/rubocop) from 0.61.0 to 0.61.1. - [Release notes](https://github.com/rubocop-hq/rubocop/releases) - [Changelog](https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop-hq/rubocop/compare/v0.61.0...v0.61.1) Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5ed69a88ee..6756bb5cde 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -525,7 +525,7 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.8.0) - rubocop (0.61.0) + rubocop (0.61.1) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) From dfd123d5b3f2bef19833cff8beb3ba2edb17c68a Mon Sep 17 00:00:00 2001 From: ysksn Date: Sat, 8 Dec 2018 00:53:55 +0900 Subject: [PATCH 10/14] Remove pending spec (#9453) --- spec/models/account_pin_spec.rb | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 spec/models/account_pin_spec.rb diff --git a/spec/models/account_pin_spec.rb b/spec/models/account_pin_spec.rb deleted file mode 100644 index 4f226b1277..0000000000 --- a/spec/models/account_pin_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe AccountPin, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end From 6cf12aa74cf4bd50f14aaca78bd545bcb3d8155d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Miko=C5=82ajczak?= Date: Fri, 7 Dec 2018 21:08:29 +0100 Subject: [PATCH 11/14] i18n: Update Polish translation (#9459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- config/locales/pl.yml | 21 +++++++++++++++++++++ config/locales/simple_form.pl.yml | 2 ++ 2 files changed, 23 insertions(+) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 6b274227bb..ef3b4b4d2a 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -54,6 +54,7 @@ pl: other: Śledzących following: Śledzonych joined: Dołączył(a) %{date} + last_active: ostatnio aktywny(-a) link_verified_on: Własność tego odnośnika została sprawdzona %{date} media: Zawartość multimedialna moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:" @@ -122,6 +123,7 @@ pl: media_attachments: Załączniki multimedialne memorialize: Przełącz na „In Memoriam” moderation: + active: Aktywne all: Wszystkie silenced: Wyciszone suspended: Zawieszone @@ -445,6 +447,14 @@ pl: proceed: Przejdź title: Zawieś %{acct} warning_html: 'Zawieszenie konta będzie skutkowało nieodwracalnym usunięciem danych z tego konta, wliczając:' + tags: + accounts: Konta + hidden: Ukryte + hide: Ukryj w katalogu + name: Hashtag + title: Hashtagi + unhide: Pokazuj w katalogu + visible: Widoczne title: Administracja admin_mailer: new_report: @@ -523,6 +533,17 @@ pl: success_msg: Twoje konto zostało pomyślnie usunięte warning_html: Możemy usunąć zawartość jedynie w obrębie tej instancji. Zawartość udostępniona publicznie pozostawia trwałe ślady. Serwery niepodłączone do sieci bądź nieśledzące Twoich aktualizacji mogą zachować Twoje dane. warning_title: Dostępność usuniętej zawartości + directories: + directory: Katalog profilów + explanation: Poznaj profile na podstawie zainteresowań + explore_mastodon: Odkrywaj %{title} + most_popular: Napopularniejsi + most_recently_active: Ostatnio aktywni + people: + few: "%{count} osoby" + many: "%{count} osób" + one: "%{count} osoba" + other: "%{count} osób" errors: '403': Nie masz uprawnień, aby wyświetlić tę stronę. '404': Strona, którą próbujesz odwiedzić, nie istnieje. diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 8febad4886..87b265e38a 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -8,6 +8,7 @@ pl: bot: To konto wykonuje głównie zautomatyzowane działania i może nie być monitorowane context: Jedno lub wiele miejsc, w których filtr zostanie zastosowany digest: Wysyłane tylko po długiej nieaktywności, jeżeli w tym czasie otrzymaleś jakąś wiadomość bezpośrednią + discoverable_html: Katalog pozwala znaleźć konta na podstawie zainteresowań i aktywności. Profil musi śledzić przynajmniej %{min_followers} osób fields: Możesz ustawić maksymalnie 4 niestandardowe pola wyświetlane jako tabela na Twoim profilu header: PNG, GIF lub JPG. Maksymalnie %{size}. Zostanie zmniejszony do %{dimensions}px inbox_url: Skopiuj adres ze strony głównej przekaźnika, którego chcesz użyć @@ -42,6 +43,7 @@ pl: context: Filtruj zawartość current_password: Obecne hasło data: Dane + discoverable: Wyświetlaj ten profil w katalogu display_name: Widoczna nazwa email: Adres e-mail expires_in: Wygaśnie po From 4638b1682247df52ab5b22d06cd2a3df3277b2f9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Dec 2018 00:40:11 +0100 Subject: [PATCH 12/14] Make notification time smaller and reduce contrast (#9464) --- app/javascript/styles/mastodon/components.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b2e9bd1df4..c880e99a9f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -876,7 +876,8 @@ } } -.status__relative-time { +.status__relative-time, +.notification__relative_time { color: $dark-text-color; float: right; font-size: 14px; From 6b78e5b5ab813625152149b7e215d0568a595cdc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Dec 2018 01:32:26 +0100 Subject: [PATCH 13/14] Cache hashtag sample accounts, and exclude ineligible ones (#9465) --- app/models/account.rb | 2 +- app/models/tag.rb | 7 ++++++- app/views/directories/index.html.haml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/models/account.rb b/app/models/account.rb index c22836afe5..9767e37675 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -94,7 +94,7 @@ class Account < ApplicationRecord scope :discoverable, -> { where(silenced: false).where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) } scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) } scope :popular, -> { order('account_stats.followers_count desc') } - scope :by_recent_status, -> { order('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc') } + scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) } delegate :email, :unconfirmed_email, diff --git a/app/models/tag.rb b/app/models/tag.rb index 41e58e3ca1..99830ae92c 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -12,6 +12,7 @@ class Tag < ApplicationRecord has_and_belongs_to_many :statuses has_and_belongs_to_many :accounts + has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account' has_one :account_tag_stat, dependent: :destroy @@ -20,7 +21,7 @@ class Tag < ApplicationRecord validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/i } - scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order('account_tag_stats.accounts_count desc') } + 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 }) } delegate :accounts_count, @@ -36,6 +37,10 @@ class Tag < ApplicationRecord super || build_account_tag_stat end + def cached_sample_accounts + Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { sample_accounts } + end + def to_param name end diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml index 219950a518..f70eb964a6 100644 --- a/app/views/directories/index.html.haml +++ b/app/views/directories/index.html.haml @@ -57,5 +57,5 @@ %small= t('directories.people', count: tag.accounts_count) .avatar-stack - - tag.accounts.limit(3).each do |account| + - tag.cached_sample_accounts.each do |account| = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar' From 81bda7d67c984c9bfcb5bca94e50cec6405b492e Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 9 Dec 2018 13:03:01 +0100 Subject: [PATCH 14/14] Add setting to not aggregate reblogs (#9248) * Add setting to not aggregate reblogs Fixes #9222 * Handle cases where user is nil in add_to_home and add_to_list * Add hint for setting_aggregate_reblogs option * Reword setting_aggregate_reblogs label --- app/controllers/settings/preferences_controller.rb | 1 + app/lib/feed_manager.rb | 12 ++++++------ app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 6 +++++- app/views/settings/preferences/show.html.haml | 3 +++ config/locales/simple_form.en.yml | 2 ++ config/settings.yml | 1 + 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 7bb5fb112a..70e71b4a24 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -48,6 +48,7 @@ class Settings::PreferencesController < ApplicationController :setting_noindex, :setting_theme, :setting_hide_network, + :setting_aggregate_reblogs, notification_emails: %i(follow follow_request reblog favourite mention digest report), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 31ff53860b..f99df33e54 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -27,7 +27,7 @@ class FeedManager end def push_to_home(account, status) - return false unless add_to_feed(:home, account.id, 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}") true @@ -45,7 +45,7 @@ class FeedManager should_filter &&= !ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists? return false if should_filter end - return false unless add_to_feed(:list, list.id, status) + return false unless add_to_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?) trim(:list, list.id) PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}") true @@ -93,7 +93,7 @@ class FeedManager query.each do |status| next if status.direct_visibility? || status.limited_visibility? || filter?(:home, status, into_account) - add_to_feed(:home, into_account.id, status) + add_to_feed(:home, into_account.id, status, into_account.user&.aggregates_reblogs?) end trim(:home, into_account.id) @@ -131,7 +131,7 @@ class FeedManager statuses.each do |status| next if filter_from_home?(status, account) - added += 1 if add_to_feed(:home, account.id, status) + added += 1 if add_to_feed(:home, account.id, status, account.user&.aggregates_reblogs?) end break unless added.zero? @@ -230,11 +230,11 @@ class FeedManager # added, and false if it was not added to the feed. Note that this is # an internal helper: callers must call trim or push updates if # either action is appropriate. - def add_to_feed(timeline_type, account_id, status) + 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') - if status.reblog? + if status.reblog? && (aggregate_reblogs.nil? || aggregate_reblogs) # If the original status or a reblog of it is within # REBLOG_FALLOFF statuses from the top, do not re-insert it into # the feed diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 40973c7079..19b8544103 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -31,6 +31,7 @@ class UserSettingsDecorator user.settings['noindex'] = noindex_preference if change?('setting_noindex') user.settings['theme'] = theme_preference if change?('setting_theme') user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') + user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') end def merged_notification_emails @@ -97,6 +98,10 @@ class UserSettingsDecorator settings['setting_default_language'] end + def aggregate_reblogs_preference + boolean_cast_setting 'setting_aggregate_reblogs' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 453ffa8b07..f4130d7b1c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -95,7 +95,7 @@ class User < ApplicationRecord delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, - :expand_spoilers, :default_language, to: :settings, prefix: :setting, allow_nil: false + :expand_spoilers, :default_language, :aggregate_reblogs, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code @@ -231,6 +231,10 @@ class User < ApplicationRecord @hides_network ||= settings.hide_network end + def aggregates_reblogs? + @aggregates_reblogs ||= settings.aggregate_reblogs + end + def token_for_app(a) return nil if a.nil? || a.owner != self Doorkeeper::AccessToken diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index ecb789f93c..a2c61c9a68 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -47,6 +47,9 @@ = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label = f.input :setting_delete_modal, as: :boolean, wrapper: :with_label + .fields-group + = f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label + .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label = f.input :setting_expand_spoilers, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index e24d8f4e6a..ce6a62e870 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -19,6 +19,7 @@ en: password: Use at least 8 characters phrase: Will be matched regardless of casing in text or content warning of a toot scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. + setting_aggregate_reblogs: Do not show new boosts for toots that have been recently boosted (only affects newly-received boosts) setting_default_language: The language of your toots can be detected automatically, but it's not always accurate setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide all media @@ -65,6 +66,7 @@ en: otp_attempt: Two-factor code password: Password phrase: Keyword or phrase + setting_aggregate_reblogs: Group boosts in timelines setting_auto_play_gif: Auto-play animated GIFs setting_boost_modal: Show confirmation dialog before boosting setting_default_language: Posting language diff --git a/config/settings.yml b/config/settings.yml index 2bc9fe289d..4036d419fe 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -33,6 +33,7 @@ defaults: &defaults system_font_ui: false noindex: false theme: 'default' + aggregate_reblogs: true notification_emails: follow: false reblog: false