diff options
author | Claire <claire.github-309c@sitedethib.com> | 2021-12-16 19:45:17 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-16 19:45:17 +0100 |
commit | 7efef7db9ed6f7b25a28166c1b75a28adbaf7cdb (patch) | |
tree | ad8ebb28e0466649785afa167e8711debb271e68 /app | |
parent | d911c17f521d6b13861caa886715a50b644007a1 (diff) | |
parent | 50d62fe2e749a5ab2b2bed81d5a9449b5e733964 (diff) |
Merge pull request #1648 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app')
33 files changed, 427 insertions, 233 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 1dd7430e0..948e70d5b 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -2,13 +2,24 @@ module Admin class AccountsController < BaseController - before_action :set_account, except: [:index] + before_action :set_account, except: [:index, :batch] before_action :require_remote_account!, only: [:redownload] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] def index authorize :account, :index? + @accounts = filtered_accounts.page(params[:page]) + @form = Form::AccountBatch.new + end + + def batch + @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.accounts.no_account_selected') + ensure + redirect_to admin_accounts_path(filter_params) end def show @@ -38,13 +49,13 @@ module Admin def approve authorize @account.user, :approve? @account.user.approve! - redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) + redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct) end def reject authorize @account.user, :reject? DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false) - redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) + redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct) end def destroy @@ -121,11 +132,25 @@ module Admin end def filtered_accounts - AccountFilter.new(filter_params).results + AccountFilter.new(filter_params.with_defaults(order: 'recent')).results end def filter_params params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS) end + + def form_account_batch_params + params.require(:form_account_batch).permit(:action, account_ids: []) + end + + def action_from_button + if params[:suspend] + 'suspend' + elsif params[:approve] + 'approve' + elsif params[:reject] + 'reject' + end + end end end diff --git a/app/controllers/admin/pending_accounts_controller.rb b/app/controllers/admin/pending_accounts_controller.rb deleted file mode 100644 index b62a9bc84..000000000 --- a/app/controllers/admin/pending_accounts_controller.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -module Admin - class PendingAccountsController < BaseController - before_action :set_accounts, only: :index - - def index - @form = Form::AccountBatch.new - end - - def batch - @form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button)) - @form.save - rescue ActionController::ParameterMissing - flash[:alert] = I18n.t('admin.accounts.no_account_selected') - ensure - redirect_to admin_pending_accounts_path(current_params) - end - - def approve_all - Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save - redirect_to admin_pending_accounts_path(current_params) - end - - def reject_all - Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save - redirect_to admin_pending_accounts_path(current_params) - end - - private - - def set_accounts - @accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page]) - end - - def form_account_batch_params - params.require(:form_account_batch).permit(:action, account_ids: []) - end - - def action_from_button - if params[:approve] - 'approve' - elsif params[:reject] - 'reject' - end - end - - def current_params - params.slice(:page).permit(:page) - end - end -end diff --git a/app/controllers/concerns/accountable_concern.rb b/app/controllers/concerns/accountable_concern.rb index 3cdcffc51..87d62478d 100644 --- a/app/controllers/concerns/accountable_concern.rb +++ b/app/controllers/concerns/accountable_concern.rb @@ -3,7 +3,7 @@ module AccountableConcern extend ActiveSupport::Concern - def log_action(action, target) - Admin::ActionLog.create(account: current_account, action: action, target: target) + def log_action(action, target, options = {}) + Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys) end end diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb index 39dd71fca..c9477a1d4 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -57,7 +57,7 @@ module TwoFactorAuthenticationConcern if valid_webauthn_credential?(user, webauthn_credential) on_authentication_success(user, :webauthn) - render json: { redirect_path: root_path }, status: :ok + render json: { redirect_path: after_sign_in_path_for(user) }, status: :ok else on_authentication_failure(user, :webauthn, :invalid_credential) render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index e9a298a24..ae96f7a34 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -36,6 +36,8 @@ module Admin::ActionLogsHelper def log_target_from_history(type, attributes) case type + when 'User' + attributes['username'] when 'CustomEmoji' attributes['shortcode'] when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb index 4ee2cdef4..32aaf9f5e 100644 --- a/app/helpers/admin/dashboard_helper.rb +++ b/app/helpers/admin/dashboard_helper.rb @@ -1,10 +1,41 @@ # frozen_string_literal: true module Admin::DashboardHelper - def feature_hint(feature, enabled) - indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ') - class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint' + def relevant_account_ip(account, ip_query) + default_ip = [account.user_current_sign_in_ip || account.user_sign_up_ip] - safe_join([feature, content_tag(:span, indicator, class: class_names)]) + matched_ip = begin + ip_query_addr = IPAddr.new(ip_query) + account.user.recent_ips.find { |(_, ip)| ip_query_addr.include?(ip) } || default_ip + rescue IPAddr::Error + default_ip + end.last + + if matched_ip + link_to matched_ip, admin_accounts_path(ip: matched_ip) + else + '-' + end + end + + def relevant_account_timestamp(account) + timestamp, exact = begin + if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago + [account.user_current_sign_in_at, true] + elsif account.user_current_sign_in_at + [account.user_current_sign_in_at, false] + elsif account.user_pending? + [account.user_created_at, true] + elsif account.last_status_at.present? + [account.last_status_at, true] + else + [nil, false] + end + end + + return '-' if timestamp.nil? + return t('generic.today') unless exact + + content_tag(:time, l(timestamp), class: 'time-ago', datetime: timestamp.iso8601, title: l(timestamp)) end end diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 96931546c..9af8b6d20 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -39,6 +39,7 @@ export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS'; export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; +export const COMPOSE_SUGGESTION_IGNORE = 'COMPOSE_SUGGESTION_IGNORE'; export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; @@ -562,13 +563,25 @@ export function selectComposeSuggestion(position, token, suggestion, path) { completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']); } - dispatch({ - type: COMPOSE_SUGGESTION_SELECT, - position, - token, - completion, - path, - }); + // We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that + // the suggestions are dismissed and the cursor moves forward. + if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) { + dispatch({ + type: COMPOSE_SUGGESTION_SELECT, + position, + token, + completion, + path, + }); + } else { + dispatch({ + type: COMPOSE_SUGGESTION_IGNORE, + position, + token, + completion, + path, + }); + } }; }; diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index dccdbc8d0..a92f3d5a8 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -99,7 +99,9 @@ function main() { delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => { const password = document.getElementById('registration_user_password'); const confirmation = document.getElementById('registration_user_password_confirmation'); - if (password.value && password.value !== confirmation.value) { + if (confirmation.value && confirmation.value.length > password.maxLength) { + confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format()); + } else if (password.value && password.value !== confirmation.value) { confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); } else { confirmation.setCustomValidity(''); @@ -111,7 +113,9 @@ function main() { const confirmation = document.getElementById('user_password_confirmation'); if (!confirmation) return; - if (password.value && password.value !== confirmation.value) { + if (confirmation.value && confirmation.value.length > password.maxLength) { + confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format()); + } else if (password.value && password.value !== confirmation.value) { confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); } else { confirmation.setCustomValidity(''); diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 1735cfb4d..d2ea0a924 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -22,6 +22,7 @@ import { COMPOSE_SUGGESTIONS_CLEAR, COMPOSE_SUGGESTIONS_READY, COMPOSE_SUGGESTION_SELECT, + COMPOSE_SUGGESTION_IGNORE, COMPOSE_SUGGESTION_TAGS_UPDATE, COMPOSE_TAG_HISTORY_UPDATE, COMPOSE_ADVANCED_OPTIONS_CHANGE, @@ -252,6 +253,17 @@ const insertSuggestion = (state, position, token, completion, path) => { }); }; +const ignoreSuggestion = (state, position, token, completion, path) => { + return state.withMutations(map => { + map.updateIn(path, oldText => `${oldText.slice(0, position + token.length)} ${oldText.slice(position + token.length)}`); + map.set('suggestion_token', null); + map.set('suggestions', ImmutableList()); + map.set('focusDate', new Date()); + map.set('caretPosition', position + token.length + 1); + map.set('idempotencyKey', uuid()); + }); +}; + const sortHashtagsByUse = (state, tags) => { const personalHistory = state.get('tagHistory'); @@ -499,6 +511,8 @@ export default function compose(state = initialState, action) { return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion, action.path); + case COMPOSE_SUGGESTION_IGNORE: + return ignoreSuggestion(state, action.position, action.token, action.completion, action.path); case COMPOSE_SUGGESTION_TAGS_UPDATE: return updateSuggestionTags(state, action.token); case COMPOSE_TAG_HISTORY_UPDATE: diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index fe7dfc20f..56b143fe6 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -328,7 +328,12 @@ } } -.batch-table__row--muted .pending-account__header { +.batch-table__row--muted { + color: lighten($ui-base-color, 26%); +} + +.batch-table__row--muted .pending-account__header, +.batch-table__row--muted .accounts-table { &, a, strong { @@ -336,10 +341,31 @@ } } -.batch-table__row--attention .pending-account__header { +.batch-table__row--muted .accounts-table { + tbody td.accounts-table__extra, + &__count, + &__count small { + color: lighten($ui-base-color, 26%); + } +} + +.batch-table__row--attention { + color: $gold-star; +} + +.batch-table__row--attention .pending-account__header, +.batch-table__row--attention .accounts-table { &, a, strong { color: $gold-star; } } + +.batch-table__row--attention .accounts-table { + tbody td.accounts-table__extra, + &__count, + &__count small { + color: $gold-star; + } +} diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss index ec2ee7c1c..12c84a6c9 100644 --- a/app/javascript/flavours/glitch/styles/tables.scss +++ b/app/javascript/flavours/glitch/styles/tables.scss @@ -237,6 +237,11 @@ a.table-action-link { flex: 1 1 auto; } + &__quote { + padding: 12px; + padding-top: 0; + } + &__extra { flex: 0 0 auto; text-align: right; diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index 06bf55e1e..a88f3b2c7 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -434,6 +434,24 @@ } } + tbody td.accounts-table__extra { + width: 120px; + text-align: right; + color: $darker-text-color; + padding-right: 16px; + + a { + text-decoration: none; + color: inherit; + + &:focus, + &:hover, + &:active { + text-decoration: underline; + } + } + } + &__comment { width: 50%; vertical-align: initial !important; diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 40d566d24..afd42bdef 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -37,6 +37,7 @@ export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS'; export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; +export const COMPOSE_SUGGESTION_IGNORE = 'COMPOSE_SUGGESTION_IGNORE'; export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; @@ -536,13 +537,25 @@ export function selectComposeSuggestion(position, token, suggestion, path) { startPosition = position; } - dispatch({ - type: COMPOSE_SUGGESTION_SELECT, - position: startPosition, - token, - completion, - path, - }); + // We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that + // the suggestions are dismissed and the cursor moves forward. + if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) { + dispatch({ + type: COMPOSE_SUGGESTION_SELECT, + position: startPosition, + token, + completion, + path, + }); + } else { + dispatch({ + type: COMPOSE_SUGGESTION_IGNORE, + position: startPosition, + token, + completion, + path, + }); + } }; }; diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 34c7c4dea..06a908e9d 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -21,6 +21,7 @@ import { COMPOSE_SUGGESTIONS_CLEAR, COMPOSE_SUGGESTIONS_READY, COMPOSE_SUGGESTION_SELECT, + COMPOSE_SUGGESTION_IGNORE, COMPOSE_SUGGESTION_TAGS_UPDATE, COMPOSE_TAG_HISTORY_UPDATE, COMPOSE_SENSITIVITY_CHANGE, @@ -165,6 +166,17 @@ const insertSuggestion = (state, position, token, completion, path) => { }); }; +const ignoreSuggestion = (state, position, token, completion, path) => { + return state.withMutations(map => { + map.updateIn(path, oldText => `${oldText.slice(0, position + token.length)} ${oldText.slice(position + token.length)}`); + map.set('suggestion_token', null); + map.set('suggestions', ImmutableList()); + map.set('focusDate', new Date()); + map.set('caretPosition', position + token.length + 1); + map.set('idempotencyKey', uuid()); + }); +}; + const sortHashtagsByUse = (state, tags) => { const personalHistory = state.get('tagHistory'); @@ -398,6 +410,8 @@ export default function compose(state = initialState, action) { return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion, action.path); + case COMPOSE_SUGGESTION_IGNORE: + return ignoreSuggestion(state, action.position, action.token, action.completion, action.path); case COMPOSE_SUGGESTION_TAGS_UPDATE: return updateSuggestionTags(state, action.token); case COMPOSE_TAG_HISTORY_UPDATE: diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 2166d8df0..7ebe8b4d0 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -103,7 +103,9 @@ function main() { delegate(document, '#registration_user_password_confirmation,#registration_user_password', 'input', () => { const password = document.getElementById('registration_user_password'); const confirmation = document.getElementById('registration_user_password_confirmation'); - if (password.value && password.value !== confirmation.value) { + if (confirmation.value && confirmation.value.length > password.maxLength) { + confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format()); + } else if (password.value && password.value !== confirmation.value) { confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); } else { confirmation.setCustomValidity(''); @@ -115,7 +117,9 @@ function main() { const confirmation = document.getElementById('user_password_confirmation'); if (!confirmation) return; - if (password.value && password.value !== confirmation.value) { + if (confirmation.value && confirmation.value.length > password.maxLength) { + confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.exceeds_maxlength'] || 'Password confirmation exceeds the maximum password length', locale)).format()); + } else if (password.value && password.value !== confirmation.value) { confirmation.setCustomValidity((new IntlMessageFormat(messages['password_confirmation.mismatching'] || 'Password confirmation does not match', locale)).format()); } else { confirmation.setCustomValidity(''); diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index b8a6c8018..485fe4a9d 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -326,7 +326,12 @@ } } -.batch-table__row--muted .pending-account__header { +.batch-table__row--muted { + color: lighten($ui-base-color, 26%); +} + +.batch-table__row--muted .pending-account__header, +.batch-table__row--muted .accounts-table { &, a, strong { @@ -334,10 +339,31 @@ } } -.batch-table__row--attention .pending-account__header { +.batch-table__row--muted .accounts-table { + tbody td.accounts-table__extra, + &__count, + &__count small { + color: lighten($ui-base-color, 26%); + } +} + +.batch-table__row--attention { + color: $gold-star; +} + +.batch-table__row--attention .pending-account__header, +.batch-table__row--attention .accounts-table { &, a, strong { color: $gold-star; } } + +.batch-table__row--attention .accounts-table { + tbody td.accounts-table__extra, + &__count, + &__count small { + color: $gold-star; + } +} diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 62f5554ff..36bc07a72 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -237,6 +237,11 @@ a.table-action-link { flex: 1 1 auto; } + &__quote { + padding: 12px; + padding-top: 0; + } + &__extra { flex: 0 0 auto; text-align: right; diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index 4e03868a6..43284eb48 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -443,6 +443,24 @@ } } + tbody td.accounts-table__extra { + width: 120px; + text-align: right; + color: $darker-text-color; + padding-right: 16px; + + a { + text-decoration: none; + color: inherit; + + &:focus, + &:hover, + &:active { + text-decoration: underline; + } + } + } + &__comment { width: 50%; vertical-align: initial !important; diff --git a/app/models/account.rb b/app/models/account.rb index e0e916eb2..5476272f9 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -129,6 +129,8 @@ class Account < ApplicationRecord :unconfirmed_email, :current_sign_in_ip, :current_sign_in_at, + :created_at, + :sign_up_ip, :confirmed?, :approved?, :pending?, diff --git a/app/models/account_filter.rb b/app/models/account_filter.rb index 2b001385f..defd531ac 100644 --- a/app/models/account_filter.rb +++ b/app/models/account_filter.rb @@ -2,18 +2,15 @@ class AccountFilter KEYS = %i( - local - remote - by_domain - active - pending - silenced - suspended + origin + status + permissions username + by_domain display_name email ip - staff + invited_by order ).freeze @@ -21,11 +18,10 @@ class AccountFilter def initialize(params) @params = params - set_defaults! end def results - scope = Account.includes(:user).reorder(nil) + scope = Account.includes(:account_stat, user: [:session_activations, :invite_request]).without_instance_actor.reorder(nil) params.each do |key, value| scope.merge!(scope_for(key, value.to_s.strip)) if value.present? @@ -36,30 +32,16 @@ class AccountFilter private - def set_defaults! - params['local'] = '1' if params['remote'].blank? - params['active'] = '1' if params['suspended'].blank? && params['silenced'].blank? && params['pending'].blank? - params['order'] = 'recent' if params['order'].blank? - end - def scope_for(key, value) case key.to_s - when 'local' - Account.local.without_instance_actor - when 'remote' - Account.remote + when 'origin' + origin_scope(value) + when 'permissions' + permissions_scope(value) + when 'status' + status_scope(value) when 'by_domain' Account.where(domain: value) - when 'active' - Account.without_suspended - when 'pending' - accounts_with_users.merge(User.pending) - when 'disabled' - accounts_with_users.merge(User.disabled) - when 'silenced' - Account.silenced - when 'suspended' - Account.suspended when 'username' Account.matches_username(value) when 'display_name' @@ -68,8 +50,8 @@ class AccountFilter accounts_with_users.merge(User.matches_email(value)) when 'ip' valid_ip?(value) ? accounts_with_users.merge(User.matches_ip(value)) : Account.none - when 'staff' - accounts_with_users.merge(User.staff) + when 'invited_by' + invited_by_scope(value) when 'order' order_scope(value) else @@ -77,21 +59,56 @@ class AccountFilter end end + def origin_scope(value) + case value.to_s + when 'local' + Account.local + when 'remote' + Account.remote + else + raise "Unknown origin: #{value}" + end + end + + def status_scope(value) + case value.to_s + when 'active' + Account.without_suspended + when 'pending' + accounts_with_users.merge(User.pending) + when 'suspended' + Account.suspended + else + raise "Unknown status: #{value}" + end + end + def order_scope(value) - case value + case value.to_s when 'active' - params['remote'] ? Account.joins(:account_stat).by_recent_status : Account.joins(:user).by_recent_sign_in + accounts_with_users.left_joins(:account_stat).order(Arel.sql('coalesce(users.current_sign_in_at, account_stats.last_status_at, to_timestamp(0)) desc, accounts.id desc')) when 'recent' Account.recent - when 'alphabetic' - Account.alphabetic else raise "Unknown order: #{value}" end end + def invited_by_scope(value) + Account.left_joins(user: :invite).merge(Invite.where(user_id: value.to_s)) + end + + def permissions_scope(value) + case value.to_s + when 'staff' + accounts_with_users.merge(User.staff) + else + raise "Unknown permissions: #{value}" + end + end + def accounts_with_users - Account.joins(:user) + Account.left_joins(:user) end def valid_ip?(value) diff --git a/app/models/admin/action_log.rb b/app/models/admin/action_log.rb index 1d1db1b7a..852bff713 100644 --- a/app/models/admin/action_log.rb +++ b/app/models/admin/action_log.rb @@ -17,7 +17,7 @@ class Admin::ActionLog < ApplicationRecord serialize :recorded_changes belongs_to :account - belongs_to :target, polymorphic: true + belongs_to :target, polymorphic: true, optional: true default_scope -> { order('id desc') } diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb index 6e19dcf70..2af9d7c9c 100644 --- a/app/models/admin/action_log_filter.rb +++ b/app/models/admin/action_log_filter.rb @@ -11,6 +11,8 @@ class Admin::ActionLogFilter assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze, change_email_user: { target_type: 'User', action: 'change_email' }.freeze, confirm_user: { target_type: 'User', action: 'confirm' }.freeze, + approve_user: { target_type: 'User', action: 'approve' }.freeze, + reject_user: { target_type: 'User', action: 'reject' }.freeze, create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze, create_announcement: { target_type: 'Announcement', action: 'create' }.freeze, create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze, diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index f1e1c8a65..4bf1775bb 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -3,6 +3,7 @@ class Form::AccountBatch include ActiveModel::Model include Authorization + include AccountableConcern include Payloadable attr_accessor :account_ids, :action, :current_account @@ -25,19 +26,21 @@ class Form::AccountBatch suppress_follow_recommendation! when 'unsuppress_follow_recommendation' unsuppress_follow_recommendation! + when 'suspend' + suspend! end end private def follow! - accounts.find_each do |target_account| + accounts.each do |target_account| FollowService.new.call(current_account, target_account) end end def unfollow! - accounts.find_each do |target_account| + accounts.each do |target_account| UnfollowService.new.call(current_account, target_account) end end @@ -61,23 +64,31 @@ class Form::AccountBatch end def approve! - users = accounts.includes(:user).map(&:user) - - users.each { |user| authorize(user, :approve?) } - .each(&:approve!) + accounts.includes(:user).find_each do |account| + approve_account(account) + end end def reject! - records = accounts.includes(:user) + accounts.includes(:user).find_each do |account| + reject_account(account) + end + end - records.each { |account| authorize(account.user, :reject?) } - .each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) } + def suspend! + accounts.find_each do |account| + if account.user_pending? + reject_account(account) + else + suspend_account(account) + end + end end def suppress_follow_recommendation! authorize(:follow_recommendation, :suppress?) - accounts.each do |account| + accounts.find_each do |account| FollowRecommendationSuppression.create(account: account) end end @@ -87,4 +98,24 @@ class Form::AccountBatch FollowRecommendationSuppression.where(account_id: account_ids).destroy_all end + + def reject_account(account) + authorize(account.user, :reject?) + log_action(:reject, account.user, username: account.username) + account.suspend!(origin: :local) + AccountDeletionWorker.perform_async(account.id, reserve_username: false) + end + + def suspend_account(account) + authorize(account, :suspend?) + log_action(:suspend, account) + account.suspend!(origin: :local) + Admin::SuspensionWorker.perform_async(account.id) + end + + def approve_account(account) + authorize(account.user, :approve?) + log_action(:approve, account.user) + account.user.approve! + end end diff --git a/app/models/trends/tags.rb b/app/models/trends/tags.rb index 13e0ab56b..a425fd207 100644 --- a/app/models/trends/tags.rb +++ b/app/models/trends/tags.rb @@ -4,7 +4,7 @@ class Trends::Tags < Trends::Base PREFIX = 'trending_tags' self.default_options = { - threshold: 15, + threshold: 5, review_threshold: 10, max_score_cooldown: 2.days.freeze, max_score_halflife: 4.hours.freeze, diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml index c9bd8c686..2df91301e 100644 --- a/app/views/admin/accounts/_account.html.haml +++ b/app/views/admin/accounts/_account.html.haml @@ -1,24 +1,35 @@ -%tr - %td - = admin_account_link_to(account) - %td - %div.account-badges= account_badge(account, all: true) - %td - - if account.user_current_sign_in_ip - %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip - - else - \- - %td - - if account.user_current_sign_in_at - %time.time-ago{ datetime: account.user_current_sign_in_at.iso8601, title: l(account.user_current_sign_in_at) }= l account.user_current_sign_in_at - - elsif account.last_status_at.present? - %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at - - else - \- - %td - - if account.local? && account.user_pending? - = table_link_to 'check', t('admin.accounts.approve'), approve_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:approve, account.user) - = table_link_to 'times', t('admin.accounts.reject'), reject_admin_account_path(account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:reject, account.user) - - else - = table_link_to 'circle', t('admin.accounts.web'), web_path("accounts/#{account.id}") - = table_link_to 'globe', t('admin.accounts.public'), ActivityPub::TagManager.instance.url_for(account) +.batch-table__row{ class: [!account.suspended? && account.user_pending? && 'batch-table__row--attention', account.suspended? && 'batch-table__row--muted'] } + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id + .batch-table__row__content.batch-table__row__content--unpadded + %table.accounts-table + %tbody + %tr + %td + = account_link_to account, path: admin_account_path(account.id) + %td.accounts-table__count.optional + - if account.suspended? || account.user_pending? + \- + - else + = friendly_number_to_human account.statuses_count + %small= t('accounts.posts', count: account.statuses_count).downcase + %td.accounts-table__count.optional + - if account.suspended? || account.user_pending? + \- + - else + = friendly_number_to_human account.followers_count + %small= t('accounts.followers', count: account.followers_count).downcase + %td.accounts-table__count + = relevant_account_timestamp(account) + %small= t('accounts.last_active') + %td.accounts-table__extra + - if account.local? + - if account.user_email + = link_to account.user_email.split('@').last, admin_accounts_path(email: "%@#{account.user_email.split('@').last}"), title: account.user_email + - else + \- + %br/ + %samp.ellipsized-ip= relevant_account_ip(account, params[:ip]) + - if !account.suspended? && account.user_pending? && account.user&.invite_request&.text&.present? + .batch-table__row__content__quote + %p= account.user&.invite_request&.text diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 398ab4bb4..fc667b376 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -5,30 +5,30 @@ .filter-subset %strong= t('admin.accounts.location.title') %ul - %li= filter_link_to t('admin.accounts.location.local'), remote: nil - %li= filter_link_to t('admin.accounts.location.remote'), remote: '1' + %li= filter_link_to t('generic.all'), origin: nil + %li= filter_link_to t('admin.accounts.location.local'), origin: 'local' + %li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote' .filter-subset %strong= t('admin.accounts.moderation.title') %ul - %li= link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), admin_pending_accounts_path - %li= filter_link_to t('admin.accounts.moderation.active'), silenced: nil, suspended: nil, pending: nil - %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1', suspended: nil, pending: nil - %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1', silenced: nil, pending: nil + %li= filter_link_to t('generic.all'), status: nil + %li= filter_link_to t('admin.accounts.moderation.active'), status: 'active' + %li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended' + %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending' .filter-subset %strong= t('admin.accounts.role') %ul - %li= filter_link_to t('admin.accounts.moderation.all'), staff: nil - %li= filter_link_to t('admin.accounts.roles.staff'), staff: '1' + %li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil + %li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff' .filter-subset %strong= t 'generic.order_by' %ul %li= filter_link_to t('relationships.most_recent'), order: nil - %li= filter_link_to t('admin.accounts.username'), order: 'alphabetic' %li= filter_link_to t('relationships.last_active'), order: 'active' = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do .fields-group - - AccountFilter::KEYS.each do |key| + - (AccountFilter::KEYS - %i(origin status permissions)).each do |key| - if params[key].present? = hidden_field_tag key, params[key] @@ -41,16 +41,27 @@ %button.button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative' -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.accounts.username') - %th= t('admin.accounts.role') - %th= t('admin.accounts.most_recent_ip') - %th= t('admin.accounts.most_recent_activity') - %th - %tbody - = render partial: 'account', collection: @accounts += form_for(@form, url: batch_admin_accounts_path) do |f| + = hidden_field_tag :page, params[:page] || 1 + + - AccountFilter::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 + - if @accounts.any? { |account| account.user_pending? } + = f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + + = f.button safe_join([fa_icon('lock'), t('admin.accounts.perform_full_suspension')]), name: :suspend, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + .batch-table__body + - if @accounts.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = render partial: 'account', collection: @accounts, locals: { f: f } = paginate @accounts diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index b27676e4c..2ee13b9e2 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -35,7 +35,7 @@ %span= t('admin.dashboard.pending_reports_html', count: @pending_reports_count) = fa_icon 'chevron-right fw' - = link_to admin_pending_accounts_path, class: 'dashboard__quick-access' do + = link_to admin_accounts_path(status: 'pending'), class: 'dashboard__quick-access' do %span= t('admin.dashboard.pending_users_html', count: @pending_users_count) = fa_icon 'chevron-right fw' diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index 462529338..d6542ac3e 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -15,7 +15,7 @@ .dashboard__counters %div - = link_to admin_accounts_path(remote: '1', by_domain: @instance.domain) do + = link_to admin_accounts_path(origin: 'remote', by_domain: @instance.domain) do .dashboard__counters__num= number_with_delimiter @instance.accounts_count .dashboard__counters__label= t 'admin.accounts.title' %div diff --git a/app/views/admin/ip_blocks/_ip_block.html.haml b/app/views/admin/ip_blocks/_ip_block.html.haml index e07e2b444..b8d3ac0e8 100644 --- a/app/views/admin/ip_blocks/_ip_block.html.haml +++ b/app/views/admin/ip_blocks/_ip_block.html.haml @@ -1,9 +1,9 @@ .batch-table__row %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox = f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id - .batch-table__row__content - .batch-table__row__content__text - %samp= "#{ip_block.ip}/#{ip_block.ip.prefix}" + .batch-table__row__content.pending-account + .pending-account__header + %samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}") - if ip_block.comment.present? • = ip_block.comment diff --git a/app/views/admin/pending_accounts/_account.html.haml b/app/views/admin/pending_accounts/_account.html.haml deleted file mode 100644 index 5b475b59a..000000000 --- a/app/views/admin/pending_accounts/_account.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.batch-table__row - %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox - = f.check_box :account_ids, { multiple: true, include_hidden: false }, account.id - .batch-table__row__content.pending-account - .pending-account__header - = link_to admin_account_path(account.id) do - %strong= account.user_email - = "(@#{account.username})" - %br/ - %samp= account.user_current_sign_in_ip - • - = t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at) - - - if account.user&.invite_request&.text&.present? - .pending-account__body - %p= account.user&.invite_request&.text diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml deleted file mode 100644 index 8101d7f99..000000000 --- a/app/views/admin/pending_accounts/index.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -- content_for :page_title do - = t('admin.pending_accounts.title', count: User.pending.count) - -= form_for(@form, url: batch_admin_pending_accounts_path) do |f| - = hidden_field_tag :page, params[:page] || 1 - - .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('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } - - = f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } - .batch-table__body - - if @accounts.empty? - = nothing_here 'nothing-here--under-tabs' - - else - = render partial: 'account', collection: @accounts, locals: { f: f } - -= paginate @accounts - -%hr.spacer/ - -%div.action-buttons - %div - = link_to t('admin.accounts.approve_all'), approve_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' - - %div - = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' diff --git a/app/views/admin_mailer/new_pending_account.text.erb b/app/views/admin_mailer/new_pending_account.text.erb index a466ee2de..bcc251819 100644 --- a/app/views/admin_mailer/new_pending_account.text.erb +++ b/app/views/admin_mailer/new_pending_account.text.erb @@ -9,4 +9,4 @@ <%= quote_wrap(@account.user&.invite_request&.text) %> <% end %> -<%= raw t('application_mailer.view')%> <%= admin_pending_accounts_url %> +<%= raw t('application_mailer.view')%> <%= admin_accounts_url(status: 'pending') %> diff --git a/app/workers/scheduler/follow_recommendations_scheduler.rb b/app/workers/scheduler/follow_recommendations_scheduler.rb index cb1e15961..effc63e59 100644 --- a/app/workers/scheduler/follow_recommendations_scheduler.rb +++ b/app/workers/scheduler/follow_recommendations_scheduler.rb @@ -16,12 +16,12 @@ class Scheduler::FollowRecommendationsScheduler AccountSummary.refresh FollowRecommendation.refresh - fallback_recommendations = FollowRecommendation.limit(SET_SIZE).index_by(&:account_id) + fallback_recommendations = FollowRecommendation.order(rank: :desc).limit(SET_SIZE).index_by(&:account_id) I18n.available_locales.each do |locale| recommendations = begin if AccountSummary.safe.filtered.localized(locale).exists? # We can skip the work if no accounts with that language exist - FollowRecommendation.localized(locale).limit(SET_SIZE).index_by(&:account_id) + FollowRecommendation.localized(locale).order(rank: :desc).limit(SET_SIZE).index_by(&:account_id) else {} end |