Merge pull request #849 from ThibG/glitch-soc/merge-upstream

Merge upstream changes
This commit is contained in:
ThibG 2018-12-09 16:38:09 +01:00 committed by GitHub
commit c6b7b98489
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 929 additions and 43 deletions

View File

@ -527,7 +527,7 @@ GEM
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.8.0) rspec-support (3.8.0)
rubocop (0.61.0) rubocop (0.61.1)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1) parser (>= 2.5, != 2.5.1.1)

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,53 @@
# 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
before_action :set_pack
def index
render :index
end
def show
render :index
end
private
def set_pack
use_pack 'share'
end
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

@ -43,6 +43,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_system_font_ui, :setting_system_font_ui,
:setting_noindex, :setting_noindex,
:setting_hide_network, :setting_hide_network,
:setting_aggregate_reblogs,
notification_emails: %i(follow follow_request reblog favourite mention digest report), notification_emails: %i(follow follow_request reblog favourite mention digest report),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)
) )

View File

@ -25,7 +25,7 @@ class Settings::ProfilesController < Settings::BaseController
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

@ -1,13 +1,17 @@
// This file will be loaded on settings pages, regardless of theme. // This file will be loaded on settings pages, regardless of theme.
import escapeTextContentForBrowser from 'escape-html';
const { delegate } = require('rails-ujs'); const { delegate } = require('rails-ujs');
import emojify from '../mastodon/features/emoji/emoji'; import emojify from '../mastodon/features/emoji/emoji';
delegate(document, '#account_display_name', 'input', ({ target }) => { delegate(document, '#account_display_name', 'input', ({ target }) => {
const name = document.querySelector('.card .display-name strong'); const name = document.querySelector('.card .display-name strong');
if (name) { 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;
}
} }
}); });

View File

@ -191,6 +191,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,171 @@
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;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.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

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

@ -876,7 +876,8 @@
} }
} }
.status__relative-time { .status__relative-time,
.notification__relative_time {
color: $dark-text-color; color: $dark-text-color;
float: right; float: right;
font-size: 14px; font-size: 14px;

View File

@ -240,3 +240,171 @@
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;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.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

@ -27,7 +27,7 @@ class FeedManager
end end
def push_to_home(account, status) 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) trim(:home, account.id)
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}") PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}") if push_update_required?("timeline:#{account.id}")
true true
@ -46,7 +46,7 @@ class FeedManager
should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?)
return false if should_filter return false if should_filter
end 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) trim(:list, list.id)
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}") PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}") if push_update_required?("timeline:list:#{list.id}")
true true
@ -94,7 +94,7 @@ class FeedManager
query.each do |status| query.each do |status|
next if status.direct_visibility? || status.limited_visibility? || filter?(:home, status, into_account) 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 end
trim(:home, into_account.id) trim(:home, into_account.id)
@ -132,7 +132,7 @@ class FeedManager
statuses.each do |status| statuses.each do |status|
next if filter_from_home?(status, account) 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 end
break unless added.zero? break unless added.zero?
@ -231,11 +231,11 @@ class FeedManager
# added, and false if it was not added to the feed. Note that this is # 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 # an internal helper: callers must call trim or push updates if
# either action is appropriate. # 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) timeline_key = key(timeline_type, account_id)
reblog_key = key(timeline_type, account_id, 'reblogs') 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 # If the original status or a reblog of it is within
# REBLOG_FALLOFF statuses from the top, do not re-insert it into # REBLOG_FALLOFF statuses from the top, do not re-insert it into
# the feed # the feed

View File

@ -33,6 +33,7 @@ class UserSettingsDecorator
user.settings['flavour'] = flavour_preference if change?('setting_flavour') user.settings['flavour'] = flavour_preference if change?('setting_flavour')
user.settings['skin'] = skin_preference if change?('setting_skin') user.settings['skin'] = skin_preference if change?('setting_skin')
user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') 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 end
def merged_notification_emails def merged_notification_emails
@ -107,6 +108,10 @@ class UserSettingsDecorator
settings['setting_default_language'] settings['setting_default_language']
end end
def aggregate_reblogs_preference
boolean_cast_setting 'setting_aggregate_reblogs'
end
def boolean_cast_setting(key) def boolean_cast_setting(key)
ActiveModel::Type::Boolean.new.cast(settings[key]) ActiveModel::Type::Boolean.new.cast(settings[key])
end end

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
@ -93,6 +95,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(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, delegate :email,
:unconfirmed_email, :unconfirmed_email,
@ -178,6 +184,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.to_s }
tag_names.uniq!
# 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] = 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

@ -50,5 +50,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,36 @@
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_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'
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(Arel.sql('account_tag_stats.accounts_count desc')) }
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 cached_sample_accounts
Rails.cache.fetch("#{cache_key}/sample_accounts", expires_in: 12.hours) { sample_accounts }
end
def to_param def to_param
name name
end end
@ -43,4 +67,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

@ -95,7 +95,7 @@ class User < ApplicationRecord
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :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 attr_reader :invite_code
@ -231,6 +231,10 @@ class User < ApplicationRecord
@hides_network ||= settings.hide_network @hides_network ||= settings.hide_network
end end
def aggregates_reblogs?
@aggregates_reblogs ||= settings.aggregate_reblogs
end
def token_for_app(a) def token_for_app(a)
return nil if a.nil? || a.owner != self return nil if a.nil? || a.owner != self
Doorkeeper::AccessToken Doorkeeper::AccessToken

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

@ -9,6 +9,7 @@
= image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
.display-name .display-name
%span{id: "default_account_display_name", style: "display:none;"}= account.username
%bdi %bdi
%strong.emojify.p-name= display_name(account, custom_emojify: true) %strong.emojify.p-name= display_name(account, custom_emojify: true)
%span %span

View File

@ -0,0 +1,61 @@
- content_for :page_title do
= t('directories.explore_mastodon', title: site_title)
- content_for :header_tags do
%meta{ name: 'description', content: t('directories.explanation') }
= 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)
%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).downcase
%td.accounts-table__count
= number_to_human account.followers_count, strip_insignificant_zeros: true
%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
- 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.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'

View File

@ -5,6 +5,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

@ -42,6 +42,9 @@
= f.input :setting_favourite_modal, as: :boolean, wrapper: :with_label = f.input :setting_favourite_modal, as: :boolean, wrapper: :with_label
= f.input :setting_delete_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 .fields-group
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
= f.input :setting_display_media, collection: ['default', 'show_all', 'hide_all'], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_display_media_#{item}"), content_tag(:span, t("simple_form.hints.defaults.setting_display_media_#{item}"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' = f.input :setting_display_media, collection: ['default', 'show_all', 'hide_all'], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_display_media_#{item}"), content_tag(:span, t("simple_form.hints.defaults.setting_display_media_#{item}"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'

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

@ -54,6 +54,7 @@ pl:
other: Śledzących other: Śledzących
following: Śledzonych following: Śledzonych
joined: Dołączył(a) %{date} joined: Dołączył(a) %{date}
last_active: ostatnio aktywny(-a)
link_verified_on: Własność tego odnośnika została sprawdzona %{date} link_verified_on: Własność tego odnośnika została sprawdzona %{date}
media: Zawartość multimedialna media: Zawartość multimedialna
moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:" moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:"
@ -122,6 +123,7 @@ pl:
media_attachments: Załączniki multimedialne media_attachments: Załączniki multimedialne
memorialize: Przełącz na „In Memoriam” memorialize: Przełącz na „In Memoriam”
moderation: moderation:
active: Aktywne
all: Wszystkie all: Wszystkie
silenced: Wyciszone silenced: Wyciszone
suspended: Zawieszone suspended: Zawieszone
@ -445,6 +447,14 @@ pl:
proceed: Przejdź proceed: Przejdź
title: Zawieś %{acct} title: Zawieś %{acct}
warning_html: 'Zawieszenie konta będzie skutkowało <strong>nieodwracalnym</strong> usunięciem danych z tego konta, wliczając:' warning_html: 'Zawieszenie konta będzie skutkowało <strong>nieodwracalnym</strong> 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 title: Administracja
admin_mailer: admin_mailer:
new_report: new_report:
@ -523,6 +533,17 @@ pl:
success_msg: Twoje konto zostało pomyślnie usunięte 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_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 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: errors:
'403': Nie masz uprawnień, aby wyświetlić tę stronę. '403': Nie masz uprawnień, aby wyświetlić tę stronę.
'404': Strona, którą próbujesz odwiedzić, nie istnieje. '404': Strona, którą próbujesz odwiedzić, nie istnieje.

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
@ -18,6 +19,7 @@ en:
password: Use at least 8 characters password: Use at least 8 characters
phrase: Will be matched regardless of casing in text or content warning of a toot 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. 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_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_default: Hide media marked as sensitive
setting_display_media_hide_all: Always hide all media setting_display_media_hide_all: Always hide all media
@ -48,6 +50,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
@ -63,6 +66,7 @@ en:
otp_attempt: Two-factor code otp_attempt: Two-factor code
password: Password password: Password
phrase: Keyword or phrase phrase: Keyword or phrase
setting_aggregate_reblogs: Group boosts in timelines
setting_auto_play_gif: Auto-play animated GIFs setting_auto_play_gif: Auto-play animated GIFs
setting_boost_modal: Show confirmation dialog before boosting setting_boost_modal: Show confirmation dialog before boosting
setting_default_language: Posting language setting_default_language: Posting language

View File

@ -8,6 +8,7 @@ pl:
bot: To konto wykonuje głównie zautomatyzowane działania i może nie być monitorowane 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 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ą digest: Wysyłane tylko po długiej nieaktywności, jeżeli w tym czasie otrzymaleś jakąś wiadomość bezpośrednią
discoverable_html: <a href="%{path}" target="_blank">Katalog</a> 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 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 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ć inbox_url: Skopiuj adres ze strony głównej przekaźnika, którego chcesz użyć
@ -42,6 +43,7 @@ pl:
context: Filtruj zawartość context: Filtruj zawartość
current_password: Obecne hasło current_password: Obecne hasło
data: Dane data: Dane
discoverable: Wyświetlaj ten profil w katalogu
display_name: Widoczna nazwa display_name: Widoczna nazwa
email: Adres e-mail email: Adres e-mail
expires_in: Wygaśnie po expires_in: Wygaśnie po

View File

@ -34,6 +34,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]
@ -210,6 +215,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

@ -35,6 +35,7 @@ defaults: &defaults
noindex: false noindex: false
flavour: 'glitch' flavour: 'glitch'
skin: 'default' skin: 'default'
aggregate_reblogs: true
notification_emails: notification_emails:
follow: false follow: false
reblog: false reblog: false

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_165847) 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_165847) 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_165847) 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_165847) 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
@ -649,6 +667,7 @@ ActiveRecord::Schema.define(version: 2018_11_27_165847) 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

@ -1,7 +1,29 @@
# frozen_string_literal: true
require 'rails_helper' require 'rails_helper'
RSpec.describe ActivityPub::InboxesController, type: :controller do RSpec.describe ActivityPub::InboxesController, type: :controller do
describe 'POST #create' 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
end end

View File

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

View File

@ -1,15 +1,55 @@
# frozen_string_literal: true
require 'rails_helper' 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 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 end

View File

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

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AccountTagStat, type: :model do
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

View File

@ -15,14 +15,6 @@ describe UniqueUsernameValidator do
expect(account).to be_valid expect(account).to be_valid
end 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 it 'adds an error when the username is already used with ignoring cases' do
Fabricate(:account, username: 'ABCdef') Fabricate(:account, username: 'ABCdef')
account = double(username: 'abcDEF', persisted?: false, errors: double(add: nil)) account = double(username: 'abcDEF', persisted?: false, errors: double(add: nil))