mirror of
https://framagit.org/tykayn/mastodon.git
synced 2023-08-25 08:33:12 +02:00
Merge pull request #1476 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
92cfcf168c
1
Gemfile
1
Gemfile
@ -82,6 +82,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
|||||||
gem 'rqrcode', '~> 1.1'
|
gem 'rqrcode', '~> 1.1'
|
||||||
gem 'ruby-progressbar', '~> 1.10'
|
gem 'ruby-progressbar', '~> 1.10'
|
||||||
gem 'sanitize', '~> 5.2'
|
gem 'sanitize', '~> 5.2'
|
||||||
|
gem 'scenic', '~> 1.5'
|
||||||
gem 'sidekiq', '~> 6.1'
|
gem 'sidekiq', '~> 6.1'
|
||||||
gem 'sidekiq-scheduler', '~> 3.0'
|
gem 'sidekiq-scheduler', '~> 3.0'
|
||||||
gem 'sidekiq-unique-jobs', '~> 6.0'
|
gem 'sidekiq-unique-jobs', '~> 6.0'
|
||||||
|
@ -562,6 +562,9 @@ GEM
|
|||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.8.0)
|
nokogiri (>= 1.8.0)
|
||||||
nokogumbo (~> 2.0)
|
nokogumbo (~> 2.0)
|
||||||
|
scenic (1.5.4)
|
||||||
|
activerecord (>= 4.0.0)
|
||||||
|
railties (>= 4.0.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (2.3.0)
|
semantic_range (2.3.0)
|
||||||
sidekiq (6.1.2)
|
sidekiq (6.1.2)
|
||||||
@ -784,6 +787,7 @@ DEPENDENCIES
|
|||||||
rubocop-rails (~> 2.8)
|
rubocop-rails (~> 2.8)
|
||||||
ruby-progressbar (~> 1.10)
|
ruby-progressbar (~> 1.10)
|
||||||
sanitize (~> 5.2)
|
sanitize (~> 5.2)
|
||||||
|
scenic (~> 1.5)
|
||||||
sidekiq (~> 6.1)
|
sidekiq (~> 6.1)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 3.0)
|
sidekiq-scheduler (~> 3.0)
|
||||||
|
@ -29,6 +29,7 @@ module Admin
|
|||||||
@domain_block = existing_domain_block
|
@domain_block = existing_domain_block
|
||||||
@domain_block.update(resource_params)
|
@domain_block.update(resource_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @domain_block.save
|
if @domain_block.save
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
log_action :create, @domain_block
|
log_action :create, @domain_block
|
||||||
@ -40,7 +41,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :update?
|
||||||
|
|
||||||
@domain_block.update(update_params)
|
@domain_block.update(update_params)
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ module Admin
|
|||||||
|
|
||||||
if @domain_block.save
|
if @domain_block.save
|
||||||
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
|
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
|
||||||
log_action :create, @domain_block
|
log_action :update, @domain_block
|
||||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||||
else
|
else
|
||||||
render :edit
|
render :edit
|
||||||
|
@ -2,65 +2,31 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class InstancesController < BaseController
|
class InstancesController < BaseController
|
||||||
before_action :set_domain_block, only: :show
|
before_action :set_instances, only: :index
|
||||||
before_action :set_domain_allow, only: :show
|
|
||||||
before_action :set_instance, only: :show
|
before_action :set_instance, only: :show
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :instance, :index?
|
authorize :instance, :index?
|
||||||
|
|
||||||
@instances = ordered_instances
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize :instance, :show?
|
authorize :instance, :show?
|
||||||
|
|
||||||
@following_count = Follow.where(account: Account.where(domain: params[:id])).count
|
|
||||||
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
|
|
||||||
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
|
|
||||||
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
|
|
||||||
@available = DeliveryFailureTracker.available?(params[:id])
|
|
||||||
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
|
|
||||||
@private_comment = @domain_block&.private_comment
|
|
||||||
@public_comment = @domain_block&.public_comment
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_domain_block
|
|
||||||
@domain_block = DomainBlock.rule_for(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_domain_allow
|
|
||||||
@domain_allow = DomainAllow.rule_for(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_instance
|
def set_instance
|
||||||
resource = Account.by_domain_accounts.find_by(domain: params[:id])
|
@instance = Instance.find(params[:id])
|
||||||
resource ||= @domain_block
|
|
||||||
resource ||= @domain_allow
|
|
||||||
|
|
||||||
if resource
|
|
||||||
@instance = Instance.new(resource)
|
|
||||||
else
|
|
||||||
not_found
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_instances
|
||||||
|
@instances = filtered_instances.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_instances
|
def filtered_instances
|
||||||
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
|
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated_instances
|
|
||||||
filtered_instances.page(params[:page])
|
|
||||||
end
|
|
||||||
|
|
||||||
helper_method :paginated_instances
|
|
||||||
|
|
||||||
def ordered_instances
|
|
||||||
paginated_instances.map { |resource| Instance.new(resource) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS)
|
params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
@ -40,7 +40,7 @@ class Api::BaseController < ApplicationController
|
|||||||
render json: { error: 'This action is not allowed' }, status: 403
|
render json: { error: 'This action is not allowed' }, status: 403
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from Mastodon::RaceConditionError do
|
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight do
|
||||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ class Api::V1::Instances::PeersController < Api::BaseController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
expires_in 1.day, public: true
|
expires_in 1.day, public: true
|
||||||
render_with_cache(expires_in: 1.day) { Account.remote.domains }
|
render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -29,7 +29,7 @@ class ApplicationController < ActionController::Base
|
|||||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||||
rescue_from Mastodon::RaceConditionError, with: :service_unavailable
|
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, with: :service_unavailable
|
||||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||||
|
|
||||||
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
||||||
|
@ -92,22 +92,6 @@ module StatusesHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rtl_status?(status)
|
|
||||||
status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text))
|
|
||||||
end
|
|
||||||
|
|
||||||
def rtl?(text)
|
|
||||||
text = simplified_text(text)
|
|
||||||
rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m)
|
|
||||||
|
|
||||||
if rtl_words.present?
|
|
||||||
total_size = text.size.to_f
|
|
||||||
rtl_size(rtl_words) / total_size > 0.3
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fa_visibility_icon(status)
|
def fa_visibility_icon(status)
|
||||||
case status.visibility
|
case status.visibility
|
||||||
when 'public'
|
when 'public'
|
||||||
@ -143,10 +127,6 @@ module StatusesHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rtl_size(words)
|
|
||||||
words.reduce(0) { |acc, elem| acc + elem.size }.to_f
|
|
||||||
end
|
|
||||||
|
|
||||||
def embedded_view?
|
def embedded_view?
|
||||||
params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION
|
params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION
|
||||||
end
|
end
|
||||||
|
@ -59,18 +59,46 @@ const onEnableBootstrapTimelineAccountsChange = (target) => {
|
|||||||
bootstrapTimelineAccountsField.disabled = !target.checked;
|
bootstrapTimelineAccountsField.disabled = !target.checked;
|
||||||
if (target.checked) {
|
if (target.checked) {
|
||||||
bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
|
bootstrapTimelineAccountsField.parentElement.classList.remove('disabled');
|
||||||
|
bootstrapTimelineAccountsField.parentElement.parentElement.classList.remove('disabled');
|
||||||
} else {
|
} else {
|
||||||
bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
|
bootstrapTimelineAccountsField.parentElement.classList.add('disabled');
|
||||||
|
bootstrapTimelineAccountsField.parentElement.parentElement.classList.add('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
|
delegate(document, '#form_admin_settings_enable_bootstrap_timeline_accounts', 'change', ({ target }) => onEnableBootstrapTimelineAccountsChange(target));
|
||||||
|
|
||||||
|
const onChangeRegistrationMode = (target) => {
|
||||||
|
const enabled = target.value === 'approved';
|
||||||
|
|
||||||
|
[].forEach.call(document.querySelectorAll('#form_admin_settings_require_invite_text'), (input) => {
|
||||||
|
input.disabled = !enabled;
|
||||||
|
if (enabled) {
|
||||||
|
let element = input;
|
||||||
|
do {
|
||||||
|
element.classList.remove('disabled');
|
||||||
|
element = element.parentElement;
|
||||||
|
} while (element && !element.classList.contains('fields-group'));
|
||||||
|
} else {
|
||||||
|
let element = input;
|
||||||
|
do {
|
||||||
|
element.classList.add('disabled');
|
||||||
|
element = element.parentElement;
|
||||||
|
} while (element && !element.classList.contains('fields-group'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
delegate(document, '#form_admin_settings_registrations_mode', 'change', ({ target }) => onChangeRegistrationMode(target));
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
|
const domainBlockSeverityInput = document.getElementById('domain_block_severity');
|
||||||
if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
|
if (domainBlockSeverityInput) onDomainBlockSeverityChange(domainBlockSeverityInput);
|
||||||
|
|
||||||
const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
|
const enableBootstrapTimelineAccounts = document.getElementById('form_admin_settings_enable_bootstrap_timeline_accounts');
|
||||||
if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
|
if (enableBootstrapTimelineAccounts) onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
|
||||||
|
|
||||||
|
const registrationMode = document.getElementById('form_admin_settings_registrations_mode');
|
||||||
|
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji';
|
|||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isRtl } from 'flavours/glitch/util/rtl';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
@ -189,11 +188,6 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
||||||
const { suggestionsHidden } = this.state;
|
const { suggestionsHidden } = this.state;
|
||||||
const style = { direction: 'ltr' };
|
|
||||||
|
|
||||||
if (isRtl(value)) {
|
|
||||||
style.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-input'>
|
<div className='autosuggest-input'>
|
||||||
@ -212,7 +206,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
style={style}
|
dir='auto'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
id={id}
|
id={id}
|
||||||
className={className}
|
className={className}
|
||||||
|
@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji';
|
|||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isRtl } from 'flavours/glitch/util/rtl';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -195,11 +194,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
|
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
|
||||||
const { suggestionsHidden } = this.state;
|
const { suggestionsHidden } = this.state;
|
||||||
const style = { direction: 'ltr' };
|
|
||||||
|
|
||||||
if (isRtl(value)) {
|
|
||||||
style.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||||
@ -220,7 +214,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
style={style}
|
dir='auto'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isRtl } from 'flavours/glitch/util/rtl';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@ -277,16 +276,11 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
|
|
||||||
const content = { __html: status.get('contentHtml') };
|
const content = { __html: status.get('contentHtml') };
|
||||||
const spoilerContent = { __html: status.get('spoilerHtml') };
|
const spoilerContent = { __html: status.get('spoilerHtml') };
|
||||||
const directionStyle = { direction: 'ltr' };
|
|
||||||
const classNames = classnames('status__content', {
|
const classNames = classnames('status__content', {
|
||||||
'status__content--with-action': parseClick && !disabled,
|
'status__content--with-action': parseClick && !disabled,
|
||||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isRtl(status.get('search_index'))) {
|
|
||||||
directionStyle.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
if (status.get('spoiler_text').length > 0) {
|
||||||
let mentionsPlaceholder = '';
|
let mentionsPlaceholder = '';
|
||||||
|
|
||||||
@ -346,7 +340,6 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
<div
|
<div
|
||||||
ref={this.setContentsRef}
|
ref={this.setContentsRef}
|
||||||
key={`contents-${tagLinks}`}
|
key={`contents-${tagLinks}`}
|
||||||
style={directionStyle}
|
|
||||||
tabIndex={!hidden ? 0 : null}
|
tabIndex={!hidden ? 0 : null}
|
||||||
dangerouslySetInnerHTML={content}
|
dangerouslySetInnerHTML={content}
|
||||||
className='status__content__text'
|
className='status__content__text'
|
||||||
@ -360,7 +353,6 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames}
|
className={classNames}
|
||||||
style={directionStyle}
|
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
onMouseUp={this.handleMouseUp}
|
onMouseUp={this.handleMouseUp}
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
@ -380,7 +372,6 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='status__content'
|
className='status__content'
|
||||||
style={directionStyle}
|
|
||||||
tabIndex='0'
|
tabIndex='0'
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
>
|
>
|
||||||
|
@ -168,7 +168,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
|
|
||||||
{suspended ? (
|
{suspended ? (
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
<FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||||
|
@ -117,7 +117,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (suspended) {
|
if (suspended) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
} else if (remote && statusIds.isEmpty()) {
|
} else if (remote && statusIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
} else {
|
} else {
|
||||||
|
@ -10,9 +10,6 @@ import AccountContainer from 'flavours/glitch/containers/account_container';
|
|||||||
import IconButton from 'flavours/glitch/components/icon_button';
|
import IconButton from 'flavours/glitch/components/icon_button';
|
||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
|
|
||||||
// Utils.
|
|
||||||
import { isRtl } from 'flavours/glitch/util/rtl';
|
|
||||||
|
|
||||||
// Messages.
|
// Messages.
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancel: {
|
cancel: {
|
||||||
@ -71,7 +68,6 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||||||
<div
|
<div
|
||||||
className='content'
|
className='content'
|
||||||
dangerouslySetInnerHTML={{ __html: content || '' }}
|
dangerouslySetInnerHTML={{ __html: content || '' }}
|
||||||
style={{ direction: isRtl(content) ? 'rtl' : 'ltr' }}
|
|
||||||
/>
|
/>
|
||||||
{attachments.size > 0 && (
|
{attachments.size > 0 && (
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
|
@ -70,6 +70,7 @@
|
|||||||
p, pre, blockquote {
|
p, pre, blockquote {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -152,6 +153,7 @@
|
|||||||
a {
|
a {
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -368,11 +368,6 @@ code {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus:invalid:not(:placeholder-shown),
|
|
||||||
&:required:invalid:not(:placeholder-shown) {
|
|
||||||
border-color: lighten($error-red, 12%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:required:valid {
|
&:required:valid {
|
||||||
border-color: $valid-value-color;
|
border-color: $valid-value-color;
|
||||||
}
|
}
|
||||||
@ -388,6 +383,16 @@ code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=text],
|
||||||
|
input[type=number],
|
||||||
|
input[type=email],
|
||||||
|
input[type=password] {
|
||||||
|
&:focus:invalid:not(:placeholder-shown),
|
||||||
|
&:required:invalid:not(:placeholder-shown) {
|
||||||
|
border-color: lighten($error-red, 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input.field_with_errors {
|
.input.field_with_errors {
|
||||||
label {
|
label {
|
||||||
color: lighten($error-red, 12%);
|
color: lighten($error-red, 12%);
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
// U+0590 to U+05FF - Hebrew
|
|
||||||
// U+0600 to U+06FF - Arabic
|
|
||||||
// U+0700 to U+074F - Syriac
|
|
||||||
// U+0750 to U+077F - Arabic Supplement
|
|
||||||
// U+0780 to U+07BF - Thaana
|
|
||||||
// U+07C0 to U+07FF - N'Ko
|
|
||||||
// U+0800 to U+083F - Samaritan
|
|
||||||
// U+08A0 to U+08FF - Arabic Extended-A
|
|
||||||
// U+FB1D to U+FB4F - Hebrew presentation forms
|
|
||||||
// U+FB50 to U+FDFF - Arabic presentation forms A
|
|
||||||
// U+FE70 to U+FEFF - Arabic presentation forms B
|
|
||||||
|
|
||||||
const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg;
|
|
||||||
|
|
||||||
export function isRtl(text) {
|
|
||||||
if (text.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, '');
|
|
||||||
text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, '');
|
|
||||||
text = text.replace(/\s+/g, '');
|
|
||||||
text = text.replace(/(\w\S+\.\w{2,}\S*)/g, '');
|
|
||||||
|
|
||||||
const matches = text.match(rtlChars);
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches.length / text.length > 0.3;
|
|
||||||
};
|
|
@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji';
|
|||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isRtl } from '../rtl';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
@ -189,11 +188,6 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props;
|
||||||
const { suggestionsHidden } = this.state;
|
const { suggestionsHidden } = this.state;
|
||||||
const style = { direction: 'ltr' };
|
|
||||||
|
|
||||||
if (isRtl(value)) {
|
|
||||||
style.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-input'>
|
<div className='autosuggest-input'>
|
||||||
@ -212,7 +206,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
style={style}
|
dir='auto'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
id={id}
|
id={id}
|
||||||
className={className}
|
className={className}
|
||||||
|
@ -4,7 +4,6 @@ import AutosuggestEmoji from './autosuggest_emoji';
|
|||||||
import AutosuggestHashtag from './autosuggest_hashtag';
|
import AutosuggestHashtag from './autosuggest_hashtag';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isRtl } from '../rtl';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@ -195,11 +194,6 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
|
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props;
|
||||||
const { suggestionsHidden } = this.state;
|
const { suggestionsHidden } = this.state;
|
||||||
const style = { direction: 'ltr' };
|
|
||||||
|
|
||||||
if (isRtl(value)) {
|
|
||||||
style.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||||
@ -220,7 +214,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
style={style}
|
dir='auto'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { isRtl } from '../rtl';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
@ -186,17 +185,12 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
|
|
||||||
const content = { __html: status.get('contentHtml') };
|
const content = { __html: status.get('contentHtml') };
|
||||||
const spoilerContent = { __html: status.get('spoilerHtml') };
|
const spoilerContent = { __html: status.get('spoilerHtml') };
|
||||||
const directionStyle = { direction: 'ltr' };
|
|
||||||
const classNames = classnames('status__content', {
|
const classNames = classnames('status__content', {
|
||||||
'status__content--with-action': this.props.onClick && this.context.router,
|
'status__content--with-action': this.props.onClick && this.context.router,
|
||||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||||
'status__content--collapsed': renderReadMore,
|
'status__content--collapsed': renderReadMore,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isRtl(status.get('search_index'))) {
|
|
||||||
directionStyle.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
const showThreadButton = (
|
const showThreadButton = (
|
||||||
<button className='status__content__read-more-button' onClick={this.props.onClick}>
|
<button className='status__content__read-more-button' onClick={this.props.onClick}>
|
||||||
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
|
||||||
@ -225,7 +219,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
<div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} />
|
<span dangerouslySetInnerHTML={spoilerContent} />
|
||||||
{' '}
|
{' '}
|
||||||
@ -234,7 +228,7 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
||||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} />
|
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
{!hidden && !!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||||
|
|
||||||
@ -243,8 +237,8 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
} else if (this.props.onClick) {
|
} else if (this.props.onClick) {
|
||||||
const output = [
|
const output = [
|
||||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
|
<div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'>
|
||||||
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
|
<div className='status__content__text status__content__text--visible' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||||
|
|
||||||
@ -259,8 +253,8 @@ export default class StatusContent extends React.PureComponent {
|
|||||||
return output;
|
return output;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}>
|
<div className={classNames} ref={this.setRef} tabIndex='0'>
|
||||||
<div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} />
|
<div className='status__content__text status__content__text--visible' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
{!!status.get('poll') && <PollContainer pollId={status.get('poll')} />}
|
||||||
|
|
||||||
|
@ -152,6 +152,14 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let emptyMessage;
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (blockedBy) {
|
||||||
|
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<ColumnBackButton multiColumn={multiColumn} />
|
<ColumnBackButton multiColumn={multiColumn} />
|
||||||
@ -162,7 +170,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
|
|
||||||
{(suspended || blockedBy) ? (
|
{(suspended || blockedBy) ? (
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
{emptyMessage}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||||
|
@ -136,7 +136,9 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (suspended || blockedBy) {
|
if (suspended) {
|
||||||
|
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
|
||||||
|
} else if (blockedBy) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||||
} else if (remote && statusIds.isEmpty()) {
|
} else if (remote && statusIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
|
@ -6,7 +6,6 @@ import IconButton from '../../../components/icon_button';
|
|||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { isRtl } from '../../../rtl';
|
|
||||||
import AttachmentList from 'mastodon/components/attachment_list';
|
import AttachmentList from 'mastodon/components/attachment_list';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@ -45,9 +44,6 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = { __html: status.get('contentHtml') };
|
const content = { __html: status.get('contentHtml') };
|
||||||
const style = {
|
|
||||||
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='reply-indicator'>
|
<div className='reply-indicator'>
|
||||||
@ -60,7 +56,7 @@ class ReplyIndicator extends ImmutablePureComponent {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
|
<div className='reply-indicator__content' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{status.get('media_attachments').size > 0 && (
|
{status.get('media_attachments').size > 0 && (
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
// U+0590 to U+05FF - Hebrew
|
|
||||||
// U+0600 to U+06FF - Arabic
|
|
||||||
// U+0700 to U+074F - Syriac
|
|
||||||
// U+0750 to U+077F - Arabic Supplement
|
|
||||||
// U+0780 to U+07BF - Thaana
|
|
||||||
// U+07C0 to U+07FF - N'Ko
|
|
||||||
// U+0800 to U+083F - Samaritan
|
|
||||||
// U+08A0 to U+08FF - Arabic Extended-A
|
|
||||||
// U+FB1D to U+FB4F - Hebrew presentation forms
|
|
||||||
// U+FB50 to U+FDFF - Arabic presentation forms A
|
|
||||||
// U+FE70 to U+FEFF - Arabic presentation forms B
|
|
||||||
|
|
||||||
const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg;
|
|
||||||
|
|
||||||
export function isRtl(text) {
|
|
||||||
if (text.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = text.replace(/(?:^|[^\/\w])@([a-z0-9_]+(@[a-z0-9\.\-]+)?)/ig, '');
|
|
||||||
text = text.replace(/(?:^|[^\/\w])#([\S]+)/ig, '');
|
|
||||||
text = text.replace(/\s+/g, '');
|
|
||||||
text = text.replace(/(\w\S+\.\w{2,}\S*)/g, '');
|
|
||||||
|
|
||||||
const matches = text.match(rtlChars);
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return matches.length / text.length > 0.3;
|
|
||||||
};
|
|
@ -58,6 +58,16 @@ td {
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-dir {
|
||||||
|
p {
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.email-table,
|
.email-table,
|
||||||
.content-section,
|
.content-section,
|
||||||
.column,
|
.column,
|
||||||
@ -96,7 +106,7 @@ body {
|
|||||||
.col-3,
|
.col-3,
|
||||||
.col-4,
|
.col-4,
|
||||||
.col-5,
|
.col-5,
|
||||||
.col-6, {
|
.col-6 {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -831,6 +831,7 @@
|
|||||||
p {
|
p {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -840,6 +841,7 @@
|
|||||||
a {
|
a {
|
||||||
color: $secondary-text-color;
|
color: $secondary-text-color;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
@ -377,11 +377,6 @@ code {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus:invalid:not(:placeholder-shown),
|
|
||||||
&:required:invalid:not(:placeholder-shown) {
|
|
||||||
border-color: lighten($error-red, 12%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:required:valid {
|
&:required:valid {
|
||||||
border-color: $valid-value-color;
|
border-color: $valid-value-color;
|
||||||
}
|
}
|
||||||
@ -397,6 +392,16 @@ code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=text],
|
||||||
|
input[type=number],
|
||||||
|
input[type=email],
|
||||||
|
input[type=password] {
|
||||||
|
&:focus:invalid:not(:placeholder-shown),
|
||||||
|
&:required:invalid:not(:placeholder-shown) {
|
||||||
|
border-color: lighten($error-red, 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input.field_with_errors {
|
.input.field_with_errors {
|
||||||
label {
|
label {
|
||||||
color: lighten($error-red, 12%);
|
color: lighten($error-red, 12%);
|
||||||
|
@ -228,6 +228,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
|
emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
|
||||||
emoji.image_remote_url = image_url
|
emoji.image_remote_url = image_url
|
||||||
emoji.save
|
emoji.save
|
||||||
|
rescue Seahorse::Client::NetworkingError
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_attachments
|
def process_attachments
|
||||||
@ -250,6 +252,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||||||
media_attachment.save
|
media_attachment.save
|
||||||
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
|
||||||
RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id)
|
RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id)
|
||||||
|
rescue Seahorse::Client::NetworkingError
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ class Account < ApplicationRecord
|
|||||||
include Paginable
|
include Paginable
|
||||||
include AccountCounters
|
include AccountCounters
|
||||||
include DomainNormalizable
|
include DomainNormalizable
|
||||||
|
include DomainMaterializable
|
||||||
include AccountMerging
|
include AccountMerging
|
||||||
|
|
||||||
MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
|
MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
|
||||||
@ -107,7 +108,6 @@ class Account < ApplicationRecord
|
|||||||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||||
scope :groups, -> { where(actor_type: 'Group') }
|
scope :groups, -> { where(actor_type: 'Group') }
|
||||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||||
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
|
|
||||||
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
||||||
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}%")) }
|
||||||
@ -440,10 +440,6 @@ class Account < ApplicationRecord
|
|||||||
super - %w(statuses_count following_count followers_count)
|
super - %w(statuses_count following_count followers_count)
|
||||||
end
|
end
|
||||||
|
|
||||||
def domains
|
|
||||||
reorder(nil).pluck(Arel.sql('distinct accounts.domain'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def inboxes
|
def inboxes
|
||||||
urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
|
urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
|
||||||
DeliveryFailureTracker.without_unavailable(urls)
|
DeliveryFailureTracker.without_unavailable(urls)
|
||||||
|
13
app/models/concerns/domain_materializable.rb
Normal file
13
app/models/concerns/domain_materializable.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DomainMaterializable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
after_create_commit :refresh_instances_view
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_instances_view
|
||||||
|
Instance.refresh unless domain.nil? || Instance.where(domain: domain).exists?
|
||||||
|
end
|
||||||
|
end
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
class DomainAllow < ApplicationRecord
|
class DomainAllow < ApplicationRecord
|
||||||
include DomainNormalizable
|
include DomainNormalizable
|
||||||
|
include DomainMaterializable
|
||||||
|
|
||||||
validates :domain, presence: true, uniqueness: true, domain: true
|
validates :domain, presence: true, uniqueness: true, domain: true
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
class DomainBlock < ApplicationRecord
|
class DomainBlock < ApplicationRecord
|
||||||
include DomainNormalizable
|
include DomainNormalizable
|
||||||
|
include DomainMaterializable
|
||||||
|
|
||||||
enum severity: [:silence, :suspend, :noop]
|
enum severity: [:silence, :suspend, :noop]
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ class Form::AdminSettings
|
|||||||
show_domain_blocks_rationale
|
show_domain_blocks_rationale
|
||||||
noindex
|
noindex
|
||||||
outgoing_spoilers
|
outgoing_spoilers
|
||||||
|
require_invite_text
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
BOOLEAN_KEYS = %i(
|
BOOLEAN_KEYS = %i(
|
||||||
@ -62,6 +63,7 @@ class Form::AdminSettings
|
|||||||
trends
|
trends
|
||||||
trendable_by_default
|
trendable_by_default
|
||||||
noindex
|
noindex
|
||||||
|
require_invite_text
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
UPLOAD_KEYS = %i(
|
UPLOAD_KEYS = %i(
|
||||||
|
@ -1,26 +1,63 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: instances
|
||||||
|
#
|
||||||
|
# domain :string primary key
|
||||||
|
# accounts_count :bigint(8)
|
||||||
|
#
|
||||||
|
|
||||||
class Instance
|
class Instance < ApplicationRecord
|
||||||
include ActiveModel::Model
|
self.primary_key = :domain
|
||||||
|
|
||||||
attr_accessor :domain, :accounts_count, :domain_block
|
has_many :accounts, foreign_key: :domain, primary_key: :domain
|
||||||
|
|
||||||
def initialize(resource)
|
belongs_to :domain_block, foreign_key: :domain, primary_key: :domain
|
||||||
@domain = resource.domain
|
belongs_to :domain_allow, foreign_key: :domain, primary_key: :domain
|
||||||
@accounts_count = resource.respond_to?(:accounts_count) ? resource.accounts_count : nil
|
|
||||||
@domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain)
|
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||||
@domain_allow = resource.is_a?(DomainAllow) ? resource : DomainAllow.rule_for(domain)
|
|
||||||
|
def self.refresh
|
||||||
|
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def countable?
|
def readonly?
|
||||||
@accounts_count.present?
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def delivery_failure_tracker
|
||||||
|
@delivery_failure_tracker ||= DeliveryFailureTracker.new(domain)
|
||||||
|
end
|
||||||
|
|
||||||
|
def following_count
|
||||||
|
@following_count ||= Follow.where(account: accounts).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers_count
|
||||||
|
@followers_count ||= Follow.where(target_account: accounts).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def reports_count
|
||||||
|
@reports_count ||= Report.where(target_account: accounts).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def blocks_count
|
||||||
|
@blocks_count ||= Block.where(target_account: accounts).count
|
||||||
|
end
|
||||||
|
|
||||||
|
def public_comment
|
||||||
|
domain_block&.public_comment
|
||||||
|
end
|
||||||
|
|
||||||
|
def private_comment
|
||||||
|
domain_block&.private_comment
|
||||||
|
end
|
||||||
|
|
||||||
|
def media_storage
|
||||||
|
@media_storage ||= MediaAttachment.where(account: accounts).sum(:file_file_size)
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
domain
|
domain
|
||||||
end
|
end
|
||||||
|
|
||||||
def cache_key
|
|
||||||
domain
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -13,18 +13,27 @@ class InstanceFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
if params[:limited].present?
|
scope = Instance.includes(:domain_block, :domain_allow).order(accounts_count: :desc)
|
||||||
scope = DomainBlock
|
|
||||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
params.each do |key, value|
|
||||||
scope.order(id: :desc)
|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||||
elsif params[:allowed].present?
|
end
|
||||||
scope = DomainAllow
|
|
||||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
scope
|
||||||
scope.order(id: :desc)
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def scope_for(key, value)
|
||||||
|
case key.to_s
|
||||||
|
when 'limited'
|
||||||
|
Instance.joins(:domain_block).reorder(Arel.sql('domain_blocks.id desc'))
|
||||||
|
when 'allowed'
|
||||||
|
Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc'))
|
||||||
|
when 'by_domain'
|
||||||
|
Instance.matches_domain(value)
|
||||||
else
|
else
|
||||||
scope = Account.remote
|
raise "Unknown filter: #{key}"
|
||||||
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
|
|
||||||
scope.by_domain_accounts
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# target_account_id :bigint(8) not null
|
# target_account_id :bigint(8) not null
|
||||||
# assigned_account_id :bigint(8)
|
# assigned_account_id :bigint(8)
|
||||||
# uri :string
|
# uri :string
|
||||||
|
# forwarded :boolean
|
||||||
#
|
#
|
||||||
|
|
||||||
class Report < ApplicationRecord
|
class Report < ApplicationRecord
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
class UnavailableDomain < ApplicationRecord
|
class UnavailableDomain < ApplicationRecord
|
||||||
include DomainNormalizable
|
include DomainNormalizable
|
||||||
|
|
||||||
|
validates :domain, presence: true, uniqueness: true
|
||||||
|
|
||||||
after_commit :reset_cache!
|
after_commit :reset_cache!
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -82,7 +82,8 @@ class User < ApplicationRecord
|
|||||||
has_many :webauthn_credentials, dependent: :destroy
|
has_many :webauthn_credentials, dependent: :destroy
|
||||||
|
|
||||||
has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
|
has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
|
||||||
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? }
|
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
|
||||||
|
validates :invite_request, presence: true, on: :create, if: -> { Setting.require_invite_text }
|
||||||
|
|
||||||
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
|
||||||
validates_with BlacklistedEmailValidator, on: :create
|
validates_with BlacklistedEmailValidator, on: :create
|
||||||
|
@ -13,6 +13,10 @@ class DomainBlockPolicy < ApplicationPolicy
|
|||||||
admin?
|
admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
admin?
|
||||||
|
end
|
||||||
|
|
||||||
def destroy?
|
def destroy?
|
||||||
admin?
|
admin?
|
||||||
end
|
end
|
||||||
|
@ -29,7 +29,7 @@ class InstancePresenter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def domain_count
|
def domain_count
|
||||||
Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
|
Rails.cache.fetch('distinct_domain_count') { Instance.count }
|
||||||
end
|
end
|
||||||
|
|
||||||
def sample_accounts
|
def sample_accounts
|
||||||
|
@ -7,7 +7,7 @@ class ManifestSerializer < ActiveModel::Serializer
|
|||||||
attributes :name, :short_name, :description,
|
attributes :name, :short_name, :description,
|
||||||
:icons, :theme_color, :background_color,
|
:icons, :theme_color, :background_color,
|
||||||
:display, :start_url, :scope,
|
:display, :start_url, :scope,
|
||||||
:share_target
|
:share_target, :shortcuts
|
||||||
|
|
||||||
def name
|
def name
|
||||||
object.site_title
|
object.site_title
|
||||||
@ -64,4 +64,42 @@ class ManifestSerializer < ActiveModel::Serializer
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def shortcuts
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'New toot',
|
||||||
|
url: '/web/statuses/new',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/shortcuts/new-status.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Notifications',
|
||||||
|
url: '/web/notifications',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/shortcuts/notifications.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Direct messages',
|
||||||
|
url: '/web/timelines/direct',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/shortcuts/direct.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -24,7 +24,8 @@ class ReportService < BaseService
|
|||||||
target_account: @target_account,
|
target_account: @target_account,
|
||||||
status_ids: @status_ids,
|
status_ids: @status_ids,
|
||||||
comment: @comment,
|
comment: @comment,
|
||||||
uri: @options[:uri]
|
uri: @options[:uri],
|
||||||
|
forwarded: ActiveModel::Type::Boolean.new.cast(@options[:forward])
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
- if approved_registrations?
|
- if approved_registrations?
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.simple_fields_for :invite_request do |invite_request_fields|
|
= f.simple_fields_for :invite_request do |invite_request_fields|
|
||||||
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: false
|
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), required: true, disabled: closed_registrations?
|
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), required: true, disabled: closed_registrations?
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
.row__information-board
|
.row__information-board
|
||||||
.information-board__section
|
.information-board__section
|
||||||
%span= t 'about.user_count_before'
|
%span= t 'about.user_count_before'
|
||||||
%strong= number_with_delimiter @instance_presenter.user_count
|
%strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
|
||||||
%span= t 'about.user_count_after', count: @instance_presenter.user_count
|
%span= t 'about.user_count_after', count: @instance_presenter.user_count
|
||||||
.information-board__section
|
.information-board__section
|
||||||
%span= t 'about.status_count_before'
|
%span= t 'about.status_count_before'
|
||||||
%strong= number_with_delimiter @instance_presenter.status_count
|
%strong= number_to_human @instance_presenter.status_count, strip_insignificant_zeros: true
|
||||||
%span= t 'about.status_count_after', count: @instance_presenter.status_count
|
%span= t 'about.status_count_after', count: @instance_presenter.status_count
|
||||||
.row__mascot
|
.row__mascot
|
||||||
.landing-page__mascot
|
.landing-page__mascot
|
||||||
|
@ -242,3 +242,13 @@
|
|||||||
|
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('admin.account_moderation_notes.create'), type: :submit
|
= f.button :button, t('admin.account_moderation_notes.create'), type: :submit
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
|
- if @account.user&.invite_request&.text&.present?
|
||||||
|
%div.speech-bubble
|
||||||
|
%div.speech-bubble__bubble
|
||||||
|
= @account.user&.invite_request&.text
|
||||||
|
%div.speech-bubble__owner
|
||||||
|
= admin_account_link_to @account
|
||||||
|
= t('admin.accounts.invite_request_text')
|
||||||
|
25
app/views/admin/instances/_instance.html.haml
Normal file
25
app/views/admin/instances/_instance.html.haml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.directory__tag
|
||||||
|
= link_to admin_instance_path(instance) do
|
||||||
|
%h4
|
||||||
|
= instance.domain
|
||||||
|
%small
|
||||||
|
- if instance.domain_block
|
||||||
|
- first_item = true
|
||||||
|
- if !instance.domain_block.noop?
|
||||||
|
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
|
||||||
|
- first_item = false
|
||||||
|
- unless instance.domain_block.suspend?
|
||||||
|
- if instance.domain_block.reject_media?
|
||||||
|
- unless first_item
|
||||||
|
•
|
||||||
|
= t('admin.domain_blocks.rejecting_media')
|
||||||
|
- first_item = false
|
||||||
|
- if instance.domain_block.reject_reports?
|
||||||
|
- unless first_item
|
||||||
|
•
|
||||||
|
= t('admin.domain_blocks.rejecting_reports')
|
||||||
|
- elsif whitelist_mode?
|
||||||
|
= t('admin.accounts.whitelisted')
|
||||||
|
- else
|
||||||
|
= t('admin.accounts.no_limits_imposed')
|
||||||
|
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
|
@ -32,32 +32,10 @@
|
|||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
- @instances.each do |instance|
|
- if @instances.empty?
|
||||||
.directory__tag
|
%div.muted-hint.center-text
|
||||||
= link_to admin_instance_path(instance) do
|
= t 'admin.instances.empty'
|
||||||
%h4
|
- else
|
||||||
= instance.domain
|
= render @instances
|
||||||
%small
|
|
||||||
- if instance.domain_block
|
|
||||||
- first_item = true
|
|
||||||
- if !instance.domain_block.noop?
|
|
||||||
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
|
|
||||||
- first_item = false
|
|
||||||
- unless instance.domain_block.suspend?
|
|
||||||
- if instance.domain_block.reject_media?
|
|
||||||
- unless first_item
|
|
||||||
•
|
|
||||||
= t('admin.domain_blocks.rejecting_media')
|
|
||||||
- first_item = false
|
|
||||||
- if instance.domain_block.reject_reports?
|
|
||||||
- unless first_item
|
|
||||||
•
|
|
||||||
= t('admin.domain_blocks.rejecting_reports')
|
|
||||||
- elsif whitelist_mode?
|
|
||||||
= t('admin.accounts.whitelisted')
|
|
||||||
- else
|
|
||||||
= t('admin.accounts.no_limits_imposed')
|
|
||||||
- if instance.countable?
|
|
||||||
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
|
|
||||||
|
|
||||||
= paginate paginated_instances
|
= paginate @instances
|
||||||
|
@ -3,57 +3,59 @@
|
|||||||
|
|
||||||
.dashboard__counters
|
.dashboard__counters
|
||||||
%div
|
%div
|
||||||
%div
|
= link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do
|
||||||
.dashboard__counters__num= number_with_delimiter @following_count
|
.dashboard__counters__num= number_with_delimiter @instance.accounts_count
|
||||||
.dashboard__counters__label= t 'admin.instances.total_followed_by_them'
|
.dashboard__counters__label= t 'admin.accounts.title'
|
||||||
%div
|
|
||||||
%div
|
|
||||||
.dashboard__counters__num= number_with_delimiter @followers_count
|
|
||||||
.dashboard__counters__label= t 'admin.instances.total_followed_by_us'
|
|
||||||
%div
|
|
||||||
%div
|
|
||||||
.dashboard__counters__num= number_to_human_size @media_storage
|
|
||||||
.dashboard__counters__label= t 'admin.instances.total_storage'
|
|
||||||
%div
|
|
||||||
%div
|
|
||||||
.dashboard__counters__num= number_with_delimiter @blocks_count
|
|
||||||
.dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
|
|
||||||
%div
|
%div
|
||||||
= link_to admin_reports_path(by_target_domain: @instance.domain) do
|
= link_to admin_reports_path(by_target_domain: @instance.domain) do
|
||||||
.dashboard__counters__num= number_with_delimiter @reports_count
|
.dashboard__counters__num= number_with_delimiter @instance.reports_count
|
||||||
.dashboard__counters__label= t 'admin.instances.total_reported'
|
.dashboard__counters__label= t 'admin.instances.total_reported'
|
||||||
|
%div
|
||||||
|
%div
|
||||||
|
.dashboard__counters__num= number_to_human_size @instance.media_storage
|
||||||
|
.dashboard__counters__label= t 'admin.instances.total_storage'
|
||||||
|
%div
|
||||||
|
%div
|
||||||
|
.dashboard__counters__num= number_with_delimiter @instance.following_count
|
||||||
|
.dashboard__counters__label= t 'admin.instances.total_followed_by_them'
|
||||||
|
%div
|
||||||
|
%div
|
||||||
|
.dashboard__counters__num= number_with_delimiter @instance.followers_count
|
||||||
|
.dashboard__counters__label= t 'admin.instances.total_followed_by_us'
|
||||||
|
%div
|
||||||
|
%div
|
||||||
|
.dashboard__counters__num= number_with_delimiter @instance.blocks_count
|
||||||
|
.dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
|
||||||
|
|
||||||
%div
|
%div
|
||||||
%div
|
%div
|
||||||
.dashboard__counters__num
|
.dashboard__counters__num
|
||||||
- if @available
|
- if @instance.delivery_failure_tracker.available?
|
||||||
= fa_icon 'check'
|
= fa_icon 'check'
|
||||||
- else
|
- else
|
||||||
= fa_icon 'times'
|
= fa_icon 'times'
|
||||||
.dashboard__counters__label= t 'admin.instances.delivery_available'
|
.dashboard__counters__label= t 'admin.instances.delivery_available'
|
||||||
|
|
||||||
- if @private_comment.present?
|
- if @instance.private_comment.present?
|
||||||
.speech-bubble
|
.speech-bubble
|
||||||
.speech-bubble__bubble
|
.speech-bubble__bubble
|
||||||
= simple_format(h(@private_comment))
|
= simple_format(h(@instance.private_comment))
|
||||||
.speech-bubble__owner= t 'admin.instances.private_comment'
|
.speech-bubble__owner= t 'admin.instances.private_comment'
|
||||||
|
|
||||||
- if @public_comment.present?
|
- if @instance.public_comment.present?
|
||||||
.speech-bubble
|
.speech-bubble
|
||||||
.speech-bubble__bubble
|
.speech-bubble__bubble
|
||||||
= simple_format(h(@public_comment))
|
= simple_format(h(@instance.public_comment))
|
||||||
.speech-bubble__owner= t 'admin.instances.public_comment'
|
.speech-bubble__owner= t 'admin.instances.public_comment'
|
||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
%div.action-buttons
|
%div.action-buttons
|
||||||
%div
|
%div
|
||||||
= link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button'
|
- if @instance.domain_allow
|
||||||
|
= link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@instance.domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
|
||||||
%div
|
- elsif @instance.domain_block
|
||||||
- if @domain_allow
|
= link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@instance.domain_block), class: 'button'
|
||||||
= link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
|
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button'
|
||||||
- elsif @domain_block
|
|
||||||
= link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@domain_block), class: 'button'
|
|
||||||
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button'
|
|
||||||
- else
|
- else
|
||||||
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
|
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
|
||||||
|
@ -59,6 +59,10 @@
|
|||||||
= fa_icon('camera')
|
= fa_icon('camera')
|
||||||
= report.media_attachments.count
|
= report.media_attachments.count
|
||||||
|
|
||||||
|
- if report.forwarded?
|
||||||
|
·
|
||||||
|
= t('admin.reports.forwarded_to', domain: target_account.domain)
|
||||||
|
|
||||||
.report-card__summary__item__assigned
|
.report-card__summary__item__assigned
|
||||||
- if report.assigned_account.present?
|
- if report.assigned_account.present?
|
||||||
= admin_account_link_to report.assigned_account
|
= admin_account_link_to report.assigned_account
|
||||||
|
@ -43,6 +43,16 @@
|
|||||||
%td{ colspan: 2 }
|
%td{ colspan: 2 }
|
||||||
- if @report.action_taken?
|
- if @report.action_taken?
|
||||||
= table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
|
= table_link_to 'envelope-open', t('admin.reports.reopen'), admin_report_path(@report, outcome: 'reopen'), method: :put
|
||||||
|
- unless @report.target_account.local?
|
||||||
|
%tr
|
||||||
|
%th= t('admin.reports.forwarded')
|
||||||
|
%td{ colspan: 3 }
|
||||||
|
- if @report.forwarded.nil?
|
||||||
|
\-
|
||||||
|
- elsif @report.forwarded?
|
||||||
|
= t('simple_form.yes')
|
||||||
|
- else
|
||||||
|
= t('simple_form.no')
|
||||||
- if !@report.action_taken_by_account.nil?
|
- if !@report.action_taken_by_account.nil?
|
||||||
%tr
|
%tr
|
||||||
%th= t('admin.reports.action_taken_by')
|
%th= t('admin.reports.action_taken_by')
|
||||||
|
@ -40,6 +40,12 @@
|
|||||||
|
|
||||||
%hr.spacer/
|
%hr.spacer/
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :require_invite_text, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.require_invite_text.title'), hint: t('admin.settings.registrations.require_invite_text.desc_html'), disabled: !approved_registrations?
|
||||||
|
.fields-group
|
||||||
|
|
||||||
|
%hr.spacer/
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :enable_bootstrap_timeline_accounts, as: :boolean, wrapper: :with_label, label: t('admin.settings.enable_bootstrap_timeline_accounts.title')
|
= f.input :enable_bootstrap_timeline_accounts, as: :boolean, wrapper: :with_label, label: t('admin.settings.enable_bootstrap_timeline_accounts.title')
|
||||||
.fields-group
|
.fields-group
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
- if approved_registrations? && !@invite.present?
|
- if approved_registrations? && !@invite.present?
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.simple_fields_for :invite_request, resource.invite_request || resource.build_invite_request do |invite_request_fields|
|
= f.simple_fields_for :invite_request, resource.invite_request || resource.build_invite_request do |invite_request_fields|
|
||||||
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: false
|
= invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: Setting.require_invite_text
|
||||||
|
|
||||||
= f.input :invite_code, as: :hidden
|
= f.input :invite_code, as: :hidden
|
||||||
|
|
||||||
|
@ -26,11 +26,11 @@
|
|||||||
= "@#{status.account.acct}"
|
= "@#{status.account.acct}"
|
||||||
|
|
||||||
- if status.spoiler_text?
|
- if status.spoiler_text?
|
||||||
%div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
|
%div.auto-dir
|
||||||
%p
|
%p
|
||||||
= Formatter.instance.format_spoiler(status)
|
= Formatter.instance.format_spoiler(status)
|
||||||
|
|
||||||
%div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
|
%div.auto-dir
|
||||||
= Formatter.instance.format(status)
|
= Formatter.instance.format(status)
|
||||||
|
|
||||||
- if status.media_attachments.size > 0
|
- if status.media_attachments.size > 0
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
%p<
|
%p<
|
||||||
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}
|
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}
|
||||||
%button.status__content__spoiler-link= t('statuses.show_more')
|
%button.status__content__spoiler-link= t('statuses.show_more')
|
||||||
.e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
|
.e-content
|
||||||
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
|
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
|
||||||
- if status.preloadable_poll
|
- if status.preloadable_poll
|
||||||
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
%p<
|
%p<
|
||||||
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}
|
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}
|
||||||
%button.status__content__spoiler-link= t('statuses.show_more')
|
%button.status__content__spoiler-link= t('statuses.show_more')
|
||||||
.e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }<
|
.e-content<
|
||||||
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
|
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
|
||||||
- if status.preloadable_poll
|
- if status.preloadable_poll
|
||||||
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
|
||||||
|
11
app/workers/scheduler/instance_refresh_scheduler.rb
Normal file
11
app/workers/scheduler/instance_refresh_scheduler.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Scheduler::InstanceRefreshScheduler
|
||||||
|
include Sidekiq::Worker
|
||||||
|
|
||||||
|
sidekiq_options lock: :until_executed, retry: 0
|
||||||
|
|
||||||
|
def perform
|
||||||
|
Instance.refresh
|
||||||
|
end
|
||||||
|
end
|
@ -110,6 +110,7 @@ worker.sidekiq:
|
|||||||
mailers: bundle exec sidekiq -c 5 -q mailers -L /app/log/sidekiq.log
|
mailers: bundle exec sidekiq -c 5 -q mailers -L /app/log/sidekiq.log
|
||||||
pull: bundle exec sidekiq -c 5 -q pull -L /app/log/sidekiq.log
|
pull: bundle exec sidekiq -c 5 -q pull -L /app/log/sidekiq.log
|
||||||
push: bundle exec sidekiq -c 5 -q push -L /app/log/sidekiq.log
|
push: bundle exec sidekiq -c 5 -q push -L /app/log/sidekiq.log
|
||||||
|
scheduler: bundle exec sidekiq -c 5 -q scheduler -L /app/log/sidekiq.log
|
||||||
|
|
||||||
writable_dirs:
|
writable_dirs:
|
||||||
- tmp
|
- tmp
|
||||||
|
@ -102,6 +102,37 @@
|
|||||||
"confidence": "Weak",
|
"confidence": "Weak",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "Dynamic Render Path",
|
||||||
|
"warning_code": 15,
|
||||||
|
"fingerprint": "4704e8093e3e0561bf705f892e8fc6780419f8255f4440b1c0afd09339bd6446",
|
||||||
|
"check_name": "Render",
|
||||||
|
"message": "Render path contains parameter value",
|
||||||
|
"file": "app/views/admin/instances/index.html.haml",
|
||||||
|
"line": 39,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
|
||||||
|
"code": "render(action => filtered_instances.page(params[:page]), {})",
|
||||||
|
"render_path": [
|
||||||
|
{
|
||||||
|
"type": "controller",
|
||||||
|
"class": "Admin::InstancesController",
|
||||||
|
"method": "index",
|
||||||
|
"line": 10,
|
||||||
|
"file": "app/controllers/admin/instances_controller.rb",
|
||||||
|
"rendered": {
|
||||||
|
"name": "admin/instances/index",
|
||||||
|
"file": "app/views/admin/instances/index.html.haml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"location": {
|
||||||
|
"type": "template",
|
||||||
|
"template": "admin/instances/index"
|
||||||
|
},
|
||||||
|
"user_input": "params[:page]",
|
||||||
|
"confidence": "Weak",
|
||||||
|
"note": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Redirect",
|
"warning_type": "Redirect",
|
||||||
"warning_code": 18,
|
"warning_code": 18,
|
||||||
@ -122,6 +153,26 @@
|
|||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "SQL Injection",
|
||||||
|
"warning_code": 0,
|
||||||
|
"fingerprint": "6e4051854bb62e2ddbc671f82d6c2328892e1134b8b28105ecba9b0122540714",
|
||||||
|
"check_name": "SQL",
|
||||||
|
"message": "Possible SQL injection",
|
||||||
|
"file": "app/models/account.rb",
|
||||||
|
"line": 491,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
|
"code": "find_by_sql([\" WITH first_degree AS (\\n SELECT target_account_id\\n FROM follows\\n WHERE account_id = ?\\n UNION ALL\\n SELECT ?\\n )\\n SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)\\n WHERE accounts.id IN (SELECT * FROM first_degree)\\n AND #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, account.id, limit, offset])",
|
||||||
|
"render_path": null,
|
||||||
|
"location": {
|
||||||
|
"type": "method",
|
||||||
|
"class": "Account",
|
||||||
|
"method": "advanced_search_for"
|
||||||
|
},
|
||||||
|
"user_input": "textsearch",
|
||||||
|
"confidence": "Medium",
|
||||||
|
"note": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "SQL Injection",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 0,
|
"warning_code": 0,
|
||||||
@ -163,23 +214,23 @@
|
|||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Mass Assignment",
|
"warning_type": "SQL Injection",
|
||||||
"warning_code": 105,
|
"warning_code": 0,
|
||||||
"fingerprint": "8f63dec68951d9bcf7eddb15af9392b2e1333003089c41fb76688dfd3579f394",
|
"fingerprint": "9251d682c4e2840e1b2fea91e7d758efe2097ecb7f6255c065e3750d25eb178c",
|
||||||
"check_name": "PermitAttributes",
|
"check_name": "SQL",
|
||||||
"message": "Potentially dangerous key allowed for mass assignment",
|
"message": "Possible SQL injection",
|
||||||
"file": "app/controllers/api/v1/crypto/deliveries_controller.rb",
|
"file": "app/models/account.rb",
|
||||||
"line": 23,
|
"line": 460,
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
"code": "params.require(:device).permit(:account_id, :device_id, :type, :body, :hmac)",
|
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, limit, offset])",
|
||||||
"render_path": null,
|
"render_path": null,
|
||||||
"location": {
|
"location": {
|
||||||
"type": "method",
|
"type": "method",
|
||||||
"class": "Api::V1::Crypto::DeliveriesController",
|
"class": "Account",
|
||||||
"method": "resource_params"
|
"method": "search_for"
|
||||||
},
|
},
|
||||||
"user_input": ":account_id",
|
"user_input": "textsearch",
|
||||||
"confidence": "High",
|
"confidence": "Medium",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -273,6 +324,26 @@
|
|||||||
"confidence": "High",
|
"confidence": "High",
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"warning_type": "SQL Injection",
|
||||||
|
"warning_code": 0,
|
||||||
|
"fingerprint": "e21d8fee7a5805761679877ca35ed1029c64c45ef3b4012a30262623e1ba8bb9",
|
||||||
|
"check_name": "SQL",
|
||||||
|
"message": "Possible SQL injection",
|
||||||
|
"file": "app/models/account.rb",
|
||||||
|
"line": 507,
|
||||||
|
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||||
|
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, limit, offset])",
|
||||||
|
"render_path": null,
|
||||||
|
"location": {
|
||||||
|
"type": "method",
|
||||||
|
"class": "Account",
|
||||||
|
"method": "advanced_search_for"
|
||||||
|
},
|
||||||
|
"user_input": "textsearch",
|
||||||
|
"confidence": "Medium",
|
||||||
|
"note": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"warning_type": "Mass Assignment",
|
"warning_type": "Mass Assignment",
|
||||||
"warning_code": 105,
|
"warning_code": 105,
|
||||||
@ -294,6 +365,6 @@
|
|||||||
"note": ""
|
"note": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2020-06-01 18:18:02 +0200",
|
"updated": "2020-12-07 01:17:13 +0100",
|
||||||
"brakeman_version": "4.8.0"
|
"brakeman_version": "4.10.0"
|
||||||
}
|
}
|
||||||
|
@ -113,3 +113,14 @@ else
|
|||||||
end
|
end
|
||||||
|
|
||||||
Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES }
|
Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES }
|
||||||
|
|
||||||
|
# In some places in the code, we rescue this exception, but we don't always
|
||||||
|
# load the S3 library, so it may be an undefined constant:
|
||||||
|
|
||||||
|
unless defined?(Seahorse)
|
||||||
|
module Seahorse
|
||||||
|
module Client
|
||||||
|
class NetworkingError < StandardError; end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@ -131,6 +131,7 @@ en:
|
|||||||
follows: Follows
|
follows: Follows
|
||||||
header: Header
|
header: Header
|
||||||
inbox_url: Inbox URL
|
inbox_url: Inbox URL
|
||||||
|
invite_request_text: Reasons for joining
|
||||||
invited_by: Invited by
|
invited_by: Invited by
|
||||||
ip: IP
|
ip: IP
|
||||||
joined: Joined
|
joined: Joined
|
||||||
@ -255,6 +256,7 @@ en:
|
|||||||
unsuspend_account: Unsuspend Account
|
unsuspend_account: Unsuspend Account
|
||||||
update_announcement: Update Announcement
|
update_announcement: Update Announcement
|
||||||
update_custom_emoji: Update Custom Emoji
|
update_custom_emoji: Update Custom Emoji
|
||||||
|
update_domain_block: Update Domain Block
|
||||||
update_status: Update Status
|
update_status: Update Status
|
||||||
actions:
|
actions:
|
||||||
assigned_to_self_report: "%{name} assigned report %{target} to themselves"
|
assigned_to_self_report: "%{name} assigned report %{target} to themselves"
|
||||||
@ -295,6 +297,7 @@ en:
|
|||||||
unsuspend_account: "%{name} unsuspended %{target}'s account"
|
unsuspend_account: "%{name} unsuspended %{target}'s account"
|
||||||
update_announcement: "%{name} updated announcement %{target}"
|
update_announcement: "%{name} updated announcement %{target}"
|
||||||
update_custom_emoji: "%{name} updated emoji %{target}"
|
update_custom_emoji: "%{name} updated emoji %{target}"
|
||||||
|
update_domain_block: "%{name} updated domain block for %{target}"
|
||||||
update_status: "%{name} updated status by %{target}"
|
update_status: "%{name} updated status by %{target}"
|
||||||
deleted_status: "(deleted status)"
|
deleted_status: "(deleted status)"
|
||||||
empty: No logs found.
|
empty: No logs found.
|
||||||
@ -437,6 +440,7 @@ en:
|
|||||||
instances:
|
instances:
|
||||||
by_domain: Domain
|
by_domain: Domain
|
||||||
delivery_available: Delivery is available
|
delivery_available: Delivery is available
|
||||||
|
empty: No domains found.
|
||||||
known_accounts:
|
known_accounts:
|
||||||
one: "%{count} known account"
|
one: "%{count} known account"
|
||||||
other: "%{count} known accounts"
|
other: "%{count} known accounts"
|
||||||
@ -514,6 +518,8 @@ en:
|
|||||||
comment:
|
comment:
|
||||||
none: None
|
none: None
|
||||||
created_at: Reported
|
created_at: Reported
|
||||||
|
forwarded: Forwarded
|
||||||
|
forwarded_to: Forwarded to %{domain}
|
||||||
mark_as_resolved: Mark as resolved
|
mark_as_resolved: Mark as resolved
|
||||||
mark_as_unresolved: Mark as unresolved
|
mark_as_unresolved: Mark as unresolved
|
||||||
notes:
|
notes:
|
||||||
@ -583,6 +589,9 @@ en:
|
|||||||
min_invite_role:
|
min_invite_role:
|
||||||
disabled: No one
|
disabled: No one
|
||||||
title: Allow invitations by
|
title: Allow invitations by
|
||||||
|
require_invite_text:
|
||||||
|
desc_html: When registrations require manual approval, make the “Why do you want to join?” invite request text mandatory rather than optional
|
||||||
|
title: Require new users to fill an invite request text
|
||||||
registrations_mode:
|
registrations_mode:
|
||||||
modes:
|
modes:
|
||||||
approved: Approval required for sign up
|
approved: Approval required for sign up
|
||||||
|
@ -79,6 +79,7 @@ defaults: &defaults
|
|||||||
show_domain_blocks: 'disabled'
|
show_domain_blocks: 'disabled'
|
||||||
show_domain_blocks_rationale: 'disabled'
|
show_domain_blocks_rationale: 'disabled'
|
||||||
outgoing_spoilers: ''
|
outgoing_spoilers: ''
|
||||||
|
require_invite_text: false
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
@ -5,34 +5,51 @@
|
|||||||
- [push, 4]
|
- [push, 4]
|
||||||
- [mailers, 2]
|
- [mailers, 2]
|
||||||
- [pull]
|
- [pull]
|
||||||
|
- [scheduler]
|
||||||
|
:scheduler:
|
||||||
|
:listened_queues_only: true
|
||||||
:schedule:
|
:schedule:
|
||||||
scheduled_statuses_scheduler:
|
scheduled_statuses_scheduler:
|
||||||
every: '5m'
|
every: '5m'
|
||||||
class: Scheduler::ScheduledStatusesScheduler
|
class: Scheduler::ScheduledStatusesScheduler
|
||||||
|
queue: scheduler
|
||||||
trending_tags_scheduler:
|
trending_tags_scheduler:
|
||||||
every: '5m'
|
every: '5m'
|
||||||
class: Scheduler::TrendingTagsScheduler
|
class: Scheduler::TrendingTagsScheduler
|
||||||
|
queue: scheduler
|
||||||
media_cleanup_scheduler:
|
media_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||||
class: Scheduler::MediaCleanupScheduler
|
class: Scheduler::MediaCleanupScheduler
|
||||||
|
queue: scheduler
|
||||||
feed_cleanup_scheduler:
|
feed_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * *'
|
||||||
class: Scheduler::FeedCleanupScheduler
|
class: Scheduler::FeedCleanupScheduler
|
||||||
|
queue: scheduler
|
||||||
doorkeeper_cleanup_scheduler:
|
doorkeeper_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * 0'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(0..2) %> * * 0'
|
||||||
class: Scheduler::DoorkeeperCleanupScheduler
|
class: Scheduler::DoorkeeperCleanupScheduler
|
||||||
|
queue: scheduler
|
||||||
user_cleanup_scheduler:
|
user_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *'
|
||||||
class: Scheduler::UserCleanupScheduler
|
class: Scheduler::UserCleanupScheduler
|
||||||
|
queue: scheduler
|
||||||
ip_cleanup_scheduler:
|
ip_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||||
class: Scheduler::IpCleanupScheduler
|
class: Scheduler::IpCleanupScheduler
|
||||||
|
queue: scheduler
|
||||||
email_scheduler:
|
email_scheduler:
|
||||||
cron: '0 10 * * 2'
|
cron: '0 10 * * 2'
|
||||||
class: Scheduler::EmailScheduler
|
class: Scheduler::EmailScheduler
|
||||||
|
queue: scheduler
|
||||||
backup_cleanup_scheduler:
|
backup_cleanup_scheduler:
|
||||||
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
|
||||||
class: Scheduler::BackupCleanupScheduler
|
class: Scheduler::BackupCleanupScheduler
|
||||||
|
queue: scheduler
|
||||||
pghero_scheduler:
|
pghero_scheduler:
|
||||||
cron: '0 0 * * *'
|
cron: '0 0 * * *'
|
||||||
class: Scheduler::PgheroScheduler
|
class: Scheduler::PgheroScheduler
|
||||||
|
queue: scheduler
|
||||||
|
instance_refresh_scheduler:
|
||||||
|
cron: '0 * * * *'
|
||||||
|
class: Scheduler::InstanceRefreshScheduler
|
||||||
|
queue: scheduler
|
||||||
|
5
db/migrate/20200309150742_add_forwarded_to_reports.rb
Normal file
5
db/migrate/20200309150742_add_forwarded_to_reports.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class AddForwardedToReports < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :reports, :forwarded, :boolean
|
||||||
|
end
|
||||||
|
end
|
9
db/migrate/20201206004238_create_instances.rb
Normal file
9
db/migrate/20201206004238_create_instances.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
class CreateInstances < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
create_view :instances, materialized: true
|
||||||
|
|
||||||
|
# To be able to refresh the view concurrently,
|
||||||
|
# at least one unique index is required
|
||||||
|
safety_assured { add_index :instances, :domain, unique: true }
|
||||||
|
end
|
||||||
|
end
|
28
db/schema.rb
28
db/schema.rb
@ -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: 2020_10_17_234926) do
|
ActiveRecord::Schema.define(version: 2020_12_06_004238) 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"
|
||||||
@ -717,6 +717,7 @@ ActiveRecord::Schema.define(version: 2020_10_17_234926) do
|
|||||||
t.bigint "target_account_id", null: false
|
t.bigint "target_account_id", null: false
|
||||||
t.bigint "assigned_account_id"
|
t.bigint "assigned_account_id"
|
||||||
t.string "uri"
|
t.string "uri"
|
||||||
|
t.boolean "forwarded"
|
||||||
t.index ["account_id"], name: "index_reports_on_account_id"
|
t.index ["account_id"], name: "index_reports_on_account_id"
|
||||||
t.index ["target_account_id"], name: "index_reports_on_target_account_id"
|
t.index ["target_account_id"], name: "index_reports_on_target_account_id"
|
||||||
end
|
end
|
||||||
@ -1047,4 +1048,29 @@ ActiveRecord::Schema.define(version: 2020_10_17_234926) do
|
|||||||
add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade
|
add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade
|
||||||
add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade
|
add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade
|
||||||
add_foreign_key "webauthn_credentials", "users"
|
add_foreign_key "webauthn_credentials", "users"
|
||||||
|
|
||||||
|
create_view "instances", materialized: true, sql_definition: <<-SQL
|
||||||
|
WITH domain_counts(domain, accounts_count) AS (
|
||||||
|
SELECT accounts.domain,
|
||||||
|
count(*) AS accounts_count
|
||||||
|
FROM accounts
|
||||||
|
WHERE (accounts.domain IS NOT NULL)
|
||||||
|
GROUP BY accounts.domain
|
||||||
|
)
|
||||||
|
SELECT domain_counts.domain,
|
||||||
|
domain_counts.accounts_count
|
||||||
|
FROM domain_counts
|
||||||
|
UNION
|
||||||
|
SELECT domain_blocks.domain,
|
||||||
|
COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count
|
||||||
|
FROM (domain_blocks
|
||||||
|
LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_blocks.domain)::text)))
|
||||||
|
UNION
|
||||||
|
SELECT domain_allows.domain,
|
||||||
|
COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count
|
||||||
|
FROM (domain_allows
|
||||||
|
LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text)));
|
||||||
|
SQL
|
||||||
|
add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true
|
||||||
|
|
||||||
end
|
end
|
||||||
|
17
db/views/instances_v01.sql
Normal file
17
db/views/instances_v01.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
WITH domain_counts(domain, accounts_count)
|
||||||
|
AS (
|
||||||
|
SELECT domain, COUNT(*) as accounts_count
|
||||||
|
FROM accounts
|
||||||
|
WHERE domain IS NOT NULL
|
||||||
|
GROUP BY domain
|
||||||
|
)
|
||||||
|
SELECT domain, accounts_count
|
||||||
|
FROM domain_counts
|
||||||
|
UNION
|
||||||
|
SELECT domain_blocks.domain, COALESCE(domain_counts.accounts_count, 0)
|
||||||
|
FROM domain_blocks
|
||||||
|
LEFT OUTER JOIN domain_counts ON domain_counts.domain = domain_blocks.domain
|
||||||
|
UNION
|
||||||
|
SELECT domain_allows.domain, COALESCE(domain_counts.accounts_count, 0)
|
||||||
|
FROM domain_allows
|
||||||
|
LEFT OUTER JOIN domain_counts ON domain_counts.domain = domain_allows.domain
|
@ -53,6 +53,8 @@ module Mastodon
|
|||||||
custom_emojis_count = custom_emojis.count
|
custom_emojis_count = custom_emojis.count
|
||||||
custom_emojis.destroy_all unless options[:dry_run]
|
custom_emojis.destroy_all unless options[:dry_run]
|
||||||
|
|
||||||
|
Instance.refresh unless options[:dry_run]
|
||||||
|
|
||||||
say("Removed #{custom_emojis_count} custom emojis", :green)
|
say("Removed #{custom_emojis_count} custom emojis", :green)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -83,7 +85,7 @@ module Mastodon
|
|||||||
processed = Concurrent::AtomicFixnum.new(0)
|
processed = Concurrent::AtomicFixnum.new(0)
|
||||||
failed = Concurrent::AtomicFixnum.new(0)
|
failed = Concurrent::AtomicFixnum.new(0)
|
||||||
start_at = Time.now.to_f
|
start_at = Time.now.to_f
|
||||||
seed = start ? [start] : Account.remote.domains
|
seed = start ? [start] : Instance.pluck(:domain)
|
||||||
blocked_domains = Regexp.new('\\.?' + DomainBlock.where(severity: 1).pluck(:domain).join('|') + '$')
|
blocked_domains = Regexp.new('\\.?' + DomainBlock.where(severity: 1).pluck(:domain).join('|') + '$')
|
||||||
progress = create_progress_bar
|
progress = create_progress_bar
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
MIN_SUPPORTED_VERSION = 2019_10_01_213028
|
MIN_SUPPORTED_VERSION = 2019_10_01_213028
|
||||||
MAX_SUPPORTED_VERSION = 2020_10_17_234926
|
MAX_SUPPORTED_VERSION = 2020_12_06_004238
|
||||||
|
|
||||||
# Stubs to enjoy ActiveRecord queries while not depending on a particular
|
# Stubs to enjoy ActiveRecord queries while not depending on a particular
|
||||||
# version of the code/database
|
# version of the code/database
|
||||||
|
@ -39,6 +39,23 @@ module Paperclip
|
|||||||
def default_url(style_name = default_style)
|
def default_url(style_name = default_style)
|
||||||
@url_generator.for_as_default(style_name)
|
@url_generator.for_as_default(style_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
STOPLIGHT_THRESHOLD = 10
|
||||||
|
STOPLIGHT_COOLDOWN = 30
|
||||||
|
|
||||||
|
# We overwrite this method to put a circuit breaker around
|
||||||
|
# calls to object storage, to stop hitting APIs that are slow
|
||||||
|
# to respond or don't respond at all and as such minimize the
|
||||||
|
# impact of object storage outages on application throughput
|
||||||
|
def save
|
||||||
|
Stoplight('object-storage') { super }.with_threshold(STOPLIGHT_THRESHOLD).with_cool_off_time(STOPLIGHT_COOLDOWN).with_error_handler do |error, handle|
|
||||||
|
if error.is_a?(Seahorse::Client::NetworkingError)
|
||||||
|
handle.call(error)
|
||||||
|
else
|
||||||
|
raise error
|
||||||
|
end
|
||||||
|
end.run
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
BIN
public/shortcuts/direct.png
Normal file
BIN
public/shortcuts/direct.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
BIN
public/shortcuts/new-status.png
Normal file
BIN
public/shortcuts/new-status.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
BIN
public/shortcuts/notifications.png
Normal file
BIN
public/shortcuts/notifications.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
BIN
public/shortcuts/profile.png
Normal file
BIN
public/shortcuts/profile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
@ -9,10 +9,10 @@ RSpec.describe Admin::InstancesController, type: :controller do
|
|||||||
|
|
||||||
describe 'GET #index' do
|
describe 'GET #index' do
|
||||||
around do |example|
|
around do |example|
|
||||||
default_per_page = Account.default_per_page
|
default_per_page = Instance.default_per_page
|
||||||
Account.paginates_per 1
|
Instance.paginates_per 1
|
||||||
example.run
|
example.run
|
||||||
Account.paginates_per default_per_page
|
Instance.paginates_per default_per_page
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders instances' do
|
it 'renders instances' do
|
||||||
|
@ -149,22 +149,4 @@ RSpec.describe StatusesHelper, type: :helper do
|
|||||||
expect(css_class).to eq 'h-cite'
|
expect(css_class).to eq 'h-cite'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#rtl?' do
|
|
||||||
it 'is false if text is empty' do
|
|
||||||
expect(helper).not_to be_rtl ''
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is false if there are no right to left characters' do
|
|
||||||
expect(helper).not_to be_rtl 'hello world'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is false if right to left characters are fewer than 1/3 of total text' do
|
|
||||||
expect(helper).not_to be_rtl 'hello ݟ world'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is true if right to left characters are greater than 1/3 of total text' do
|
|
||||||
expect(helper).to be_rtl 'aaݟaaݟ'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -440,13 +440,6 @@ RSpec.describe Account, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.domains' do
|
|
||||||
it 'returns domains' do
|
|
||||||
Fabricate(:account, domain: 'domain')
|
|
||||||
expect(Account.remote.domains).to match_array(['domain'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#statuses_count' do
|
describe '#statuses_count' do
|
||||||
subject { Fabricate(:account) }
|
subject { Fabricate(:account) }
|
||||||
|
|
||||||
@ -737,20 +730,6 @@ RSpec.describe Account, type: :model do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'by_domain_accounts' do
|
|
||||||
it 'returns accounts grouped by domain sorted by accounts' do
|
|
||||||
2.times { Fabricate(:account, domain: 'example.com') }
|
|
||||||
Fabricate(:account, domain: 'example2.com')
|
|
||||||
|
|
||||||
results = Account.where('id > 0').by_domain_accounts
|
|
||||||
expect(results.length).to eq 2
|
|
||||||
expect(results.first.domain).to eq 'example.com'
|
|
||||||
expect(results.first.accounts_count).to eq 2
|
|
||||||
expect(results.last.domain).to eq 'example2.com'
|
|
||||||
expect(results.last.accounts_count).to eq 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'local' do
|
describe 'local' do
|
||||||
it 'returns an array of accounts who do not have a domain' do
|
it 'returns an array of accounts who do not have a domain' do
|
||||||
account_1 = Fabricate(:account, domain: nil)
|
account_1 = Fabricate(:account, domain: nil)
|
||||||
|
@ -5533,9 +5533,9 @@ inherits@2.0.3:
|
|||||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||||
|
|
||||||
ini@^1.3.4, ini@^1.3.5:
|
ini@^1.3.4, ini@^1.3.5:
|
||||||
version "1.3.5"
|
version "1.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
|
||||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
|
||||||
|
|
||||||
inquirer@^0.12.0:
|
inquirer@^0.12.0:
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user