Merge Upstream

master
Tuan 2 years ago
commit 5ec28758f0
  1. 335
      .circleci/config.yml
  2. 5
      .codeclimate.yml
  3. 1
      .env.nanobox
  4. 10
      .env.production.sample
  5. 2
      .github/CODEOWNERS
  6. 34
      .github/workflows/build-image.yml
  7. 2
      .github/workflows/check-i18n.yml
  8. 2
      .nvmrc
  9. 2
      .ruby-version
  10. 4
      AUTHORS.md
  11. 2698
      CHANGELOG.md
  12. 4
      CONTRIBUTING.md
  13. 18
      Dockerfile
  14. 47
      Gemfile
  15. 388
      Gemfile.lock
  16. 14
      Vagrantfile
  17. 4
      app.json
  18. 24
      app/chewy/accounts_index.rb
  19. 48
      app/chewy/statuses_index.rb
  20. 16
      app/chewy/tags_index.rb
  21. 4
      app/controllers/activitypub/followers_synchronizations_controller.rb
  22. 37
      app/controllers/admin/dashboard_controller.rb
  23. 2
      app/controllers/admin/statuses_controller.rb
  24. 7
      app/controllers/api/base_controller.rb
  25. 9
      app/controllers/api/v1/accounts_controller.rb
  26. 23
      app/controllers/api/v1/admin/dimensions_controller.rb
  27. 22
      app/controllers/api/v1/admin/measures_controller.rb
  28. 22
      app/controllers/api/v1/admin/retention_controller.rb
  29. 16
      app/controllers/api/v1/admin/trends_controller.rb
  30. 27
      app/controllers/api/v1/instances/activity_controller.rb
  31. 7
      app/controllers/application_controller.rb
  32. 2
      app/controllers/auth/omniauth_callbacks_controller.rb
  33. 1
      app/controllers/auth/passwords_controller.rb
  34. 3
      app/controllers/auth/registrations_controller.rb
  35. 26
      app/controllers/auth/sessions_controller.rb
  36. 20
      app/controllers/concerns/sign_in_token_authentication_concern.rb
  37. 24
      app/controllers/concerns/two_factor_authentication_concern.rb
  38. 25
      app/controllers/home_controller.rb
  39. 7
      app/controllers/media_controller.rb
  40. 2
      app/controllers/settings/deletes_controller.rb
  41. 3
      app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb
  42. 40
      app/controllers/statuses_cleanup_controller.rb
  43. 4
      app/helpers/application_helper.rb
  44. 1
      app/helpers/settings_helper.rb
  45. 32
      app/javascript/flavours/glitch/actions/accounts.js
  46. 37
      app/javascript/flavours/glitch/actions/compose.js
  47. 6
      app/javascript/flavours/glitch/actions/notifications.js
  48. 4
      app/javascript/flavours/glitch/components/account.js
  49. 115
      app/javascript/flavours/glitch/components/admin/Counter.js
  50. 92
      app/javascript/flavours/glitch/components/admin/Dimension.js
  51. 141
      app/javascript/flavours/glitch/components/admin/Retention.js
  52. 73
      app/javascript/flavours/glitch/components/admin/Trends.js
  53. 36
      app/javascript/flavours/glitch/components/attachment_list.js
  54. 2
      app/javascript/flavours/glitch/components/avatar_composite.js
  55. 6
      app/javascript/flavours/glitch/components/column_header.js
  56. 4
      app/javascript/flavours/glitch/components/display_name.js
  57. 8
      app/javascript/flavours/glitch/components/error_boundary.js
  58. 61
      app/javascript/flavours/glitch/components/hashtag.js
  59. 34
      app/javascript/flavours/glitch/components/modal_root.js
  60. 27
      app/javascript/flavours/glitch/components/poll.js
  61. 12
      app/javascript/flavours/glitch/components/scrollable_list.js
  62. 11
      app/javascript/flavours/glitch/components/skeleton.js
  63. 72
      app/javascript/flavours/glitch/components/status.js
  64. 6
      app/javascript/flavours/glitch/components/status_action_bar.js
  65. 63
      app/javascript/flavours/glitch/components/status_content.js
  66. 6
      app/javascript/flavours/glitch/components/status_header.js
  67. 31
      app/javascript/flavours/glitch/components/status_icons.js
  68. 1
      app/javascript/flavours/glitch/components/status_list.js
  69. 2
      app/javascript/flavours/glitch/components/status_prepend.js
  70. 26
      app/javascript/flavours/glitch/containers/admin_component.js
  71. 28
      app/javascript/flavours/glitch/containers/mastodon.js
  72. 2
      app/javascript/flavours/glitch/containers/media_container.js
  73. 18
      app/javascript/flavours/glitch/containers/scroll_container.js
  74. 6
      app/javascript/flavours/glitch/features/account/components/action_bar.js
  75. 73
      app/javascript/flavours/glitch/features/account_gallery/index.js
  76. 6
      app/javascript/flavours/glitch/features/account_timeline/components/header.js
  77. 2
      app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js
  78. 65
      app/javascript/flavours/glitch/features/account_timeline/index.js
  79. 6
      app/javascript/flavours/glitch/features/compose/components/header.js
  80. 4
      app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
  81. 2
      app/javascript/flavours/glitch/features/compose/components/search_results.js
  82. 1
      app/javascript/flavours/glitch/features/compose/containers/header_container.js
  83. 5
      app/javascript/flavours/glitch/features/compose/containers/upload_container.js
  84. 4
      app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
  85. 2
      app/javascript/flavours/glitch/features/directory/components/account_card.js
  86. 7
      app/javascript/flavours/glitch/features/directory/index.js
  87. 2
      app/javascript/flavours/glitch/features/follow_recommendations/components/account.js
  88. 2
      app/javascript/flavours/glitch/features/follow_recommendations/index.js
  89. 2
      app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js
  90. 73
      app/javascript/flavours/glitch/features/followers/index.js
  91. 73
      app/javascript/flavours/glitch/features/following/index.js
  92. 6
      app/javascript/flavours/glitch/features/getting_started/components/announcements.js
  93. 2
      app/javascript/flavours/glitch/features/getting_started/components/trends.js
  94. 12
      app/javascript/flavours/glitch/features/getting_started/index.js
  95. 2
      app/javascript/flavours/glitch/features/lists/index.js
  96. 15
      app/javascript/flavours/glitch/features/notifications/components/column_settings.js
  97. 4
      app/javascript/flavours/glitch/features/notifications/components/follow.js
  98. 6
      app/javascript/flavours/glitch/features/notifications/components/follow_request.js
  99. 4
      app/javascript/flavours/glitch/features/notifications/index.js
  100. 8
      app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,255 +1,152 @@
version: 2 version: 2.1
aliases: orbs:
- &defaults ruby: circleci/ruby@1.2.0
node: circleci/node@4.7.0
executors:
default:
parameters:
ruby-version:
type: string
docker: docker:
- image: circleci/ruby:2.7-buster-node - image: cimg/ruby:<< parameters.ruby-version >>
environment: &ruby_environment environment:
BUNDLE_JOBS: 3 BUNDLE_JOBS: 3
BUNDLE_RETRY: 3 BUNDLE_RETRY: 3
BUNDLE_APP_CONFIG: ./.bundle/ CONTINUOUS_INTEGRATION: true
BUNDLE_PATH: ./vendor/bundle/
DB_HOST: localhost DB_HOST: localhost
DB_USER: root DB_USER: root
RAILS_ENV: test
ALLOW_NOPAM: true
CONTINUOUS_INTEGRATION: true
DISABLE_SIMPLECOV: true DISABLE_SIMPLECOV: true
PAM_ENABLED: true RAILS_ENV: test
PAM_DEFAULT_SERVICE: pam_test - image: cimg/postgres:14.0
PAM_CONTROLLED_SERVICE: pam_test_controlled environment:
working_directory: ~/projects/mastodon/ POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- &attach_workspace - image: circleci/redis:6-alpine
attach_workspace:
at: ~/projects/
- &persist_to_workspace
persist_to_workspace:
root: ~/projects/
paths:
- ./mastodon/
- &restore_ruby_dependencies
restore_cache:
keys:
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
- v3-ruby-dependencies-
- &install_steps commands:
install-system-dependencies:
steps: steps:
- checkout
- *attach_workspace
- restore_cache:
keys:
- v2-node-dependencies-{{ checksum "yarn.lock" }}
- v2-node-dependencies-
- run: - run:
name: Install yarn dependencies name: Install system dependencies
command: yarn install --frozen-lockfile command: |
- save_cache: sudo apt-get update
key: v2-node-dependencies-{{ checksum "yarn.lock" }} sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
paths: install-ruby-dependencies:
- ./node_modules/ parameters:
- *persist_to_workspace ruby-version:
type: string
- &install_system_dependencies steps:
run: - run:
name: Install system dependencies command: |
command: | bundle config clean 'true'
sudo apt-get update bundle config frozen 'true'
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler bundle config without 'development production'
name: Set bundler settings
- &install_ruby_dependencies - ruby/install-deps:
steps: bundler-version: '2.2.31'
- *attach_workspace key: ruby<< parameters.ruby-version >>-gems-v1
- *install_system_dependencies wait-db:
- run: steps:
name: Set Ruby version - run:
command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
- *restore_ruby_dependencies name: Wait for PostgreSQL and Redis
- run:
name: Set bundler settings
command: |
bundle config --local clean 'true'
bundle config --local deployment 'true'
bundle config --local with 'pam_authentication'
bundle config --local without 'development production'
bundle config --local frozen 'true'
bundle config --local path $BUNDLE_PATH
- run:
name: Install bundler dependencies
command: bundle check || (bundle install && bundle clean)
- save_cache:
key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
paths:
- ./.bundle/
- ./vendor/bundle/
- persist_to_workspace:
root: ~/projects/
paths:
- ./mastodon/.bundle/
- ./mastodon/vendor/bundle/
- &test_steps
parallelism: 4
steps:
- *attach_workspace
- *install_system_dependencies
- run:
name: Install FFMPEG
command: sudo apt-get install -y ffmpeg
- run:
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: jobs:
install:
<<: *defaults
<<: *install_steps
install-ruby2.7:
<<: *defaults
<<: *install_ruby_dependencies
install-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6-buster-node
environment: *ruby_environment
<<: *install_ruby_dependencies
install-ruby3.0:
<<: *defaults
docker:
- image: circleci/ruby:3.0-buster-node
environment: *ruby_environment
<<: *install_ruby_dependencies
build: build:
<<: *defaults docker:
- image: cimg/ruby:3.0-node
environment:
RAILS_ENV: test
steps: steps:
- *attach_workspace - checkout
- *install_system_dependencies - install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- node/install-packages:
cache-version: v1
pkg-manager: yarn
- run: - run:
name: Precompile assets
command: ./bin/rails assets:precompile command: ./bin/rails assets:precompile
name: Precompile assets
- persist_to_workspace: - persist_to_workspace:
root: ~/projects/
paths: paths:
- ./mastodon/public/assets - public/assets
- ./mastodon/public/packs-test/ - public/packs-test
root: .
test:
parameters:
ruby-version:
type: string
executor:
name: default
ruby-version: << parameters.ruby-version >>
environment:
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
parallelism: 4
steps:
- checkout
- install-system-dependencies
- run:
command: sudo apt-get install -y ffmpeg imagemagick libpam-dev
name: Install additional system dependencies
- run:
command: bundle config with 'pam_authentication'
name: Enable PAM authentication
- install-ruby-dependencies:
ruby-version: << parameters.ruby-version >>
- attach_workspace:
at: .
- wait-db
- run:
command: ./bin/rails db:create db:schema:load db:seed
name: Load database schema
- ruby/rspec-test
test-migrations: test-migrations:
<<: *defaults executor:
docker: name: default
- image: circleci/ruby:2.7-buster-node ruby-version: '3.0'
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
steps: steps:
- *attach_workspace - checkout
- *install_system_dependencies - install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.0'
- wait-db
- run: - run:
name: Create database
command: ./bin/rails db:create command: ./bin/rails db:create
name: Create database
- run: - run:
name: Run migrations
command: ./bin/rails db:migrate command: ./bin/rails db:migrate
name: Run migrations
test-ruby2.7:
<<: *defaults
docker:
- image: circleci/ruby:2.7-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-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6-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-ruby3.0:
<<: *defaults
docker:
- image: circleci/ruby:3.0-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:
name: Run jest
command: yarn test:jest
workflows: workflows:
version: 2 version: 2
build-and-test: build-and-test:
jobs: jobs:
- install - build
- install-ruby2.7: - test:
requires: matrix:
- install parameters:
- install-ruby2.6: ruby-version:
requires: - '2.7'
- install - '3.0'
- install-ruby2.7 name: test-ruby<< matrix.ruby-version >>
- install-ruby3.0:
requires: requires:
- install
- install-ruby2.7
- build:
requires:
- install-ruby2.7
- test-migrations:
requires:
- install-ruby2.7
- test-ruby2.7:
requires:
- install-ruby2.7
- build - build
- test-ruby2.6: - test-migrations:
requires: requires:
- install-ruby2.6
- build - build
- test-ruby3.0: - node/run:
cache-version: v1
name: test-webui
pkg-manager: yarn
requires: requires:
- install-ruby3.0
- build - build
- test-webui: version: lts
requires: yarn-run: test:jest
- install

@ -35,4 +35,7 @@ plugins:
enabled: true enabled: true
exclude_patterns: exclude_patterns:
- spec/ - spec/
- vendor/asset - vendor/asset/
- app/javascript/mastodon/locales/**/*.json
- config/locales/**/*.yml

@ -228,6 +228,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# CAS_LOCATION_KEY='location' # CAS_LOCATION_KEY='location'
# CAS_IMAGE_KEY='image' # CAS_IMAGE_KEY='image'
# CAS_PHONE_KEY='phone' # CAS_PHONE_KEY='phone'
# CAS_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# Optional SAML authentication (cf. omniauth-saml) # Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true # SAML_ENABLED=true

@ -4,6 +4,12 @@
# not demonstrate all available configuration options. Please look at # not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation. # https://docs.joinmastodon.org/admin/config/ for the full documentation.
# Note that this file accepts slightly different syntax depending on whether
# you are using `docker-compose` or not. In particular, if you use
# `docker-compose`, the value of each declared variable will be taken verbatim,
# including surrounding quotes.
# See: https://github.com/mastodon/mastodon/issues/16895
# Federation # Federation
# ---------- # ----------
# This identifies your server and cannot be changed safely later # This identifies your server and cannot be changed safely later
@ -55,7 +61,9 @@ DB_PORT=5432
#ES_ENABLED=true #ES_ENABLED=true
#ES_HOST=localhost #ES_HOST=localhost
#ES_PORT=9200 #ES_PORT=9200
# Authentication for ES (optional)
#ES_USER=elastic
#ES_PASS=password
# Secrets # Secrets
# ------- # -------

@ -1,4 +1,4 @@
# CODEOWNERS for tootsuite/mastodon # CODEOWNERS for mastodon/mastodon
# Translators # Translators
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address. # To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.

@ -0,0 +1,34 @@
name: Build container image
on:
workflow_dispatch:
push:
branches:
- "main"
tags:
- "*"
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/metadata-action@v3
id: meta
with:
images: tootsuite/mastodon
flavor: |
latest=auto
tags: |
type=edge,branch=main
type=semver,pattern={{ raw }}
- uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=tootsuite/mastodon:latest
cache-to: type=inline

@ -22,7 +22,7 @@ jobs:
- name: Set up Ruby - name: Set up Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '2.7' ruby-version: '3.0'
bundler-cache: true bundler-cache: true
- name: Check locale file normalization - name: Check locale file normalization
run: bundle exec i18n-tasks check-normalized run: bundle exec i18n-tasks check-normalized

@ -1 +1 @@
12 14

@ -1 +1 @@
2.7.4 3.0.2

@ -1,7 +1,7 @@
Authors Authors
======= =======
Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon) Mastodon is available on [GitHub](https://github.com/mastodon/mastodon)
and provided thanks to the work of the following contributors: and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron) * [Gargron](https://github.com/Gargron)
@ -719,7 +719,7 @@ and provided thanks to the work of the following contributors:
* [西小倉宏信](mailto:nishiko@mindia.jp) * [西小倉宏信](mailto:nishiko@mindia.jp)
* [雨宮美羽](mailto:k737566@gmail.com) * [雨宮美羽](mailto:k737566@gmail.com)
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead. This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/mastodon/mastodon/graphs/contributors) instead.
## Translators ## Translators

File diff suppressed because it is too large Load Diff

@ -48,7 +48,7 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
## Bug reports ## Bug reports
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected. Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
## Translations ## Translations
@ -78,6 +78,6 @@ It is not always possible to phrase every change in such a manner, but it is des
## Documentation ## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to tootsuite/documentation](https://github.com/tootsuite/documentation). The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).
</blockquote> </blockquote>

@ -2,9 +2,10 @@ FROM ubuntu:20.04 as build-dep
# Use bash for the shell # Use bash for the shell
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-c"]
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
# Install Node v12 (LTS) # Install Node v16 (LTS)
ENV NODE_VER="12.21.0" ENV NODE_VER="16.13.0"
RUN ARCH= && \ RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \ dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \ case "${dpkgArch##*-}" in \
@ -18,15 +19,15 @@ RUN ARCH= && \
esac && \ esac && \
echo "Etc/UTC" > /etc/localtime && \ echo "Etc/UTC" > /etc/localtime && \
apt-get update && \ apt-get update && \
apt-get install -y --no-install-recommends ca-certificates wget python && \ apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \
cd ~ && \ cd ~ && \
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \ wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \ tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \ rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
mv node-v$NODE_VER-linux-$ARCH /opt/node mv node-v$NODE_VER-linux-$ARCH /opt/node
# Install Ruby # Install Ruby 3.0
ENV RUBY_VER="2.7.4" ENV RUBY_VER="3.0.2"
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential \ apt-get install -y --no-install-recommends build-essential \
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \ bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
@ -45,7 +46,8 @@ RUN apt-get update && \
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin" ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
RUN npm install -g yarn && \ RUN npm install -g npm@latest && \
npm install -g yarn && \
gem install bundler && \ gem install bundler && \
apt-get update && \ apt-get update && \
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \ apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
@ -56,6 +58,7 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \ RUN cd /opt/mastodon && \
bundle config set deployment 'true' && \ bundle config set deployment 'true' && \
bundle config set without 'development test' && \ bundle config set without 'development test' && \
bundle config set silence_root_warning true && \
bundle install -j"$(nproc)" && \ bundle install -j"$(nproc)" && \
yarn install --pure-lockfile yarn install --pure-lockfile
@ -81,11 +84,12 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Install mastodon runtime deps # Install mastodon runtime deps
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
RUN apt-get update && \ RUN apt-get update && \
apt-get -y --no-install-recommends install \ apt-get -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \ libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
libicu66 libprotobuf17 libidn11 libyaml-0-2 \ libicu66 libprotobuf17 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline8 gcc tini && \ file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
ln -s /opt/mastodon /mastodon && \ ln -s /opt/mastodon /mastodon && \
gem install bundler && \ gem install bundler && \
rm -rf /var/cache && \ rm -rf /var/cache && \

@ -4,8 +4,9 @@ source 'https://rubygems.org'
ruby '>= 2.5.0', '< 3.1.0' ruby '>= 2.5.0', '< 3.1.0'
gem 'pkg-config', '~> 1.4' gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
gem 'puma', '~> 5.3' gem 'puma', '~> 5.5'
gem 'rails', '~> 6.1.4' gem 'rails', '~> 6.1.4'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.1' gem 'thor', '~> 1.1'
@ -17,19 +18,19 @@ gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8' gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.96', require: false gem 'aws-sdk-s3', '~> 1.106', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'kt-paperclip', '~> 7.0'
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8' gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.6.0', require: false gem 'bootsnap', '~> 1.9.1', require: false
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.7' gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639' gem 'iso-639'
gem 'chewy', '~> 5.2' gem 'chewy', '~> 7.2'
gem 'cld3', '~> 3.4.2' gem 'cld3', '~> 3.4.2'
gem 'devise', '~> 4.8' gem 'devise', '~> 4.8'
gem 'devise-two-factor', '~> 4.0' gem 'devise-two-factor', '~> 4.0'
@ -53,19 +54,18 @@ gem 'fastimage'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.8' gem 'redis-namespace', '~> 1.8'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.4' gem 'http', '~> 5.0'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.5.0' gem 'httplog', '~> 1.5.0'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.11' gem 'nokogiri', '~> 1.12'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.11' gem 'oj', '~> 3.13'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'
gem 'parallel', '~> 1.20'
gem 'posix-spawn' gem 'posix-spawn'
gem 'pundit', '~> 2.1' gem 'pundit', '~> 2.1'
gem 'premailer-rails' gem 'premailer-rails'
@ -73,19 +73,19 @@ gem 'rack-attack', '~> 6.5'
gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 6.0' gem 'rails-i18n', '~> 6.0'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.3', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.0' gem 'rqrcode', '~> 2.1'
gem 'ruby-progressbar', '~> 1.11' gem 'ruby-progressbar', '~> 1.11'
gem 'sanitize', '~> 5.2' gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.5' gem 'scenic', '~> 1.5'
gem 'sidekiq', '~> 6.2' gem 'sidekiq', '~> 6.3'
gem 'sidekiq-scheduler', '~> 3.1' gem 'sidekiq-scheduler', '~> 3.1'
gem 'sidekiq-unique-jobs', '~> 7.1' gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~>0.2.0' gem 'sidekiq-bulk', '~>0.2.0'
gem 'simple-navigation', '~> 4.3' gem 'simple-navigation', '~> 4.3'
gem 'simple_form', '~> 5.1' gem 'simple_form', '~> 5.1'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.1' gem 'stoplight', '~> 2.2.1'
gem 'strong_migrations', '~> 0.7' gem 'strong_migrations', '~> 0.7'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
@ -115,15 +115,14 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.35' gem 'capybara', '~> 3.36'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.18' gem 'faker', '~> 2.19'
gem 'microformats', '~> 4.2' gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1' gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.21', require: false gem 'simplecov', '~> 0.21', require: false
gem 'webmock', '~> 3.13' gem 'webmock', '~> 3.14'
gem 'parallel_tests', '~> 3.7'
gem 'rspec_junit_formatter', '~> 0.4' gem 'rspec_junit_formatter', '~> 0.4'
end end
@ -134,12 +133,12 @@ group :development do
gem 'binding_of_caller', '~> 1.0' gem 'binding_of_caller', '~> 1.0'
gem 'bullet', '~> 6.1' gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4' gem 'letter_opener_web', '~> 2.0'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 1.18', require: false gem 'rubocop', '~> 1.23', require: false
gem 'rubocop-rails', '~> 2.11', require: false gem 'rubocop-rails', '~> 2.12', require: false
gem 'brakeman', '~> 5.0', require: false gem 'brakeman', '~> 5.1', require: false
gem 'bundler-audit', '~> 0.8', require: false gem 'bundler-audit', '~> 0.9', require: false
gem 'capistrano', '~> 3.16' gem 'capistrano', '~> 3.16'
gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rails', '~> 1.6'

@ -1,40 +1,40 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.4) actioncable (6.1.4.1)
actionpack (= 6.1.4) actionpack (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.4) actionmailbox (6.1.4.1)
actionpack (= 6.1.4) actionpack (= 6.1.4.1)
activejob (= 6.1.4) activejob (= 6.1.4.1)
activerecord (= 6.1.4) activerecord (= 6.1.4.1)
activestorage (= 6.1.4) activestorage (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.4) actionmailer (6.1.4.1)
actionpack (= 6.1.4) actionpack (= 6.1.4.1)
actionview (= 6.1.4) actionview (= 6.1.4.1)
activejob (= 6.1.4) activejob (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.4) actionpack (6.1.4.1)
actionview (= 6.1.4) actionview (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.4) actiontext (6.1.4.1)
actionpack (= 6.1.4) actionpack (= 6.1.4.1)
activerecord (= 6.1.4) activerecord (= 6.1.4.1)
activestorage (= 6.1.4) activestorage (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.4) actionview (6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -45,22 +45,22 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (6.1.4) activejob (6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.4) activemodel (6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
activerecord (6.1.4) activerecord (6.1.4.1)
activemodel (= 6.1.4) activemodel (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
activestorage (6.1.4) activestorage (6.1.4.1)
actionpack (= 6.1.4) actionpack (= 6.1.4.1)
activejob (= 6.1.4) activejob (= 6.1.4.1)
activerecord (= 6.1.4) activerecord (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
marcel (~> 1.0.0) marcel (~> 1.0.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.4) activesupport (6.1.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -78,21 +78,21 @@ GEM
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
awrence (1.1.1) awrence (1.1.1)
aws-eventstream (1.1.1) aws-eventstream (1.2.0)
aws-partitions (1.467.0) aws-partitions (1.532.0)
aws-sdk-core (3.114.2) aws-sdk-core (3.122.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.43.0) aws-sdk-kms (1.51.0)
aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.96.1) aws-sdk-s3 (1.106.0)
aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-core (~> 3, >= 3.122.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.4)
aws-sigv4 (1.2.3) aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.16) bcrypt (3.1.16)
better_errors (2.9.1) better_errors (2.9.1)
@ -104,18 +104,18 @@ GEM
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
blurhash (0.1.5) blurhash (0.1.5)
ffi (~> 1.14) ffi (~> 1.14)
bootsnap (1.6.0) bootsnap (1.9.1)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (5.0.4) brakeman (5.1.2)
browser (4.2.0) browser (4.2.0)
brpoplpush-redis_script (0.1.2) brpoplpush-redis_script (0.1.2)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, <= 5.0) redis (>= 1.0, <= 5.0)
builder (3.2.4) builder (3.2.4)
bullet (6.1.4) bullet (6.1.5)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundler-audit (0.8.0) bundler-audit (0.9.0.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
byebug (11.1.3) byebug (11.1.3)
@ -134,8 +134,9 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.35.3) capybara (3.36.0)
addressable addressable
matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
rack (>= 1.6.0) rack (>= 1.6.0)
@ -146,9 +147,9 @@ GEM
activesupport activesupport
cbor (0.5.9.6) cbor (0.5.9.6)
charlock_holmes (0.7.7) charlock_holmes (0.7.7)
chewy (5.2.0) chewy (7.2.3)
activesupport (>= 5.2) activesupport (>= 5.2)
elasticsearch (>= 2.0.0) elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl elasticsearch-dsl
chunky_png (1.4.0) chunky_png (1.4.0)
cld3 (3.4.2) cld3 (3.4.2)
@ -173,7 +174,7 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (4.0.0) devise-two-factor (4.0.1)
activesupport (< 6.2) activesupport (< 6.2)
attr_encrypted (>= 1.3, < 4, != 2) attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0) devise (~> 4.0)
@ -188,7 +189,7 @@ GEM
docile (1.3.4) docile (1.3.4)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.5.2) doorkeeper (5.5.4)
railties (>= 5) railties (>= 5)
dotenv (2.7.6) dotenv (2.7.6)
dotenv-rails (2.7.6) dotenv-rails (2.7.6)
@ -196,13 +197,13 @@ GEM
railties (>= 3.2) railties (>= 3.2)
e2mmap (0.1.0) e2mmap (0.1.0)
ed25519 (1.2.4) ed25519 (1.2.4)
elasticsearch (7.10.1) elasticsearch (7.13.3)
elasticsearch-api (= 7.10.1) elasticsearch-api (= 7.13.3)
elasticsearch-transport (= 7.10.1) elasticsearch-transport (= 7.13.3)
elasticsearch-api (7.10.1) elasticsearch-api (7.13.3)
multi_json multi_json
elasticsearch-dsl (0.1.9) elasticsearch-dsl (0.1.10)
elasticsearch-transport (7.10.1) elasticsearch-transport (7.13.3)
faraday (~> 1) faraday (~> 1)
multi_json multi_json
encryptor (3.0.0) encryptor (3.0.0)
@ -211,16 +212,30 @@ GEM
tzinfo tzinfo
excon (0.76.0) excon (0.76.0)
fabrication (2.22.0) fabrication (2.22.0)
faker (2.18.0) faker (2.19.0)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (1.3.0) faraday (1.8.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0) faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
ruby2_keywords ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
fast_blank (1.0.0) faraday-net_http_persistent (1.2.0)
fastimage (2.2.4) faraday-patron (1.0.0)
ffi (1.15.0) faraday-rack (1.0.0)
fast_blank (1.0.1)
fastimage (2.2.5)
ffi (1.15.4)
ffi-compiler (1.0.1) ffi-compiler (1.0.1)
ffi (>= 1.0.0) ffi (>= 1.0.0)
rake rake
@ -243,8 +258,8 @@ GEM
fuubar (2.5.1) fuubar (2.5.1)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
globalid (0.4.2) globalid (0.5.2)
activesupport (>= 4.2.0) activesupport (>= 5.0)
hamlit (2.13.0) hamlit (2.13.0)
temple (>= 0.8.2) temple (>= 0.8.2)
thor thor
@ -262,23 +277,21 @@ GEM
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
http (4.4.1) http (5.0.4)
addressable (~> 2.3) addressable (~> 2.8)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.2) http-form_data (~> 2.2)
http-parser (~> 1.2.0) llhttp-ffi (~> 0.4.0)
http-cookie (1.0.3) http-cookie (1.0.4)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.3.0) http-form_data (2.3.0)
http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.5.0) httplog (1.5.0)
rack (>= 1.0) rack (>= 1.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.8.10) i18n (1.8.11)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.34) i18n-tasks (0.9.35)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
erubi erubi
@ -288,20 +301,20 @@ GEM
rails-i18n rails-i18n
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.2) idn-ruby (0.1.4)
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.3.5) iso-639 (0.3.5)
jmespath (1.4.0) jmespath (1.4.0)
json (2.5.1) json (2.5.1)
json-canonicalization (0.2.1) json-canonicalization (0.2.1)
json-ld (3.1.9) json-ld (3.1.10)
htmlentities (~> 4.3) htmlentities (~> 4.3)
json-canonicalization (~> 0.2) json-canonicalization (~> 0.2)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.14) multi_json (~> 1.14)
rack (~> 2.0) rack (~> 2.0)
rdf (~> 3.1) rdf (~> 3.1)
json-ld-preloaded (3.1.5) json-ld-preloaded (3.1.6)
json-ld (~> 3.1) json-ld (~> 3.1)
rdf (~> 3.1) rdf (~> 3.1)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
@ -318,21 +331,31 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.1) kaminari-core (= 1.2.1)
kaminari-core (1.2.1) kaminari-core (1.2.1)
kt-paperclip (7.0.1)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
marcel (~> 1.0.1)
mime-types
terrapin (~> 0.6.0)
launchy (2.5.0) launchy (2.5.0)
addressable (~> 2.7) addressable (~> 2.7)
letter_opener (1.7.0) letter_opener (1.7.0)
launchy (~> 2.2) launchy (~> 2.2)
letter_opener_web (1.4.0) letter_opener_web (2.0.0)
actionmailer (>= 3.2) actionmailer (>= 5.2)
letter_opener (~> 1.0) letter_opener (~> 1.7)
railties (>= 3.2) railties (>= 5.2)
rexml
link_header (0.0.8) link_header (0.0.8)
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.11.2) lograge (0.11.2)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.10.0) loofah (2.12.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -342,19 +365,17 @@ GEM
marcel (1.0.1) marcel (1.0.1)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
matrix (0.4.2)
memory_profiler (1.0.0) memory_profiler (1.0.0)
method_source (1.0.0) method_source (1.0.0)
microformats (4.3.1) microformats (4.3.1)
json (~> 2.2) json (~> 2.2)
nokogiri (~> 1.10) nokogiri (~> 1.10)
mime-types (3.3.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2020.0512) mime-types-data (3.2021.1115)
mimemagic (0.3.10) mini_mime (1.1.2)
nokogiri (~> 1) mini_portile2 (2.6.1)
rake
mini_mime (1.1.0)
mini_portile2 (2.5.3)
minitest (5.14.4) minitest (5.14.4)
msgpack (1.4.2) msgpack (1.4.2)
multi_json (1.15.0) multi_json (1.15.0)
@ -363,18 +384,16 @@ GEM
net-scp (3.0.0) net-scp (3.0.0)
net-ssh (>= 2.6.5, < 7.0.0) net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.1.0) net-ssh (6.1.0)
nio4r (2.5.7) nio4r (2.5.8)
nokogiri (1.11.7) nokogiri (1.12.5)
mini_portile2 (~> 2.5.0) mini_portile2 (~> 2.6.1)
racc (~> 1.4) racc (~> 1.4)
nokogumbo (2.0.4)
nokogiri (~> 1.8, >= 1.8.4)
nsa (0.2.8) nsa (0.2.8)
activesupport (>= 4.2, < 7) activesupport (>= 4.2, < 7)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.11.8) oj (3.13.9)
omniauth (1.9.1) omniauth (1.9.1)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -391,17 +410,9 @@ GEM
openssl (2.2.0) openssl (2.2.0)
openssl-signature_algorithm (0.4.0) openssl-signature_algorithm (0.4.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.14.5) ox (2.14.6)
paperclip (6.0.0) parallel (1.21.0)
activemodel (>= 4.2.0) parser (3.0.2.0)
activesupport (>= 4.2.0)
mime-types
mimemagic (~> 0.3.0)
terrapin (~> 0.6.0)
parallel (1.20.1)
parallel_tests (3.7.0)
parallel
parser (3.0.1.1)
ast (~> 2.4.1) ast (~> 2.4.1)
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
@ -428,35 +439,35 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (5.3.2) puma (5.5.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.5.2) racc (1.6.0)
rack (2.2.3) rack (2.2.3)
rack-attack (6.5.0) rack-attack (6.5.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-proxy (0.6.5) rack-proxy (0.7.0)
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (6.1.4) rails (6.1.4.1)
actioncable (= 6.1.4) actioncable (= 6.1.4.1)
actionmailbox (= 6.1.4) actionmailbox (= 6.1.4.1)
actionmailer (= 6.1.4) actionmailer (= 6.1.4.1)
actionpack (= 6.1.4) actionpack (= 6.1.4.1)
actiontext (= 6.1.4) actiontext (= 6.1.4.1)
actionview (= 6.1.4) actionview (= 6.1.4.1)
activejob (= 6.1.4) activejob (= 6.1.4.1)
activemodel (= 6.1.4) activemodel (= 6.1.4.1)
activerecord (= 6.1.4) activerecord (= 6.1.4.1)
activestorage (= 6.1.4) activestorage (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.4) railties (= 6.1.4.1)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -465,28 +476,28 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0) rails-html-sanitizer (1.4.2)
loofah (~> 2.3) loofah (~> 2.3)
rails-i18n (6.0.0) rails-i18n (6.0.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (6.1.4) railties (6.1.4.1)
actionpack (= 6.1.4) actionpack (= 6.1.4.1)
activesupport (= 6.1.4) activesupport (= 6.1.4.1)
method_source method_source
rake (>= 0.13) rake (>= 0.13)
thor (~> 1.0) thor (~> 1.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.3) rake (13.0.6)
rdf (3.1.13) rdf (3.1.15)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.4.0) rdf-normalize (0.4.0)
rdf (~> 3.1) rdf (~> 3.1)
redcarpet (3.5.1) redcarpet (3.5.1)
redis (4.3.1) redis (4.5.1)
redis-namespace (1.8.1) redis-namespace (1.8.1)
redis (>= 3.0.4) redis (>= 3.0.4)
regexp_parser (2.1.1) regexp_parser (2.1.1)
@ -498,10 +509,10 @@ GEM
rexml (3.2.5) rexml (3.2.5)
rotp (6.2.0) rotp (6.2.0)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (2.0.0) rqrcode (2.1.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.0.0) rqrcode_core (1.2.0)
rspec-core (3.10.1) rspec-core (3.10.1)
rspec-support (~> 3.10.0) rspec-support (~> 3.10.0)
rspec-expectations (3.10.1) rspec-expectations (3.10.1)
@ -510,7 +521,7 @@ GEM
rspec-mocks (3.10.2) rspec-mocks (3.10.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0) rspec-support (~> 3.10.0)
rspec-rails (5.0.1) rspec-rails (5.0.2)
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
@ -524,39 +535,39 @@ GEM
rspec-support (3.10.2) rspec-support (3.10.2)
rspec_junit_formatter (0.4.1) rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.18.3) rubocop (1.23.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.0.0.0) parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml rexml
rubocop-ast (>= 1.7.0, < 2.0) rubocop-ast (>= 1.12.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0) unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.7.0) rubocop-ast (1.13.0)
parser (>= 3.0.1.1) parser (>= 3.0.1.1)
rubocop-rails (2.11.2) rubocop-rails (2.12.4)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
ruby-saml (1.11.0) ruby-saml (1.13.0)
nokogiri (>= 1.5.10) nokogiri (>= 1.10.5)
ruby2_keywords (0.0.4) rexml
ruby2_keywords (0.0.5)
rufus-scheduler (3.7.0) rufus-scheduler (3.7.0)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0) safety_net_attestation (0.4.0)
jwt (~> 2.0) jwt (~> 2.0)
sanitize (5.2.3) sanitize (6.0.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.8.0) nokogiri (>= 1.12.0)
nokogumbo (~> 2.0)
scenic (1.5.4) scenic (1.5.4)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
securecompare (1.0.0) securecompare (1.0.0)
semantic_range (3.0.0) semantic_range (3.0.0)
sidekiq (6.2.1) sidekiq (6.3.1)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.2.0) redis (>= 4.2.0)
@ -569,11 +580,11 @@ GEM
sidekiq (>= 3) sidekiq (>= 3)
thwait thwait
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.2) sidekiq-unique-jobs (7.1.8)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 7.0) sidekiq (>= 5.0, < 8.0)
thor (>= 0.20, < 2.0) thor (>= 0.20, < 3.0)
simple-navigation (4.3.0) simple-navigation (4.3.0)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (5.1.0) simple_form (5.1.0)
@ -588,9 +599,9 @@ GEM
sprockets (3.7.2) sprockets (3.7.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.2) sprockets-rails (3.4.0)
actionpack (>= 4.0) actionpack (>= 5.2)
activesupport (>= 4.0) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.21.2) sshkit (1.21.2)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
@ -598,11 +609,11 @@ GEM
stackprof (0.2.17) stackprof (0.2.17)
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
stoplight (2.2.1) stoplight (2.2.1)
strong_migrations (0.7.7) strong_migrations (0.7.8)
activerecord (>= 5) activerecord (>= 5)
temple (0.8.2) temple (0.8.2)
terminal-table (3.0.0) terminal-table (3.0.2)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (1.1.0) thor (1.1.0)
@ -627,13 +638,13 @@ GEM
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (2.0.4) tzinfo (2.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2021.1) tzinfo-data (1.2021.5)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.7) unf_ext (0.0.8)
unicode-display_width (1.7.0) unicode-display_width (2.1.0)
uniform_notifier (1.14.1) uniform_notifier (1.14.2)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
webauthn (3.0.0.alpha1) webauthn (3.0.0.alpha1)
@ -646,11 +657,11 @@ GEM
safety_net_attestation (~> 0.4.0) safety_net_attestation (~> 0.4.0)
securecompare (~> 1.0) securecompare (~> 1.0)
tpm-key_attestation (~> 0.9.0) tpm-key_attestation (~> 0.9.0)
webmock (3.13.0) webmock (3.14.0)
addressable (>= 2.3.6) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.4.0) webpacker (5.4.3)
activesupport (>= 5.2) activesupport (>= 5.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
@ -665,7 +676,7 @@ GEM
xorcist (1.1.2) xorcist (1.1.2)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.4.2) zeitwerk (2.5.1)
PLATFORMS PLATFORMS
ruby ruby
@ -675,22 +686,22 @@ DEPENDENCIES
active_record_query_trace (~> 1.8) active_record_query_trace (~> 1.8)
addressable (~> 2.8) addressable (~> 2.8)
annotate (~> 3.1) annotate (~> 3.1)
aws-sdk-s3 (~> 1.96) aws-sdk-s3 (~> 1.106)
better_errors (~> 2.9) better_errors (~> 2.9)
binding_of_caller (~> 1.0) binding_of_caller (~> 1.0)
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.6.0) bootsnap (~> 1.9.1)
brakeman (~> 5.0) brakeman (~> 5.1)
browser browser
bullet (~> 6.1) bullet (~> 6.1)
bundler-audit (~> 0.8) bundler-audit (~> 0.9)
capistrano (~> 3.16) capistrano (~> 3.16)
capistrano-rails (~> 1.6) capistrano-rails (~> 1.6)
capistrano-rbenv (~> 2.2) capistrano-rbenv (~> 2.2)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.35) capybara (~> 3.36)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
chewy (~> 5.2) chewy (~> 7.2)
cld3 (~> 3.4.2) cld3 (~> 3.4.2)
climate_control (~> 0.2) climate_control (~> 0.2)
color_diff (~> 0.1) color_diff (~> 0.1)
@ -704,7 +715,7 @@ DEPENDENCIES
dotenv-rails (~> 2.7) dotenv-rails (~> 2.7)
ed25519 (~> 1.2) ed25519 (~> 1.2)
fabrication (~> 2.22) fabrication (~> 2.22)
faker (~> 2.18) faker (~> 2.19)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.1.0) fog-core (<= 2.1.0)
@ -713,7 +724,7 @@ DEPENDENCIES
hamlit-rails (~> 0.2) hamlit-rails (~> 0.2)
hiredis (~> 0.6) hiredis (~> 0.6)
htmlentities (~> 4.3) htmlentities (~> 4.3)
http (~> 4.4) http (~> 5.0)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
httplog (~> 1.5.0) httplog (~> 1.5.0)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
@ -722,27 +733,25 @@ DEPENDENCIES
json-ld json-ld
json-ld-preloaded (~> 3.1) json-ld-preloaded (~> 3.1)
kaminari (~> 1.2) kaminari (~> 1.2)
kt-paperclip (~> 7.0)
letter_opener (~> 1.7) letter_opener (~> 1.7)
letter_opener_web (~> 1.4) letter_opener_web (~> 2.0)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.11) lograge (~> 0.11)
makara (~> 0.5) makara (~> 0.5)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.2) microformats (~> 4.2)
mime-types (~> 3.3.1) mime-types (~> 3.4.1)
net-ldap (~> 0.17) net-ldap (~> 0.17)
nokogiri (~> 1.11) nokogiri (~> 1.12)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.11) oj (~> 3.13)
omniauth (~> 1.9) omniauth (~> 1.9)
omniauth-cas (~> 2.0) omniauth-cas (~> 2.0)
omniauth-rails_csrf_protection (~> 0.1) omniauth-rails_csrf_protection (~> 0.1)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
ox (~> 2.14) ox (~> 2.14)
paperclip (~> 6.0)
parallel (~> 1.20)
parallel_tests (~> 3.7)
parslet parslet
pg (~> 1.2) pg (~> 1.2)
pghero (~> 2.8) pghero (~> 2.8)
@ -752,7 +761,7 @@ DEPENDENCIES
private_address_check (~> 0.5) private_address_check (~> 0.5)
pry-byebug (~> 3.9) pry-byebug (~> 3.9)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 5.3) puma (~> 5.5)
pundit (~> 2.1) pundit (~> 2.1)
rack (~> 2.2.3) rack (~> 2.2.3)
rack-attack (~> 6.5) rack-attack (~> 6.5)
@ -763,18 +772,19 @@ DEPENDENCIES
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.4) rdf-normalize (~> 0.4)
redcarpet (~> 3.5) redcarpet (~> 3.5)
redis (~> 4.3) redis (~> 4.5)
redis-namespace (~> 1.8) redis-namespace (~> 1.8)
rqrcode (~> 2.0) rexml (~> 3.2)
rqrcode (~> 2.1)
rspec-rails (~> 5.0) rspec-rails (~> 5.0)
rspec-sidekiq (~> 3.1) rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4) rspec_junit_formatter (~> 0.4)
rubocop (~> 1.18) rubocop (~> 1.23)
rubocop-rails (~> 2.11) rubocop-rails (~> 2.12)
ruby-progressbar (~> 1.11) ruby-progressbar (~> 1.11)
sanitize (~> 5.2) sanitize (~> 6.0)
scenic (~> 1.5) scenic (~> 1.5)
sidekiq (~> 6.2) sidekiq (~> 6.3)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.1) sidekiq-scheduler (~> 3.1)
sidekiq-unique-jobs (~> 7.1) sidekiq-unique-jobs (~> 7.1)
@ -782,7 +792,7 @@ DEPENDENCIES
simple_form (~> 5.1) simple_form (~> 5.1)
simplecov (~> 0.21) simplecov (~> 0.21)
sprockets (~> 3.7.2) sprockets (~> 3.7.2)
sprockets-rails (~> 3.2) sprockets-rails (~> 3.4)
stackprof stackprof
stoplight (~> 2.2.1) stoplight (~> 2.2.1)
strong_migrations (~> 0.7) strong_migrations (~> 0.7)
@ -791,7 +801,7 @@ DEPENDENCIES
twitter-text (~> 3.1.0) twitter-text (~> 3.1.0)
tzinfo-data (~> 1.2021) tzinfo-data (~> 1.2021)
webauthn (~> 3.0.0.alpha1) webauthn (~> 3.0.0.alpha1)
webmock (~> 3.13) webmock (~> 3.14)
webpacker (~> 5.4) webpacker (~> 5.4)
webpush (~> 0.3) webpush (~> 0.3)
xorcist (~> 1.1) xorcist (~> 1.1)

14
Vagrantfile vendored

@ -12,7 +12,7 @@ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main' sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main'
# Add repo for NodeJS # Add repo for NodeJS
curl -sL https://deb.nodesource.com/setup_12.x | sudo bash - curl -sL https://deb.nodesource.com/setup_14.x | sudo bash -
# Add firewall rule to redirect 80 to PORT and save # Add firewall rule to redirect 80 to PORT and save
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]} sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]}
@ -45,16 +45,8 @@ sudo apt-get install \
# Install rvm # Install rvm
read RUBY_VERSION < .ruby-version read RUBY_VERSION < .ruby-version
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB" curl -sSL https://rvm.io/mpapis.asc | gpg --import
$($gpg_command) curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
if [ $? -ne 0 ];then
echo "GPG command failed, This prevented RVM from installing."
echo "Retrying once..." && $($gpg_command)
if [ $? -ne 0 ];then
echo "GPG failed for the second time, please ensure network connectivity."
echo "Exiting..." && exit 1
fi
fi
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
source /home/vagrant/.rvm/scripts/rvm source /home/vagrant/.rvm/scripts/rvm

@ -1,8 +1,8 @@
{ {
"name": "Mastodon", "name": "Mastodon",
"description": "A GNU Social-compatible microblogging server", "description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon", "repository": "https://github.com/mastodon/mastodon",
"logo": "https://github.com/tootsuite.png", "logo": "https://github.com/mastodon.png",
"env": { "env": {
"HEROKU": { "HEROKU": {
"description": "Leave this as true", "description": "Leave this as true",

@ -23,21 +23,21 @@ class AccountsIndex < Chewy::Index
}, },
} }
define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? }
root date_detection: false do
field :id, type: 'long'
field :display_name, type: 'text', analyzer: 'content' do root date_detection: false do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' field :id, type: 'long'
end
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do field :display_name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end end
field :following_count, type: 'long', value: ->(account) { account.following.local.count } field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count } field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end end
field :following_count, type: 'long', value: ->(account) { account.following.local.count }
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end end
end end

@ -31,36 +31,36 @@ class StatusesIndex < Chewy::Index
}, },
} }
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
crutch :mentions do |collection|
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :favourites do |collection| crutch :mentions do |collection|
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id) data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end end
crutch :reblogs do |collection| crutch :favourites do |collection|
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id) data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end end
crutch :bookmarks do |collection| crutch :reblogs do |collection|
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id) data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) } data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end end
root date_detection: false do crutch :bookmarks do |collection|
field :id, type: 'long' data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
field :account_id, type: 'long' data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do root date_detection: false do
field :stemmed, type: 'text', analyzer: 'content' field :id, type: 'long'
end field :account_id, type: 'long'
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content'
end end
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
end end
end end

@ -23,15 +23,15 @@ class TagsIndex < Chewy::Index
}, },
} }
define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do index_scope ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? }
root date_detection: false do
field :name, type: 'text', analyzer: 'content' do
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? } root date_detection: false do
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } } field :name, type: 'text', analyzer: 'content' do
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at } field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
end end
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
end end
end end

@ -19,11 +19,11 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
private private
def uri_prefix def uri_prefix
signed_request_account.uri[/http(s?):\/\/[^\/]+\//] signed_request_account.uri[Account::URL_PREFIX_RE]
end end
def set_items def set_items
@items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%', false, true)).pluck(:uri) @items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%", false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri)
end end
def collection_presenter def collection_presenter

@ -1,50 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'sidekiq/api'
module Admin module Admin
class DashboardController < BaseController class DashboardController < BaseController
def index def index
@system_checks = Admin::SystemCheck.perform @system_checks = Admin::SystemCheck.perform
@users_count = User.count @time_period = (1.month.ago.to_date...Time.now.utc.to_date)
@pending_users_count = User.pending.count @pending_users_count = User.pending.count
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0 @pending_reports_count = Report.unresolved.count
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
@relay_enabled = Relay.enabled.exists?
@single_user_mode = Rails.configuration.x.single_user_mode
@registrations_enabled = Setting.registrations_mode != 'none'
@deletions_enabled = Setting.open_deletion
@invites_enabled = Setting.min_invite_role == 'user'
@search_enabled = Chewy.enabled?
@version = Mastodon::Version.to_s
@database_version = ActiveRecord::Base.connection.execute('SELECT VERSION()').first['version'].match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
@redis_version = redis_info['redis_version']
@reports_count = Report.unresolved.count
@queue_backlog = Sidekiq::Stats.new.enqueued
@recent_users = User.confirmed.recent.includes(:account).limit(8)
@database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
@redis_size = redis_info['used_memory']
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
@cas_enabled = ENV['CAS_ENABLED'] == 'true'
@saml_enabled = ENV['SAML_ENABLED'] == 'true'
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
@trending_hashtags = TrendingTags.get(10, filtered: false)
@pending_tags_count = Tag.pending_review.count @pending_tags_count = Tag.pending_review.count
@authorized_fetch = authorized_fetch_mode?
@whitelist_enabled = whitelist_mode?
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
@keybase_integration = Setting.enable_keybase
@trends_enabled = Setting.trends
end end
private private
def current_week
@current_week ||= Time.now.utc.to_date.cweek
end
def redis_info def redis_info
@redis_info ||= begin @redis_info ||= begin
if Redis.current.is_a?(Redis::Namespace) if Redis.current.is_a?(Redis::Namespace)

@ -14,7 +14,7 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted]) @statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media] if params[:media]
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)) @statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)).reorder('statuses.id desc')
end end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)

@ -40,7 +40,12 @@ class Api::BaseController < ApplicationController
render json: { error: 'This action is not allowed' }, status: 403 render json: { error: 'This action is not allowed' }, status: 403
end end
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight do rescue_from Seahorse::Client::NetworkingError do |e|
Rails.logger.warn "Storage server error: #{e}"
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503 render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end end

@ -1,8 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::AccountsController < Api::BaseController class Api::V1::AccountsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute] before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow] before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock] before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
@ -53,6 +53,11 @@ class Api::V1::AccountsController < Api::BaseController
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end end
def remove_from_followers
RemoveFromFollowersService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
def unblock def unblock
UnblockService.new.call(current_user.account, @account) UnblockService.new.call(current_user.account, @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships

@ -0,0 +1,23 @@
# frozen_string_literal: true
class Api::V1::Admin::DimensionsController < Api::BaseController
protect_from_forgery with: :exception
before_action :require_staff!
before_action :set_dimensions
def create
render json: @dimensions, each_serializer: REST::Admin::DimensionSerializer
end
private
def set_dimensions
@dimensions = Admin::Metrics::Dimension.retrieve(
params[:keys],
params[:start_at],
params[:end_at],
params[:limit]
)
end
end

@ -0,0 +1,22 @@
# frozen_string_literal: true
class Api::V1::Admin::MeasuresController < Api::BaseController
protect_from_forgery with: :exception
before_action :require_staff!
before_action :set_measures
def create
render json: @measures, each_serializer: REST::Admin::MeasureSerializer
end
private
def set_measures
@measures = Admin::Metrics::Measure.retrieve(
params[:keys],
params[:start_at],
params[:end_at]
)
end
end

@ -0,0 +1,22 @@
# frozen_string_literal: true
class Api::V1::Admin::RetentionController < Api::BaseController
protect_from_forgery with: :exception
before_action :require_staff!
before_action :set_cohorts
def create
render json: @cohorts, each_serializer: REST::Admin::CohortSerializer
end
private
def set_cohorts
@cohorts = Admin::Metrics::Retention.new(
params[:start_at],
params[:end_at],
params[:frequency]
).cohorts
end
end

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1::Admin::TrendsController < Api::BaseController
before_action :require_staff!
before_action :set_trends
def index
render json: @trends, each_serializer: REST::Admin::TagSerializer
end
private
def set_trends
@trends = TrendingTags.get(10, filtered: false)
end
end

@ -14,22 +14,21 @@ class Api::V1::Instances::ActivityController < Api::BaseController
private private
def activity def activity
weeks = [] statuses_tracker = ActivityTracker.new('activity:statuses:local', :basic)
logins_tracker = ActivityTracker.new('activity:logins', :unique)
12.times do |i| registrations_tracker = ActivityTracker.new('activity:accounts:local', :basic)
day = i.weeks.ago.to_date
week_id = day.cweek (0...12).map do |i|
week = Date.commercial(day.cwyear, week_id) start_of_week = i.weeks.ago
end_of_week = start_of_week + 6.days
weeks << {
week: week.to_time.to_i.to_s, {
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0', week: start_of_week.to_i.to_s,
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s, statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s,
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0', logins: logins_tracker.sum(start_of_week, end_of_week).to_s,
registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s,
} }
end end
weeks
end end
def require_enabled_api! def require_enabled_api!

@ -27,7 +27,12 @@ class ApplicationController < ActionController::Base
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
rescue_from Mastodon::RaceConditionError, Seahorse::Client::NetworkingError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
rescue_from Seahorse::Client::NetworkingError do |e|
Rails.logger.warn "Storage server error: #{e}"
service_unavailable
end
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :require_functional!, if: :user_signed_in? before_action :require_functional!, if: :user_signed_in?

@ -11,7 +11,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
if @user.persisted? if @user.persisted?
LoginActivity.create( LoginActivity.create(
user: user, user: @user,
success: true, success: true,
authentication_method: :omniauth, authentication_method: :omniauth,
provider: provider, provider: provider,

@ -11,7 +11,6 @@ class Auth::PasswordsController < Devise::PasswordsController
super do |resource| super do |resource|
if resource.errors.empty? if resource.errors.empty?
resource.session_activations.destroy_all resource.session_activations.destroy_all
resource.forget_me!
end end
end end
end end

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::RegistrationsController < Devise::RegistrationsController class Auth::RegistrationsController < Devise::RegistrationsController
include Devise::Controllers::Rememberable
include RegistrationSpamConcern include RegistrationSpamConcern
layout :determine_layout layout :determine_layout
@ -31,8 +30,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super do |resource| super do |resource|
if resource.saved_change_to_encrypted_password? if resource.saved_change_to_encrypted_password?
resource.clear_other_sessions(current_session.session_id) resource.clear_other_sessions(current_session.session_id)
resource.forget_me!
remember_me(resource)
end end
end end
end end

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::SessionsController < Devise::SessionsController class Auth::SessionsController < Devise::SessionsController
include Devise::Controllers::Rememberable
layout 'auth' layout 'auth'
skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_no_authentication, only: [:create]
@ -44,10 +42,13 @@ class Auth::SessionsController < Devise::SessionsController
end end
def webauthn_options def webauthn_options
user = find_user user = User.find_by(id: session[:attempt_user_id])
if user&.webauthn_enabled? if user&.webauthn_enabled?
options_for_get = WebAuthn::Credential.options_for_get(allow: user.webauthn_credentials.pluck(:external_id)) options_for_get = WebAuthn::Credential.options_for_get(
allow: user.webauthn_credentials.pluck(:external_id),
user_verification: 'discouraged'
)
session[:webauthn_challenge] = options_for_get.challenge session[:webauthn_challenge] = options_for_get.challenge
@ -60,16 +61,20 @@ class Auth::SessionsController < Devise::SessionsController
protected protected
def find_user def find_user
if session[:attempt_user_id] if user_params[:email].present?
find_user_from_params
elsif session[:attempt_user_id]
User.find_by(id: session[:attempt_user_id]) User.find_by(id: session[:attempt_user_id])
else
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end end
end end
def find_user_from_params
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end
def user_params def user_params
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {}) params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
end end
@ -149,7 +154,6 @@ class Auth::SessionsController < Devise::SessionsController
clear_attempt_from_session clear_attempt_from_session
user.update_sign_in!(request, new_sign_in: true) user.update_sign_in!(request, new_sign_in: true)
remember_me(user)
sign_in(user) sign_in(user)
flash.delete(:notice) flash.delete(:notice)

@ -16,14 +16,18 @@ module SignInTokenAuthenticationConcern
end end
def authenticate_with_sign_in_token def authenticate_with_sign_in_token
user = self.resource = find_user if user_params[:email].present?
user = self.resource = find_user_from_params
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password])
restart_session elsif session[:attempt_user_id]
elsif user_params.key?(:sign_in_token_attempt) && session[:attempt_user_id] user = self.resource = User.find_by(id: session[:attempt_user_id])
authenticate_with_sign_in_token_attempt(user) return if user.nil?
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_sign_in_token(user) if session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user_params.key?(:sign_in_token_attempt)
authenticate_with_sign_in_token_attempt(user)
end
end end
end end

@ -35,16 +35,20 @@ module TwoFactorAuthenticationConcern
end end
def authenticate_with_two_factor def authenticate_with_two_factor
user = self.resource = find_user if user_params[:email].present?
user = self.resource = find_user_from_params
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s prompt_for_two_factor(user) if user&.external_or_valid_password?(user_params[:password])
restart_session elsif session[:attempt_user_id]
elsif user.webauthn_enabled? && user_params.key?(:credential) && session[:attempt_user_id] user = self.resource = User.find_by(id: session[:attempt_user_id])
authenticate_with_two_factor_via_webauthn(user) return if user.nil?
elsif user_params.key?(:otp_attempt) && session[:attempt_user_id]
authenticate_with_two_factor_via_otp(user) if session[:attempt_user_updated_at] != user.updated_at.to_s
elsif user.present? && user.external_or_valid_password?(user_params[:password]) restart_session
prompt_for_two_factor(user) elsif user.webauthn_enabled? && user_params.key?(:credential)
authenticate_with_two_factor_via_webauthn(user)
elsif user_params.key?(:otp_attempt)
authenticate_with_two_factor_via_otp(user)
end
end end
end end

@ -16,30 +16,7 @@ class HomeController < ApplicationController
def redirect_unauthenticated_to_permalinks! def redirect_unauthenticated_to_permalinks!
return if user_signed_in? return if user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/) redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
if matches
case matches[1]
when 'statuses'
status = Status.find_by(id: matches[2])
if status&.distributable?
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
when 'accounts'
account = Account.find_by(id: matches[2])
if account
redirect_to(ActivityPub::TagManager.instance.url_for(account))
return
end
end
end
matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
end end
def set_pack def set_pack

@ -28,7 +28,12 @@ class MediaController < ApplicationController
private private
def set_media_attachment def set_media_attachment
@media_attachment = MediaAttachment.attached.find_by!(shortcode: params[:id] || params[:medium_id]) id = params[:id] || params[:medium_id]
return if id.nil?
scope = MediaAttachment.local.attached
# If id is 19 characters long, it's a shortcode, otherwise it's an identifier
@media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find_by!(id: id)
end end
def verify_permitted_status! def verify_permitted_status!

@ -42,7 +42,7 @@ class Settings::DeletesController < Settings::BaseController
end end
def destroy_account! def destroy_account!
current_account.suspend!(origin: :local) current_account.suspend!(origin: :local, block_email: false)
AccountDeletionWorker.perform_async(current_user.account_id) AccountDeletionWorker.perform_async(current_user.account_id)
sign_out sign_out
end end

@ -21,7 +21,8 @@ module Settings
display_name: current_user.account.username, display_name: current_user.account.username,
id: current_user.webauthn_id, id: current_user.webauthn_id,
}, },
exclude: current_user.webauthn_credentials.pluck(:external_id) exclude: current_user.webauthn_credentials.pluck(:external_id),
authenticator_selection: { user_verification: 'discouraged' }
) )
session[:webauthn_challenge] = options_for_create.challenge session[:webauthn_challenge] = options_for_create.challenge

@ -0,0 +1,40 @@
# frozen_string_literal: true
class StatusesCleanupController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :set_policy
before_action :set_body_classes
before_action :set_pack
def show; end
def update
if @policy.update(resource_params)
redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg')
else
render action: :show
end
rescue ActionController::ParameterMissing
# Do nothing
end
private
def set_pack
use_pack 'settings'
end
def set_policy
@policy = current_account.statuses_cleanup_policy || current_account.build_statuses_cleanup_policy(enabled: false)
end
def resource_params
params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs)
end
def set_body_classes
@body_classes = 'admin'
end
end

@ -137,6 +137,10 @@ module ApplicationHelper
end end
end end
def react_admin_component(name, props = {})
content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) })
end
def body_classes def body_classes
output = (@body_classes || '').split(' ') output = (@body_classes || '').split(' ')
output << "flavour-#{current_flavour.parameterize}" output << "flavour-#{current_flavour.parameterize}"

@ -41,6 +41,7 @@ module SettingsHelper
ka: 'ქართული', ka: 'ქართული',
kab: 'Taqbaylit', kab: 'Taqbaylit',
kk: 'Қазақша', kk: 'Қазақша',
kmr: 'Kurmancî',
kn: 'ಕನನಡ', kn: 'ಕನನಡ',
ko: '한국어', ko: '한국어',
ku: 'سۆرانی', ku: 'سۆرانی',

@ -5,6 +5,10 @@ export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
@ -104,6 +108,34 @@ export function fetchAccount(id) {
}; };
}; };
export const lookupAccount = acct => (dispatch, getState) => {
dispatch(lookupAccountRequest(acct));
api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
dispatch(lookupAccountSuccess());
}).catch(error => {
dispatch(lookupAccountFail(acct, error));
});
};
export const lookupAccountRequest = (acct) => ({
type: ACCOUNT_LOOKUP_REQUEST,
acct,
});
export const lookupAccountSuccess = () => ({
type: ACCOUNT_LOOKUP_SUCCESS,
});
export const lookupAccountFail = (acct, error) => ({
type: ACCOUNT_LOOKUP_FAIL,
acct,
error,
skipAlert: true,
});
export function fetchAccountRequest(id) { export function fetchAccountRequest(id) {
return { return {
type: ACCOUNT_FETCH_REQUEST, type: ACCOUNT_FETCH_REQUEST,

@ -10,6 +10,7 @@ import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines'; import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts'; import { showAlertForError } from './alerts';
import { showAlert } from './alerts'; import { showAlert } from './alerts';
import { openModal } from './modal';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags; let cancelFetchComposeSuggestionsAccounts, cancelFetchComposeSuggestionsTags;
@ -69,6 +70,11 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE';
export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE';
export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
const messages = defineMessages({ const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@ -78,7 +84,7 @@ const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
export const ensureComposeIsVisible = (getState, routerHistory) => { export const ensureComposeIsVisible = (getState, routerHistory) => {
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) {
routerHistory.push('/statuses/new'); routerHistory.push('/publish');
} }
}; };
@ -171,7 +177,8 @@ export function submitCompose(routerHistory) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
}, },
}).then(function (response) { }).then(function (response) {
if (routerHistory && routerHistory.location.pathname === '/statuses/new' if (routerHistory
&& (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new')
&& window.history.state && window.history.state
&& !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) { && !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
routerHistory.goBack(); routerHistory.goBack();
@ -347,6 +354,32 @@ export const uploadThumbnailFail = error => ({
skipLoading: true, skipLoading: true,
}); });
export function initMediaEditModal(id) {
return dispatch => {
dispatch({
type: INIT_MEDIA_EDIT_MODAL,
id,
});
dispatch(openModal('FOCAL_POINT', { id }));
};
};
export function onChangeMediaDescription(description) {
return {
type: COMPOSE_CHANGE_MEDIA_DESCRIPTION,
description,
};
};
export function onChangeMediaFocus(focusX, focusY) {
return {
type: COMPOSE_CHANGE_MEDIA_FOCUS,
focusX,
focusY,
};
};
export function changeUploadCompose(id, params) { export function changeUploadCompose(id, params) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(changeUploadComposeRequest()); dispatch(changeUploadComposeRequest());

@ -1,6 +1,6 @@
import api, { getLinks } from 'flavours/glitch/util/api'; import api, { getLinks } from 'flavours/glitch/util/api';
import IntlMessageFormat from 'intl-messageformat'; import IntlMessageFormat from 'intl-messageformat';
import { fetchRelationships } from './accounts'; import { fetchFollowRequests, fetchRelationships } from './accounts';
import { import {
importFetchedAccount, importFetchedAccount,
importFetchedAccounts, importFetchedAccounts,
@ -90,6 +90,10 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
filtered = regex && regex.test(searchIndex); filtered = regex && regex.test(searchIndex);
} }
if (['follow_request'].includes(notification.type)) {
dispatch(fetchFollowRequests());
}
dispatch(submitMarkers()); dispatch(submitMarkers());
if (showInColumn) { if (showInColumn) {

@ -128,7 +128,7 @@ class Account extends ImmutablePureComponent {
<Permalink <Permalink
className='account small' className='account small'
href={account.get('url')} href={account.get('url')}
to={`/accounts/${account.get('id')}`} to={`/@${account.get('acct')}`}
> >
<div className='account__avatar-wrapper'> <div className='account__avatar-wrapper'>
<Avatar <Avatar
@ -144,7 +144,7 @@ class Account extends ImmutablePureComponent {
) : ( ) : (
<div className='account'> <div className='account'>
<div className='account__wrapper'> <div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
{mute_expires_at} {mute_expires_at}
<DisplayName account={account} /> <DisplayName account={account} />

@ -0,0 +1,115 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import { FormattedNumber } from 'react-intl';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import classNames from 'classnames';
import Skeleton from 'flavours/glitch/components/skeleton';
const percIncrease = (a, b) => {
let percent;
if (b !== 0) {
if (a !== 0) {
percent = (b - a) / a;
} else {
percent = 1;
}
} else if (b === 0 && a === 0) {
percent = 0;
} else {
percent = - 1;
}
return percent;
};
export default class Counter extends React.PureComponent {
static propTypes = {
measure: PropTypes.string.isRequired,
start_at: PropTypes.string.isRequired,
end_at: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
href: PropTypes.string,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { measure, start_at, end_at } = this.props;
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { label, href } = this.props;
const { loading, data } = this.state;
let content;
if (loading) {
content = (
<React.Fragment>
<span className='sparkline__value__total'><Skeleton width={43} /></span>
<span className='sparkline__value__change'><Skeleton width={43} /></span>
</React.Fragment>
);
} else {
const measure = data[0];
const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
content = (
<React.Fragment>
<span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
</React.Fragment>
);
}
const inner = (
<React.Fragment>
<div className='sparkline__value'>
{content}
</div>
<div className='sparkline__label'>
{label}
</div>
<div className='sparkline__graph'>
{!loading && (
<Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
<SparklinesCurve />
</Sparklines>
)}
</div>
</React.Fragment>
);
if (href) {
return (
<a href={href} className='sparkline'>
{inner}
</a>
);
} else {
return (
<div className='sparkline'>
{inner}
</div>
);
}
}
}

@ -0,0 +1,92 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import { FormattedNumber } from 'react-intl';
import { roundTo10 } from 'flavours/glitch/util/numbers';
import Skeleton from 'flavours/glitch/components/skeleton';
export default class Dimension extends React.PureComponent {
static propTypes = {
dimension: PropTypes.string.isRequired,
start_at: PropTypes.string.isRequired,
end_at: PropTypes.string.isRequired,
limit: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { start_at, end_at, dimension, limit } = this.props;
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { label, limit } = this.props;
const { loading, data } = this.state;
let content;
if (loading) {
content = (
<table>
<tbody>
{Array.from(Array(limit)).map((_, i) => (
<tr className='dimension__item' key={i}>
<td className='dimension__item__key'>
<Skeleton width={100} />
</td>
<td className='dimension__item__value'>
<Skeleton width={60} />
</td>
</tr>
))}
</tbody>
</table>
);
} else {
const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
content = (
<table>
<tbody>
{data[0].data.map(item => (
<tr className='dimension__item' key={item.key}>
<td className='dimension__item__key'>
<span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
<span title={item.key}>{item.human_key}</span>
</td>
<td className='dimension__item__value'>
{typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
</td>
</tr>
))}
</tbody>
</table>
);
}
return (
<div className='dimension'>
<h4>{label}</h4>
{content}
</div>
);
}
}

@ -0,0 +1,141 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
import classNames from 'classnames';
import { roundTo10 } from 'flavours/glitch/util/numbers';
const dateForCohort = cohort => {
switch(cohort.frequency) {
case 'day':
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
default:
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
}
};
export default class Retention extends React.PureComponent {
static propTypes = {
start_at: PropTypes.string,
end_at: PropTypes.string,
frequency: PropTypes.string,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { start_at, end_at, frequency } = this.props;
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { loading, data } = this.state;
let content;
if (loading) {
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
} else {
content = (
<table className='retention__table'>
<thead>
<tr>
<th>
<div className='retention__table__date retention__table__label'>
<FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
</div>
</th>
<th>
<div className='retention__table__number retention__table__label'>
<FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
</div>
</th>
{data[0].data.slice(1).map((retention, i) => (
<th key={retention.date}>
<div className='retention__table__number retention__table__label'>
{i + 1}
</div>
</th>
))}
</tr>
<tr>
<td>
<div className='retention__table__date retention__table__average'>
<FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
</div>
</td>
<td>
<div className='retention__table__size'>
<FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
</div>
</td>
{data[0].data.slice(1).map((retention, i) => {
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].percent - sum)/(k + 1) : sum, 0);
return (
<td key={retention.date}>
<div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
<FormattedNumber value={average} style='percent' />
</div>
</td>
);
})}
</tr>
</thead>
<tbody>
{data.slice(0, -1).map(cohort => (
<tr key={cohort.period}>
<td>
<div className='retention__table__date'>
{dateForCohort(cohort)}
</div>
</td>
<td>
<div className='retention__table__size'>
<FormattedNumber value={cohort.data[0].value} />
</div>
</td>
{cohort.data.slice(1).map(retention => (
<td key={retention.date}>
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.percent * 100)}`)}>
<FormattedNumber value={retention.percent} style='percent' />
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
return (
<div className='retention'>
<h4><FormattedMessage id='admin.dashboard.retention' defaultMessage='Retention' /></h4>
{content}
</div>
);
}
}

@ -0,0 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from 'flavours/glitch/util/api';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Hashtag from 'flavours/glitch/components/hashtag';
export default class Trends extends React.PureComponent {
static propTypes = {
limit: PropTypes.number.isRequired,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { limit } = this.props;
api().get('/api/v1/admin/trends', { params: { limit } }).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { limit } = this.props;
const { loading, data } = this.state;
let content;
if (loading) {
content = (
<div>
{Array.from(Array(limit)).map((_, i) => (
<Hashtag key={i} />
))}
</div>
);
} else {
content = (
<div>
{data.map(hashtag => (
<Hashtag
key={hashtag.name}
name={hashtag.name}
href={`/admin/tags/${hashtag.id}`}
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
history={hashtag.history.reverse().map(day => day.uses)}
className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
/>
))}
</div>
);
}
return (
<div className='trends trends--compact'>
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
{content}
</div>
);
}
}

@ -2,6 +2,8 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon'; import Icon from 'flavours/glitch/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
@ -16,29 +18,13 @@ export default class AttachmentList extends ImmutablePureComponent {
render () { render () {
const { media, compact } = this.props; const { media, compact } = this.props;
if (compact) {
return (
<div className='attachment-list compact'>
<ul className='attachment-list__list'>
{media.map(attachment => {
const displayUrl = attachment.get('remote_url') || attachment.get('url');
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a>
</li>
);
})}
</ul>
</div>
);
}
return ( return (
<div className='attachment-list'> <div className={classNames('attachment-list', { compact })}>
<div className='attachment-list__icon'> {!compact && (
<Icon id='link' /> <div className='attachment-list__icon'>
</div> <Icon id='link' />
</div>
)}
<ul className='attachment-list__list'> <ul className='attachment-list__list'>
{media.map(attachment => { {media.map(attachment => {
@ -46,7 +32,11 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<li key={attachment.get('id')}> <li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a> <a href={displayUrl} target='_blank' rel='noopener noreferrer'>
{compact && <Icon id='link' />}
{compact && ' ' }
{displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />}
</a>
</li> </li>
); );
})} })}

@ -82,7 +82,7 @@ export default class AvatarComposite extends React.PureComponent {
<a <a
href={account.get('url')} href={account.get('url')}
target='_blank' target='_blank'
onClick={(e) => this.props.onAccountClick(account.get('id'), e)} onClick={(e) => this.props.onAccountClick(account.get('acct'), e)}
title={`@${account.get('acct')}`} title={`@${account.get('acct')}`}
key={account.get('id')} key={account.get('id')}
> >

@ -124,8 +124,8 @@ class ColumnHeader extends React.PureComponent {
moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <div key='move-buttons' className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
</div> </div>
); );
} else if (multiColumn && this.props.onPin) { } else if (multiColumn && this.props.onPin) {
@ -146,8 +146,8 @@ class ColumnHeader extends React.PureComponent {
]; ];
if (multiColumn) { if (multiColumn) {
collapsedContent.push(moveButtons);
collapsedContent.push(pinButton); collapsedContent.push(pinButton);
collapsedContent.push(moveButtons);
} }
if (children || (multiColumn && this.props.onPin)) { if (children || (multiColumn && this.props.onPin)) {

@ -61,7 +61,7 @@ export default class DisplayName extends React.PureComponent {
<a <a
href={a.get('url')} href={a.get('url')}
target='_blank' target='_blank'
onClick={(e) => onAccountClick(a.get('id'), e)} onClick={(e) => onAccountClick(a.get('acct'), e)}
title={`@${a.get('acct')}`} title={`@${a.get('acct')}`}
rel='noopener noreferrer' rel='noopener noreferrer'
> >
@ -76,7 +76,7 @@ export default class DisplayName extends React.PureComponent {
} }
suffix = ( suffix = (
<a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)} rel='noopener noreferrer'> <a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('acct'), e)} rel='noopener noreferrer'>
<span className='display-name__account'>@{acct}</span> <span className='display-name__account'>@{acct}</span>
</a> </a>
); );

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { source_url } from 'flavours/glitch/util/initial_state';
import { preferencesLink } from 'flavours/glitch/util/backend_links'; import { preferencesLink } from 'flavours/glitch/util/backend_links';
import StackTrace from 'stacktrace-js'; import StackTrace from 'stacktrace-js';
@ -64,6 +65,11 @@ export default class ErrorBoundary extends React.PureComponent {
debugInfo += 'React component stack\n---------------------\n\n```\n' + componentStack.toString() + '\n```'; debugInfo += 'React component stack\n---------------------\n\n```\n' + componentStack.toString() + '\n```';
} }
let issueTracker = source_url;
if (source_url.match(/^https:\/\/github\.com\/[^/]+\/[^/]+\/?$/)) {
issueTracker = source_url + '/issues';
}
return ( return (
<div tabIndex='-1'> <div tabIndex='-1'>
<div className='error-boundary'> <div className='error-boundary'>
@ -84,7 +90,7 @@ export default class ErrorBoundary extends React.PureComponent {
<FormattedMessage <FormattedMessage
id='web_app_crash.report_issue' id='web_app_crash.report_issue'
defaultMessage='Report a bug in the {issuetracker}' defaultMessage='Report a bug in the {issuetracker}'
values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener noreferrer' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }} values={{ issuetracker: <a href={issueTracker} rel='noopener noreferrer' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }}
/> />
{ debugInfo !== '' && ( { debugInfo !== '' && (
<details> <details>

@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from './permalink'; import Permalink from './permalink';
import ShortNumber from 'flavours/glitch/components/short_number'; import ShortNumber from 'flavours/glitch/components/short_number';
import Skeleton from 'flavours/glitch/components/skeleton';
import classNames from 'classnames';
class SilentErrorBoundary extends React.Component { class SilentErrorBoundary extends React.Component {
@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => (
/> />
); );
const Hashtag = ({ hashtag }) => ( export const ImmutableHashtag = ({ hashtag }) => (
<div className='trends__item'> <Hashtag
name={hashtag.get('name')}
href={hashtag.get('url')}
to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
);
ImmutableHashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired,
};
const Hashtag = ({ name, href, to, people, uses, history, className }) => (
<div className={classNames('trends__item', className)}>
<div className='trends__item__name'> <div className='trends__item__name'>
<Permalink <Permalink href={href} to={to}>
href={hashtag.get('url')} {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
to={`/timelines/tag/${hashtag.get('name')}`}
>
#<span>{hashtag.get('name')}</span>
</Permalink> </Permalink>
<ShortNumber {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
value={
hashtag.getIn(['history', 0, 'accounts']) * 1 +
hashtag.getIn(['history', 1, 'accounts']) * 1
}
renderer={accountsCountRenderer}
/>
</div> </div>
<div className='trends__item__current'> <div className='trends__item__current'>
<ShortNumber {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
value={
hashtag.getIn(['history', 0, 'uses']) * 1 +
hashtag.getIn(['history', 1, 'uses']) * 1
}
/>
</div> </div>
<div className='trends__item__sparkline'> <div className='trends__item__sparkline'>
<SilentErrorBoundary> <SilentErrorBoundary>
<Sparklines <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
width={50}
height={28}
data={hashtag
.get('history')
.reverse()
.map((day) => day.get('uses'))
.toArray()}
>
<SparklinesCurve style={{ fill: 'none' }} /> <SparklinesCurve style={{ fill: 'none' }} />
</Sparklines> </Sparklines>
</SilentErrorBoundary> </SilentErrorBoundary>
@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => (
); );
Hashtag.propTypes = { Hashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired, name: PropTypes.string,
href: PropTypes.string,
to: PropTypes.string,
people: PropTypes.number,
uses: PropTypes.number,
history: PropTypes.arrayOf(PropTypes.number),
className: PropTypes.string,
}; };
export default Hashtag; export default Hashtag;

@ -76,10 +76,13 @@ export default class ModalRoot extends React.PureComponent {
this.activeElement = null; this.activeElement = null;
}).catch(console.error); }).catch(console.error);
this.handleModalClose(); this._handleModalClose();
} }
if (this.props.children && !prevProps.children) { if (this.props.children && !prevProps.children) {
this.handleModalOpen(); this._handleModalOpen();
}
if (this.props.children) {
this._ensureHistoryBuffer();
} }
} }
@ -88,22 +91,29 @@ export default class ModalRoot extends React.PureComponent {
window.removeEventListener('keydown', this.handleKeyDown); window.removeEventListener('keydown', this.handleKeyDown);
} }
handleModalClose () { _handleModalOpen () {
this._modalHistoryKey = Date.now();
this.unlistenHistory = this.history.listen((_, action) => {
if (action === 'POP') {
this.props.onClose();
}
});
}
_handleModalClose () {
this.unlistenHistory(); this.unlistenHistory();
const state = this.history.location.state; const { state } = this.history.location;
if (state && state.mastodonModalOpen) { if (state && state.mastodonModalKey === this._modalHistoryKey) {
this.history.goBack(); this.history.goBack();
} }
} }
handleModalOpen () { _ensureHistoryBuffer () {
const history = this.history; const { pathname, state } = this.history.location;
const state = {...history.location.state, mastodonModalOpen: true}; if (!state || state.mastodonModalKey !== this._modalHistoryKey) {
history.push(history.location.pathname, state); this.history.push(pathname, { ...state, mastodonModalKey: this._modalHistoryKey });
this.unlistenHistory = history.listen(() => { }
this.props.onClose();
});
} }
getSiblings = () => { getSiblings = () => {

@ -12,8 +12,18 @@ import RelativeTimestamp from './relative_timestamp';
import Icon from 'flavours/glitch/components/icon'; import Icon from 'flavours/glitch/components/icon';
const messages = defineMessages({ const messages = defineMessages({
closed: { id: 'poll.closed', defaultMessage: 'Closed' }, closed: {
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' }, id: 'poll.closed',
defaultMessage: 'Closed',
},
voted: {
id: 'poll.voted',
defaultMessage: 'You voted for this answer',
},
votes: {
id: 'poll.votes',
defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
},
}); });
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
@ -148,9 +158,16 @@ class Poll extends ImmutablePureComponent {
data-index={optionIndex} data-index={optionIndex}
/> />
)} )}
{showResults && <span className='poll__number'> {showResults && (
{Math.round(percent)}% <span
</span>} className='poll__number'
title={intl.formatMessage(messages.votes, {
votes: option.get('votes_count'),
})}
>
{Math.round(percent)}%
</span>
)}
<span <span
className='poll__option__text translate' className='poll__option__text translate'

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4'; import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
import LoadMore from './load_more'; import LoadMore from './load_more';
@ -34,7 +34,6 @@ class ScrollableList extends PureComponent {
onScrollToTop: PropTypes.func, onScrollToTop: PropTypes.func,
onScroll: PropTypes.func, onScroll: PropTypes.func,
trackScroll: PropTypes.bool, trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
showLoading: PropTypes.bool, showLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
@ -264,11 +263,6 @@ class ScrollableList extends PureComponent {
this.props.onLoadMore(); this.props.onLoadMore();
} }
defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false;
return !(location.state && location.state.mastodonModalOpen);
}
handleLoadPending = e => { handleLoadPending = e => {
e.preventDefault(); e.preventDefault();
this.props.onLoadPending(); this.props.onLoadPending();
@ -282,7 +276,7 @@ class ScrollableList extends PureComponent {
} }
render () { render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props; const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
const childrenCount = React.Children.count(children); const childrenCount = React.Children.count(children);
@ -348,7 +342,7 @@ class ScrollableList extends PureComponent {
if (trackScroll) { if (trackScroll) {
return ( return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll || this.defaultShouldUpdateScroll}> <ScrollContainer scrollKey={scrollKey}>
{scrollableArea} {scrollableArea}
</ScrollContainer> </ScrollContainer>
); );

@ -0,0 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>&zwnj;</span>;
Skeleton.propTypes = {
width: PropTypes.number,
height: PropTypes.number,
};
export default Skeleton;

@ -346,7 +346,9 @@ class Status extends ImmutablePureComponent {
return; return;
} else { } else {
if (destination === undefined) { if (destination === undefined) {
destination = `/statuses/${ destination = `/@${
status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct']))
}/${
status.getIn(['reblog', 'id'], status.get('id')) status.getIn(['reblog', 'id'], status.get('id'))
}`; }`;
} }
@ -362,16 +364,6 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia }); this.setState({ showMedia: !this.state.showMedia });
} }
handleAccountClick = (e) => {
if (this.context.router && e.button === 0) {
const id = e.currentTarget.getAttribute('data-id');
e.preventDefault();
let state = {...this.context.router.history.location.state};
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/accounts/${id}`, state);
}
}
handleExpandedToggle = () => { handleExpandedToggle = () => {
if (this.props.status.get('spoiler_text')) { if (this.props.status.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded); this.setExpansion(!this.state.isExpanded);
@ -433,13 +425,14 @@ class Status extends ImmutablePureComponent {
handleHotkeyOpen = () => { handleHotkeyOpen = () => {
let state = {...this.context.router.history.location.state}; let state = {...this.context.router.history.location.state};
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state); const status = this.props.status;
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state);
} }
handleHotkeyOpenProfile = () => { handleHotkeyOpenProfile = () => {
let state = {...this.context.router.history.location.state}; let state = {...this.context.router.history.location.state};
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`, state); this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
} }
handleHotkeyMoveUp = e => { handleHotkeyMoveUp = e => {
@ -516,8 +509,8 @@ class Status extends ImmutablePureComponent {
const { isExpanded, isCollapsed, forceFilter } = this.state; const { isExpanded, isCollapsed, forceFilter } = this.state;
let background = null; let background = null;
let attachments = null; let attachments = null;
let media = null; let media = [];
let mediaIcon = null; let mediaIcons = [];
if (status === null) { if (status === null) {
return null; return null;
@ -543,9 +536,8 @@ class Status extends ImmutablePureComponent {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div ref={this.handleRef} className='status focusable' tabIndex='0'> <div ref={this.handleRef} className='status focusable' tabIndex='0'>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} <span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
{' '} <span>{status.get('content')}</span>
{status.get('content')}
</div> </div>
</HotKeys> </HotKeys>
); );
@ -587,25 +579,27 @@ class Status extends ImmutablePureComponent {
// After we have generated our appropriate media element and stored it in // After we have generated our appropriate media element and stored it in
// `media`, we snatch the thumbnail to use as our `background` if media // `media`, we snatch the thumbnail to use as our `background` if media
// backgrounds for collapsed statuses are enabled. // backgrounds for collapsed statuses are enabled.
attachments = status.get('media_attachments'); attachments = status.get('media_attachments');
if (status.get('poll')) { if (status.get('poll')) {
media = <PollContainer pollId={status.get('poll')} />; media.push(<PollContainer pollId={status.get('poll')} />);
mediaIcon = 'tasks'; mediaIcons.push('tasks');
} else if (usingPiP) { }
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; if (usingPiP) {
mediaIcon = 'video-camera'; media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />);
mediaIcons.push('video-camera');
} else if (attachments.size > 0) { } else if (attachments.size > 0) {
if (muted || attachments.some(item => item.get('type') === 'unknown')) { if (muted || attachments.some(item => item.get('type') === 'unknown')) {
media = ( media.push(
<AttachmentList <AttachmentList
compact compact
media={status.get('media_attachments')} media={status.get('media_attachments')}
/> />,
); );
} else if (attachments.getIn([0, 'type']) === 'audio') { } else if (attachments.getIn([0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
media = ( media.push(
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => ( {Component => (
<Component <Component
@ -622,13 +616,13 @@ class Status extends ImmutablePureComponent {
deployPictureInPicture={this.handleDeployPictureInPicture} deployPictureInPicture={this.handleDeployPictureInPicture}
/> />
)} )}
</Bundle> </Bundle>,
); );
mediaIcon = 'music'; mediaIcons.push('music');
} else if (attachments.getIn([0, 'type']) === 'video') { } else if (attachments.getIn([0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
media = ( media.push(
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (<Component {Component => (<Component
preview={attachment.get('preview_url')} preview={attachment.get('preview_url')}
@ -648,11 +642,11 @@ class Status extends ImmutablePureComponent {
visible={this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/>)} />)}
</Bundle> </Bundle>,
); );
mediaIcon = 'video-camera'; mediaIcons.push('video-camera');
} else { // Media type is 'image' or 'gifv' } else { // Media type is 'image' or 'gifv'
media = ( media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => ( {Component => (
<Component <Component
@ -668,16 +662,16 @@ class Status extends ImmutablePureComponent {
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
/> />
)} )}
</Bundle> </Bundle>,
); );
mediaIcon = 'picture-o'; mediaIcons.push('picture-o');
} }
if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) { if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) {
background = attachments.getIn([0, 'preview_url']); background = attachments.getIn([0, 'preview_url']);
} }
} else if (status.get('card') && settings.get('inline_preview_cards')) { } else if (status.get('card') && settings.get('inline_preview_cards')) {
media = ( media.push(
<Card <Card
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
card={status.get('card')} card={status.get('card')}
@ -685,9 +679,9 @@ class Status extends ImmutablePureComponent {
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
/> />,
); );
mediaIcon = 'link'; mediaIcons.push('link');
} }
// Here we prepare extra data-* attributes for CSS selectors. // Here we prepare extra data-* attributes for CSS selectors.
@ -754,7 +748,7 @@ class Status extends ImmutablePureComponent {
</span> </span>
<StatusIcons <StatusIcons
status={status} status={status}
mediaIcon={mediaIcon} mediaIcons={mediaIcons}
collapsible={settings.getIn(['collapsed', 'enabled'])} collapsible={settings.getIn(['collapsed', 'enabled'])}
collapsed={isCollapsed} collapsed={isCollapsed}
setCollapsed={setCollapsed} setCollapsed={setCollapsed}
@ -764,7 +758,7 @@ class Status extends ImmutablePureComponent {
<StatusContent <StatusContent
status={status} status={status}
media={media} media={media}
mediaIcon={mediaIcon} mediaIcons={mediaIcons}
expanded={isExpanded} expanded={isExpanded}
onExpandedToggle={this.handleExpandedToggle} onExpandedToggle={this.handleExpandedToggle}
parseClick={parseClick} parseClick={parseClick}

@ -147,11 +147,11 @@ class StatusActionBar extends ImmutablePureComponent {
handleOpen = () => { handleOpen = () => {
let state = {...this.context.router.history.location.state}; let state = {...this.context.router.history.location.state};
if (state.mastodonModalOpen) { if (state.mastodonModalKey) {
this.context.router.history.replace(`/statuses/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 }); this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 });
} else { } else {
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state); this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, state);
} }
} }

@ -69,8 +69,8 @@ export default class StatusContent extends React.PureComponent {
expanded: PropTypes.bool, expanded: PropTypes.bool,
collapsed: PropTypes.bool, collapsed: PropTypes.bool,
onExpandedToggle: PropTypes.func, onExpandedToggle: PropTypes.func,
media: PropTypes.element, media: PropTypes.node,
mediaIcon: PropTypes.string, mediaIcons: PropTypes.arrayOf(PropTypes.string),
parseClick: PropTypes.func, parseClick: PropTypes.func,
disabled: PropTypes.bool, disabled: PropTypes.bool,
onUpdate: PropTypes.func, onUpdate: PropTypes.func,
@ -197,7 +197,7 @@ export default class StatusContent extends React.PureComponent {
onMentionClick = (mention, e) => { onMentionClick = (mention, e) => {
if (this.props.parseClick) { if (this.props.parseClick) {
this.props.parseClick(e, `/accounts/${mention.get('id')}`); this.props.parseClick(e, `/@${mention.get('acct')}`);
} }
} }
@ -205,7 +205,7 @@ export default class StatusContent extends React.PureComponent {
hashtag = hashtag.replace(/^#/, ''); hashtag = hashtag.replace(/^#/, '');
if (this.props.parseClick) { if (this.props.parseClick) {
this.props.parseClick(e, `/timelines/tag/${hashtag}`); this.props.parseClick(e, `/tags/${hashtag}`);
} }
} }
@ -256,7 +256,7 @@ export default class StatusContent extends React.PureComponent {
const { const {
status, status,
media, media,
mediaIcon, mediaIcons,
parseClick, parseClick,
disabled, disabled,
tagLinks, tagLinks,
@ -277,7 +277,7 @@ export default class StatusContent extends React.PureComponent {
const mentionLinks = status.get('mentions').map(item => ( const mentionLinks = status.get('mentions').map(item => (
<Permalink <Permalink
to={`/accounts/${item.get('id')}`} to={`/@${item.get('acct')}`}
href={item.get('url')} href={item.get('url')}
key={item.get('id')} key={item.get('id')}
className='mention' className='mention'
@ -286,28 +286,37 @@ export default class StatusContent extends React.PureComponent {
</Permalink> </Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []); )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
const toggleText = hidden ? [ let toggleText = null;
<FormattedMessage if (hidden) {
id='status.show_more' toggleText = [
defaultMessage='Show more' <FormattedMessage
key='0' id='status.show_more'
/>, defaultMessage='Show more'
mediaIcon ? ( key='0'
<Icon />,
fixedWidth ];
className='status__content__spoiler-icon' if (mediaIcons) {
id={mediaIcon} mediaIcons.forEach((mediaIcon, idx) => {
aria-hidden='true' toggleText.push(
key='1' <Icon
fixedWidth
className='status__content__spoiler-icon'
id={mediaIcon}
aria-hidden='true'
key={`icon-${idx}`}
/>,
);
});
}
} else {
toggleText = (
<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
/> />
) : null, );
] : [ }
<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
/>,
];
if (hidden) { if (hidden) {
mentionsPlaceholder = <div>{mentionLinks}</div>; mentionsPlaceholder = <div>{mentionLinks}</div>;

@ -19,14 +19,14 @@ export default class StatusHeader extends React.PureComponent {
}; };
// Handles clicks on account name/image // Handles clicks on account name/image
handleClick = (id, e) => { handleClick = (acct, e) => {
const { parseClick } = this.props; const { parseClick } = this.props;
parseClick(e, `/accounts/${id}`); parseClick(e, `/@${acct}`);
} }
handleAccountClick = (e) => { handleAccountClick = (e) => {
const { status } = this.props; const { status } = this.props;
this.handleClick(status.getIn(['account', 'id']), e); this.handleClick(status.getIn(['account', 'acct']), e);
} }
// Rendering. // Rendering.

@ -27,7 +27,7 @@ class StatusIcons extends React.PureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
mediaIcon: PropTypes.string, mediaIcons: PropTypes.arrayOf(PropTypes.string),
collapsible: PropTypes.bool, collapsible: PropTypes.bool,
collapsed: PropTypes.bool, collapsed: PropTypes.bool,
directMessage: PropTypes.bool, directMessage: PropTypes.bool,
@ -44,8 +44,8 @@ class StatusIcons extends React.PureComponent {
} }
} }
mediaIconTitleText () { mediaIconTitleText (mediaIcon) {
const { intl, mediaIcon } = this.props; const { intl } = this.props;
switch (mediaIcon) { switch (mediaIcon) {
case 'link': case 'link':
@ -61,11 +61,24 @@ class StatusIcons extends React.PureComponent {
} }
} }
renderIcon (mediaIcon) {
return (
<Icon
fixedWidth
className='status__media-icon'
key={`media-icon--${mediaIcon}`}
id={mediaIcon}
aria-hidden='true'
title={this.mediaIconTitleText(mediaIcon)}
/>
);
}
// Rendering. // Rendering.
render () { render () {
const { const {
status, status,
mediaIcon, mediaIcons,
collapsible, collapsible,
collapsed, collapsed,
directMessage, directMessage,
@ -90,15 +103,7 @@ class StatusIcons extends React.PureComponent {
aria-hidden='true' aria-hidden='true'
title={intl.formatMessage(messages.localOnly)} title={intl.formatMessage(messages.localOnly)}
/>} />}
{mediaIcon ? ( { !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon)) }
<Icon
fixedWidth
className='status__media-icon'
id={mediaIcon}
aria-hidden='true'
title={this.mediaIconTitleText()}
/>
) : null}
{!directMessage && <VisibilityIcon visibility={status.get('visibility')} />} {!directMessage && <VisibilityIcon visibility={status.get('visibility')} />}
{collapsible ? ( {collapsible ? (
<IconButton <IconButton

@ -18,7 +18,6 @@ export default class StatusList extends ImmutablePureComponent {
onScrollToTop: PropTypes.func, onScrollToTop: PropTypes.func,
onScroll: PropTypes.func, onScroll: PropTypes.func,
trackScroll: PropTypes.bool, trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
isPartial: PropTypes.bool, isPartial: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,

@ -17,7 +17,7 @@ export default class StatusPrepend extends React.PureComponent {
handleClick = (e) => { handleClick = (e) => {
const { account, parseClick } = this.props; const { account, parseClick } = this.props;
parseClick(e, `/accounts/${account.get('id')}`); parseClick(e, `/@${account.get('acct')}`);
} }
Message = () => { Message = () => {

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from 'mastodon/locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class AdminComponent extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
render () {
const { locale, children } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
{children}
</IntlProvider>
);
}
}

@ -23,14 +23,38 @@ store.dispatch(hydrateAction);
// load custom emojis // load custom emojis
store.dispatch(fetchCustomEmojis()); store.dispatch(fetchCustomEmojis());
const createIdentityContext = state => ({
signedIn: !!state.meta.me,
accountId: state.meta.me,
accessToken: state.meta.access_token,
});
export default class Mastodon extends React.PureComponent { export default class Mastodon extends React.PureComponent {
static propTypes = { static propTypes = {
locale: PropTypes.string.isRequired, locale: PropTypes.string.isRequired,
}; };
static childContextTypes = {
identity: PropTypes.shape({
signedIn: PropTypes.bool.isRequired,
accountId: PropTypes.string,
accessToken: PropTypes.string,
}).isRequired,
};
identity = createIdentityContext(initialState);
getChildContext() {
return {
identity: this.identity,
};
}
componentDidMount() { componentDidMount() {
this.disconnect = store.dispatch(connectUserStream()); if (this.identity.signedIn) {
this.disconnect = store.dispatch(connectUserStream());
}
} }
componentWillUnmount () { componentWillUnmount () {
@ -41,7 +65,7 @@ export default class Mastodon extends React.PureComponent {
} }
shouldUpdateScroll (_, { location }) { shouldUpdateScroll (_, { location }) {
return !(location.state && location.state.mastodonModalOpen); return !(location.state?.mastodonModalKey);
} }
render () { render () {

@ -7,7 +7,7 @@ import { getLocale } from 'mastodon/locales';
import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar'; import { getScrollbarWidth } from 'flavours/glitch/util/scrollbar';
import MediaGallery from 'flavours/glitch/components/media_gallery'; import MediaGallery from 'flavours/glitch/components/media_gallery';
import Poll from 'flavours/glitch/components/poll'; import Poll from 'flavours/glitch/components/poll';
import Hashtag from 'flavours/glitch/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import ModalRoot from 'flavours/glitch/components/modal_root'; import ModalRoot from 'flavours/glitch/components/modal_root';
import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
import Video from 'flavours/glitch/features/video'; import Video from 'flavours/glitch/features/video';

@ -0,0 +1,18 @@
import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4';
// ScrollContainer is used to automatically scroll to the top when pushing a
// new history state and remembering the scroll position when going back.
// There are a few things we need to do differently, though.
const defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
// If the change is caused by opening a modal, do not scroll to top
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
};
export default
class ScrollContainer extends OriginalScrollContainer {
static defaultProps = {
shouldUpdateScroll: defaultShouldUpdateScroll,
};
}

@ -62,17 +62,17 @@ class ActionBar extends React.PureComponent {
<div className='account__action-bar'> <div className='account__action-bar'>
<div className='account__action-bar-links'> <div className='account__action-bar-links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}`}>
<FormattedMessage id='account.posts' defaultMessage='Posts' /> <FormattedMessage id='account.posts' defaultMessage='Posts' />
<strong><FormattedNumber value={account.get('statuses_count')} /></strong> <strong><FormattedNumber value={account.get('statuses_count')} /></strong>
</NavLink> </NavLink>
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/following`}>
<FormattedMessage id='account.follows' defaultMessage='Follows' /> <FormattedMessage id='account.follows' defaultMessage='Follows' />
<strong><FormattedNumber value={account.get('following_count')} /></strong> <strong><FormattedNumber value={account.get('following_count')} /></strong>
</NavLink> </NavLink>
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/followers`}>
<FormattedMessage id='account.followers' defaultMessage='Followers' /> <FormattedMessage id='account.followers' defaultMessage='Followers' />
<strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong> <strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong>
</NavLink> </NavLink>

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fetchAccount } from 'flavours/glitch/actions/accounts'; import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import Column from 'flavours/glitch/features/ui/components/column'; import Column from 'flavours/glitch/features/ui/components/column';
@ -11,18 +11,29 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from 'flavours/glitch/selectors'; import { getAccountGallery } from 'flavours/glitch/selectors';
import MediaItem from './components/media_item'; import MediaItem from './components/media_item';
import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
import { ScrollContainer } from 'react-router-scroll-4'; import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import LoadMore from 'flavours/glitch/components/load_more'; import LoadMore from 'flavours/glitch/components/load_more';
import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, { params: { acct, id } }) => {
isAccount: !!state.getIn(['accounts', props.params.accountId]), const accountId = id || state.getIn(['accounts_map', acct]);
attachments: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), if (!accountId) {
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), return {
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false), isLoading: true,
}); };
}
return {
accountId,
isAccount: !!state.getIn(['accounts', accountId]),
attachments: getAccountGallery(state, accountId),
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
};
};
class LoadMoreMedia extends ImmutablePureComponent { class LoadMoreMedia extends ImmutablePureComponent {
@ -50,7 +61,11 @@ export default @connect(mapStateToProps)
class AccountGallery extends ImmutablePureComponent { class AccountGallery extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
attachments: ImmutablePropTypes.list.isRequired, attachments: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
@ -64,15 +79,30 @@ class AccountGallery extends ImmutablePureComponent {
width: 323, width: 323,
}; };
_load () {
const { accountId, isAccount, dispatch } = this.props;
if (!isAccount) dispatch(fetchAccount(accountId));
dispatch(expandAccountMediaTimeline(accountId));
}
componentDidMount () { componentDidMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId)); const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
}
} }
componentWillReceiveProps (nextProps) { componentDidUpdate (prevProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
} }
} }
@ -96,7 +126,7 @@ class AccountGallery extends ImmutablePureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
}; };
handleLoadOlder = e => { handleLoadOlder = e => {
@ -104,11 +134,6 @@ class AccountGallery extends ImmutablePureComponent {
this.handleScrollToBottom(); this.handleScrollToBottom();
} }
shouldUpdateScroll = (prevRouterProps, { location }) => {
if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false;
return !(location.state && location.state.mastodonModalOpen);
}
setColumnRef = c => { setColumnRef = c => {
this.column = c; this.column = c;
} }
@ -165,9 +190,9 @@ class AccountGallery extends ImmutablePureComponent {
<Column ref={this.setColumnRef}> <Column ref={this.setColumnRef}>
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
<ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}> <ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} /> <HeaderContainer accountId={this.props.accountId} />
{suspended ? ( {suspended ? (
<div className='empty-column-indicator'> <div className='empty-column-indicator'>

@ -128,9 +128,9 @@ export default class Header extends ImmutablePureComponent {
{!hideTabs && ( {!hideTabs && (
<div className='account__section-headline'> <div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink> <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink> <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>
<NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div> </div>
)} )}
</div> </div>

@ -23,7 +23,7 @@ export default class MovedNote extends ImmutablePureComponent {
e.preventDefault(); e.preventDefault();
let state = {...this.context.router.history.location.state}; let state = {...this.context.router.history.location.state};
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/accounts/${this.props.to.get('id')}`, state); this.context.router.history.push(`/@${this.props.to.get('acct')}`, state);
} }
e.stopPropagation(); e.stopPropagation();

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fetchAccount } from 'flavours/glitch/actions/accounts'; import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines'; import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -19,10 +19,19 @@ import TimelineHint from 'flavours/glitch/components/timeline_hint';
const emptyList = ImmutableList(); const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) => {
const accountId = id || state.getIn(['accounts_map', acct]);
if (!accountId) {
return {
isLoading: true,
};
}
const path = withReplies ? `${accountId}:with_replies` : accountId; const path = withReplies ? `${accountId}:with_replies` : accountId;
return { return {
accountId,
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']), remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]), isAccount: !!state.getIn(['accounts', accountId]),
@ -46,7 +55,11 @@ export default @connect(mapStateToProps)
class AccountTimeline extends ImmutablePureComponent { class AccountTimeline extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list, statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list, featuredStatusIds: ImmutablePropTypes.list,
@ -60,25 +73,47 @@ class AccountTimeline extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { _load () {
const { params: { accountId }, withReplies } = this.props; const { accountId, withReplies, dispatch } = this.props;
this.props.dispatch(fetchAccount(accountId)); dispatch(fetchAccount(accountId));
this.props.dispatch(fetchAccountIdentityProofs(accountId)); dispatch(fetchAccountIdentityProofs(accountId));
if (!withReplies) { if (!withReplies) {
this.props.dispatch(expandAccountFeaturedTimeline(accountId)); dispatch(expandAccountFeaturedTimeline(accountId));
}
dispatch(expandAccountTimeline(accountId, { withReplies }));
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
}
}
componentDidUpdate (prevProps) {
const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
} }
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
const { dispatch } = this.props;
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) {
this.props.dispatch(fetchAccount(nextProps.params.accountId)); dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); dispatch(fetchAccountIdentityProofs(nextProps.params.accountId));
if (!nextProps.withReplies) { if (!nextProps.withReplies) {
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
} }
this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies }));
} }
} }
@ -87,7 +122,7 @@ class AccountTimeline extends ImmutablePureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
} }
setRef = c => { setRef = c => {
@ -131,7 +166,7 @@ class AccountTimeline extends ImmutablePureComponent {
<ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} /> <ProfileColumnHeader onClick={this.handleHeaderClick} multiColumn={multiColumn} />
<StatusList <StatusList
prepend={<HeaderContainer accountId={this.props.params.accountId} />} prepend={<HeaderContainer accountId={this.props.accountId} />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
scrollKey='account_timeline' scrollKey='account_timeline'

@ -87,7 +87,7 @@ class Header extends ImmutablePureComponent {
<Link <Link
aria-label={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}
title={intl.formatMessage(messages.home_timeline)} title={intl.formatMessage(messages.home_timeline)}
to='/timelines/home' to='/home'
><Icon id='home' /></Link> ><Icon id='home' /></Link>
))} ))}
{renderForColumn('NOTIFICATIONS', ( {renderForColumn('NOTIFICATIONS', (
@ -106,14 +106,14 @@ class Header extends ImmutablePureComponent {
<Link <Link
aria-label={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}
title={intl.formatMessage(messages.community)} title={intl.formatMessage(messages.community)}
to='/timelines/public/local' to='/public/local'
><Icon id='users' /></Link> ><Icon id='users' /></Link>
))} ))}
{renderForColumn('PUBLIC', ( {renderForColumn('PUBLIC', (
<Link <Link
aria-label={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}
title={intl.formatMessage(messages.public)} title={intl.formatMessage(messages.public)}
to='/timelines/public' to='/public'
><Icon id='globe' /></Link> ><Icon id='globe' /></Link>
))} ))}
<a <a

@ -15,13 +15,13 @@ export default class NavigationBar extends ImmutablePureComponent {
render () { render () {
return ( return (
<div className='drawer--account'> <div className='drawer--account'>
<Permalink className='avatar' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> <Permalink className='avatar' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
<Avatar account={this.props.account} size={48} /> <Avatar account={this.props.account} size={48} />
</Permalink> </Permalink>
<div className='navigation-bar__profile'> <div className='navigation-bar__profile'>
<Permalink className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> <Permalink className='acct' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
<strong>@{this.props.account.get('acct')}</strong> <strong>@{this.props.account.get('acct')}</strong>
</Permalink> </Permalink>

@ -5,7 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import AccountContainer from 'flavours/glitch/containers/account_container'; import AccountContainer from 'flavours/glitch/containers/account_container';
import StatusContainer from 'flavours/glitch/containers/status_container'; import StatusContainer from 'flavours/glitch/containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from 'flavours/glitch/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import Icon from 'flavours/glitch/components/icon'; import Icon from 'flavours/glitch/components/icon';
import { searchEnabled } from 'flavours/glitch/util/initial_state'; import { searchEnabled } from 'flavours/glitch/util/initial_state';
import LoadMore from 'flavours/glitch/components/load_more'; import LoadMore from 'flavours/glitch/components/load_more';

@ -27,6 +27,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage), message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm), confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(), onConfirm: () => logOut(),
})); }));
}, },

@ -1,7 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Upload from '../components/upload'; import Upload from '../components/upload';
import { undoUploadCompose } from 'flavours/glitch/actions/compose'; import { undoUploadCompose, initMediaEditModal } from 'flavours/glitch/actions/compose';
import { openModal } from 'flavours/glitch/actions/modal';
import { submitCompose } from 'flavours/glitch/actions/compose'; import { submitCompose } from 'flavours/glitch/actions/compose';
const mapStateToProps = (state, { id }) => ({ const mapStateToProps = (state, { id }) => ({
@ -15,7 +14,7 @@ const mapDispatchToProps = dispatch => ({
}, },
onOpenFocalPoint: id => { onOpenFocalPoint: id => {
dispatch(openModal('FOCAL_POINT', { id })); dispatch(initMediaEditModal(id));
}, },
onSubmit (router) { onSubmit (router) {

@ -104,7 +104,7 @@ class Conversation extends ImmutablePureComponent {
markRead(); markRead();
} }
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
} }
handleMarkAsRead = () => { handleMarkAsRead = () => {
@ -163,7 +163,7 @@ class Conversation extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]); const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
const handlers = { const handlers = {
reply: this.handleReply, reply: this.handleReply,

@ -213,7 +213,7 @@ class AccountCard extends ImmutablePureComponent {
<Permalink <Permalink
className='directory__card__bar__name' className='directory__card__bar__name'
href={account.get('url')} href={account.get('url')}
to={`/accounts/${account.get('id')}`} to={`/@${account.get('acct')}`}
> >
<Avatar account={account} size={48} /> <Avatar account={account} size={48} />
<DisplayName account={account} /> <DisplayName account={account} />

@ -12,7 +12,7 @@ import AccountCard from './components/account_card';
import RadioButton from 'flavours/glitch/components/radio_button'; import RadioButton from 'flavours/glitch/components/radio_button';
import classNames from 'classnames'; import classNames from 'classnames';
import LoadMore from 'flavours/glitch/components/load_more'; import LoadMore from 'flavours/glitch/components/load_more';
import { ScrollContainer } from 'react-router-scroll-4'; import ScrollContainer from 'flavours/glitch/containers/scroll_container';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@ -40,7 +40,6 @@ class Directory extends React.PureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list.isRequired, accountIds: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string, columnId: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -125,7 +124,7 @@ class Directory extends React.PureComponent {
} }
render () { render () {
const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props; const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props;
const { order, local } = this.getParams(this.props, this.state); const { order, local } = this.getParams(this.props, this.state);
const pinned = !!columnId; const pinned = !!columnId;
@ -163,7 +162,7 @@ class Directory extends React.PureComponent {
multiColumn={multiColumn} multiColumn={multiColumn}
/> />
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea} {multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
</Column> </Column>
); );
} }

@ -66,7 +66,7 @@ class Account extends ImmutablePureComponent {
return ( return (
<div className='account follow-recommendations-account'> <div className='account follow-recommendations-account'>
<div className='account__wrapper'> <div className='account__wrapper'>
<Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}> <Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} /> <DisplayName account={account} />

@ -68,7 +68,7 @@ class FollowRecommendations extends ImmutablePureComponent {
} }
})); }));
router.history.push('/timelines/home'); router.history.push('/home');
} }
render () { render () {

@ -30,7 +30,7 @@ class AccountAuthorize extends ImmutablePureComponent {
return ( return (
<div className='account-authorize__wrapper'> <div className='account-authorize__wrapper'>
<div className='account-authorize'> <div className='account-authorize'>
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'> <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div> <div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
<DisplayName account={account} /> <DisplayName account={account} />
</Permalink> </Permalink>

@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import { import {
lookupAccount,
fetchAccount, fetchAccount,
fetchFollowers, fetchFollowers,
expandFollowers, expandFollowers,
@ -19,14 +20,25 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import ScrollableList from 'flavours/glitch/components/scrollable_list'; import ScrollableList from 'flavours/glitch/components/scrollable_list';
import TimelineHint from 'flavours/glitch/components/timeline_hint'; import TimelineHint from 'flavours/glitch/components/timeline_hint';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, { params: { acct, id } }) => {
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])), const accountId = id || state.getIn(['accounts_map', acct]);
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
isAccount: !!state.getIn(['accounts', props.params.accountId]), if (!accountId) {
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), return {
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), isLoading: true,
isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true), };
}); }
return {
accountId,
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
};
};
const RemoteHint = ({ url }) => ( const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} /> <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
@ -40,7 +52,11 @@ export default @connect(mapStateToProps)
class Followers extends ImmutablePureComponent { class Followers extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
@ -51,32 +67,45 @@ class Followers extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { _load () {
if (!this.props.accountIds) { const { accountId, isAccount, dispatch } = this.props;
this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowers(this.props.params.accountId)); if (!isAccount) dispatch(fetchAccount(accountId));
} dispatch(fetchFollowers(accountId));
} }
componentWillReceiveProps (nextProps) { componentDidMount () {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowers(nextProps.params.accountId)); if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
} }
} }
handleHeaderClick = () => { componentDidUpdate (prevProps) {
this.column.scrollTop(); const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
}
} }
handleLoadMore = debounce(() => { handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowers(this.props.params.accountId)); this.props.dispatch(expandFollowers(this.props.accountId));
}, 300, { leading: true }); }, 300, { leading: true });
setRef = c => { setRef = c => {
this.column = c; this.column = c;
} }
handleHeaderClick = () => {
this.column.scrollTop();
}
render () { render () {
const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props; const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
@ -115,7 +144,7 @@ class Followers extends ImmutablePureComponent {
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import { import {
lookupAccount,
fetchAccount, fetchAccount,
fetchFollowing, fetchFollowing,
expandFollowing, expandFollowing,
@ -19,14 +20,25 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator';
import ScrollableList from 'flavours/glitch/components/scrollable_list'; import ScrollableList from 'flavours/glitch/components/scrollable_list';
import TimelineHint from 'flavours/glitch/components/timeline_hint'; import TimelineHint from 'flavours/glitch/components/timeline_hint';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, { params: { acct, id } }) => {
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])), const accountId = id || state.getIn(['accounts_map', acct]);
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
isAccount: !!state.getIn(['accounts', props.params.accountId]), if (!accountId) {
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), return {
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), isLoading: true,
isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true), };
}); }
return {
accountId,
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
};
};
const RemoteHint = ({ url }) => ( const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} /> <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} />
@ -40,7 +52,11 @@ export default @connect(mapStateToProps)
class Following extends ImmutablePureComponent { class Following extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
@ -51,32 +67,45 @@ class Following extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { _load () {
if (!this.props.accountIds) { const { accountId, isAccount, dispatch } = this.props;
this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowing(this.props.params.accountId)); if (!isAccount) dispatch(fetchAccount(accountId));
} dispatch(fetchFollowing(accountId));
} }
componentWillReceiveProps (nextProps) { componentDidMount () {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowing(nextProps.params.accountId)); if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
} }
} }
handleHeaderClick = () => { componentDidUpdate (prevProps) {
this.column.scrollTop(); const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
}
} }
handleLoadMore = debounce(() => { handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowing(this.props.params.accountId)); this.props.dispatch(expandFollowing(this.props.accountId));
}, 300, { leading: true }); }, 300, { leading: true });
setRef = c => { setRef = c => {
this.column = c; this.column = c;
} }
handleHeaderClick = () => {
this.column.scrollTop();
}
render () { render () {
const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props; const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
@ -115,7 +144,7 @@ class Following extends ImmutablePureComponent {
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

@ -87,7 +87,7 @@ class Content extends ImmutablePureComponent {
onMentionClick = (mention, e) => { onMentionClick = (mention, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/accounts/${mention.get('id')}`); this.context.router.history.push(`/@${mention.get('acct')}`);
} }
} }
@ -96,14 +96,14 @@ class Content extends ImmutablePureComponent {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/timelines/tag/${hashtag}`); this.context.router.history.push(`/tags/${hashtag}`);
} }
} }
onStatusClick = (status, e) => { onStatusClick = (status, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/statuses/${status.get('id')}`); this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
} }
} }

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Hashtag from 'flavours/glitch/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export default class Trends extends ImmutablePureComponent { export default class Trends extends ImmutablePureComponent {

@ -111,7 +111,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
const { fetchFollowRequests, multiColumn } = this.props; const { fetchFollowRequests, multiColumn } = this.props;
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
this.context.router.history.replace('/timelines/home'); this.context.router.history.replace('/home');
return; return;
} }
@ -126,7 +126,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
if (multiColumn) { if (multiColumn) {
if (!columns.find(item => item.get('id') === 'HOME')) { if (!columns.find(item => item.get('id') === 'HOME')) {
navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />); navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />);
} }
if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) { if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) {
@ -134,16 +134,16 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
} }
if (!columns.find(item => item.get('id') === 'COMMUNITY')) { if (!columns.find(item => item.get('id') === 'COMMUNITY')) {
navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />); navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />);
} }
if (!columns.find(item => item.get('id') === 'PUBLIC')) { if (!columns.find(item => item.get('id') === 'PUBLIC')) {
navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />); navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />);
} }
} }
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) { if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />); navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />);
} }
if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) { if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) {
@ -164,7 +164,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
<div key='9'> <div key='9'>
<ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' /> <ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
{lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list => {lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list =>
<ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> <ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
)} )}
</div>, </div>,
]); ]);

@ -73,7 +73,7 @@ class Lists extends ImmutablePureComponent {
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{lists.map(list => {lists.map(list =>
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />, <ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
)} )}
</ScrollableList> </ScrollableList>
</Column> </Column>

@ -27,11 +27,12 @@ export default class ColumnSettings extends React.PureComponent {
render () { render () {
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props; const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />; const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />; const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
@ -58,11 +59,11 @@ export default class ColumnSettings extends React.PureComponent {
<div role='group' aria-labelledby='notifications-unread-markers'> <div role='group' aria-labelledby='notifications-unread-markers'>
<span id='notifications-unread-markers' className='column-settings__section'> <span id='notifications-unread-markers' className='column-settings__section'>
<FormattedMessage id='notifications.column_settings.unread_markers.category' defaultMessage='Unread notification markers' /> <FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
</span> </span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={filterShowStr} /> <SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
</div> </div>
</div> </div>
@ -72,7 +73,7 @@ export default class ColumnSettings extends React.PureComponent {
</span> </span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} /> <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} /> <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
</div> </div>
</div> </div>

@ -39,7 +39,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
handleOpenProfile = () => { handleOpenProfile = () => {
const { notification } = this.props; const { notification } = this.props;
this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
} }
handleMention = e => { handleMention = e => {
@ -70,7 +70,7 @@ export default class NotificationFollow extends ImmutablePureComponent {
className='notification__display-name' className='notification__display-name'
href={account.get('url')} href={account.get('url')}
title={account.get('acct')} title={account.get('acct')}
to={`/accounts/${account.get('id')}`} to={`/@${account.get('acct')}`}
dangerouslySetInnerHTML={{ __html: displayName }} dangerouslySetInnerHTML={{ __html: displayName }}
/></bdi> /></bdi>
); );

@ -45,7 +45,7 @@ class FollowRequest extends ImmutablePureComponent {
handleOpenProfile = () => { handleOpenProfile = () => {
const { notification } = this.props; const { notification } = this.props;
this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
} }
handleMention = e => { handleMention = e => {
@ -89,7 +89,7 @@ class FollowRequest extends ImmutablePureComponent {
className='notification__display-name' className='notification__display-name'
href={account.get('url')} href={account.get('url')}
title={account.get('acct')} title={account.get('acct')}
to={`/accounts/${account.get('id')}`} to={`/@${account.get('acct')}`}
dangerouslySetInnerHTML={{ __html: displayName }} dangerouslySetInnerHTML={{ __html: displayName }}
/></bdi> /></bdi>
); );
@ -111,7 +111,7 @@ class FollowRequest extends ImmutablePureComponent {
<div className='account'> <div className='account'>
<div className='account__wrapper'> <div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}> <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} /> <DisplayName account={account} />
</Permalink> </Permalink>

@ -99,7 +99,6 @@ class Notifications extends React.PureComponent {
notifications: ImmutablePropTypes.list.isRequired, notifications: ImmutablePropTypes.list.isRequired,
showFilterBar: PropTypes.bool.isRequired, showFilterBar: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
isUnread: PropTypes.bool, isUnread: PropTypes.bool,
@ -220,7 +219,7 @@ class Notifications extends React.PureComponent {
} }
render () { render () {
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props; const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const { notifCleaning, notifCleaningActive } = this.props; const { notifCleaning, notifCleaningActive } = this.props;
const { animatingNCD } = this.state; const { animatingNCD } = this.state;
const pinned = !!columnId; const pinned = !!columnId;
@ -273,7 +272,6 @@ class Notifications extends React.PureComponent {
onLoadPending={this.handleLoadPending} onLoadPending={this.handleLoadPending}
onScrollToTop={this.handleScrollToTop} onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll} onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{scrollableContent} {scrollableContent}

@ -116,9 +116,13 @@ class Footer extends ImmutablePureComponent {
return; return;
} }
const { status } = this.props; const { status, onClose } = this.props;
router.history.push(`/statuses/${status.get('id')}`); if (onClose) {
onClose();
}
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
} }
render () { render () {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save