Merge pull request #1662 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
@ -14,7 +14,7 @@ module Admin
|
||||
else
|
||||
@account = @account_moderation_note.target_account
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
@warnings = @account.strikes.custom.latest
|
||||
|
||||
render template: 'admin/accounts/show'
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ module Admin
|
||||
@deletion_request = @account.deletion_request
|
||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
@warnings = @account.strikes.custom.latest
|
||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||
end
|
||||
|
||||
|
@ -14,20 +14,17 @@ module Admin
|
||||
if params[:create_and_resolve]
|
||||
@report.resolve!(current_account)
|
||||
log_action :resolve, @report
|
||||
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
|
||||
return
|
||||
end
|
||||
|
||||
if params[:create_and_unresolve]
|
||||
elsif params[:create_and_unresolve]
|
||||
@report.unresolve!
|
||||
log_action :reopen, @report
|
||||
end
|
||||
|
||||
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
|
||||
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
|
||||
else
|
||||
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
|
||||
@form = Form::StatusBatch.new
|
||||
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
||||
@action_logs = @report.history.includes(:target)
|
||||
@form = Admin::StatusBatchAction.new
|
||||
@statuses = @report.statuses.with_includes
|
||||
|
||||
render template: 'admin/reports/show'
|
||||
end
|
||||
@ -41,6 +38,14 @@ module Admin
|
||||
|
||||
private
|
||||
|
||||
def after_create_redirect_path
|
||||
if params[:create_and_resolve]
|
||||
admin_reports_path
|
||||
else
|
||||
admin_report_path(@report)
|
||||
end
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:report_note).permit(
|
||||
:content,
|
||||
|
@ -1,44 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class ReportedStatusesController < BaseController
|
||||
before_action :set_report
|
||||
|
||||
def create
|
||||
authorize :status, :update?
|
||||
|
||||
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
||||
|
||||
redirect_to admin_report_path(@report)
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
|
||||
|
||||
redirect_to admin_report_path(@report)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def status_params
|
||||
params.require(:status).permit(:sensitive)
|
||||
end
|
||||
|
||||
def form_status_batch_params
|
||||
params.require(:form_status_batch).permit(status_ids: [])
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:nsfw_on]
|
||||
'nsfw_on'
|
||||
elsif params[:nsfw_off]
|
||||
'nsfw_off'
|
||||
elsif params[:delete]
|
||||
'delete'
|
||||
end
|
||||
end
|
||||
|
||||
def set_report
|
||||
@report = Report.find(params[:report_id])
|
||||
end
|
||||
end
|
||||
end
|
@ -13,8 +13,10 @@ module Admin
|
||||
authorize @report, :show?
|
||||
|
||||
@report_note = @report.notes.new
|
||||
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
|
||||
@form = Form::StatusBatch.new
|
||||
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
||||
@action_logs = @report.history.includes(:target)
|
||||
@form = Admin::StatusBatchAction.new
|
||||
@statuses = @report.statuses.with_includes
|
||||
end
|
||||
|
||||
def assign_to_self
|
||||
|
@ -2,71 +2,57 @@
|
||||
|
||||
module Admin
|
||||
class StatusesController < BaseController
|
||||
helper_method :current_params
|
||||
|
||||
before_action :set_account
|
||||
before_action :set_statuses
|
||||
|
||||
PER_PAGE = 20
|
||||
|
||||
def index
|
||||
authorize :status, :index?
|
||||
|
||||
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
|
||||
|
||||
if params[:media]
|
||||
@statuses = @statuses.merge(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)).reorder('statuses.id desc')
|
||||
end
|
||||
|
||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
||||
@form = Form::StatusBatch.new
|
||||
@status_batch_action = Admin::StatusBatchAction.new
|
||||
end
|
||||
|
||||
def show
|
||||
authorize :status, :index?
|
||||
|
||||
@statuses = @account.statuses.where(id: params[:id])
|
||||
authorize @statuses.first, :show?
|
||||
|
||||
@form = Form::StatusBatch.new
|
||||
end
|
||||
|
||||
def create
|
||||
authorize :status, :update?
|
||||
|
||||
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
||||
|
||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||
def batch
|
||||
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
|
||||
@status_batch_action.save!
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
|
||||
|
||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||
ensure
|
||||
redirect_to after_create_redirect_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def form_status_batch_params
|
||||
params.require(:form_status_batch).permit(:action, status_ids: [])
|
||||
def admin_status_batch_action_params
|
||||
params.require(:admin_status_batch_action).permit(status_ids: [])
|
||||
end
|
||||
|
||||
def after_create_redirect_path
|
||||
if @status_batch_action.report_id.present?
|
||||
admin_report_path(@status_batch_action.report_id)
|
||||
else
|
||||
admin_account_statuses_path(params[:account_id], current_params)
|
||||
end
|
||||
end
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def current_params
|
||||
page = (params[:page] || 1).to_i
|
||||
def set_statuses
|
||||
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
{
|
||||
media: params[:media],
|
||||
page: page > 1 && page,
|
||||
}.select { |_, value| value.present? }
|
||||
def filter_params
|
||||
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
|
||||
end
|
||||
|
||||
def action_from_button
|
||||
if params[:nsfw_on]
|
||||
'nsfw_on'
|
||||
elsif params[:nsfw_off]
|
||||
'nsfw_off'
|
||||
if params[:report]
|
||||
'report'
|
||||
elsif params[:remove_from_report]
|
||||
'remove_from_report'
|
||||
elsif params[:delete]
|
||||
'delete'
|
||||
end
|
||||
|
@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }
|
||||
before_action :require_staff!
|
||||
before_action :set_account
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::AccountsController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
include Authorization
|
||||
include AccountableConcern
|
||||
|
||||
LIMIT = 100
|
||||
|
||||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
|
||||
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
|
||||
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
|
||||
before_action :require_staff!
|
||||
before_action :set_accounts, only: :index
|
||||
before_action :set_account, except: :index
|
||||
|
@ -3,6 +3,7 @@
|
||||
class Api::V1::Admin::DimensionsController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
before_action :require_staff!
|
||||
before_action :set_dimensions
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
class Api::V1::Admin::MeasuresController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
before_action :require_staff!
|
||||
before_action :set_measures
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::ReportsController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
include Authorization
|
||||
include AccountableConcern
|
||||
|
||||
LIMIT = 100
|
||||
|
||||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
|
||||
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
|
||||
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
|
||||
before_action :require_staff!
|
||||
before_action :set_reports, only: :index
|
||||
before_action :set_report, except: :index
|
||||
@ -32,6 +34,12 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
||||
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @report, :update?
|
||||
@report.update!(report_params)
|
||||
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||
end
|
||||
|
||||
def assign_to_self
|
||||
authorize @report, :update?
|
||||
@report.update!(assigned_account_id: current_account.id)
|
||||
@ -74,6 +82,10 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
||||
ReportFilter.new(filter_params).results
|
||||
end
|
||||
|
||||
def report_params
|
||||
params.permit(:category, rule_ids: [])
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.permit(*FILTER_PARAMS)
|
||||
end
|
||||
|
@ -3,6 +3,7 @@
|
||||
class Api::V1::Admin::RetentionController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
before_action :require_staff!
|
||||
before_action :set_cohorts
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
||||
protect_from_forgery with: :exception
|
||||
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||
before_action :require_staff!
|
||||
before_action :set_tags
|
||||
|
||||
|
@ -13,6 +13,7 @@ module Admin::FilterHelper
|
||||
RelationshipFilter::KEYS,
|
||||
AnnouncementFilter::KEYS,
|
||||
Admin::ActionLogFilter::KEYS,
|
||||
Admin::StatusFilter::KEYS,
|
||||
].flatten.freeze
|
||||
|
||||
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||
|
@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from 'flavours/glitch/util/api';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
other: { id: 'report.categories.other', defaultMessage: 'Other' },
|
||||
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
|
||||
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
|
||||
});
|
||||
|
||||
class Category extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { id, disabled, onSelect } = this.props;
|
||||
|
||||
if (!disabled) {
|
||||
onSelect(id);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { id, text, disabled, selected, children } = this.props;
|
||||
|
||||
return (
|
||||
<div tabIndex='0' role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
|
||||
{selected && <input type='hidden' name='report[category]' value={id} />}
|
||||
|
||||
<div className='report-reason-selector__category__label'>
|
||||
<span className={classNames('poll__input', { active: selected, disabled })} />
|
||||
{text}
|
||||
</div>
|
||||
|
||||
{(selected && children) && (
|
||||
<div className='report-reason-selector__category__rules'>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Rule extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { id, disabled, onToggle } = this.props;
|
||||
|
||||
if (!disabled) {
|
||||
onToggle(id);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { id, text, disabled, selected } = this.props;
|
||||
|
||||
return (
|
||||
<div tabIndex='0' role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
|
||||
<span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
|
||||
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class ReportReasonSelector extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
rule_ids: PropTypes.arrayOf(PropTypes.string),
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
category: this.props.category,
|
||||
rule_ids: this.props.rule_ids || [],
|
||||
rules: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
api().get('/api/v1/instance').then(res => {
|
||||
this.setState({
|
||||
rules: res.data.rules,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
_save = () => {
|
||||
const { id, disabled } = this.props;
|
||||
const { category, rule_ids } = this.state;
|
||||
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
api().put(`/api/v1/admin/reports/${id}`, {
|
||||
category,
|
||||
rule_ids,
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
handleSelect = id => {
|
||||
this.setState({ category: id }, () => this._save());
|
||||
};
|
||||
|
||||
handleToggle = id => {
|
||||
const { rule_ids } = this.state;
|
||||
|
||||
if (rule_ids.includes(id)) {
|
||||
this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save());
|
||||
} else {
|
||||
this.setState({ rule_ids: [...rule_ids, id] }, () => this._save());
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { disabled, intl } = this.props;
|
||||
const { rules, category, rule_ids } = this.state;
|
||||
|
||||
return (
|
||||
<div className='report-reason-selector'>
|
||||
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
|
||||
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
|
||||
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
|
||||
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
|
||||
</Category>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -595,39 +595,44 @@ body,
|
||||
|
||||
.log-entry {
|
||||
line-height: 20px;
|
||||
padding: 15px 0;
|
||||
padding: 15px;
|
||||
padding-left: 15px * 2 + 40px;
|
||||
background: $ui-base-color;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: $darker-text-color;
|
||||
font-size: 14px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
margin-right: 10px;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 15px;
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
border-radius: 4px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
&__title {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@ -643,6 +648,14 @@ body,
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.name-tag,
|
||||
@ -671,8 +684,9 @@ a.inline-name-tag,
|
||||
|
||||
a.name-tag,
|
||||
.name-tag {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: top;
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
@ -1130,3 +1144,287 @@ a.sparkline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-reason-selector {
|
||||
border-radius: 4px;
|
||||
background: $ui-base-color;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__category {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
&__rules {
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&__rule {
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: grid;
|
||||
grid-gap: 15px;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
|
||||
&__details {
|
||||
&__item {
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
padding: 15px 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
font-weight: 600;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
|
||||
.report-header__details__item {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-card {
|
||||
background: $ui-base-color;
|
||||
border-radius: 4px;
|
||||
|
||||
&__header {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
height: 128px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: darken($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-top: -25px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&__avatar {
|
||||
padding: 15px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: darken($ui-base-color, 8%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
color: $darker-text-color;
|
||||
padding-bottom: 15px;
|
||||
font-size: 15px;
|
||||
|
||||
bdi {
|
||||
display: block;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bio {
|
||||
padding: 0 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
max-height: 18px * 2;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 15px;
|
||||
background: linear-gradient(to left, $ui-base-color, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
|
||||
&__button {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&__counters {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
|
||||
&__item {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
color: $primary-text-color;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
color: $darker-text-color;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-notes {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__item {
|
||||
background: $ui-base-color;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
padding-left: 15px * 2 + 40px;
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 15px;
|
||||
border-radius: 4px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
color: $darker-text-color;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.username a {
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
margin-right: 5px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
time {
|
||||
margin-left: 5px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
color: $primary-text-color;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
white-space: pre-wrap;
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
border: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 18px;
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__button {
|
||||
flex: 0 0 auto;
|
||||
width: 100px;
|
||||
padding: 15px;
|
||||
padding-right: 0;
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
padding: 15px;
|
||||
font-size: 14px;
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -150,6 +150,21 @@
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border-color: $dark-text-color;
|
||||
|
||||
&.active {
|
||||
background: $dark-text-color;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: $dark-text-color;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__number {
|
||||
|
@ -3,7 +3,7 @@ export const profileLink = '/settings/profile';
|
||||
export const signOutLink = '/auth/sign_out';
|
||||
export const termsLink = '/terms';
|
||||
export const accountAdminLink = (id) => `/admin/accounts/${id}`;
|
||||
export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`;
|
||||
export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses?id=${status_id}`;
|
||||
export const filterEditLink = (id) => `/filters/${id}/edit`;
|
||||
export const relationshipsLink = '/relationships';
|
||||
export const securityLink = '/auth/edit';
|
||||
|
159
app/javascript/mastodon/components/admin/ReportReasonSelector.js
Normal file
159
app/javascript/mastodon/components/admin/ReportReasonSelector.js
Normal file
@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from 'mastodon/api';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
other: { id: 'report.categories.other', defaultMessage: 'Other' },
|
||||
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
|
||||
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
|
||||
});
|
||||
|
||||
class Category extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { id, disabled, onSelect } = this.props;
|
||||
|
||||
if (!disabled) {
|
||||
onSelect(id);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { id, text, disabled, selected, children } = this.props;
|
||||
|
||||
return (
|
||||
<div tabIndex='0' role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
|
||||
{selected && <input type='hidden' name='report[category]' value={id} />}
|
||||
|
||||
<div className='report-reason-selector__category__label'>
|
||||
<span className={classNames('poll__input', { active: selected, disabled })} />
|
||||
{text}
|
||||
</div>
|
||||
|
||||
{(selected && children) && (
|
||||
<div className='report-reason-selector__category__rules'>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Rule extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { id, disabled, onToggle } = this.props;
|
||||
|
||||
if (!disabled) {
|
||||
onToggle(id);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { id, text, disabled, selected } = this.props;
|
||||
|
||||
return (
|
||||
<div tabIndex='0' role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
|
||||
<span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
|
||||
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class ReportReasonSelector extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
rule_ids: PropTypes.arrayOf(PropTypes.string),
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
category: this.props.category,
|
||||
rule_ids: this.props.rule_ids || [],
|
||||
rules: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
api().get('/api/v1/instance').then(res => {
|
||||
this.setState({
|
||||
rules: res.data.rules,
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
_save = () => {
|
||||
const { id, disabled } = this.props;
|
||||
const { category, rule_ids } = this.state;
|
||||
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
api().put(`/api/v1/admin/reports/${id}`, {
|
||||
category,
|
||||
rule_ids,
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
handleSelect = id => {
|
||||
this.setState({ category: id }, () => this._save());
|
||||
};
|
||||
|
||||
handleToggle = id => {
|
||||
const { rule_ids } = this.state;
|
||||
|
||||
if (rule_ids.includes(id)) {
|
||||
this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save());
|
||||
} else {
|
||||
this.setState({ rule_ids: [...rule_ids, id] }, () => this._save());
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { disabled, intl } = this.props;
|
||||
const { rules, category, rule_ids } = this.state;
|
||||
|
||||
return (
|
||||
<div className='report-reason-selector'>
|
||||
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
|
||||
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
|
||||
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
|
||||
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
|
||||
</Category>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -291,7 +291,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
if (isStaff) {
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
|
||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,7 +245,7 @@ class ActionBar extends React.PureComponent {
|
||||
if (isStaff) {
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
|
||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -533,6 +533,10 @@ ul {
|
||||
}
|
||||
}
|
||||
|
||||
ul.rules-list {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) {
|
||||
body {
|
||||
min-height: 1024px !important;
|
||||
|
@ -595,39 +595,44 @@ body,
|
||||
|
||||
.log-entry {
|
||||
line-height: 20px;
|
||||
padding: 15px 0;
|
||||
padding: 15px;
|
||||
padding-left: 15px * 2 + 40px;
|
||||
background: $ui-base-color;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 4%);
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
color: $darker-text-color;
|
||||
font-size: 14px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
margin-right: 10px;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 15px;
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
border-radius: 4px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
&__title {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@ -643,6 +648,14 @@ body,
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.name-tag,
|
||||
@ -671,8 +684,9 @@ a.inline-name-tag,
|
||||
|
||||
a.name-tag,
|
||||
.name-tag {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: top;
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
@ -1130,3 +1144,287 @@ a.sparkline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-reason-selector {
|
||||
border-radius: 4px;
|
||||
background: $ui-base-color;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__category {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
&__rules {
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
&__rule {
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: grid;
|
||||
grid-gap: 15px;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
|
||||
&__details {
|
||||
&__item {
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
padding: 15px 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
font-weight: 600;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
|
||||
.report-header__details__item {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-card {
|
||||
background: $ui-base-color;
|
||||
border-radius: 4px;
|
||||
|
||||
&__header {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
height: 128px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background: darken($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-top: -25px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&__avatar {
|
||||
padding: 15px;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: darken($ui-base-color, 8%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
color: $darker-text-color;
|
||||
padding-bottom: 15px;
|
||||
font-size: 15px;
|
||||
|
||||
bdi {
|
||||
display: block;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bio {
|
||||
padding: 0 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
max-height: 18px * 2;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 15px;
|
||||
background: linear-gradient(to left, $ui-base-color, transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
|
||||
&__button {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&__counters {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
|
||||
&__item {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
color: $primary-text-color;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
color: $darker-text-color;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-notes {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__item {
|
||||
background: $ui-base-color;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
padding-left: 15px * 2 + 40px;
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($ui-base-color, 4%);
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 15px;
|
||||
border-radius: 4px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
color: $darker-text-color;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.username a {
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
margin-right: 5px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
time {
|
||||
margin-left: 5px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
color: $primary-text-color;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
white-space: pre-wrap;
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.report-actions {
|
||||
border: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 18px;
|
||||
border-bottom: 1px solid darken($ui-base-color, 8%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&__button {
|
||||
flex: 0 0 auto;
|
||||
width: 100px;
|
||||
padding: 15px;
|
||||
padding-right: 0;
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
padding: 15px;
|
||||
font-size: 14px;
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +143,21 @@
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border-color: $dark-text-color;
|
||||
|
||||
&.active {
|
||||
background: $dark-text-color;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: $dark-text-color;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__number {
|
||||
|
@ -6,11 +6,11 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure:
|
||||
end
|
||||
|
||||
def total
|
||||
Report.resolved.where(updated_at: time_period).count
|
||||
Report.resolved.where(action_taken_at: time_period).count
|
||||
end
|
||||
|
||||
def previous_total
|
||||
Report.resolved.where(updated_at: previous_time_period).count
|
||||
Report.resolved.where(action_taken_at: previous_time_period).count
|
||||
end
|
||||
|
||||
def data
|
||||
@ -19,8 +19,7 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure:
|
||||
WITH resolved_reports AS (
|
||||
SELECT reports.id
|
||||
FROM reports
|
||||
WHERE action_taken
|
||||
AND date_trunc('day', reports.updated_at)::date = axis.period
|
||||
WHERE date_trunc('day', reports.action_taken_at)::date = axis.period
|
||||
)
|
||||
SELECT count(*) FROM resolved_reports
|
||||
) AS value
|
||||
|
@ -160,11 +160,11 @@ class UserMailer < Devise::Mailer
|
||||
end
|
||||
end
|
||||
|
||||
def warning(user, warning, status_ids = nil)
|
||||
def warning(user, warning)
|
||||
@resource = user
|
||||
@warning = warning
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
@statuses = Status.where(id: status_ids).includes(:account) if status_ids.is_a?(Array)
|
||||
@statuses = @warning.statuses.includes(:account, :preloadable_poll, :media_attachments, active_mentions: [:account])
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email,
|
||||
|
@ -10,14 +10,30 @@
|
||||
# text :text default(""), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# report_id :bigint(8)
|
||||
# status_ids :string is an Array
|
||||
#
|
||||
|
||||
class AccountWarning < ApplicationRecord
|
||||
enum action: %i(none disable sensitive silence suspend), _suffix: :action
|
||||
enum action: {
|
||||
none: 0,
|
||||
disable: 1_000,
|
||||
delete_statuses: 1_500,
|
||||
sensitive: 2_000,
|
||||
silence: 3_000,
|
||||
suspend: 4_000,
|
||||
}, _suffix: :action
|
||||
|
||||
belongs_to :account, inverse_of: :account_warnings
|
||||
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings
|
||||
belongs_to :target_account, class_name: 'Account', inverse_of: :strikes
|
||||
belongs_to :report, optional: true
|
||||
|
||||
scope :latest, -> { order(created_at: :desc) }
|
||||
has_one :appeal, dependent: :destroy
|
||||
|
||||
scope :latest, -> { order(id: :desc) }
|
||||
scope :custom, -> { where.not(text: '') }
|
||||
|
||||
def statuses
|
||||
Status.with_discarded.where(id: status_ids || [])
|
||||
end
|
||||
end
|
||||
|
@ -33,7 +33,7 @@ class Admin::AccountAction
|
||||
def save!
|
||||
ApplicationRecord.transaction do
|
||||
process_action!
|
||||
process_warning!
|
||||
process_strike!
|
||||
end
|
||||
|
||||
process_email!
|
||||
@ -74,20 +74,14 @@ class Admin::AccountAction
|
||||
end
|
||||
end
|
||||
|
||||
def process_warning!
|
||||
return unless warnable?
|
||||
|
||||
authorize(target_account, :warn?)
|
||||
|
||||
@warning = AccountWarning.create!(target_account: target_account,
|
||||
account: current_account,
|
||||
action: type,
|
||||
text: text_for_warning)
|
||||
|
||||
# A log entry is only interesting if the warning contains
|
||||
# custom text from someone. Otherwise it's just noise.
|
||||
|
||||
log_action(:create, warning) if warning.text.present?
|
||||
def process_strike!
|
||||
@warning = target_account.strikes.create!(
|
||||
account: current_account,
|
||||
report: report,
|
||||
action: type,
|
||||
text: text_for_warning,
|
||||
status_ids: status_ids
|
||||
)
|
||||
end
|
||||
|
||||
def process_reports!
|
||||
@ -143,7 +137,7 @@ class Admin::AccountAction
|
||||
end
|
||||
|
||||
def process_email!
|
||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
|
||||
UserMailer.warning(target_account.user, warning).deliver_later! if warnable?
|
||||
end
|
||||
|
||||
def warnable?
|
||||
@ -151,7 +145,7 @@ class Admin::AccountAction
|
||||
end
|
||||
|
||||
def status_ids
|
||||
report.status_ids if report && include_statuses
|
||||
report.status_ids if with_report? && include_statuses
|
||||
end
|
||||
|
||||
def reports
|
||||
|
92
app/models/admin/status_batch_action.rb
Normal file
92
app/models/admin/status_batch_action.rb
Normal file
@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::StatusBatchAction
|
||||
include ActiveModel::Model
|
||||
include AccountableConcern
|
||||
include Authorization
|
||||
|
||||
attr_accessor :current_account, :type,
|
||||
:status_ids, :report_id
|
||||
|
||||
def save!
|
||||
process_action!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def statuses
|
||||
Status.with_discarded.where(id: status_ids)
|
||||
end
|
||||
|
||||
def process_action!
|
||||
return if status_ids.empty?
|
||||
|
||||
case type
|
||||
when 'delete'
|
||||
handle_delete!
|
||||
when 'report'
|
||||
handle_report!
|
||||
when 'remove_from_report'
|
||||
handle_remove_from_report!
|
||||
end
|
||||
end
|
||||
|
||||
def handle_delete!
|
||||
statuses.each { |status| authorize(status, :destroy?) }
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
statuses.each do |status|
|
||||
status.discard
|
||||
log_action(:destroy, status)
|
||||
end
|
||||
|
||||
if with_report?
|
||||
report.resolve!(current_account)
|
||||
log_action(:resolve, report)
|
||||
end
|
||||
|
||||
@warning = target_account.strikes.create!(
|
||||
action: :delete_statuses,
|
||||
account: current_account,
|
||||
report: report,
|
||||
status_ids: status_ids
|
||||
)
|
||||
|
||||
statuses.each { |status| Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true) } unless target_account.local?
|
||||
end
|
||||
|
||||
UserMailer.warning(target_account.user, @warning).deliver_later! if target_account.local?
|
||||
RemovalWorker.push_bulk(status_ids) { |status_id| [status_id, preserve: target_account.local?, immediate: !target_account.local?] }
|
||||
end
|
||||
|
||||
def handle_report!
|
||||
@report = Report.new(report_params) unless with_report?
|
||||
@report.status_ids = (@report.status_ids + status_ids.map(&:to_i)).uniq
|
||||
@report.save!
|
||||
|
||||
@report_id = @report.id
|
||||
end
|
||||
|
||||
def handle_remove_from_report!
|
||||
return unless with_report?
|
||||
|
||||
report.status_ids -= status_ids.map(&:to_i)
|
||||
report.save!
|
||||
end
|
||||
|
||||
def report
|
||||
@report ||= Report.find(report_id) if report_id.present?
|
||||
end
|
||||
|
||||
def with_report?
|
||||
!report.nil?
|
||||
end
|
||||
|
||||
def target_account
|
||||
@target_account ||= statuses.first.account
|
||||
end
|
||||
|
||||
def report_params
|
||||
{ account: current_account, target_account: target_account }
|
||||
end
|
||||
end
|
41
app/models/admin/status_filter.rb
Normal file
41
app/models/admin/status_filter.rb
Normal file
@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::StatusFilter
|
||||
KEYS = %i(
|
||||
media
|
||||
id
|
||||
report_id
|
||||
).freeze
|
||||
|
||||
attr_reader :params
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def results
|
||||
scope = @account.statuses.where(visibility: [:public, :unlisted])
|
||||
|
||||
params.each do |key, value|
|
||||
next if %w(page report_id).include?(key.to_s)
|
||||
|
||||
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
|
||||
end
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scope_for(key, value)
|
||||
case key.to_s
|
||||
when 'media'
|
||||
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
|
||||
when 'id'
|
||||
Status.where(id: value)
|
||||
else
|
||||
raise "Unknown filter: #{key}"
|
||||
end
|
||||
end
|
||||
end
|
@ -42,7 +42,7 @@ module AccountAssociations
|
||||
has_many :account_moderation_notes, dependent: :destroy, inverse_of: :account
|
||||
has_many :targeted_moderation_notes, class_name: 'AccountModerationNote', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
|
||||
has_many :account_warnings, dependent: :destroy, inverse_of: :account
|
||||
has_many :targeted_account_warnings, class_name: 'AccountWarning', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
|
||||
has_many :strikes, class_name: 'AccountWarning', foreign_key: :target_account_id, dependent: :destroy, inverse_of: :target_account
|
||||
|
||||
# Lists (that the account is on, not owned by the account)
|
||||
has_many :list_accounts, inverse_of: :account, dependent: :destroy
|
||||
|
@ -1,45 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Form::StatusBatch
|
||||
include ActiveModel::Model
|
||||
include AccountableConcern
|
||||
|
||||
attr_accessor :status_ids, :action, :current_account
|
||||
|
||||
def save
|
||||
case action
|
||||
when 'nsfw_on', 'nsfw_off'
|
||||
change_sensitive(action == 'nsfw_on')
|
||||
when 'delete'
|
||||
delete_statuses
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def change_sensitive(sensitive)
|
||||
media_attached_status_ids = MediaAttachment.where(status_id: status_ids).pluck(:status_id)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
Status.where(id: media_attached_status_ids).reorder(nil).find_each do |status|
|
||||
status.update!(sensitive: sensitive)
|
||||
log_action :update, status
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
false
|
||||
end
|
||||
|
||||
def delete_statuses
|
||||
Status.where(id: status_ids).reorder(nil).find_each do |status|
|
||||
status.discard
|
||||
RemovalWorker.perform_async(status.id, immediate: true)
|
||||
Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true)
|
||||
log_action :destroy, status
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
@ -6,7 +6,6 @@
|
||||
# id :bigint(8) not null, primary key
|
||||
# status_ids :bigint(8) default([]), not null, is an Array
|
||||
# comment :text default(""), not null
|
||||
# action_taken :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint(8) not null
|
||||
@ -15,9 +14,14 @@
|
||||
# assigned_account_id :bigint(8)
|
||||
# uri :string
|
||||
# forwarded :boolean
|
||||
# category :integer default("other"), not null
|
||||
# action_taken_at :datetime
|
||||
# rule_ids :bigint(8) is an Array
|
||||
#
|
||||
|
||||
class Report < ApplicationRecord
|
||||
self.ignored_columns = %w(action_taken)
|
||||
|
||||
include Paginable
|
||||
include RateLimitable
|
||||
|
||||
@ -30,11 +34,17 @@ class Report < ApplicationRecord
|
||||
|
||||
has_many :notes, class_name: 'ReportNote', foreign_key: :report_id, inverse_of: :report, dependent: :destroy
|
||||
|
||||
scope :unresolved, -> { where(action_taken: false) }
|
||||
scope :resolved, -> { where(action_taken: true) }
|
||||
scope :unresolved, -> { where(action_taken_at: nil) }
|
||||
scope :resolved, -> { where.not(action_taken_at: nil) }
|
||||
scope :with_accounts, -> { includes([:account, :target_account, :action_taken_by_account, :assigned_account].index_with({ user: [:invite_request, :invite] })) }
|
||||
|
||||
validates :comment, length: { maximum: 1000 }
|
||||
validates :comment, length: { maximum: 1_000 }
|
||||
|
||||
enum category: {
|
||||
other: 0,
|
||||
spam: 1_000,
|
||||
violation: 2_000,
|
||||
}
|
||||
|
||||
def local?
|
||||
false # Force uri_for to use uri attribute
|
||||
@ -47,13 +57,17 @@ class Report < ApplicationRecord
|
||||
end
|
||||
|
||||
def statuses
|
||||
Status.with_discarded.where(id: status_ids).includes(:account, :media_attachments, :mentions)
|
||||
Status.with_discarded.where(id: status_ids)
|
||||
end
|
||||
|
||||
def media_attachments
|
||||
MediaAttachment.where(status_id: status_ids)
|
||||
end
|
||||
|
||||
def rules
|
||||
Rule.with_discarded.where(id: rule_ids)
|
||||
end
|
||||
|
||||
def assign_to_self!(current_account)
|
||||
update!(assigned_account_id: current_account.id)
|
||||
end
|
||||
@ -63,22 +77,19 @@ class Report < ApplicationRecord
|
||||
end
|
||||
|
||||
def resolve!(acting_account)
|
||||
if account_id == -99 && target_account.trust_level == Account::TRUST_LEVELS[:untrusted]
|
||||
# This is an automated report and it is being dismissed, so it's
|
||||
# a false positive, in which case update the account's trust level
|
||||
# to prevent further spam checks
|
||||
|
||||
target_account.update(trust_level: Account::TRUST_LEVELS[:trusted])
|
||||
end
|
||||
|
||||
RemovalWorker.push_bulk(Status.with_discarded.discarded.where(id: status_ids).pluck(:id)) { |status_id| [status_id, { immediate: true }] }
|
||||
update!(action_taken: true, action_taken_by_account_id: acting_account.id)
|
||||
update!(action_taken_at: Time.now.utc, action_taken_by_account_id: acting_account.id)
|
||||
end
|
||||
|
||||
def unresolve!
|
||||
update!(action_taken: false, action_taken_by_account_id: nil)
|
||||
update!(action_taken_at: nil, action_taken_by_account_id: nil)
|
||||
end
|
||||
|
||||
def action_taken?
|
||||
action_taken_at.present?
|
||||
end
|
||||
|
||||
alias action_taken action_taken?
|
||||
|
||||
def unresolved?
|
||||
!action_taken?
|
||||
end
|
||||
@ -88,29 +99,24 @@ class Report < ApplicationRecord
|
||||
end
|
||||
|
||||
def history
|
||||
time_range = created_at..updated_at
|
||||
|
||||
sql = [
|
||||
subquery = [
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Report',
|
||||
target_id: id,
|
||||
created_at: time_range
|
||||
).unscope(:order),
|
||||
target_id: id
|
||||
).unscope(:order).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Account',
|
||||
target_id: target_account_id,
|
||||
created_at: time_range
|
||||
).unscope(:order),
|
||||
target_id: target_account_id
|
||||
).unscope(:order).arel,
|
||||
|
||||
Admin::ActionLog.where(
|
||||
target_type: 'Status',
|
||||
target_id: status_ids,
|
||||
created_at: time_range
|
||||
).unscope(:order),
|
||||
].map { |query| "(#{query.to_sql})" }.join(' UNION ALL ')
|
||||
target_id: status_ids
|
||||
).unscope(:order).arel,
|
||||
].reduce { |union, query| Arel::Nodes::UnionAll.new(union, query) }
|
||||
|
||||
Admin::ActionLog.from("(#{sql}) AS admin_action_logs")
|
||||
Admin::ActionLog.from(Arel::Nodes::As.new(subquery, Admin::ActionLog.arel_table))
|
||||
end
|
||||
|
||||
def set_uri
|
||||
|
@ -19,7 +19,7 @@ class ReportFilter
|
||||
scope = Report.unresolved
|
||||
|
||||
params.each do |key, value|
|
||||
scope = scope.merge scope_for(key, value)
|
||||
scope = scope.merge scope_for(key, value), rewhere: true
|
||||
end
|
||||
|
||||
scope
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::Admin::ReportSerializer < ActiveModel::Serializer
|
||||
attributes :id, :action_taken, :comment, :created_at, :updated_at
|
||||
attributes :id, :action_taken, :category, :comment, :created_at, :updated_at
|
||||
|
||||
has_one :account, serializer: REST::Admin::AccountSerializer
|
||||
has_one :target_account, serializer: REST::Admin::AccountSerializer
|
||||
@ -9,8 +9,13 @@ class REST::Admin::ReportSerializer < ActiveModel::Serializer
|
||||
has_one :action_taken_by_account, serializer: REST::Admin::AccountSerializer
|
||||
|
||||
has_many :statuses, serializer: REST::StatusSerializer
|
||||
has_many :rules, serializer: REST::RuleSerializer
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def statuses
|
||||
object.statuses.with_includes
|
||||
end
|
||||
end
|
||||
|
@ -48,6 +48,6 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
||||
end
|
||||
|
||||
def local_follower
|
||||
@local_follower ||= account.followers.local.without_suspended.first
|
||||
@local_follower ||= @account.followers.local.without_suspended.first
|
||||
end
|
||||
end
|
||||
|
@ -9,6 +9,7 @@ class RemoveStatusService < BaseService
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :redraft
|
||||
# @option [Boolean] :immediate
|
||||
# @option [Boolean] :preserve
|
||||
# @option [Boolean] :original_removed
|
||||
def call(status, **options)
|
||||
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
|
||||
@ -44,7 +45,7 @@ class RemoveStatusService < BaseService
|
||||
remove_media
|
||||
end
|
||||
|
||||
@status.destroy! if @options[:immediate] || !@status.reported?
|
||||
@status.destroy! if permanently?
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
end
|
||||
@ -143,11 +144,15 @@ class RemoveStatusService < BaseService
|
||||
end
|
||||
|
||||
def remove_media
|
||||
return if @options[:redraft] || (!@options[:immediate] && @status.reported?)
|
||||
return if @options[:redraft] || !permanently?
|
||||
|
||||
@status.media_attachments.destroy_all
|
||||
end
|
||||
|
||||
def permanently?
|
||||
@options[:immediate] || !(@options[:preserve] || @status.reported?)
|
||||
end
|
||||
|
||||
def lock_options
|
||||
{ redis: Redis.current, key: "distribute:#{@status.id}", autorelease: 5.minutes.seconds }
|
||||
end
|
||||
|
@ -19,7 +19,7 @@
|
||||
%div.muted-hint.center-text
|
||||
= t 'admin.action_logs.empty'
|
||||
- else
|
||||
.announcements-list
|
||||
.report-notes
|
||||
= render partial: 'action_log', collection: @action_logs
|
||||
|
||||
= paginate @action_logs
|
||||
|
@ -1,7 +1,18 @@
|
||||
.speech-bubble
|
||||
.speech-bubble__bubble
|
||||
.report-notes__item
|
||||
= image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
|
||||
|
||||
.report-notes__item__header
|
||||
%span.username
|
||||
= link_to display_name(report_note.account), admin_account_path(report_note.account_id)
|
||||
%time{ datetime: report_note.created_at.iso8601, title: l(report_note.created_at) }
|
||||
- if report_note.created_at.today?
|
||||
= t('admin.report_notes.today_at', time: l(report_note.created_at, format: :time))
|
||||
- else
|
||||
= l report_note.created_at.to_date
|
||||
|
||||
.report-notes__item__content
|
||||
= simple_format(h(report_note.content))
|
||||
.speech-bubble__owner
|
||||
= admin_account_link_to report_note.account
|
||||
%time.formatted{ datetime: report_note.created_at.iso8601 }= l report_note.created_at
|
||||
= table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete if can?(:destroy, report_note)
|
||||
|
||||
- if can?(:destroy, report_note)
|
||||
.report-notes__item__actions
|
||||
= table_link_to 'trash', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete
|
||||
|
@ -1,6 +0,0 @@
|
||||
.speech-bubble.positive
|
||||
.speech-bubble__bubble
|
||||
= t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}_html", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target'))
|
||||
.speech-bubble__owner
|
||||
= admin_account_link_to(action_log.account)
|
||||
%time.formatted{ datetime: action_log.created_at.iso8601 }= l action_log.created_at
|
@ -22,6 +22,9 @@
|
||||
= react_component :media_gallery, height: 343, sensitive: status.proper.sensitive?, visible: false, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
|
||||
|
||||
.detailed-status__meta
|
||||
- if status.application
|
||||
= status.application.name
|
||||
·
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener noreferrer' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
- if status.discarded?
|
||||
|
@ -7,122 +7,199 @@
|
||||
- else
|
||||
= link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button'
|
||||
|
||||
.table-wrapper
|
||||
%table.table.inline-table
|
||||
%tbody
|
||||
%tr
|
||||
%th= t('admin.reports.reported_account')
|
||||
%td= admin_account_link_to @report.target_account
|
||||
%td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.target_account.targeted_reports.count), admin_reports_path(target_account_id: @report.target_account.id)
|
||||
%td= table_link_to 'file', t('admin.reports.account.notes', count: @report.target_account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.target_account.id)
|
||||
%tr
|
||||
%th= t('admin.reports.reported_by')
|
||||
.report-header
|
||||
.report-header__card
|
||||
.account-card
|
||||
.account-card__header
|
||||
= image_tag @report.target_account.header.url, alt: ''
|
||||
.account-card__title
|
||||
.account-card__title__avatar
|
||||
= image_tag @report.target_account.avatar.url, alt: ''
|
||||
.display-name
|
||||
%bdi
|
||||
%strong.emojify.p-name= display_name(@report.target_account, custom_emojify: true)
|
||||
%span
|
||||
= acct(@report.target_account)
|
||||
= fa_icon('lock') if @report.target_account.locked?
|
||||
- if @report.target_account.note.present?
|
||||
.account-card__bio.emojify
|
||||
= Formatter.instance.simplified_format(@report.target_account, custom_emojify: true)
|
||||
.account-card__actions
|
||||
.account-card__counters
|
||||
.account-card__counters__item
|
||||
= friendly_number_to_human @report.target_account.statuses_count
|
||||
%small= t('accounts.posts', count: @report.target_account.statuses_count).downcase
|
||||
.account-card__counters__item
|
||||
= friendly_number_to_human @report.target_account.followers_count
|
||||
%small= t('accounts.followers', count: @report.target_account.followers_count).downcase
|
||||
.account-card__counters__item
|
||||
= friendly_number_to_human @report.target_account.following_count
|
||||
%small= t('accounts.following', count: @report.target_account.following_count).downcase
|
||||
.account-card__actions__button
|
||||
= link_to t('admin.reports.view_profile'), admin_account_path(@report.target_account_id), class: 'button'
|
||||
.report-header__details.report-header__details--horizontal
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.accounts.joined')
|
||||
.report-header__details__item__content
|
||||
%time.time-ago{ datetime: @report.target_account.created_at.iso8601, title: l(@report.target_account.created_at) }= l @report.target_account.created_at
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('accounts.last_active')
|
||||
.report-header__details__item__content
|
||||
- if @report.target_account.last_status_at.present?
|
||||
%time.time-ago{ datetime: @report.target_account.last_status_at.to_date.iso8601, title: l(@report.target_account.last_status_at.to_date) }= l @report.target_account.last_status_at
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.accounts.strikes')
|
||||
.report-header__details__item__content
|
||||
= @report.target_account.strikes.count
|
||||
|
||||
.report-header__details
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.reports.created_at')
|
||||
.report-header__details__item__content
|
||||
%time.formatted{ datetime: @report.created_at.iso8601 }
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.reports.reported_by')
|
||||
.report-header__details__item__content
|
||||
- if @report.account.instance_actor?
|
||||
%td{ colspan: 3 }= site_hostname
|
||||
= site_hostname
|
||||
- elsif @report.account.local?
|
||||
%td= admin_account_link_to @report.account
|
||||
%td= table_link_to 'flag', t('admin.reports.account.reports', count: @report.account.targeted_reports.count), admin_reports_path(target_account_id: @report.account.id)
|
||||
%td= table_link_to 'file', t('admin.reports.account.notes', count: @report.account.targeted_moderation_notes.count), admin_reports_path(target_account_id: @report.account.id)
|
||||
= admin_account_link_to @report.account
|
||||
- else
|
||||
%td{ colspan: 3 }= @report.account.domain
|
||||
%tr
|
||||
%th= t('admin.reports.created_at')
|
||||
%td{ colspan: 3 }
|
||||
%time.formatted{ datetime: @report.created_at.iso8601 }
|
||||
%tr
|
||||
%th= t('admin.reports.updated_at')
|
||||
%td{ colspan: 3 }
|
||||
%time.formatted{ datetime: @report.updated_at.iso8601 }
|
||||
%tr
|
||||
%th= t('admin.reports.status')
|
||||
%td
|
||||
- if @report.action_taken?
|
||||
= t('admin.reports.resolved')
|
||||
= @report.account.domain
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.reports.status')
|
||||
.report-header__details__item__content
|
||||
- if @report.action_taken?
|
||||
= t('admin.reports.resolved')
|
||||
- else
|
||||
= t('admin.reports.unresolved')
|
||||
- unless @report.target_account.local?
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.reports.forwarded')
|
||||
.report-header__details__item__content
|
||||
- if @report.forwarded?
|
||||
= t('simple_form.yes')
|
||||
- else
|
||||
= t('admin.reports.unresolved')
|
||||
%td{ colspan: 2 }
|
||||
- if @report.action_taken?
|
||||
= 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?
|
||||
%tr
|
||||
%th= t('admin.reports.action_taken_by')
|
||||
%td{ colspan: 3 }
|
||||
= admin_account_link_to @report.action_taken_by_account
|
||||
- else
|
||||
%tr
|
||||
%th= t('admin.reports.assigned')
|
||||
%td
|
||||
- if @report.assigned_account.nil?
|
||||
\-
|
||||
- else
|
||||
= admin_account_link_to @report.assigned_account
|
||||
%td
|
||||
- if @report.assigned_account != current_user.account
|
||||
= table_link_to 'user', t('admin.reports.assign_to_self'), assign_to_self_admin_report_path(@report), method: :post
|
||||
%td
|
||||
- if !@report.assigned_account.nil?
|
||||
= table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post
|
||||
|
||||
%hr.spacer
|
||||
|
||||
%div.action-buttons
|
||||
%div
|
||||
|
||||
- if @report.unresolved?
|
||||
%div
|
||||
- if @report.target_account.local?
|
||||
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button'
|
||||
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive'
|
||||
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
|
||||
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, type: 'suspend', report_id: @report.id), class: 'button button--destructive'
|
||||
|
||||
%hr.spacer
|
||||
|
||||
.speech-bubble
|
||||
.speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
|
||||
.speech-bubble__owner
|
||||
- if @report.account.local?
|
||||
= admin_account_link_to @report.account
|
||||
= t('simple_form.no')
|
||||
- if !@report.action_taken_by_account.nil?
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.reports.action_taken_by')
|
||||
.report-header__details__item__content
|
||||
= admin_account_link_to @report.action_taken_by_account
|
||||
- else
|
||||
= @report.account.domain
|
||||
%br/
|
||||
%time.formatted{ datetime: @report.created_at.iso8601 }
|
||||
.report-header__details__item
|
||||
.report-header__details__item__header
|
||||
%strong= t('admin.reports.assigned')
|
||||
.report-header__details__item__content
|
||||
- if @report.assigned_account.nil?
|
||||
= t 'admin.reports.no_one_assigned'
|
||||
- else
|
||||
= admin_account_link_to @report.assigned_account
|
||||
—
|
||||
- if @report.assigned_account != current_user.account
|
||||
= table_link_to 'user', t('admin.reports.assign_to_self'), assign_to_self_admin_report_path(@report), method: :post
|
||||
- elsif !@report.assigned_account.nil?
|
||||
= table_link_to 'trash', t('admin.reports.unassign'), unassign_admin_report_path(@report), method: :post
|
||||
|
||||
- unless @report.statuses.empty?
|
||||
%hr.spacer/
|
||||
%hr.spacer
|
||||
|
||||
= form_for(@form, url: admin_report_reported_statuses_path(@report.id)) do |f|
|
||||
.batch-table
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
= f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
= f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
= f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
.batch-table__body
|
||||
= render partial: 'admin/reports/status', collection: @report.statuses, locals: { f: f }
|
||||
%h3= t 'admin.reports.category'
|
||||
|
||||
%p= t 'admin.reports.category_description_html'
|
||||
|
||||
= react_admin_component :report_reason_selector, id: @report.id, category: @report.category, rule_ids: @report.rule_ids&.map(&:to_s), disabled: @report.action_taken?
|
||||
|
||||
- if @report.comment.present?
|
||||
%p= t('admin.reports.comment_description_html', name: content_tag(:strong, @report.account.username, class: 'username'))
|
||||
|
||||
.report-notes__item
|
||||
= image_tag @report.account.avatar.url, class: 'report-notes__item__avatar'
|
||||
|
||||
.report-notes__item__header
|
||||
%span.username
|
||||
= link_to display_name(@report.account), admin_account_path(@report.account_id)
|
||||
%time{ datetime: @report.created_at.iso8601, title: l(@report.created_at) }
|
||||
- if @report.created_at.today?
|
||||
= t('admin.report_notes.today_at', time: l(@report.created_at, format: :time))
|
||||
- else
|
||||
= l @report.created_at.to_date
|
||||
|
||||
.report-notes__item__content
|
||||
= simple_format(h(@report.comment))
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- @report_notes.each do |item|
|
||||
- if item.is_a?(Admin::ActionLog)
|
||||
= render partial: 'action_log', locals: { action_log: item }
|
||||
- else
|
||||
= render item
|
||||
%h3= t 'admin.reports.statuses'
|
||||
|
||||
%p
|
||||
= t 'admin.reports.statuses_description_html'
|
||||
—
|
||||
= link_to safe_join([fa_icon('plus'), t('admin.reports.add_to_report')]), admin_account_statuses_path(@report.target_account_id, report_id: @report.id), class: 'table-action-link'
|
||||
|
||||
= form_for(@form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id)) do |f|
|
||||
.batch-table
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
- if !@statuses.empty? && @report.unresolved?
|
||||
= f.button safe_join([fa_icon('times'), t('admin.statuses.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit
|
||||
= f.button safe_join([fa_icon('trash'), t('admin.reports.delete_and_resolve')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
- else
|
||||
.batch-table__body
|
||||
- if @statuses.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
||||
|
||||
- if @report.unresolved?
|
||||
%hr.spacer/
|
||||
|
||||
%p= t 'admin.reports.actions_description_html'
|
||||
|
||||
.report-actions
|
||||
.report-actions__item
|
||||
.report-actions__item__button
|
||||
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@report.target_account_id, type: 'silence', report_id: @report.id), class: 'button button--destructive'
|
||||
.report-actions__item__description
|
||||
= t('admin.reports.actions.silence_description_html')
|
||||
.report-actions__item
|
||||
.report-actions__item__button
|
||||
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@report.target_account_id, report_id: @report.id, type: 'suspend'), class: 'button button--destructive'
|
||||
.report-actions__item__description
|
||||
= t('admin.reports.actions.suspend_description_html')
|
||||
.report-actions__item
|
||||
.report-actions__item__button
|
||||
= link_to t('admin.accounts.custom'), new_admin_account_action_path(@report.target_account_id, report_id: @report.id), class: 'button'
|
||||
.report-actions__item__description
|
||||
= t('admin.reports.actions.other_description_html')
|
||||
|
||||
- unless @action_logs.empty?
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t 'admin.reports.action_log'
|
||||
|
||||
.report-notes
|
||||
= render @action_logs
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t 'admin.reports.notes.title'
|
||||
|
||||
%p= t 'admin.reports.notes_description_html'
|
||||
|
||||
.report-notes
|
||||
= render @report_notes
|
||||
|
||||
= simple_form_for @report_note, url: admin_report_notes_path do |f|
|
||||
= render 'shared/error_messages', object: @report_note
|
||||
= f.input :report_id, as: :hidden
|
||||
|
||||
.field-group
|
||||
|
@ -7,28 +7,37 @@
|
||||
.filter-subset
|
||||
%strong= t('admin.statuses.media.title')
|
||||
%ul
|
||||
%li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected'
|
||||
%li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected'
|
||||
%li= filter_link_to t('generic.all'), media: nil, id: nil
|
||||
%li= filter_link_to t('admin.statuses.with_media'), media: '1'
|
||||
.back-link
|
||||
= link_to admin_account_path(@account.id) do
|
||||
= fa_icon 'chevron-left fw'
|
||||
= t('admin.statuses.back_to_account')
|
||||
- if params[:report_id]
|
||||
= link_to admin_report_path(params[:report_id].to_i) do
|
||||
= fa_icon 'chevron-left fw'
|
||||
= t('admin.statuses.back_to_report')
|
||||
- else
|
||||
= link_to admin_account_path(@account.id) do
|
||||
= fa_icon 'chevron-left fw'
|
||||
= t('admin.statuses.back_to_account')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= form_for(@form, url: admin_account_statuses_path(@account.id)) do |f|
|
||||
= hidden_field_tag :page, params[:page]
|
||||
= hidden_field_tag :media, params[:media]
|
||||
= form_for(@status_batch_action, url: batch_admin_account_statuses_path(@account.id)) do |f|
|
||||
= hidden_field_tag :page, params[:page] || 1
|
||||
|
||||
- Admin::StatusFilter::KEYS.each do |key|
|
||||
= hidden_field_tag key, params[key] if params[key].present?
|
||||
|
||||
.batch-table
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
= f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
= f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
= f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
- unless @statuses.empty?
|
||||
= f.button safe_join([fa_icon('flag'), t('admin.statuses.batch.report')]), name: :report, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
.batch-table__body
|
||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
||||
- if @statuses.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
||||
|
||||
= paginate @statuses
|
||||
|
@ -1,27 +0,0 @@
|
||||
- content_for :page_title do
|
||||
= t('admin.statuses.title')
|
||||
\-
|
||||
= "@#{@account.acct}"
|
||||
|
||||
.filters
|
||||
.back-link
|
||||
= link_to admin_account_path(@account.id) do
|
||||
%i.fa.fa-chevron-left.fa-fw
|
||||
= t('admin.statuses.back_to_account')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
= form_for(@form, url: admin_account_statuses_path(@account.id)) do |f|
|
||||
= hidden_field_tag :page, params[:page]
|
||||
= hidden_field_tag :media, params[:media]
|
||||
|
||||
.batch-table
|
||||
.batch-table__toolbar
|
||||
%label.batch-table__toolbar__select.batch-checkbox-all
|
||||
= check_box_tag :batch_checkbox_all, nil, false
|
||||
.batch-table__toolbar__actions
|
||||
= f.button safe_join([fa_icon('eye-slash'), t('admin.statuses.batch.nsfw_on')]), name: :nsfw_on, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
= f.button safe_join([fa_icon('eye'), t('admin.statuses.batch.nsfw_off')]), name: :nsfw_off, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
= f.button safe_join([fa_icon('trash'), t('admin.statuses.batch.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
|
||||
.batch-table__body
|
||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
@ -1,8 +1,8 @@
|
||||
<% if status.spoiler_text? %>
|
||||
<%= raw status.spoiler_text %>
|
||||
----
|
||||
|
||||
> <%= raw word_wrap(status.spoiler_text, break_sequence: "\n> ") %>
|
||||
> ----
|
||||
>
|
||||
<% end %>
|
||||
<%= raw Formatter.instance.plaintext(status) %>
|
||||
> <%= raw word_wrap(Formatter.instance.plaintext(status), break_sequence: "\n> ") %>
|
||||
|
||||
<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{status.id}") %>
|
||||
|
@ -37,16 +37,26 @@
|
||||
%tr
|
||||
%td.column-cell.text-center
|
||||
- unless @warning.none_action?
|
||||
%p= t "user_mailer.warning.explanation.#{@warning.action}"
|
||||
%p= t "user_mailer.warning.explanation.#{@warning.action}", instance: @instance
|
||||
|
||||
- unless @warning.text.blank?
|
||||
= Formatter.instance.linkify(@warning.text)
|
||||
|
||||
- if !@statuses.nil? && !@statuses.empty?
|
||||
- if @warning.report && !@warning.report.other?
|
||||
%p
|
||||
%strong= t('user_mailer.warning.reason')
|
||||
= t("user_mailer.warning.categories.#{@warning.report.category}")
|
||||
|
||||
- if @warning.report.violation? && @warning.report.rule_ids.present?
|
||||
%ul.rules-list
|
||||
- @warning.report.rules.each do |rule|
|
||||
%li= rule.text
|
||||
|
||||
- unless @statuses.empty?
|
||||
%p
|
||||
%strong= t('user_mailer.warning.statuses')
|
||||
|
||||
- if !@statuses.nil? && !@statuses.empty?
|
||||
- unless @statuses.empty?
|
||||
- @statuses.each_with_index do |status, i|
|
||||
= render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
|
||||
|
||||
|
@ -3,11 +3,24 @@
|
||||
===
|
||||
|
||||
<% unless @warning.none_action? %>
|
||||
<%= t "user_mailer.warning.explanation.#{@warning.action}" %>
|
||||
<%= t "user_mailer.warning.explanation.#{@warning.action}", instance: @instance %>
|
||||
|
||||
<% end %>
|
||||
<% if @warning.text.present? %>
|
||||
<%= @warning.text %>
|
||||
<% if !@statuses.nil? && !@statuses.empty? %>
|
||||
|
||||
<% end %>
|
||||
<% if @warning.report && !@warning.report.other? %>
|
||||
**<%= t('user_mailer.warning.reason') %>** <%= t("user_mailer.warning.categories.#{@warning.report.category}") %>
|
||||
|
||||
<% if @warning.report.violation? && @warning.report.rule_ids.present? %>
|
||||
<% @warning.report.rules.each do |rule| %>
|
||||
- <%= rule.text %>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if !@statuses.empty? %>
|
||||
<%= t('user_mailer.warning.statuses') %>
|
||||
|
||||
<% @statuses.each do |status| %>
|
||||
|
@ -8,6 +8,7 @@ class Scheduler::UserCleanupScheduler
|
||||
def perform
|
||||
clean_unconfirmed_accounts!
|
||||
clean_suspended_accounts!
|
||||
clean_discarded_statuses!
|
||||
end
|
||||
|
||||
private
|
||||
@ -24,4 +25,12 @@ class Scheduler::UserCleanupScheduler
|
||||
Admin::AccountDeletionWorker.perform_async(deletion_request.account_id)
|
||||
end
|
||||
end
|
||||
|
||||
def clean_discarded_statuses!
|
||||
Status.discarded.where('deleted_at <= ?', 30.days.ago).find_in_batches do |statuses|
|
||||
RemovalWorker.push_bulk(statuses) do |status|
|
||||
[status.id, { immediate: true }]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user