Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- app/javascript/packs/public.js
- app/models/user.rb
- config/settings.yml
- db/schema.rb

Moved public.js changes to settings.js.
This commit is contained in:
Thibaut Girka 2018-12-09 13:28:09 +01:00
commit e7f1bfdc2d
47 changed files with 751 additions and 43 deletions

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -93,6 +95,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(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
delegate :email,
:unconfirmed_email,
@ -178,6 +184,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.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
(self[:fields] || []).map { |f| Field.new(self, f) }
end

View File

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

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
belongs_to :moved_to_account, class_name: 'Account', optional: true
# Hashtags
has_and_belongs_to_many :tags
end
end

View File

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

View File

@ -11,12 +11,36 @@
class Tag < ApplicationRecord
has_and_belongs_to_many :statuses
has_and_belongs_to_many :accounts
has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account'
has_one :account_tag_stat, dependent: :destroy
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(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
name
end
@ -43,4 +67,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

View File

@ -95,7 +95,7 @@ class User < ApplicationRecord
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,
:expand_spoilers, :default_language, to: :settings, prefix: :setting, allow_nil: false
:expand_spoilers, :default_language, :aggregate_reblogs, to: :settings, prefix: :setting, allow_nil: false
attr_reader :invite_code
@ -231,6 +231,10 @@ class User < ApplicationRecord
@hides_network ||= settings.hide_network
end
def aggregates_reblogs?
@aggregates_reblogs ||= settings.aggregate_reblogs
end
def token_for_app(a)
return nil if a.nil? || a.owner != self
Doorkeeper::AccessToken

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

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'
.display-name
%span{id: "default_account_display_name", style: "display:none;"}= account.username
%bdi
%strong.emojify.p-name= display_name(account, custom_emojify: true)
%span

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
= 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?

View File

@ -42,6 +42,9 @@
= f.input :setting_favourite_modal, as: :boolean, wrapper: :with_label
= f.input :setting_delete_modal, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label
.fields-group
= f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label
= f.input :setting_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))
%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

View File

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

View File

@ -54,6 +54,7 @@ pl:
other: Śledzących
following: Śledzonych
joined: Dołączył(a) %{date}
last_active: ostatnio aktywny(-a)
link_verified_on: Własność tego odnośnika została sprawdzona %{date}
media: Zawartość multimedialna
moved_html: "%{name} korzysta teraz z konta %{new_profile_link}:"
@ -122,6 +123,7 @@ pl:
media_attachments: Załączniki multimedialne
memorialize: Przełącz na „In Memoriam”
moderation:
active: Aktywne
all: Wszystkie
silenced: Wyciszone
suspended: Zawieszone
@ -445,6 +447,14 @@ pl:
proceed: Przejdź
title: Zawieś %{acct}
warning_html: 'Zawieszenie konta będzie skutkowało <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
admin_mailer:
new_report:
@ -523,6 +533,17 @@ pl:
success_msg: Twoje konto zostało pomyślnie usunięte
warning_html: Możemy usunąć zawartość jedynie w obrębie tej instancji. Zawartość udostępniona publicznie pozostawia trwałe ślady. Serwery niepodłączone do sieci bądź nieśledzące Twoich aktualizacji mogą zachować Twoje dane.
warning_title: Dostępność usuniętej zawartości
directories:
directory: Katalog profilów
explanation: Poznaj profile na podstawie zainteresowań
explore_mastodon: Odkrywaj %{title}
most_popular: Napopularniejsi
most_recently_active: Ostatnio aktywni
people:
few: "%{count} osoby"
many: "%{count} osób"
one: "%{count} osoba"
other: "%{count} osób"
errors:
'403': Nie masz uprawnień, aby wyświetlić tę stronę.
'404': Strona, którą próbujesz odwiedzić, nie istnieje.

View File

@ -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
@ -18,6 +19,7 @@ en:
password: Use at least 8 characters
phrase: Will be matched regardless of casing in text or content warning of a toot
scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones.
setting_aggregate_reblogs: Do not show new boosts for toots that have been recently boosted (only affects newly-received boosts)
setting_default_language: The language of your toots can be detected automatically, but it's not always accurate
setting_display_media_default: Hide media marked as sensitive
setting_display_media_hide_all: Always hide all media
@ -48,6 +50,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
@ -63,6 +66,7 @@ en:
otp_attempt: Two-factor code
password: Password
phrase: Keyword or phrase
setting_aggregate_reblogs: Group boosts in timelines
setting_auto_play_gif: Auto-play animated GIFs
setting_boost_modal: Show confirmation dialog before boosting
setting_default_language: Posting language

View File

@ -8,6 +8,7 @@ pl:
bot: To konto wykonuje głównie zautomatyzowane działania i może nie być monitorowane
context: Jedno lub wiele miejsc, w których filtr zostanie zastosowany
digest: Wysyłane tylko po długiej nieaktywności, jeżeli w tym czasie otrzymaleś jakąś wiadomość bezpośrednią
discoverable_html: <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
header: PNG, GIF lub JPG. Maksymalnie %{size}. Zostanie zmniejszony do %{dimensions}px
inbox_url: Skopiuj adres ze strony głównej przekaźnika, którego chcesz użyć
@ -42,6 +43,7 @@ pl:
context: Filtruj zawartość
current_password: Obecne hasło
data: Dane
discoverable: Wyświetlaj ten profil w katalogu
display_name: Widoczna nazwa
email: Adres e-mail
expires_in: Wygaśnie po

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

View File

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

View File

@ -35,6 +35,7 @@ defaults: &defaults
noindex: false
flavour: 'glitch'
skin: 'default'
aggregate_reblogs: true
notification_emails:
follow: 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.
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
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.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_165847) 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_165847) 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
@ -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", 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

View File

@ -1,7 +1,29 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ActivityPub::InboxesController, type: :controller do
describe 'POST #create' do
pending
context 'if signed_request_account' do
it 'returns 202' do
allow(controller).to receive(:signed_request_account) do
Fabricate(:account)
end
post :create
expect(response).to have_http_status(202)
end
end
context 'not signed_request_account' do
it 'returns 401' do
allow(controller).to receive(:signed_request_account) do
false
end
post :create
expect(response).to have_http_status(401)
end
end
end
end

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'
# Specs in this file have access to a helper object that includes
# the Admin::AccountModerationNotesHelper. For example:
#
# describe Admin::AccountModerationNotesHelper do
# describe "string concat" do
# it "concats two strings with spaces" do
# expect(helper.concat_strings("this","that")).to eq("this that")
# end
# end
# end
RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do
pending "add some examples to (or delete) #{__FILE__}"
include StreamEntriesHelper
describe '#admin_account_link_to' do
context 'account is nil' do
let(:account) { nil }
it 'returns nil' do
expect(helper.admin_account_link_to(account)).to be_nil
end
end
context 'with account' do
let(:account) { Fabricate(:account) }
it 'calls #link_to' do
expect(helper).to receive(:link_to).with(
admin_account_path(account.id),
class: name_tag_classes(account),
title: account.acct
)
helper.admin_account_link_to(account)
end
end
end
describe '#admin_account_inline_link_to' do
context 'account is nil' do
let(:account) { nil }
it 'returns nil' do
expect(helper.admin_account_inline_link_to(account)).to be_nil
end
end
context 'with account' do
let(:account) { Fabricate(:account) }
it 'calls #link_to' do
expect(helper).to receive(:link_to).with(
admin_account_path(account.id),
class: name_tag_classes(account, true),
title: account.acct
)
helper.admin_account_inline_link_to(account)
end
end
end
end

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
end
it 'adds an error when the username is already used with ignoring dots' do
pending 'allowing dots in username is still in development'
Fabricate(:account, username: 'abcd.ef')
account = double(username: 'ab.cdef', persisted?: false, errors: double(add: nil))
subject.validate(account)
expect(account.errors).to have_received(:add)
end
it 'adds an error when the username is already used with ignoring cases' do
Fabricate(:account, username: 'ABCdef')
account = double(username: 'abcDEF', persisted?: false, errors: double(add: nil))