Add profile directory (#9427)

Fix #5578
This commit is contained in:
Eugen Rochko 2018-12-06 17:36:11 +01:00 committed by GitHub
parent 155cf12680
commit 73be8f38c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 578 additions and 7 deletions

View File

@ -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

View File

@ -21,7 +21,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
private private
def account_params 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 end
def user_settings_params def user_settings_params

View File

@ -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

View File

@ -29,7 +29,7 @@ class Settings::ProfilesController < ApplicationController
private private
def account_params 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 end
def set_account def set_account

View File

@ -5,8 +5,9 @@ module Admin::FilterHelper
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).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) def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params) new_url = filtered_url_for(link_to_params)

View File

@ -189,6 +189,11 @@
&--under-tabs { &--under-tabs {
border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
} }
&--flexible {
box-sizing: border-box;
min-height: 100%;
}
} }
.account-role { .account-role {

View File

@ -240,3 +240,168 @@
border-radius: 0; 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;
}
}
}

View File

@ -43,11 +43,13 @@
# featured_collection_url :string # featured_collection_url :string
# fields :jsonb # fields :jsonb
# actor_type :string # actor_type :string
# discoverable :boolean
# #
class Account < ApplicationRecord class Account < ApplicationRecord
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[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 AccountAssociations
include AccountAvatar include AccountAvatar
@ -89,6 +91,10 @@ class Account < ApplicationRecord
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :searchable, -> { where(suspended: false).where(moved_to_account_id: nil) } 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, delegate :email,
:unconfirmed_email, :unconfirmed_email,
@ -174,6 +180,40 @@ class Account < ApplicationRecord
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key) @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
end 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 def fields
(self[:fields] || []).map { |f| Field.new(self, f) } (self[:fields] || []).map { |f| Field.new(self, f) }
end end

View File

@ -1,5 +1,4 @@
# frozen_string_literal: true # frozen_string_literal: true
# == Schema Information # == Schema Information
# #
# Table name: account_stats # Table name: account_stats
@ -11,16 +10,25 @@
# followers_count :bigint(8) default(0), not null # followers_count :bigint(8) default(0), not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# last_status_at :datetime
# #
class AccountStat < ApplicationRecord class AccountStat < ApplicationRecord
belongs_to :account, inverse_of: :account_stat belongs_to :account, inverse_of: :account_stat
def increment_count!(key) def increment_count!(key)
update(key => public_send(key) + 1) update(attributes_for_increment(key))
end end
def decrement_count!(key) def decrement_count!(key)
update(key => [public_send(key) - 1, 0].max) update(key => [public_send(key) - 1, 0].max)
end 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 end

View File

@ -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

View File

@ -49,5 +49,8 @@ module AccountAssociations
# Account migrations # Account migrations
belongs_to :moved_to_account, class_name: 'Account', optional: true belongs_to :moved_to_account, class_name: 'Account', optional: true
# Hashtags
has_and_belongs_to_many :tags
end end
end end

View File

@ -16,6 +16,7 @@ module AccountCounters
:followers_count=, :followers_count=,
:increment_count!, :increment_count!,
:decrement_count!, :decrement_count!,
:last_status_at,
to: :account_stat to: :account_stat
def account_stat def account_stat

View File

@ -11,12 +11,31 @@
class Tag < ApplicationRecord class Tag < ApplicationRecord
has_and_belongs_to_many :statuses 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_NAME_RE = '[[:word:]_]*[[:alpha:]_·][[:word:]_]*'
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i
validates :name, presence: true, uniqueness: true, format: { with: /\A#{HASHTAG_NAME_RE}\z/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 def to_param
name name
end end
@ -43,4 +62,11 @@ class Tag < ApplicationRecord
Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit) Tag.where('lower(name) like lower(?)', pattern).order(:name).limit(limit)
end end
end end
private
def save_account_tag_stat
return unless account_tag_stat&.changed?
account_tag_stat.save
end
end end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class TagPolicy < ApplicationPolicy
def index?
staff?
end
def hide?
staff?
end
def unhide?
staff?
end
end

View File

@ -10,6 +10,7 @@ class UpdateAccountService < BaseService
authorize_all_follow_requests(account) if was_locked && !account.locked authorize_all_follow_requests(account) if was_locked && !account.locked
check_links(account) check_links(account)
process_hashtags(account)
end end
end end
@ -24,4 +25,8 @@ class UpdateAccountService < BaseService
def check_links(account) def check_links(account)
VerifyAccountLinksWorker.perform_async(account.id) VerifyAccountLinksWorker.perform_async(account.id)
end end
def process_hashtags(account)
account.tags_as_strings = Extractor.extract_hashtags(account.note)
end
end end

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -8,6 +8,10 @@
.nav-left .nav-left
= link_to root_url, class: 'brand' do = link_to root_url, class: 'brand' do
= image_tag asset_pack_path('logo_full.svg'), alt: 'Mastodon' = 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-center
.nav-right .nav-right
- if user_signed_in? - if user_signed_in?

View File

@ -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)) = 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/ %hr.spacer/
.fields-group .fields-group
@ -27,6 +26,9 @@
.fields-group .fields-group
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') = 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/ %hr.spacer/
.fields-row .fields-row

View File

@ -48,6 +48,7 @@ en:
other: Followers other: Followers
following: Following following: Following
joined: Joined %{date} joined: Joined %{date}
last_active: last active
link_verified_on: Ownership of this link was checked on %{date} link_verified_on: Ownership of this link was checked on %{date}
media: Media media: Media
moved_html: "%{name} has moved to %{new_profile_link}:" moved_html: "%{name} has moved to %{new_profile_link}:"
@ -114,6 +115,7 @@ en:
media_attachments: Media attachments media_attachments: Media attachments
memorialize: Turn into memoriam memorialize: Turn into memoriam
moderation: moderation:
active: Active
all: All all: All
silenced: Silenced silenced: Silenced
suspended: Suspended suspended: Suspended
@ -439,6 +441,14 @@ en:
proceed: Proceed proceed: Proceed
title: Suspend %{acct} title: Suspend %{acct}
warning_html: 'Suspending this account will <strong>irreversibly</strong> delete data from this account, which includes:' warning_html: 'Suspending this account will <strong>irreversibly</strong> 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 title: Administration
admin_mailer: admin_mailer:
new_report: new_report:
@ -517,6 +527,15 @@ en:
success_msg: Your account was successfully deleted 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_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 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: errors:
'403': You don't have permission to view this page. '403': You don't have permission to view this page.
'404': The page you were looking for doesn't exist. '404': The page you were looking for doesn't exist.

View File

@ -8,6 +8,7 @@ en:
bot: This account mainly performs automated actions and might not be monitored bot: This account mainly performs automated actions and might not be monitored
context: One or multiple contexts where the filter should apply 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 digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
discoverable_html: The <a href="%{path}" target="_blank">directory</a> lets people find accounts based on interests and activity. Requires at least %{min_followers} followers
email: You will be sent a confirmation e-mail email: You will be sent a confirmation e-mail
fields: You can have up to 4 items displayed as a table on your profile 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 header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
@ -48,6 +49,7 @@ en:
context: Filter contexts context: Filter contexts
current_password: Current password current_password: Current password
data: Data data: Data
discoverable: List this account on the directory
display_name: Display name display_name: Display name
email: E-mail address email: E-mail address
expires_in: Expire after expires_in: Expire after

View File

@ -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 :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 :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 :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 :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 :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? } 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? }

View File

@ -80,6 +80,11 @@ Rails.application.routes.draw do
get '/interact/:id', to: 'remote_interaction#new', as: :remote_interaction get '/interact/:id', to: 'remote_interaction#new', as: :remote_interaction
post '/interact/:id', to: 'remote_interaction#create' 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 namespace :settings do
resource :profile, only: [:show, :update] resource :profile, only: [:show, :update]
resource :preferences, only: [:show, :update] resource :preferences, only: [:show, :update]
@ -207,6 +212,13 @@ Rails.application.routes.draw do
end end
resources :account_moderation_notes, only: [:create, :destroy] resources :account_moderation_notes, only: [:create, :destroy]
resources :tags, only: [:index] do
member do
post :hide
post :unhide
end
end
end end
get '/admin', to: redirect('/admin/dashboard', status: 302) get '/admin', to: redirect('/admin/dashboard', status: 302)

View File

@ -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

View File

@ -0,0 +1,5 @@
class AddDiscoverableToAccounts < ActiveRecord::Migration[5.2]
def change
add_column :accounts, :discoverable, :boolean
end
end

View File

@ -0,0 +1,5 @@
class AddLastStatusAtToAccountStats < ActiveRecord::Migration[5.2]
def change
add_column :account_stats, :last_status_at, :datetime
end
end

View File

@ -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

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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.bigint "followers_count", default: 0, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_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 t.index ["account_id"], name: "index_account_stats_on_account_id", unique: true
end 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| create_table "accounts", force: :cascade do |t|
t.string "username", default: "", null: false t.string "username", default: "", null: false
t.string "domain" t.string "domain"
@ -106,6 +116,7 @@ ActiveRecord::Schema.define(version: 2018_11_27_130500) do
t.string "featured_collection_url" t.string "featured_collection_url"
t.jsonb "fields" t.jsonb "fields"
t.string "actor_type" 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 "(((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 "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" 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" t.index ["url"], name: "index_accounts_on_url"
end 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| create_table "admin_action_logs", force: :cascade do |t|
t.bigint "account_id" t.bigint "account_id"
t.string "action", default: "", null: false 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", column: "target_account_id", on_delete: :cascade
add_foreign_key "account_pins", "accounts", 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_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 "accounts", "accounts", column: "moved_to_account_id", on_delete: :nullify
add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade add_foreign_key "admin_action_logs", "accounts", on_delete: :cascade
add_foreign_key "backups", "users", on_delete: :nullify add_foreign_key "backups", "users", on_delete: :nullify

View File

@ -0,0 +1,3 @@
Fabricator(:account_tag_stat) do
accounts_count ""
end

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe AccountTagStat, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end