Kay Faraday 7 months ago
parent
commit
f39c19ceee
  1. 1
      .dockerignore
  2. 11
      .github/workflows/build-image.yml
  3. 1
      .gitignore
  4. 4
      Dockerfile
  5. 14
      Gemfile
  6. 88
      Gemfile.lock
  7. 43
      app/controllers/admin/accounts_controller.rb
  8. 9
      app/controllers/admin/instances_controller.rb
  9. 52
      app/controllers/admin/pending_accounts_controller.rb
  10. 4
      app/controllers/concerns/accountable_concern.rb
  11. 2
      app/controllers/concerns/two_factor_authentication_concern.rb
  12. 6
      app/helpers/admin/action_logs_helper.rb
  13. 39
      app/helpers/admin/dashboard_helper.rb
  14. 27
      app/javascript/flavours/glitch/actions/compose.js
  15. 2
      app/javascript/flavours/glitch/actions/importer/normalizer.js
  16. 12
      app/javascript/flavours/glitch/components/admin/Retention.js
  17. 4
      app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js
  18. 8
      app/javascript/flavours/glitch/packs/public.js
  19. 14
      app/javascript/flavours/glitch/reducers/compose.js
  20. 30
      app/javascript/flavours/glitch/styles/accounts.scss
  21. 5
      app/javascript/flavours/glitch/styles/tables.scss
  22. 18
      app/javascript/flavours/glitch/styles/widgets.scss
  23. 27
      app/javascript/mastodon/actions/compose.js
  24. 2
      app/javascript/mastodon/actions/importer/normalizer.js
  25. 12
      app/javascript/mastodon/components/admin/Retention.js
  26. 4
      app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js
  27. 14
      app/javascript/mastodon/reducers/compose.js
  28. 8
      app/javascript/packs/public.js
  29. 30
      app/javascript/styles/mastodon/accounts.scss
  30. 13
      app/javascript/styles/mastodon/components.scss
  31. 5
      app/javascript/styles/mastodon/tables.scss
  32. 18
      app/javascript/styles/mastodon/widgets.scss
  33. 2
      app/models/account.rb
  34. 91
      app/models/account_filter.rb
  35. 2
      app/models/admin/action_log.rb
  36. 4
      app/models/admin/action_log_filter.rb
  37. 4
      app/models/canonical_email_block.rb
  38. 51
      app/models/form/account_batch.rb
  39. 2
      app/models/trends/tags.rb
  40. 2
      app/models/user.rb
  41. 4
      app/policies/account_policy.rb
  42. 4
      app/policies/instance_policy.rb
  43. 11
      app/services/purge_domain_service.rb
  44. 59
      app/views/admin/accounts/_account.html.haml
  45. 53
      app/views/admin/accounts/index.html.haml
  46. 4
      app/views/admin/accounts/show.html.haml
  47. 2
      app/views/admin/dashboard/index.html.haml
  48. 4
      app/views/admin/instances/show.html.haml
  49. 6
      app/views/admin/ip_blocks/_ip_block.html.haml
  50. 16
      app/views/admin/pending_accounts/_account.html.haml
  51. 30
      app/views/admin/pending_accounts/index.html.haml
  52. 2
      app/views/admin_mailer/new_pending_account.text.erb
  53. 9
      app/workers/admin/domain_purge_worker.rb
  54. 4
      app/workers/scheduler/follow_recommendations_scheduler.rb
  55. 2
      config/environments/development.rb
  56. 2
      config/initializers/sidekiq.rb
  57. 26
      config/locales/en.yml
  58. 2
      config/navigation.rb
  59. 15
      config/routes.rb
  60. 6
      crowdin.yml
  61. 24
      db/migrate/20211213040746_update_account_summaries_to_version_2.rb
  62. 4
      db/schema.rb
  63. 23
      db/views/account_summaries_v02.sql
  64. 1
      dist/mastodon-sidekiq.service
  65. 1
      dist/mastodon-streaming.service
  66. 1
      dist/mastodon-web.service
  67. 4
      lib/cli.rb
  68. 64
      lib/mastodon/canonical_email_blocks_cli.rb
  69. 186
      lib/mastodon/statuses_cli.rb
  70. 8
      lib/sidekiq_error_handler.rb
  71. 44
      package.json
  72. 46
      spec/controllers/admin/accounts_controller_spec.rb
  73. 35
      spec/controllers/admin/instances_controller_spec.rb
  74. 42
      spec/models/account_filter_spec.rb
  75. 2
      spec/policies/account_policy_spec.rb
  76. 2
      spec/policies/instance_policy_spec.rb
  77. 27
      spec/services/purge_domain_service_spec.rb
  78. 18
      spec/workers/admin/domain_purge_worker_spec.rb
  79. 131
      streaming/index.js
  80. 2450
      yarn.lock

1
.dockerignore

@ -15,6 +15,7 @@ vendor/bundle
*.swp
*~
postgres
postgres14
redis
elasticsearch
chart

11
.github/workflows/build-image.yml

@ -14,14 +14,15 @@ jobs:
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v3
id: meta
with:
images: tootsuite/mastodon
images: ghcr.io/${{ github.repository_owner }}/mastodon
flavor: |
latest=auto
latest=true
tags: |
type=edge,branch=main
type=semver,pattern={{ raw }}
@ -30,5 +31,5 @@ jobs:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=tootsuite/mastodon:latest
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest
cache-to: type=inline

1
.gitignore vendored

@ -40,6 +40,7 @@
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
/postgres
/postgres14
/redis
/elasticsearch

4
Dockerfile

@ -56,8 +56,8 @@ RUN npm install -g npm@latest && \
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \
bundle config set deployment 'true' && \
bundle config set without 'development test' && \
bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle config set silence_root_warning true && \
bundle install -j"$(nproc)" && \
yarn install --pure-lockfile

14
Gemfile

@ -18,7 +18,7 @@ gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.107', require: false
gem 'aws-sdk-s3', '~> 1.109', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.0'
@ -102,7 +102,7 @@ gem 'rdf-normalize', '~> 0.4'
gem 'redcarpet', '~> 3.5'
group :development, :test do
gem 'fabrication', '~> 2.22'
gem 'fabrication', '~> 2.23'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.9'
@ -123,7 +123,7 @@ group :test do
gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.21', require: false
gem 'webmock', '~> 3.14'
gem 'rspec_junit_formatter', '~> 0.4'
gem 'rspec_junit_formatter', '~> 0.5'
end
group :development do
@ -131,13 +131,13 @@ group :development do
gem 'annotate', '~> 3.1'
gem 'better_errors', '~> 2.9'
gem 'binding_of_caller', '~> 1.0'
gem 'bullet', '~> 6.1'
gem 'bullet', '~> 7.0'
gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 2.0'
gem 'memory_profiler'
gem 'rubocop', '~> 1.23', require: false
gem 'rubocop-rails', '~> 2.12', require: false
gem 'brakeman', '~> 5.1', require: false
gem 'rubocop', '~> 1.24', require: false
gem 'rubocop-rails', '~> 2.13', require: false
gem 'brakeman', '~> 5.2', require: false
gem 'bundler-audit', '~> 0.9', require: false
gem 'capistrano', '~> 3.16'

88
Gemfile.lock

@ -79,16 +79,16 @@ GEM
encryptor (~> 3.0.0)
awrence (1.1.1)
aws-eventstream (1.2.0)
aws-partitions (1.534.0)
aws-sdk-core (3.123.0)
aws-partitions (1.539.0)
aws-sdk-core (3.124.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.51.0)
aws-sdk-kms (1.52.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.107.0)
aws-sdk-s3 (1.109.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -106,13 +106,13 @@ GEM
ffi (~> 1.14)
bootsnap (1.9.3)
msgpack (~> 1.0)
brakeman (5.1.2)
brakeman (5.2.0)
browser (4.2.0)
brpoplpush-redis_script (0.1.2)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, <= 5.0)
builder (3.2.4)
bullet (6.1.5)
bullet (7.0.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.0.1)
@ -168,7 +168,7 @@ GEM
css_parser (1.7.1)
addressable
debug_inspector (1.0.0)
devise (4.8.0)
devise (4.8.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@ -184,8 +184,8 @@ GEM
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.4.4)
discard (1.2.0)
activerecord (>= 4.2, < 7)
discard (1.2.1)
activerecord (>= 4.2, < 8)
docile (1.3.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
@ -211,7 +211,7 @@ GEM
et-orbi (1.2.4)
tzinfo
excon (0.76.0)
fabrication (2.22.0)
fabrication (2.23.1)
faker (2.19.0)
i18n (>= 1.6, < 2)
faraday (1.8.0)
@ -234,7 +234,7 @@ GEM
faraday-patron (1.0.0)
faraday-rack (1.0.0)
fast_blank (1.0.1)
fastimage (2.2.5)
fastimage (2.2.6)
ffi (1.15.4)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
@ -291,7 +291,7 @@ GEM
rainbow (>= 2.0.0)
i18n (1.8.11)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.35)
i18n-tasks (0.9.37)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@ -319,18 +319,18 @@ GEM
rdf (~> 3.1)
jsonapi-renderer (0.2.2)
jwt (2.2.2)
kaminari (1.2.1)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
kaminari-actionview (= 1.2.2)
kaminari-activerecord (= 1.2.2)
kaminari-core (= 1.2.2)
kaminari-actionview (1.2.2)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
kaminari-core (= 1.2.2)
kaminari-activerecord (1.2.2)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
kt-paperclip (7.0.1)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@ -355,7 +355,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.12.0)
loofah (2.13.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -376,7 +376,7 @@ GEM
mime-types-data (3.2021.1115)
mini_mime (1.1.2)
mini_portile2 (2.6.1)
minitest (5.14.4)
minitest (5.15.0)
msgpack (1.4.2)
multi_json (1.15.0)
multipart-post (2.1.1)
@ -393,7 +393,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.13.9)
oj (3.13.11)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@ -412,13 +412,13 @@ GEM
orm_adapter (0.5.0)
ox (2.14.6)
parallel (1.21.0)
parser (3.0.2.0)
parser (3.1.0.0)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.2.3)
pghero (2.8.1)
pghero (2.8.2)
activerecord (>= 5)
pkg-config (1.4.6)
posix-spawn (0.3.15)
@ -500,7 +500,7 @@ GEM
redis (4.5.1)
redis-namespace (1.8.1)
redis (>= 3.0.4)
regexp_parser (2.1.1)
regexp_parser (2.2.0)
request_store (1.5.0)
rack (>= 1.4)
responders (3.0.1)
@ -532,21 +532,21 @@ GEM
rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.10.2)
rspec_junit_formatter (0.4.1)
rspec-support (3.10.3)
rspec_junit_formatter (0.5.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.23.0)
rubocop (1.24.1)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.12.0, < 2.0)
rubocop-ast (>= 1.15.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.13.0)
rubocop-ast (1.15.1)
parser (>= 3.0.1.1)
rubocop-rails (2.12.4)
rubocop-rails (2.13.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
@ -580,7 +580,7 @@ GEM
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.8)
sidekiq-unique-jobs (7.1.12)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 8.0)
@ -599,7 +599,7 @@ GEM
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.1)
sprockets-rails (3.4.2)
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
@ -609,7 +609,7 @@ GEM
stackprof (0.2.17)
statsd-ruby (1.5.0)
stoplight (2.2.1)
strong_migrations (0.7.8)
strong_migrations (0.7.9)
activerecord (>= 5)
temple (0.8.2)
terminal-table (3.0.2)
@ -676,7 +676,7 @@ GEM
xorcist (1.1.2)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.5.1)
zeitwerk (2.5.3)
PLATFORMS
ruby
@ -686,14 +686,14 @@ DEPENDENCIES
active_record_query_trace (~> 1.8)
addressable (~> 2.8)
annotate (~> 3.1)
aws-sdk-s3 (~> 1.107)
aws-sdk-s3 (~> 1.109)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.9.2)
brakeman (~> 5.1)
brakeman (~> 5.2)
browser
bullet (~> 6.1)
bullet (~> 7.0)
bundler-audit (~> 0.9)
capistrano (~> 3.16)
capistrano-rails (~> 1.6)
@ -714,7 +714,7 @@ DEPENDENCIES
doorkeeper (~> 5.5)
dotenv-rails (~> 2.7)
ed25519 (~> 1.2)
fabrication (~> 2.22)
fabrication (~> 2.23)
faker (~> 2.19)
fast_blank (~> 1.0)
fastimage
@ -778,9 +778,9 @@ DEPENDENCIES
rqrcode (~> 2.1)
rspec-rails (~> 5.0)
rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4)
rubocop (~> 1.23)
rubocop-rails (~> 2.12)
rspec_junit_formatter (~> 0.5)
rubocop (~> 1.24)
rubocop-rails (~> 2.13)
ruby-progressbar (~> 1.11)
sanitize (~> 6.0)
scenic (~> 1.5)

43
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
@ -106,6 +117,16 @@ module Admin
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
end
def unblock_email
authorize @account, :unblock_email?
CanonicalEmailBlock.where(reference_account: @account).delete_all
log_action :unblock_email, @account
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unblocked_email_msg', username: @account.acct)
end
private
def set_account
@ -121,11 +142,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

9
app/controllers/admin/instances_controller.rb

@ -14,6 +14,15 @@ module Admin
authorize :instance, :show?
end
def destroy
authorize :instance, :destroy?
Admin::DomainPurgeWorker.perform_async(@instance.domain)
log_action :destroy, @instance
redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @instance.domain)
end
def clear_delivery_errors
authorize :delivery, :clear_delivery_errors?

52
app/controllers/admin/pending_accounts_controller.rb

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

4
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

2
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

6
app/helpers/admin/action_logs_helper.rb

@ -31,11 +31,15 @@ module Admin::ActionLogsHelper
link_to truncate(record.text), edit_admin_announcement_path(record.id)
when 'IpBlock'
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
when 'Instance'
record.domain
end
end
def log_target_from_history(type, attributes)
case type
when 'User'
attributes['username']
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
@ -52,6 +56,8 @@ module Admin::ActionLogsHelper
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
when 'IpBlock'
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
when 'Instance'
attributes['domain']
end
end
end

39
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

27
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,
});
}
};
};

2
app/javascript/flavours/glitch/actions/importer/normalizer.js

@ -62,7 +62,7 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;

12
app/javascript/flavours/glitch/components/admin/Retention.js

@ -42,6 +42,7 @@ export default class Retention extends React.PureComponent {
render () {
const { loading, data } = this.state;
const { frequency } = this.props;
let content;
@ -129,9 +130,18 @@ export default class Retention extends React.PureComponent {
);
}
let title = null;
switch(frequency) {
case 'day':
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
break;
default:
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
};
return (
<div className='retention'>
<h4><FormattedMessage id='admin.dashboard.retention' defaultMessage='Retention' /></h4>
<h4>{title}</h4>
{content}
</div>

4
app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.js

@ -33,8 +33,8 @@ class ColumnSettings extends React.PureComponent {
tags (mode) {
let tags = this.props.settings.getIn(['tags', mode]) || [];
if (tags.toJSON) {
return tags.toJSON();
if (tags.toJS) {
return tags.toJS();
} else {
return tags;
}

8
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('');

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

30
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;
}
}

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

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

27
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,
});
}
};
};

2
app/javascript/mastodon/actions/importer/normalizer.js

@ -71,7 +71,7 @@ export function normalizeStatus(status, normalOldStatus) {
}
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;

12
app/javascript/mastodon/components/admin/Retention.js

@ -42,6 +42,7 @@ export default class Retention extends React.PureComponent {
render () {
const { loading, data } = this.state;
const { frequency } = this.props;
let content;
@ -129,9 +130,18 @@ export default class Retention extends React.PureComponent {
);
}
let title = null;
switch(frequency) {
case 'day':
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
break;
default:
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
};
return (
<div className='retention'>
<h4><FormattedMessage id='admin.dashboard.retention' defaultMessage='Retention' /></h4>
<h4>{title}</h4>
{content}
</div>

4
app/javascript/mastodon/features/hashtag_timeline/components/column_settings.js

@ -33,8 +33,8 @@ class ColumnSettings extends React.PureComponent {
tags (mode) {
let tags = this.props.settings.getIn(['tags', mode]) || [];
if (tags.toJSON) {
return tags.toJSON();
if (tags.toJS) {
return tags.toJS();
} else {
return tags;
}

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

8
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('');

30
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;
}
}

13
app/javascript/styles/mastodon/components.scss

@ -3074,17 +3074,20 @@ a.account__display-name {
box-sizing: border-box;
width: 100%;
margin: 0;
color: $inverted-text-color;
background: $simple-background-color;
padding: 10px;
color: $darker-text-color;
background: transparent;
padding: 7px 0;
font-family: inherit;
font-size: 14px;
resize: vertical;
border: 0;
border-bottom: 2px solid $ui-primary-color;
outline: 0;
border-radius: 4px;
&:focus {
&:focus,
&:active {
color: $primary-text-color;
border-bottom-color: $ui-highlight-color;
outline: 0;
}

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

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

2
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?,

91
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