Merge remote-tracking branch 'tootsuite/master' into glitchsoc/master

This commit is contained in:
Jenkins 2018-02-19 15:17:14 +00:00
commit 1445ba1703
25 changed files with 193 additions and 60 deletions

View File

@ -19,7 +19,6 @@ env:
- LOCAL_HTTPS=true - LOCAL_HTTPS=true
- RAILS_ENV=test - RAILS_ENV=test
- PARALLEL_TEST_PROCESSORS=2 - PARALLEL_TEST_PROCESSORS=2
- "PATH=$HOME:$PATH"
addons: addons:
postgresql: 9.4 postgresql: 9.4
@ -49,7 +48,6 @@ install:
before_script: before_script:
- ./bin/rails parallel:create parallel:load_schema parallel:prepare assets:precompile - ./bin/rails parallel:create parallel:load_schema parallel:prepare assets:precompile
- ln -s /usr/bin/x86_64-linux-gnu-g++-6 "$HOME/g++"
script: script:
- travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec - travis_retry bundle exec parallel_test spec/ --group-by filesize --type rspec

View File

@ -20,6 +20,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
private private
def account_params def account_params
params.permit(:display_name, :note, :avatar, :header) params.permit(:display_name, :note, :avatar, :header, :locked)
end end
end end

View File

@ -1,4 +1,5 @@
import api from '../api'; import api from '../api';
import { fetchRelationships } from './accounts';
export const SEARCH_CHANGE = 'SEARCH_CHANGE'; export const SEARCH_CHANGE = 'SEARCH_CHANGE';
export const SEARCH_CLEAR = 'SEARCH_CLEAR'; export const SEARCH_CLEAR = 'SEARCH_CLEAR';
@ -38,6 +39,7 @@ export function submitSearch() {
}, },
}).then(response => { }).then(response => {
dispatch(fetchSearchSuccess(response.data)); dispatch(fetchSearchSuccess(response.data));
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
}).catch(error => { }).catch(error => {
dispatch(fetchSearchFail(error)); dispatch(fetchSearchFail(error));
}); });

View File

@ -227,12 +227,8 @@ export default class MediaGallery extends React.PureComponent {
const style = {}; const style = {};
if (this.isStandaloneEligible()) { if (this.isStandaloneEligible()) {
if (!visible && width) { if (width) {
// only need to forcibly set the height in "sensitive" mode
style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
} else {
// layout automatically, using image's natural aspect ratio
style.height = '';
} }
} else { } else {
// crop the image // crop the image

View File

@ -122,5 +122,6 @@ button {
height: 100%; height: 100%;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
outline: 0 !important;
} }
} }

View File

@ -686,12 +686,13 @@
background: transparent; background: transparent;
border: 0; border: 0;
color: lighten($ui-base-color, 8%); color: lighten($ui-base-color, 8%);
font-weight: 500; font-weight: 700;
font-size: 11px; font-size: 11px;
padding: 0 6px; padding: 0 6px;
text-transform: uppercase; text-transform: uppercase;
line-height: inherit; line-height: 20px;
cursor: pointer; cursor: pointer;
vertical-align: middle;
} }
.status__prepend-icon-wrapper { .status__prepend-icon-wrapper {
@ -899,6 +900,11 @@
height: 24px; height: 24px;
margin: -1px 0 0; margin: -1px 0 0;
} }
.status__content__spoiler-link {
line-height: 24px;
margin: -1px 0 0;
}
} }
.video-player { .video-player {
@ -2667,12 +2673,16 @@ a.status-card {
background: $base-overlay-background; background: $base-overlay-background;
color: $ui-primary-color; color: $ui-primary-color;
border: 0; border: 0;
padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 4px;
appearance: none;
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
padding: 0;
color: lighten($ui-primary-color, 8%); color: lighten($ui-primary-color, 8%);
} }
} }
@ -2685,7 +2695,7 @@ a.status-card {
.media-spoiler__trigger { .media-spoiler__trigger {
display: block; display: block;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 700;
} }
.spoiler-button { .spoiler-button {
@ -4091,6 +4101,7 @@ a.status-card {
box-sizing: border-box; box-sizing: border-box;
margin-top: 8px; margin-top: 8px;
overflow: hidden; overflow: hidden;
border-radius: 4px;
position: relative; position: relative;
width: 100%; width: 100%;
} }
@ -4101,6 +4112,8 @@ a.status-card {
display: block; display: block;
float: left; float: left;
position: relative; position: relative;
border-radius: 4px;
overflow: hidden;
&.standalone { &.standalone {
.media-gallery__item-gifv-thumbnail { .media-gallery__item-gifv-thumbnail {
@ -4113,6 +4126,7 @@ a.status-card {
cursor: zoom-in; cursor: zoom-in;
display: block; display: block;
text-decoration: none; text-decoration: none;
color: $ui-secondary-color;
height: 100%; height: 100%;
line-height: 0; line-height: 0;

View File

@ -146,10 +146,10 @@
a.status__content__spoiler-link { a.status__content__spoiler-link {
color: $primary-text-color; color: $primary-text-color;
background: $ui-primary-color; background: $ui-base-color;
&:hover { &:hover {
background: lighten($ui-primary-color, 8%); background: lighten($ui-base-color, 8%);
} }
} }
} }
@ -214,10 +214,10 @@
a.status__content__spoiler-link { a.status__content__spoiler-link {
color: $primary-text-color; color: $primary-text-color;
background: $ui-primary-color; background: $ui-base-color;
&:hover { &:hover {
background: lighten($ui-primary-color, 8%); background: lighten($ui-base-color, 8%);
} }
} }
} }
@ -260,16 +260,8 @@
} }
.media-spoiler { .media-spoiler {
background: $ui-primary-color; background: $ui-base-color;
color: $white; color: $ui-primary-color;
transition: all 40ms linear;
&:hover,
&:active,
&:focus {
background: darken($ui-primary-color, 2%);
color: unset;
}
} }
.pre-header { .pre-header {

View File

@ -74,7 +74,7 @@ class ActivityPub::Activity
# Only continue if the status is supposed to have # Only continue if the status is supposed to have
# arrived in real-time # arrived in real-time
return unless @options[:override_timestamps] return unless @options[:override_timestamps] || status.within_realtime_window?
distribute_to_followers(status) distribute_to_followers(status)
end end

View File

@ -61,7 +61,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution"
LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
DistributionWorker.perform_async(status.id) if @options[:override_timestamps] DistributionWorker.perform_async(status.id) if @options[:override_timestamps] || status.within_realtime_window?
status status
end end

View File

@ -16,12 +16,16 @@ class AccountDomainBlock < ApplicationRecord
belongs_to :account belongs_to :account
validates :domain, presence: true, uniqueness: { scope: :account_id } validates :domain, presence: true, uniqueness: { scope: :account_id }
after_create :remove_blocking_cache after_commit :remove_blocking_cache
after_destroy :remove_blocking_cache after_commit :remove_relationship_cache
private private
def remove_blocking_cache def remove_blocking_cache
Rails.cache.delete("exclude_domains_for:#{account_id}") Rails.cache.delete("exclude_domains_for:#{account_id}")
end end
def remove_relationship_cache
Rails.cache.delete_matched("relationship:#{account_id}:*")
end
end end

View File

@ -12,14 +12,14 @@
class Block < ApplicationRecord class Block < ApplicationRecord
include Paginable include Paginable
include RelationshipCacheable
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
after_create :remove_blocking_cache after_commit :remove_blocking_cache
after_destroy :remove_blocking_cache
private private

View File

@ -7,9 +7,15 @@ module AccountAvatar
class_methods do class_methods do
def avatar_styles(file) def avatar_styles(file)
styles = { original: '120x120#' } styles = {}
styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif' geometry = Paperclip::Geometry.from_file(file)
styles[:original] = '120x120#' if geometry.width != geometry.height || geometry.width > 120 || geometry.height > 120
styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
styles styles
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
{}
end end
private :avatar_styles private :avatar_styles
@ -17,7 +23,7 @@ module AccountAvatar
included do included do
# Avatar upload # Avatar upload
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-quality 80 -strip' } has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-strip' }
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
validates_attachment_size :avatar, less_than: 2.megabytes validates_attachment_size :avatar, less_than: 2.megabytes
end end

View File

@ -7,9 +7,15 @@ module AccountHeader
class_methods do class_methods do
def header_styles(file) def header_styles(file)
styles = { original: '700x335#' } styles = {}
styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif' geometry = Paperclip::Geometry.from_file(file)
styles[:original] = '700x335#' unless geometry.width == 700 && geometry.height == 335
styles[:static] = { format: 'png', convert_options: '-coalesce' } if file.content_type == 'image/gif'
styles styles
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
{}
end end
private :header_styles private :header_styles
@ -17,7 +23,7 @@ module AccountHeader
included do included do
# Header upload # Header upload
has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-quality 80 -strip' } has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-strip' }
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
validates_attachment_size :header, less_than: 2.megabytes validates_attachment_size :header, less_than: 2.megabytes
end end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module RelationshipCacheable
extend ActiveSupport::Concern
included do
after_commit :remove_relationship_cache
end
private
def remove_relationship_cache
Rails.cache.delete("relationship:#{account_id}:#{target_account_id}")
Rails.cache.delete("relationship:#{target_account_id}:#{account_id}")
end
end

View File

@ -13,6 +13,7 @@
class Follow < ApplicationRecord class Follow < ApplicationRecord
include Paginable include Paginable
include RelationshipCacheable
belongs_to :account, counter_cache: :following_count belongs_to :account, counter_cache: :following_count

View File

@ -13,6 +13,7 @@
class FollowRequest < ApplicationRecord class FollowRequest < ApplicationRecord
include Paginable include Paginable
include RelationshipCacheable
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'

View File

@ -13,14 +13,14 @@
class Mute < ApplicationRecord class Mute < ApplicationRecord
include Paginable include Paginable
include RelationshipCacheable
belongs_to :account belongs_to :account
belongs_to :target_account, class_name: 'Account' belongs_to :target_account, class_name: 'Account'
validates :account_id, uniqueness: { scope: :target_account_id } validates :account_id, uniqueness: { scope: :target_account_id }
after_create :remove_blocking_cache after_commit :remove_blocking_cache
after_destroy :remove_blocking_cache
private private

View File

@ -83,6 +83,8 @@ class Status < ApplicationRecord
delegate :domain, to: :account, prefix: true delegate :domain, to: :account, prefix: true
REAL_TIME_WINDOW = 6.hours
def searchable_by(preloaded = nil) def searchable_by(preloaded = nil)
ids = [account_id] ids = [account_id]
@ -111,6 +113,10 @@ class Status < ApplicationRecord
!reblog_of_id.nil? !reblog_of_id.nil?
end end
def within_realtime_window?
created_at >= REAL_TIME_WINDOW.ago
end
def verb def verb
if destroyed? if destroyed?
:delete :delete

View File

@ -5,11 +5,67 @@ class AccountRelationshipsPresenter
:muting, :requested, :domain_blocking :muting, :requested, :domain_blocking
def initialize(account_ids, current_account_id, **options) def initialize(account_ids, current_account_id, **options)
@following = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {}) @account_ids = account_ids.map { |a| a.is_a?(Account) ? a.id : a }
@followed_by = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {}) @current_account_id = current_account_id
@blocking = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {})
@muting = Account.muting_map(account_ids, current_account_id).merge(options[:muting_map] || {}) @following = cached[:following].merge(Account.following_map(@uncached_account_ids, @current_account_id))
@requested = Account.requested_map(account_ids, current_account_id).merge(options[:requested_map] || {}) @followed_by = cached[:followed_by].merge(Account.followed_by_map(@uncached_account_ids, @current_account_id))
@domain_blocking = Account.domain_blocking_map(account_ids, current_account_id).merge(options[:domain_blocking_map] || {}) @blocking = cached[:blocking].merge(Account.blocking_map(@uncached_account_ids, @current_account_id))
@muting = cached[:muting].merge(Account.muting_map(@uncached_account_ids, @current_account_id))
@requested = cached[:requested].merge(Account.requested_map(@uncached_account_ids, @current_account_id))
@domain_blocking = cached[:domain_blocking].merge(Account.domain_blocking_map(@uncached_account_ids, @current_account_id))
cache_uncached!
@following.merge!(options[:following_map] || {})
@followed_by.merge!(options[:followed_by_map] || {})
@blocking.merge!(options[:blocking_map] || {})
@muting.merge!(options[:muting_map] || {})
@requested.merge!(options[:requested_map] || {})
@domain_blocking.merge!(options[:domain_blocking_map] || {})
end
private
def cached
return @cached if defined?(@cached)
@cached = {
following: {},
followed_by: {},
blocking: {},
muting: {},
requested: {},
domain_blocking: {},
}
@uncached_account_ids = []
@account_ids.each do |account_id|
maps_for_account = Rails.cache.read("relationship:#{@current_account_id}:#{account_id}")
if maps_for_account.is_a?(Hash)
@cached.merge!(maps_for_account)
else
@uncached_account_ids << account_id
end
end
@cached
end
def cache_uncached!
@uncached_account_ids.each do |account_id|
maps_for_account = {
following: { account_id => following[account_id] },
followed_by: { account_id => followed_by[account_id] },
blocking: { account_id => blocking[account_id] },
muting: { account_id => muting[account_id] },
requested: { account_id => requested[account_id] },
domain_blocking: { account_id => domain_blocking[account_id] },
}
Rails.cache.write("relationship:#{@current_account_id}:#{account_id}", maps_for_account, expires_in: 1.day)
end
end end
end end

View File

@ -23,6 +23,10 @@ Rails.application.configure do
config.consider_all_requests_local = true config.consider_all_requests_local = true
config.action_controller.perform_caching = false config.action_controller.perform_caching = false
# The default store, file_store is shared by processses parallely executed
# and should not be used.
config.cache_store = :memory_store
# Raise exceptions instead of rendering exception templates. # Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false config.action_dispatch.show_exceptions = false

View File

@ -8,7 +8,7 @@ RSpec.describe NotificationMailer, type: :mailer do
shared_examples 'localized subject' do |*args, **kwrest| shared_examples 'localized subject' do |*args, **kwrest|
it 'renders subject localized for the locale of the receiver' do it 'renders subject localized for the locale of the receiver' do
locale = I18n.available_locales.sample locale = %i(de en).sample
receiver.update!(locale: locale) receiver.update!(locale: locale)
expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: locale)) expect(mail.subject).to eq I18n.t(*args, kwrest.merge(locale: locale))
end end

View File

@ -42,11 +42,6 @@ RSpec.describe Setting, type: :model do
described_class[key] described_class[key]
end end
it 'calls Rails.cache.fetch' do
expect(Rails).to receive_message_chain(:cache, :fetch).with(cache_key)
described_class[key]
end
context 'Rails.cache does not exists' do context 'Rails.cache does not exists' do
before do before do
allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object) allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object)
@ -103,6 +98,14 @@ RSpec.describe Setting, type: :model do
Rails.cache.write(cache_key, cache_value) Rails.cache.write(cache_key, cache_value)
end end
it 'does not query the database' do
expect do |callback|
ActiveSupport::Notifications.subscribed callback, 'sql.active_record' do
described_class[key]
end
end.not_to yield_control
end
it 'returns the cached value' do it 'returns the cached value' do
expect(described_class[key]).to eq cache_value expect(described_class[key]).to eq cache_value
end end

View File

@ -90,9 +90,7 @@ describe InstancePresenter do
describe "user_count" do describe "user_count" do
it "returns the number of site users" do it "returns the number of site users" do
cache = double Rails.cache.write 'user_count', 123
allow(Rails).to receive(:cache).and_return(cache)
allow(cache).to receive(:fetch).with("user_count").and_return(123)
expect(instance_presenter.user_count).to eq(123) expect(instance_presenter.user_count).to eq(123)
end end
@ -100,9 +98,7 @@ describe InstancePresenter do
describe "status_count" do describe "status_count" do
it "returns the number of local statuses" do it "returns the number of local statuses" do
cache = double Rails.cache.write 'local_status_count', 234
allow(Rails).to receive(:cache).and_return(cache)
allow(cache).to receive(:fetch).with("local_status_count").and_return(234)
expect(instance_presenter.status_count).to eq(234) expect(instance_presenter.status_count).to eq(234)
end end
@ -110,9 +106,7 @@ describe InstancePresenter do
describe "domain_count" do describe "domain_count" do
it "returns the number of known domains" do it "returns the number of known domains" do
cache = double Rails.cache.write 'distinct_domain_count', 345
allow(Rails).to receive(:cache).and_return(cache)
allow(cache).to receive(:fetch).with("distinct_domain_count").and_return(345)
expect(instance_presenter.domain_count).to eq(345) expect(instance_presenter.domain_count).to eq(345)
end end

View File

@ -51,6 +51,8 @@ RSpec.configure do |config|
end end
config.after :each do config.after :each do
Rails.cache.clear
keys = Redis.current.keys keys = Redis.current.keys
Redis.current.del(keys) if keys.any? Redis.current.del(keys) if keys.any?
end end

View File

@ -1,4 +1,35 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe FetchRemoteStatusService do RSpec.describe FetchRemoteStatusService do
let(:account) { Fabricate(:account) }
let(:prefetched_body) { nil }
let(:valid_domain) { Rails.configuration.x.local_domain }
let(:note) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
id: "https://#{valid_domain}/@foo/1234",
type: 'Note',
content: 'Lorem ipsum',
attributedTo: ActivityPub::TagManager.instance.uri_for(account),
}
end
context 'protocol is :activitypub' do
subject { described_class.new.call(note[:id], prefetched_body, protocol) }
let(:prefetched_body) { Oj.dump(note) }
let(:protocol) { :activitypub }
before do
account.update(uri: ActivityPub::TagManager.instance.uri_for(account))
subject
end
it 'creates status' do
status = account.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
end
end
end end