parent
6d73528424
commit
fd8ca11f19
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -189,6 +189,11 @@
|
|||
&--under-tabs {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
&--flexible {
|
||||
box-sizing: border-box;
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.account-role {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -16,6 +16,7 @@ module AccountCounters
|
|||
:followers_count=,
|
||||
:increment_count!,
|
||||
:decrement_count!,
|
||||
:last_status_at,
|
||||
to: :account_stat
|
||||
|
||||
def account_stat
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TagPolicy < ApplicationPolicy
|
||||
def index?
|
||||
staff?
|
||||
end
|
||||
|
||||
def hide?
|
||||
staff?
|
||||
end
|
||||
|
||||
def unhide?
|
||||
staff?
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <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
|
||||
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.
|
||||
|
|
|
@ -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 <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
|
||||
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
|
||||
|
|
|
@ -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? }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
class AddDiscoverableToAccounts < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :accounts, :discoverable, :boolean
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
class AddLastStatusAtToAccountStats < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :account_stats, :last_status_at, :datetime
|
||||
end
|
||||
end
|
|
@ -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
|
21
db/schema.rb
21
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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Fabricator(:account_tag_stat) do
|
||||
accounts_count ""
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountTagStat, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
Loading…
Reference in New Issue