Browse Source

Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- `app/controllers/statuses_controller.rb`:
  Upstream disabled the embed controller for reblogs.
  Not a real conflict, but glitch-soc has an extra line to deal
  with its theming system.
  Ported upstream changes.
- `app/javascript/packs/public.js`:
  Upstream made changes to get rid of most inline CSS, this changes
  javascript for public pages, which in glitch are split between
  different files. Ported those changes.
- `app/models/status.rb`:
  Upstream changed the block check in `Status#permitted_for` to
  include domain-block checks. Not a real conflict with glitch-soc,
  but our scope is slightly different, as our scope for
  unauthenticated access do not include instance-local toots.
  Ported upstream changes.
- `app/serializers/rest/instance_serializer.rb`:
  Not a real conflict, upstream added a new field to the instance
  serializer, the conflict is one line above since we added more of
  that.
  Ported upstream changes.
- `app/views/settings/profiles/show.html.haml`:
  Upstream got rid of most inline CSS and moved hidden elements
  to data attributes in the process, in fields were we have
  different values.
  Ported upstream changes while keeping our glitch-specific
  values.
- `app/views/statuses/_simple_status.html.haml`:
  Upstream got rid of inline CSS on an HAML line we treat
  differently, stripping empty text nodes.
  Ported upstream changes to the style attribute, keeping
  the empty text node stripping behavior.
master^2
Thibaut Girka 7 months ago
parent
commit
a22e6a3683
81 changed files with 2170 additions and 624 deletions
  1. +61
    -63
      .circleci/config.yml
  2. +2
    -1
      Gemfile
  3. +12
    -9
      Gemfile.lock
  4. +7
    -7
      app/controllers/accounts_controller.rb
  5. +10
    -7
      app/controllers/activitypub/collections_controller.rb
  6. +3
    -3
      app/controllers/activitypub/outboxes_controller.rb
  7. +15
    -6
      app/controllers/activitypub/replies_controller.rb
  8. +1
    -1
      app/controllers/api/v1/polls/votes_controller.rb
  9. +1
    -1
      app/controllers/api/v1/polls_controller.rb
  10. +6
    -5
      app/controllers/api/v1/push/subscriptions_controller.rb
  11. +1
    -2
      app/controllers/api/v1/statuses/mutes_controller.rb
  12. +1
    -1
      app/controllers/api/v1/statuses_controller.rb
  13. +1
    -1
      app/controllers/media_controller.rb
  14. +1
    -1
      app/controllers/remote_interaction_controller.rb
  15. +1
    -1
      app/controllers/statuses_controller.rb
  16. +1
    -1
      app/javascript/core/settings.js
  17. +6
    -8
      app/javascript/flavours/glitch/packs/public.js
  18. +1
    -1
      app/javascript/mastodon/actions/timelines.js
  19. +1
    -1
      app/javascript/mastodon/components/dropdown_menu.js
  20. +1
    -1
      app/javascript/mastodon/features/compose/components/privacy_dropdown.js
  21. +1
    -1
      app/javascript/mastodon/reducers/statuses.js
  22. +5
    -5
      app/javascript/mastodon/reducers/timelines.js
  23. +6
    -8
      app/javascript/packs/public.js
  24. +5
    -0
      app/javascript/styles/mastodon/about.scss
  25. +20
    -0
      app/javascript/styles/mastodon/admin.scss
  26. +16
    -0
      app/javascript/styles/mastodon/basics.scss
  27. +12
    -0
      app/javascript/styles/mastodon/components.scss
  28. +21
    -0
      app/javascript/styles/mastodon/forms.scss
  29. +30
    -0
      app/javascript/styles/mastodon/polls.scss
  30. +17
    -0
      app/javascript/styles/mastodon/statuses.scss
  31. +46
    -44
      app/models/account.rb
  32. +1
    -1
      app/models/concerns/omniauthable.rb
  33. +15
    -14
      app/models/custom_emoji.rb
  34. +18
    -17
      app/models/media_attachment.rb
  35. +24
    -19
      app/models/preview_card.rb
  36. +2
    -6
      app/models/status.rb
  37. +5
    -1
      app/serializers/rest/instance_serializer.rb
  38. +12
    -1
      app/services/fetch_resource_service.rb
  39. +3
    -3
      app/views/about/show.html.haml
  40. +4
    -2
      app/views/accounts/_moved.html.haml
  41. +1
    -1
      app/views/admin/accounts/_account.html.haml
  42. +13
    -13
      app/views/admin/accounts/show.html.haml
  43. +1
    -1
      app/views/admin/instances/index.html.haml
  44. +3
    -3
      app/views/admin/instances/show.html.haml
  45. +4
    -4
      app/views/admin/pending_accounts/index.html.haml
  46. +1
    -1
      app/views/admin/relationships/index.html.haml
  47. +4
    -2
      app/views/admin/reports/show.html.haml
  48. +1
    -1
      app/views/admin/statuses/index.html.haml
  49. +1
    -1
      app/views/admin/statuses/show.html.haml
  50. +4
    -4
      app/views/admin/tags/index.html.haml
  51. +0
    -1
      app/views/application/_card.html.haml
  52. +2
    -2
      app/views/auth/registrations/new.html.haml
  53. +1
    -1
      app/views/auth/sessions/two_factor.html.haml
  54. +0
    -1
      app/views/directories/index.html.haml
  55. +1
    -1
      app/views/layouts/application.html.haml
  56. +1
    -1
      app/views/layouts/embedded.html.haml
  57. +1
    -1
      app/views/public_timelines/show.html.haml
  58. +2
    -2
      app/views/settings/preferences/appearance/show.html.haml
  59. +1
    -1
      app/views/settings/profiles/show.html.haml
  60. +3
    -3
      app/views/statuses/_detailed_status.html.haml
  61. +5
    -3
      app/views/statuses/_poll.html.haml
  62. +6
    -6
      app/views/statuses/_simple_status.html.haml
  63. +19
    -3
      config/initializers/paperclip.rb
  64. +9
    -0
      db/migrate/20200417125749_add_storage_schema_version.rb
  65. +6
    -1
      db/schema.rb
  66. +12
    -0
      ide-helper.js
  67. +4
    -0
      lib/cli.rb
  68. +4
    -0
      lib/mastodon/cli_helper.rb
  69. +43
    -1
      lib/mastodon/emoji_cli.rb
  70. +17
    -7
      lib/mastodon/media_cli.rb
  71. +148
    -0
      lib/mastodon/upgrade_cli.rb
  72. +9
    -0
      lib/paperclip/attachment_extensions.rb
  73. +122
    -10
      spec/controllers/activitypub/collections_controller_spec.rb
  74. +17
    -11
      spec/controllers/activitypub/inboxes_controller_spec.rb
  75. +162
    -8
      spec/controllers/activitypub/outboxes_controller_spec.rb
  76. +196
    -0
      spec/controllers/activitypub/replies_controller_spec.rb
  77. +768
    -75
      spec/controllers/statuses_controller_spec.rb
  78. +1
    -1
      spec/fabricators/status_pin_fabricator.rb
  79. +4
    -8
      spec/models/status_spec.rb
  80. +10
    -2
      spec/services/fetch_resource_service_spec.rb
  81. +167
    -200
      yarn.lock

+ 61
- 63
.circleci/config.yml View File

@@ -5,12 +5,13 @@ aliases:
docker:
- image: circleci/ruby:2.7-buster-node
environment: &ruby_environment
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
BUNDLE_APP_CONFIG: ./.bundle/
BUNDLE_PATH: ./vendor/bundle/
DB_HOST: localhost
DB_USER: root
RAILS_ENV: test
PARALLEL_TEST_PROCESSORS: 4
ALLOW_NOPAM: true
CONTINUOUS_INTEGRATION: true
DISABLE_SIMPLECOV: true
@@ -32,9 +33,9 @@ aliases:
- &restore_ruby_dependencies
restore_cache:
keys:
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
- v2-ruby-dependencies-
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
- v3-ruby-dependencies-

- &install_steps
steps:
@@ -42,11 +43,13 @@ aliases:
- *attach_workspace
- restore_cache:
keys:
- v1-node-dependencies-{{ checksum "yarn.lock" }}
- v1-node-dependencies-
- run: yarn install --frozen-lockfile
- v2-node-dependencies-{{ checksum "yarn.lock" }}
- v2-node-dependencies-
- run:
name: Install yarn dependencies
command: yarn install --frozen-lockfile
- save_cache:
key: v1-node-dependencies-{{ checksum "yarn.lock" }}
key: v2-node-dependencies-{{ checksum "yarn.lock" }}
paths:
- ./node_modules/
- *persist_to_workspace
@@ -57,27 +60,28 @@ aliases:
command: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
## TODO: FIX THESE BUSTER DEPENDANCES
sudo wget http://ftp.au.debian.org/debian/pool/main/i/icu/libicu57_57.1-6+deb9u3_amd64.deb
sudo dpkg -i libicu57_57.1-6+deb9u3_amd64.deb
sudo wget http://ftp.au.debian.org/debian/pool/main/p/protobuf/libprotobuf10_3.0.0-9_amd64.deb
sudo dpkg -i libprotobuf10_3.0.0-9_amd64.deb

- &install_ruby_dependencies
steps:
- *attach_workspace
- *install_system_dependencies
- run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- run:
name: Set Ruby version
command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- *restore_ruby_dependencies
- run: bundle config set clean 'true'
- run: bundle config set deployment 'true'
- run: bundle config set with 'pam_authentication'
- run: bundle config set without 'development production'
- run: bundle config set frozen 'true'
- run: bundle install --jobs 16 --retry 3 && bundle clean
- run:
name: Set bundler settings
command: |
bundle config clean 'true'
bundle config deployment 'true'
bundle config with 'pam_authentication'
bundle config without 'development production'
bundle config frozen 'true'
- run:
name: Install bundler dependencies
command: bundle check || (bundle install && bundle clean)
- save_cache:
key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
paths:
- ./.bundle/
- ./vendor/bundle/
@@ -88,17 +92,26 @@ aliases:
- ./mastodon/vendor/bundle/

- &test_steps
parallelism: 4
steps:
- *attach_workspace
- *install_system_dependencies
- run: sudo apt-get install -y ffmpeg
- run:
name: Prepare Tests
command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
name: Install FFMPEG
command: sudo apt-get install -y ffmpeg
- run:
name: Run Tests
command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec

name: Load database schema
command: ./bin/rails db:create db:schema:load db:seed
- run:
name: Run rspec in parallel
command: |
bundle exec rspec --profile 10 \
--format RspecJunitFormatter \
--out test_results/rspec.xml \
--format progress \
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
- store_test_results:
path: test_results
jobs:
install:
<<: *defaults
@@ -115,19 +128,14 @@ jobs:
environment: *ruby_environment
<<: *install_ruby_dependencies

install-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5-buster-node
environment: *ruby_environment
<<: *install_ruby_dependencies

build:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run: ./bin/rails assets:precompile
- run:
name: Precompile assets
command: ./bin/rails assets:precompile
- persist_to_workspace:
root: ~/projects/
paths:
@@ -149,10 +157,10 @@ jobs:
- *install_system_dependencies
- run:
name: Create database
command: ./bin/rails parallel:create
command: ./bin/rails db:create
- run:
name: Run migrations
command: ./bin/rails parallel:migrate
command: ./bin/rails db:migrate

test-ruby2.7:
<<: *defaults
@@ -178,35 +186,33 @@ jobs:
- image: circleci/redis:5-alpine
<<: *test_steps

test-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
<<: *test_steps

test-webui:
<<: *defaults
docker:
- image: circleci/node:12-buster
steps:
- *attach_workspace
- run: ./bin/retry yarn test:jest
- run:
name: Run jest
command: yarn test:jest

check-i18n:
<<: *defaults
steps:
- *attach_workspace
- *install_system_dependencies
- run: bundle exec i18n-tasks check-normalized
- run: bundle exec i18n-tasks unused -l en
- run: bundle exec i18n-tasks check-consistent-interpolations
- run: bundle exec rake repo:check_locales_files
- run:
name: Check locale file normalization
command: bundle exec i18n-tasks check-normalized
- run:
name: Check for unused strings
command: bundle exec i18n-tasks unused -l en
- run:
name: Check for wrong string interpolations
command: bundle exec i18n-tasks check-consistent-interpolations
- run:
name: Check that all required locale files exist
command: bundle exec rake repo:check_locales_files

workflows:
version: 2
@@ -220,10 +226,6 @@ workflows:
requires:
- install
- install-ruby2.7
- install-ruby2.5:
requires:
- install
- install-ruby2.7
- build:
requires:
- install-ruby2.7
@@ -238,10 +240,6 @@ workflows:
requires:
- install-ruby2.6
- build
- test-ruby2.5:
requires:
- install-ruby2.5
- build
- test-webui:
requires:
- install


+ 2
- 1
Gemfile View File

@@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7'

gem 'aws-sdk-s3', '~> 1.61', require: false
gem 'aws-sdk-s3', '~> 1.63', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@@ -129,6 +129,7 @@ group :test do
gem 'simplecov', '~> 0.18', require: false
gem 'webmock', '~> 3.8'
gem 'parallel_tests', '~> 2.32'
gem 'rspec_junit_formatter', '~> 0.4'
end

group :development do


+ 12
- 9
Gemfile.lock View File

@@ -92,7 +92,7 @@ GEM
av (0.9.0)
cocaine (~> 0.5.3)
aws-eventstream (1.1.0)
aws-partitions (1.296.0)
aws-partitions (1.303.0)
aws-sdk-core (3.94.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
@@ -101,7 +101,7 @@ GEM
aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.61.2)
aws-sdk-s3 (1.63.0)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
@@ -277,7 +277,7 @@ GEM
http-parser (~> 1.2.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (2.2.0)
http-form_data (2.3.0)
http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0)
http_accept_language (2.1.1)
@@ -303,7 +303,7 @@ GEM
jmespath (1.4.0)
json (2.3.0)
json-canonicalization (0.2.0)
json-ld (3.1.2)
json-ld (3.1.3)
htmlentities (~> 4.3)
json-canonicalization (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
@@ -359,7 +359,7 @@ GEM
nokogiri (~> 1.10)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mime-types-data (3.2020.0425)
mimemagic (0.3.4)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
@@ -382,7 +382,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.10.5)
oj (3.10.6)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@@ -409,7 +409,7 @@ GEM
parallel
parser (2.7.1.1)
ast (~> 2.4.0)
parslet (1.8.2)
parslet (2.0.0)
pastel (0.7.3)
equatable (~> 0.6)
tty-color (~> 0.5)
@@ -542,6 +542,8 @@ GEM
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.9.2)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (0.79.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
@@ -554,7 +556,7 @@ GEM
rack (>= 1.1)
rubocop (>= 0.72.0)
ruby-progressbar (1.10.1)
ruby-saml (1.9.0)
ruby-saml (1.11.0)
nokogiri (>= 1.5.10)
rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6)
@@ -668,7 +670,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.7)
addressable (~> 2.7)
annotate (~> 3.1)
aws-sdk-s3 (~> 1.61)
aws-sdk-s3 (~> 1.63)
better_errors (~> 2.6)
binding_of_caller (~> 0.7)
blurhash (~> 0.1)
@@ -765,6 +767,7 @@ DEPENDENCIES
rqrcode (~> 1.1)
rspec-rails (~> 4.0)
rspec-sidekiq (~> 3.0)
rspec_junit_formatter (~> 0.4)
rubocop (~> 0.79)
rubocop-rails (~> 2.5)
ruby-progressbar (~> 1.10)


+ 7
- 7
app/controllers/accounts_controller.rb View File

@@ -28,7 +28,7 @@ class AccountsController < ApplicationController
end

@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
@statuses = filtered_status_page(params)
@statuses = filtered_status_page
@statuses = cache_collection(@statuses, Status)
@rss_url = rss_url

@@ -141,12 +141,12 @@ class AccountsController < ApplicationController
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end

def filtered_status_page(params)
if params[:min_id].present?
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
else
filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
end
def filtered_status_page
filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
end
def params_slice(*keys)
params.slice(*keys).permit(*keys)
end

def restrict_fields_to


+ 10
- 7
app/controllers/activitypub/collections_controller.rb View File

@@ -24,20 +24,23 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def set_size
case params[:id]
when 'featured'
@account.pinned_statuses.count
@size = @account.pinned_statuses.count
else
raise ActiveRecord::RecordNotFound
not_found
end
end

def scope_for_collection
case params[:id]
when 'featured'
return Status.none if @account.blocking?(signed_request_account)

@account.pinned_statuses
else
raise ActiveRecord::RecordNotFound
# Because in public fetch mode we cache the response, there would be no
# benefit from performing the check below, since a blocked account or domain
# would likely be served the cache from the reverse proxy anyway
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
Status.none
else
@account.pinned_statuses
end
end
end



+ 3
- 3
app/controllers/activitypub/outboxes_controller.rb View File

@@ -11,7 +11,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers

def show
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end

@@ -50,12 +50,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
return unless page_requested?

@statuses = @account.statuses.permitted_for(@account, signed_request_account)
@statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
@statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
@statuses = cache_collection(@statuses, Status)
end

def page_requested?
params[:page] == 'true'
truthy_param?(:page)
end

def page_params


+ 15
- 6
app/controllers/activitypub/replies_controller.rb View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true

class ActivityPub::RepliesController < ActivityPub::BaseController
include SignatureAuthentication
include SignatureVerification
include Authorization
include AccountOwnedConcern

@@ -19,15 +19,19 @@ class ActivityPub::RepliesController < ActivityPub::BaseController

private

def pundit_user
signed_request_account
end

def set_status
@status = @account.statuses.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
not_found
end

def set_replies
@replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end
@@ -38,7 +42,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
type: :unordered,
part_of: account_status_replies_url(@account, @status),
next: next_page,
items: @replies.map { |status| status.local ? status : status.uri }
items: @replies.map { |status| status.local? ? status : status.uri }
)

return page if page_requested?
@@ -51,16 +55,21 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
end

def page_requested?
params[:page] == 'true'
truthy_param?(:page)
end

def only_other_accounts?
truthy_param?(:only_other_accounts)
end

def next_page
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)

account_status_replies_url(
@account,
@status,
page: true,
min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
only_other_accounts: only_other_accounts
)
end


+ 1
- 1
app/controllers/api/v1/polls/votes_controller.rb View File

@@ -18,7 +18,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
@poll = Poll.attached.find(params[:poll_id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
not_found
end

def vote_params


+ 1
- 1
app/controllers/api/v1/polls_controller.rb View File

@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
@poll = Poll.attached.find(params[:id])
authorize @poll.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
not_found
end

def refresh_poll


+ 6
- 5
app/controllers/api/v1/push/subscriptions_controller.rb View File

@@ -4,6 +4,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :push }
before_action :require_user!
before_action :set_web_push_subscription
before_action :check_web_push_subscription, only: [:show, :update]

def create
@web_subscription&.destroy!
@@ -21,16 +22,11 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
end

def show
raise ActiveRecord::RecordNotFound if @web_subscription.nil?

render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end

def update
raise ActiveRecord::RecordNotFound if @web_subscription.nil?

@web_subscription.update!(data: data_params)

render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
end

@@ -45,12 +41,17 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
end

def check_web_push_subscription
not_found if @web_subscription.nil?
end

def subscription_params
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
end

def data_params
return {} if params[:data].blank?

params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
end
end

+ 1
- 2
app/controllers/api/v1/statuses/mutes_controller.rb View File

@@ -28,8 +28,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
# Reraise in order to get a 404 instead of a 403 error code
raise ActiveRecord::RecordNotFound
not_found
end

def set_conversation


+ 1
- 1
app/controllers/api/v1/statuses_controller.rb View File

@@ -68,7 +68,7 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
not_found
end

def set_thread


+ 1
- 1
app/controllers/media_controller.rb View File

@@ -33,7 +33,7 @@ class MediaController < ApplicationController
def verify_permitted_status!
authorize @media_attachment.status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
not_found
end

def check_playable


+ 1
- 1
app/controllers/remote_interaction_controller.rb View File

@@ -42,7 +42,7 @@ class RemoteInteractionController < ApplicationController
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
raise ActiveRecord::RecordNotFound
not_found
end

def set_body_classes


+ 1
- 1
app/controllers/statuses_controller.rb View File

@@ -49,7 +49,7 @@ class StatusesController < ApplicationController

def embed
use_pack 'embed'
return not_found if @status.hidden?
return not_found if @status.hidden? || @status.reblog?

expires_in 180, public: true
response.headers['X-Frame-Options'] = 'ALLOWALL'


+ 1
- 1
app/javascript/core/settings.js View File

@@ -10,7 +10,7 @@ delegate(document, '#account_display_name', 'input', ({ target }) => {
if (target.value) {
name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
} else {
name.textContent = document.querySelector('#default_account_display_name').textContent;
name.textContent = name.textContent = target.dataset.default;
}
}
});


+ 6
- 8
app/javascript/flavours/glitch/packs/public.js View File

@@ -99,15 +99,13 @@ function main() {
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));

delegate(document, '.status__content__spoiler-link', 'click', function() {
const contentEl = this.parentNode.parentNode.querySelector('.e-content');
const statusEl = this.parentNode.parentNode;

if (contentEl.style.display === 'block') {
contentEl.style.display = 'none';
this.parentNode.style.marginBottom = 0;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
} else {
contentEl.style.display = 'block';
this.parentNode.style.marginBottom = null;
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
}

@@ -115,8 +113,8 @@ function main() {
});

[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
});


+ 1
- 1
app/javascript/mastodon/actions/timelines.js View File

@@ -42,7 +42,7 @@ export function updateTimeline(timeline, status, accept) {
export function deleteFromTimelines(id) {
return (dispatch, getState) => {
const accountId = getState().getIn(['statuses', id, 'account']);
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);

dispatch({


+ 1
- 1
app/javascript/mastodon/components/dropdown_menu.js View File

@@ -46,7 +46,7 @@ class DropdownMenu extends React.PureComponent {
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus();
this.focusedItem.focus({ preventScroll: true });
}
this.setState({ mounted: true });
}


+ 1
- 1
app/javascript/mastodon/features/compose/components/privacy_dropdown.js View File

@@ -100,7 +100,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem) this.focusedItem.focus();
if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
this.setState({ mounted: true });
}



+ 1
- 1
app/javascript/mastodon/reducers/statuses.js View File

@@ -25,7 +25,7 @@ const importStatuses = (state, statuses) =>

const deleteStatus = (state, id, references) => {
references.forEach(ref => {
state = deleteStatus(state, ref[0], []);
state = deleteStatus(state, ref, []);
});

return state.delete(id);


+ 5
- 5
app/javascript/mastodon/reducers/timelines.js View File

@@ -89,7 +89,7 @@ const updateTimeline = (state, timeline, status, usePendingItems) => {
}));
};

const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
const deleteStatus = (state, id, references, exclude_account = null) => {
state.keySeq().forEach(timeline => {
if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) {
const helper = list => list.filterNot(item => item === id);
@@ -99,7 +99,7 @@ const deleteStatus = (state, id, accountId, references, exclude_account = null)

// Remove reblogs of deleted status
references.forEach(ref => {
state = deleteStatus(state, ref[0], ref[1], [], exclude_account);
state = deleteStatus(state, ref, [], exclude_account);
});

return state;
@@ -117,8 +117,8 @@ const filterTimelines = (state, relationship, statuses) => {
return;
}

references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
state = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id);
references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id'));
state = deleteStatus(state, status.get('id'), references, relationship.id);
});

return state;
@@ -150,7 +150,7 @@ export default function timelines(state = initialState, action) {
case TIMELINE_UPDATE:
return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems);
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
return deleteStatus(state, action.id, action.references, action.reblogOf);
case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline);
case ACCOUNT_BLOCK_SUCCESS:


+ 6
- 8
app/javascript/packs/public.js View File

@@ -103,15 +103,13 @@ function main() {
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));

delegate(document, '.status__content__spoiler-link', 'click', function() {
const contentEl = this.parentNode.parentNode.querySelector('.e-content');
const statusEl = this.parentNode.parentNode;

if (contentEl.style.display === 'block') {
contentEl.style.display = 'none';
this.parentNode.style.marginBottom = 0;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format();
} else {
contentEl.style.display = 'block';
this.parentNode.style.marginBottom = null;
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format();
}

@@ -119,8 +117,8 @@ function main() {
});

[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content');
const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
});


+ 5
- 0
app/javascript/styles/mastodon/about.scss View File

@@ -757,8 +757,13 @@ $small-breakpoint: 960px;
}
}

&__counters__wrapper {
display: flex;
}

&__counter {
padding: 10px;
width: 50%;

strong {
font-family: $font-display, sans-serif;


+ 20
- 0
app/javascript/styles/mastodon/admin.scss View File

@@ -583,6 +583,18 @@ body,
}
}

.special-action-button,
.back-link {
text-align: right;
flex: 1 1 auto;
}

.action-buttons {
display: flex;
overflow: hidden;
justify-content: space-between;
}

.spacer {
flex: 1 1 auto;
}
@@ -920,3 +932,11 @@ a.name-tag,
}
}
}

.account-badges {
margin: -2px 0;
}

.dashboard__counters.admin-account-counters {
margin-top: 10px;
}

+ 16
- 0
app/javascript/styles/mastodon/basics.scss View File

@@ -229,3 +229,19 @@ button {
}
}
}

.logo-resources {
display: none;
}

// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements,
// to set the z-index to a high value, which messes with modals and dropdowns.
// Blocked elements can in theory only be media and frames/embeds, so they
// should only appear in statuses, under divs and articles.
body,
div,
article {
.__ns__pop2top {
z-index: unset !important;
}
}

+ 12
- 0
app/javascript/styles/mastodon/components.scss View File

@@ -1362,6 +1362,12 @@ a .account__avatar {
&-base {
@include avatar-radius;
@include avatar-size(36px);

img {
@include avatar-radius;
width: 100%;
height: 100%;
}
}

&-overlay {
@@ -1372,6 +1378,12 @@ a .account__avatar {
bottom: 0;
right: 0;
z-index: 1;

img {
@include avatar-radius;
width: 100%;
height: 100%;
}
}
}



+ 21
- 0
app/javascript/styles/mastodon/forms.scss View File

@@ -142,6 +142,10 @@ code {
}
}

.otp-hint {
margin-bottom: 25px;
}

.card {
margin-bottom: 15px;
}
@@ -285,6 +289,14 @@ code {
margin-bottom: 25px;
}
}

.fields-group.invited-by {
margin-bottom: 30px;

.hint {
text-align: center;
}
}
}

.input.radio_buttons .radio label {
@@ -635,6 +647,15 @@ code {
@media screen and (max-width: 740px) and (min-width: 441px) {
margin-top: 40px;
}

&.translation-prompt {
text-align: unset;
color: unset;

a {
text-decoration: underline;
}
}
}

.form-footer {


+ 30
- 0
app/javascript/styles/mastodon/polls.scss View File

@@ -19,6 +19,36 @@
}
}

progress {
border: 0;
display: block;
width: 100%;
height: 5px;
appearance: none;
background: transparent;

&::-webkit-progress-bar {
background: transparent;
}

// Those rules need to be entirely separate or they won't work, hence the
// duplication
&::-moz-progress-bar {
border-radius: 4px;
background: darken($ui-primary-color, 5%);
}

&::-ms-fill {
border-radius: 4px;
background: darken($ui-primary-color, 5%);
}

&::-webkit-progress-value {
border-radius: 4px;
background: darken($ui-primary-color, 5%);
}
}

&__option {
position: relative;
display: flex;


+ 17
- 0
app/javascript/styles/mastodon/statuses.scss View File

@@ -128,6 +128,16 @@

.embed,
.public-layout {
.status__content[data-spoiler=folded] {
.e-content {
display: none;
}

p:first-child {
margin-bottom: 0;
}
}

.detailed-status {
padding: 15px;
}
@@ -159,5 +169,12 @@
.video-player {
margin-top: 10px;
}

&__action-bar-button {
font-size: 18px;
width: 23.1429px;
height: 23.1429px;
line-height: 23.15px;
}
}
}

+ 46
- 44
app/models/account.rb View File

@@ -3,50 +3,52 @@
#
# Table name: accounts
#
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# secret :string default(""), not null
# private_key :text
# public_key :text default(""), not null
# remote_url :string default(""), not null
# salmon_url :string default(""), not null
# hub_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# note :text default(""), not null
# display_name :string default(""), not null
# uri :string default(""), not null
# url :string
# avatar_file_name :string
# avatar_content_type :string
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# subscription_expires_at :datetime
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
# trust_level :integer
# hide_collections :boolean
# id :bigint(8) not null, primary key
# username :string default(""), not null
# domain :string
# secret :string default(""), not null
# private_key :text
# public_key :text default(""), not null
# remote_url :string default(""), not null
# salmon_url :string default(""), not null
# hub_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# note :text default(""), not null
# display_name :string default(""), not null
# uri :string default(""), not null
# url :string
# avatar_file_name :string
# avatar_content_type :string
# avatar_file_size :integer
# avatar_updated_at :datetime
# header_file_name :string
# header_content_type :string
# header_file_size :integer
# header_updated_at :datetime
# avatar_remote_url :string
# subscription_expires_at :datetime
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
# inbox_url :string default(""), not null
# outbox_url :string default(""), not null
# shared_inbox_url :string default(""), not null
# followers_url :string default(""), not null
# protocol :integer default("ostatus"), not null
# memorial :boolean default(FALSE), not null
# moved_to_account_id :bigint(8)
# featured_collection_url :string
# fields :jsonb
# actor_type :string
# discoverable :boolean
# also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
# trust_level :integer
# hide_collections :boolean
# avatar_storage_schema_version :integer
# header_storage_schema_version :integer
#

class Account < ApplicationRecord


+ 1
- 1
app/models/concerns/omniauthable.rb View File

@@ -82,7 +82,7 @@ module Omniauthable
username = starting_username
i = 0

while Account.exists?(username: username)
while Account.exists?(username: username, domain: nil)
i += 1
username = "#{starting_username}_#{i}"
end


+ 15
- 14
app/models/custom_emoji.rb View File

@@ -3,20 +3,21 @@
#
# Table name: custom_emojis
#
# id :bigint(8) not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# disabled :boolean default(FALSE), not null
# uri :string
# image_remote_url :string
# visible_in_picker :boolean default(TRUE), not null
# category_id :bigint(8)
# id :bigint(8) not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# disabled :boolean default(FALSE), not null
# uri :string
# image_remote_url :string
# visible_in_picker :boolean default(TRUE), not null
# category_id :bigint(8)
# image_storage_schema_version :integer
#

class CustomEmoji < ApplicationRecord


+ 18
- 17
app/models/media_attachment.rb View File

@@ -3,23 +3,24 @@
#
# Table name: media_attachments
#
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
# processing :integer
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
# processing :integer
# file_storage_schema_version :integer
#

class MediaAttachment < ApplicationRecord


+ 24
- 19
app/models/preview_card.rb View File

@@ -3,25 +3,26 @@
#
# Table name: preview_cards
#
# id :bigint(8) not null, primary key
# url :string default(""), not null
# title :string default(""), not null
# description :string default(""), not null
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# type :integer default("link"), not null
# html :text default(""), not null
# author_name :string default(""), not null
# author_url :string default(""), not null
# provider_name :string default(""), not null
# provider_url :string default(""), not null
# width :integer default(0), not null
# height :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# embed_url :string default(""), not null
# id :bigint(8) not null, primary key
# url :string default(""), not null
# title :string default(""), not null
# description :string default(""), not null
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# type :integer default("link"), not null
# html :text default(""), not null
# author_name :string default(""), not null
# author_url :string default(""), not null
# provider_name :string default(""), not null
# provider_url :string default(""), not null
# width :integer default(0), not null
# height :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
# embed_url :string default(""), not null
# image_storage_schema_version :integer
#

class PreviewCard < ApplicationRecord
@@ -47,6 +48,10 @@ class PreviewCard < ApplicationRecord

before_save :extract_dimensions, if: :link?

def local?
false
end

def missing_image?
width.present? && height.present? && image_file_name.blank?
end


+ 2
- 6
app/models/status.rb View File

@@ -206,12 +206,8 @@ class Status < ApplicationRecord
def title
if destroyed?
"#{account.acct} deleted status"
elsif reblog?
preview = sensitive ? '<sensitive>' : text.slice(0, 10).split("\n")[0]
"#{account.acct} shared #{reblog.account.acct}'s: #{preview}"
else
preview = sensitive ? '<sensitive>' : text.slice(0, 20).split("\n")[0]
"#{account.acct}: #{preview}"
reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}"
end
end

@@ -404,7 +400,7 @@ class Status < ApplicationRecord

if account.nil?
where(visibility: visibility).not_local_only
elsif target_account.blocking?(account) # get rid of blocked peeps
elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
none
elsif account.id == target_account.id # author can see own stuff
all


+ 5
- 1
app/serializers/rest/instance_serializer.rb View File

@@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer

attributes :uri, :title, :short_description, :description, :email,
:version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits,
:languages, :registrations, :approval_required
:languages, :registrations, :approval_required, :invites_enabled

has_one :contact_account, serializer: REST::AccountSerializer

@@ -76,6 +76,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
Setting.registrations_mode == 'approved'
end

def invites_enabled
Setting.min_invite_role == 'user'
end

private

def instance_presenter


+ 12
- 1
app/services/fetch_resource_service.rb View File

@@ -25,7 +25,18 @@ class FetchResourceService < BaseService
end

def perform_request(&block)
Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).on_behalf_of(Account.representative).perform(&block)
Request.new(:get, @url).tap do |request|
request.add_headers('Accept' => ACCEPT_HEADER)

# In a real setting we want to sign all outgoing requests,
# in case the remote server has secure mode enabled and requires
# authentication on all resources. However, during development,
# sending request signatures with an inaccessible host is useless
# and prevents even public resources from being fetched, so
# don't do it

request.on_behalf_of(Account.representative) unless Rails.env.development?
end.perform(&block)
end

def process_response(response, terminal = false)


+ 3
- 3
app/views/about/show.html.haml View File

@@ -68,11 +68,11 @@
.hero-widget__footer__column
%h4= t 'about.server_stats'

%div{ style: 'display: flex' }
.hero-widget__counter{ style: 'width: 50%' }
.hero-widget__counters__wrapper
.hero-widget__counter
%strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true
%span= t 'about.user_count_after', count: @instance_presenter.user_count
.hero-widget__counter{ style: 'width: 50%' }
.hero-widget__counter
%strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true
%span
= t 'about.active_count_after'


+ 4
- 2
app/views/accounts/_moved.html.haml View File

@@ -9,8 +9,10 @@
= link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do
.detailed-status__display-avatar
.account__avatar-overlay
.account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" }
.account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" }
.account__avatar-overlay-base
= image_tag moved_to_account.avatar_static_url
.account__avatar-overlay-overlay
= image_tag account.avatar_static_url

%span.display-name
%bdi


+ 1
- 1
app/views/admin/accounts/_account.html.haml View File

@@ -2,7 +2,7 @@
%td
= admin_account_link_to(account)
%td
%div{ style: 'margin: -2px 0' }= account_badge(account, all: true)
%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


+ 13
- 13
app/views/admin/accounts/show.html.haml View File

@@ -31,7 +31,7 @@
%div
.account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)

.dashboard__counters{ style: 'margin-top: 10px' }
.dashboard__counters.admin-account-counters
%div
= link_to admin_account_statuses_path(@account.id) do
.dashboard__counters__num= number_with_delimiter @account.statuses_count
@@ -178,18 +178,8 @@
= @account.shared_inbox_url
= fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times'

%div{ style: 'overflow: hidden' }
%div{ style: 'float: right' }
- if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- if @account.user&.otp_required_for_login?
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- if !@account.memorial? && @account.user_approved?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
- else
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)

%div{ style: 'float: left' }
%div.action-buttons
%div
- if @account.local? && @account.user_approved?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
- if @account.silenced?
@@ -216,6 +206,16 @@
- else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'

%div
- if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- if @account.user&.otp_required_for_login?
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- if !@account.memorial? && @account.user_approved?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
- else
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)

%hr.spacer/

- unless @warnings.empty?


+ 1
- 1
app/views/admin/instances/index.html.haml View File

@@ -10,7 +10,7 @@
- unless whitelist_mode?
%li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'

%div{ style: 'flex: 1 1 auto; text-align: right' }
%div.special-action-button
- if whitelist_mode?
= link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button'
- else


+ 3
- 3
app/views/admin/instances/show.html.haml View File

@@ -45,11 +45,11 @@

%hr.spacer/

%div{ style: 'overflow: hidden' }
%div{ style: 'float: left' }
%div.action-buttons
%div
= link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button'

%div{ style: 'float: right' }
%div
- if @domain_allow
= link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
- elsif @domain_block


+ 4
- 4
app/views/admin/pending_accounts/index.html.haml View File

@@ -22,9 +22,9 @@

%hr.spacer/

%div{ style: 'overflow: hidden' }
%div{ style: 'float: right' }
= 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'

%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'

+ 1
- 1
app/views/admin/relationships/index.html.haml View File

@@ -17,7 +17,7 @@
%li= filter_link_to t('admin.accounts.location.local'), location: 'local'
%li= filter_link_to t('admin.accounts.location.remote'), location: 'remote'

.back-link{ style: 'flex: 1 1 auto; text-align: right' }
.back-link
= link_to admin_account_path(@account.id) do
= fa_icon 'chevron-left fw'
= t('admin.statuses.back_to_account')


+ 4
- 2
app/views/admin/reports/show.html.haml View File

@@ -65,9 +65,11 @@

%hr.spacer

%div{ style: 'overflow: hidden; margin-bottom: 20px; clear: both' }
%div.action-buttons
%div

- if @report.unresolved?
%div{ style: 'float: right' }
%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'


+ 1
- 1
app/views/admin/statuses/index.html.haml View File

@@ -9,7 +9,7 @@
%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'
.back-link{ style: 'flex: 1 1 auto; text-align: right' }
.back-link
= link_to admin_account_path(@account.id) do
= fa_icon 'chevron-left fw'
= t('admin.statuses.back_to_account')


+ 1
- 1
app/views/admin/statuses/show.html.haml View File

@@ -4,7 +4,7 @@
= "@#{@account.acct}"

.filters
.back-link{ style: 'flex: 1 1 auto; text-align: right' }
.back-link
= link_to admin_account_path(@account.id) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.statuses.back_to_account')


+ 4
- 4
app/views/admin/tags/index.html.haml View File

@@ -68,9 +68,9 @@
- if params[:pending_review] == '1' || params[:unreviewed] == '1'
%hr.spacer/

%div{ style: 'overflow: hidden' }
%div{ style: 'float: right' }
= link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'

%div.action-buttons
%div
= link_to t('admin.accounts.approve_all'), approve_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button'

%div
= link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive'

+ 0
- 1
app/views/application/_card.html.haml View File

@@ -9,7 +9,6 @@
= image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'

.display-name
%span{ id: "default_account_display_name", style: "display: none" }= account.username
%bdi
%strong.emojify.p-name= display_name(account, custom_emojify: true)
%span


+ 2
- 2
app/views/auth/registrations/new.html.haml View File

@@ -8,8 +8,8 @@
= render 'shared/error_messages', object: resource

- if @invite.present? && @invite.autofollow?
.fields-group{ style: 'margin-bottom: 30px' }
%p.hint{ style: 'text-align: center' }= t('invites.invited_by')
.fields-group.invited-by
%p.hint= t('invites.invited_by')
= render 'application/card', account: @invite.user.account

= f.simple_fields_for :account do |ff|


+ 1
- 1
app/views/auth/sessions/two_factor.html.haml View File

@@ -2,7 +2,7 @@
= t('auth.login')

= simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
%p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp')
%p.hint.otp-hint= t('simple_form.hints.sessions.otp')

.fields-group
= f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, autofocus: true


+ 0
- 1
app/views/directories/index.html.haml View File

@@ -28,7 +28,6 @@
= image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'

.display-name
%span{ id: "default_account_display_name", style: "display: none" }= account.username
%bdi
%strong.emojify.p-name= display_name(account, custom_emojify: true)
%span= acct(account)


+ 1
- 1
app/views/layouts/application.html.haml View File

@@ -40,6 +40,6 @@
%body{ class: body_classes }
= content_for?(:content) ? yield(:content) : yield

%div{ style: 'display: none'}
.logo-resources
= render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg')
= render file: Rails.root.join('app', 'javascript', 'images', 'logo_full.svg')

+ 1
- 1
app/views/layouts/embedded.html.haml View File

@@ -23,5 +23,5 @@
%body.embed
= yield

%div{ style: 'display: none'}
.logo-resources
= render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg')

+ 1
- 1
app/views/public_timelines/show.html.haml View File

@@ -12,5 +12,5 @@
- else
%p= t('about.browse_local_posts')

#mastodon-timeline{ data: { props: Oj.dump(default_props) }}
#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(local: !Setting.show_known_fediverse_at_about_page)) }}
#modal-container

+ 2
- 2
app/views/settings/preferences/appearance/show.html.haml View File

@@ -9,8 +9,8 @@
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false

- unless I18n.locale == :en
.flash-message{ style: "text-align: unset; color: unset" }
#{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")}
.flash-message.translation-prompt
#{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener")}

%h4= t 'appearance.advanced_web_interface'



+ 1
- 1
app/views/settings/profiles/show.html.haml View File

@@ -9,7 +9,7 @@

.fields-row
.fields-row__column.fields-group.fields-row__column-6
= f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH }, hint: false
= f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH, data: { default: @account.username } }, hint: false
= f.input :note, wrapper: :with_label, input_html: { maxlength: Account::MAX_NOTE_LENGTH }, hint: false

.fields-row


+ 3
- 3
app/views/statuses/_detailed_status.html.haml View File

@@ -15,12 +15,12 @@

= account_action_button(status.account)

.status__content.emojify<
.status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text?
%p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
%p<
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
.e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }
.e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
- if status.preloadable_poll
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do


+ 5
- 3
app/views/statuses/_poll.html.haml View File

@@ -10,13 +10,15 @@
- percent = total_votes_count > 0 ? 100 * option.votes_count / total_votes_count : 0
%label.poll__option><
%span.poll__number><
- if own_votes.include?(index)
%i.poll__voted__mark.fa.fa-check
= "#{percent.round}%"
%span.poll__option__text
= Formatter.instance.format_poll_option(status, option, autoplay: autoplay)
- if own_votes.include?(index)
%span.poll__voted
%i.poll__voted__mark.fa.fa-check

%span.poll__chart{ style: "width: #{percent}%" }
%progress{ max: 100, value: percent < 1 ? 1 : percent, 'aria-hidden': 'true' }
%span.poll__chart
- else
%label.poll__option><
%span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}><


+ 6
- 6
app/views/statuses/_simple_status.html.haml View File

@@ -19,12 +19,12 @@
%span.display-name__account
= acct(status.account)
= fa_icon('lock') if status.account.locked?
.status__content.emojify<
.status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }<
- if status.spoiler_text?
%p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
%p<
%span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
%button.status__content__spoiler-link= t('statuses.show_more')
.e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
.e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }<
= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
- if status.preloadable_poll
= react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do
@@ -51,18 +51,18 @@

.status__action-bar
.status__action-bar__counter
= link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
= link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button' do
- if status.in_reply_to_id.nil?
= fa_icon 'reply fw'
- else
= fa_icon 'reply-all fw'
.status__action-bar__counter__label= obscured_counter status.replies_count
= link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
= link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do
- if status.distributable?
= fa_icon 'retweet fw'
- elsif status.private_visibility? || status.limited_visibility?
= fa_icon 'lock fw'
- else
= fa_icon 'envelope fw'
= link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
= link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do
= fa_icon 'star fw'

+ 19
- 3
config/initializers/paperclip.rb View File

@@ -10,9 +10,25 @@ Paperclip.interpolates :filename do |attachment, style|
end
end

Paperclip.interpolates :prefix_path do |attachment, style|
if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local?
'cache' + File::SEPARATOR
else
''
end
end

Paperclip.interpolates :prefix_url do |attachment, style|
if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local?
'cache/'
else
''
end
end

Paperclip::Attachment.default_options.merge!(
use_timestamp: false,
path: ':class/:attachment/:id_partition/:style/:filename',
path: ':prefix_url:class/:attachment/:id_partition/:style/:filename',
storage: :fog
)

@@ -91,7 +107,7 @@ else
Paperclip::Attachment.default_options.merge!(
storage: :filesystem,
use_timestamp: true,
path: File.join(ENV.fetch('PAPERCLIP_ROOT_PATH', File.join(':rails_root', 'public', 'system')), ':class', ':attachment', ':id_partition', ':style', ':filename'),
url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:class/:attachment/:id_partition/:style/:filename',
path: File.join(ENV.fetch('PAPERCLIP_ROOT_PATH', File.join(':rails_root', 'public', 'system')), ':prefix_path:class', ':attachment', ':id_partition', ':style', ':filename'),
url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:prefix_url:class/:attachment/:id_partition/:style/:filename',
)
end

+ 9
- 0
db/migrate/20200417125749_add_storage_schema_version.rb View File

@@ -0,0 +1,9 @@
class AddStorageSchemaVersion < ActiveRecord::Migration[5.2]
def change
add_column :preview_cards, :image_storage_schema_version, :integer
add_column :accounts, :avatar_storage_schema_version, :integer
add_column :accounts, :header_storage_schema_version, :integer
add_column :media_attachments, :file_storage_schema_version, :integer
add_column :custom_emojis, :image_storage_schema_version, :integer
end
end

+ 6
- 1
db/schema.rb View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2020_04_07_202420) do
ActiveRecord::Schema.define(version: 2020_04_17_125749) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -172,6 +172,8 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do
t.datetime "suspended_at"
t.integer "trust_level"
t.boolean "hide_collections"
t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
@@ -299,6 +301,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do
t.string "image_remote_url"
t.boolean "visible_in_picker", default: true, null: false
t.bigint "category_id"
t.integer "image_storage_schema_version"
t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true
end

@@ -465,6 +468,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do
t.bigint "scheduled_status_id"
t.string "blurhash"
t.integer "processing"
t.integer "file_storage_schema_version"
t.index ["account_id"], name: "index_media_attachments_on_account_id"