Merge pull request #3074 from ClearlyClaire/glitch-soc/merge-upstream

[Breaking changes] Merge upstram changes up to a13756148d353c7479f68e65a210f6d88d26c785
This commit is contained in:
Claire 2025-05-25 20:49:36 +02:00 committed by GitHub
commit c9c2fbb27d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
410 changed files with 5467 additions and 8203 deletions

View File

@ -9,6 +9,7 @@ services:
environment: environment:
RAILS_ENV: development RAILS_ENV: development
NODE_ENV: development NODE_ENV: development
VITE_RUBY_HOST: 0.0.0.0
BIND: 0.0.0.0 BIND: 0.0.0.0
BOOTSNAP_CACHE_DIR: /tmp BOOTSNAP_CACHE_DIR: /tmp
REDIS_HOST: redis REDIS_HOST: redis
@ -22,11 +23,12 @@ services:
ES_PORT: '9200' ES_PORT: '9200'
LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000
LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000} LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000}
VITE_DEV_SERVER_PUBLIC: ${VITE_DEV_SERVER_PUBLIC:-localhost:3036}
# Overrides default command so things don't shut down after the process ends. # Overrides default command so things don't shut down after the process ends.
command: sleep infinity command: sleep infinity
ports: ports:
- '3000:3000' - '3000:3000'
- '3035:3035' - '3036:3036'
- '4000:4000' - '4000:4000'
networks: networks:
- external_network - external_network

View File

@ -71,7 +71,6 @@ DB_PORT=5432
# Generate each with the `RAILS_ENV=production bundle exec rails secret` task (`docker-compose run --rm web bundle exec rails secret` if you use docker compose) # Generate each with the `RAILS_ENV=production bundle exec rails secret` task (`docker-compose run --rm web bundle exec rails secret` if you use docker compose)
# ------- # -------
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET=
# Encryption secrets # Encryption secrets
# ------------------ # ------------------

View File

@ -25,26 +25,12 @@
'tesseract.js', // Requires code changes 'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes 'react-hotkeys', // Requires code changes
// Requires Webpacker upgrade or replacement
'@svgr/webpack',
'@types/webpack',
'babel-loader',
'compression-webpack-plugin',
'css-loader',
'imports-loader',
'mini-css-extract-plugin',
'postcss-loader',
'sass-loader',
'terser-webpack-plugin',
'webpack',
'webpack-assets-manifest',
'webpack-bundle-analyzer',
'webpack-dev-server',
'webpack-cli',
// react-router: Requires manual upgrade // react-router: Requires manual upgrade
'history', 'history',
'react-router-dom', 'react-router-dom',
// react-spring: Requires manual upgrade when upgrading react
'@react-spring/web',
], ],
matchUpdateTypes: ['major'], matchUpdateTypes: ['major'],
dependencyDashboardApproval: true, dependencyDashboardApproval: true,

View File

@ -49,7 +49,7 @@ jobs:
public/assets public/assets
public/packs public/packs
public/packs-test public/packs-test
tmp/cache/webpacker tmp/cache/vite
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }} key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
restore-keys: | restore-keys: |
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }} ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
@ -63,7 +63,7 @@ jobs:
- name: Archive asset artifacts - name: Archive asset artifacts
run: | run: |
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: matrix.mode == 'test' if: matrix.mode == 'test'
@ -143,7 +143,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg imagemagick libpam-dev additional-system-dependencies: ffmpeg libpam-dev
- name: Load database schema - name: Load database schema
run: | run: |
@ -173,8 +173,8 @@ jobs:
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-libvips: test-imagemagick:
name: Libvips tests name: ImageMagick tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
@ -220,7 +220,7 @@ jobs:
CAS_ENABLED: true CAS_ENABLED: true
BUNDLE_WITH: 'pam_authentication test' BUNDLE_WITH: 'pam_authentication test'
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
MASTODON_USE_LIBVIPS: true MASTODON_USE_LIBVIPS: false
strategy: strategy:
fail-fast: false fail-fast: false
@ -245,7 +245,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg libpam-dev additional-system-dependencies: ffmpeg imagemagick libpam-dev
- name: Load database schema - name: Load database schema
run: './bin/rails db:create db:schema:load db:seed' run: './bin/rails db:create db:schema:load db:seed'
@ -324,7 +324,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg imagemagick additional-system-dependencies: ffmpeg
- name: Set up Javascript environment - name: Set up Javascript environment
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript
@ -443,7 +443,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg imagemagick additional-system-dependencies: ffmpeg
- name: Set up Javascript environment - name: Set up Javascript environment
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript

3
.gitignore vendored
View File

@ -21,10 +21,11 @@
/public/system /public/system
/public/assets /public/assets
/public/packs /public/packs
/public/packs-dev
/public/packs-test /public/packs-test
.env .env
.env.production .env.production
/node_modules/ node_modules/
/build/ /build/
# Ignore Vagrant files # Ignore Vagrant files

2
.nvmrc
View File

@ -1 +1 @@
22.15 22.16

View File

@ -18,10 +18,6 @@
!/log/.keep !/log/.keep
/tmp /tmp
/coverage /coverage
/public/system
/public/assets
/public/packs
/public/packs-test
.env .env
.env.production .env.production
.env.development .env.development
@ -60,6 +56,7 @@ docker-compose.override.yml
/public/packs /public/packs
/public/packs-test /public/packs-test
/public/system /public/system
/public/vite*
# Ignore emoji map file # Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json /app/javascript/mastodon/features/emoji/emoji_map.json

View File

@ -70,13 +70,6 @@ Style/OptionalBooleanParameter:
- 'app/workers/domain_block_worker.rb' - 'app/workers/domain_block_worker.rb'
- 'app/workers/unfollow_follow_worker.rb' - 'app/workers/unfollow_follow_worker.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: short, verbose
Style/PreferredHashMethods:
Exclude:
- 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
Style/RedundantConstantBase: Style/RedundantConstantBase:
Exclude: Exclude:

View File

@ -1 +1 @@
3.4.3 3.4.4

View File

@ -62,6 +62,11 @@ Bug reports and feature suggestions must use descriptive and concise titles and
be submitted to [GitHub Issues]. Please use the search function to make sure be submitted to [GitHub Issues]. Please use the search function to make sure
there are not duplicate bug reports or feature requests. there are not duplicate bug reports or feature requests.
## Security Issues
If you believe you have identified a security issue in Mastodon or our own apps,
check [SECURITY].
## Translations ## Translations
Translations are community contributed via [Crowdin]. They are periodically Translations are community contributed via [Crowdin]. They are periodically
@ -124,3 +129,4 @@ and API docs. Improvements are made via PRs to the [documentation repository].
[GitHub Issues]: https://github.com/mastodon/mastodon/issues [GitHub Issues]: https://github.com/mastodon/mastodon/issues
[keepachangelog]: https://keepachangelog.com/en/1.0.0/ [keepachangelog]: https://keepachangelog.com/en/1.0.0/
[Mastodon documentation]: https://docs.joinmastodon.org [Mastodon documentation]: https://docs.joinmastodon.org
[SECURITY]: SECURITY.md

View File

@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby # renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.3" ARG RUBY_VERSION="3.4.4"
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node # renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22" ARG NODE_MAJOR_VERSION="22"

View File

@ -62,7 +62,7 @@ gem 'inline_svg'
gem 'irb', '~> 1.8' gem 'irb', '~> 1.8'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'linzer', '~> 0.6.1' gem 'linzer', '~> 0.7.2'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar' gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar'
gem 'mutex_m' gem 'mutex_m'
@ -78,7 +78,6 @@ gem 'rack-cors', '~> 2.0', require: 'rack/cors'
gem 'rails-i18n', '~> 8.0' gem 'rails-i18n', '~> 8.0'
gem 'redcarpet', '~> 3.6' gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'redis-namespace', '~> 1.10'
gem 'rqrcode', '~> 3.0' gem 'rqrcode', '~> 3.0'
gem 'ruby-progressbar', '~> 1.13' gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0' gem 'sanitize', '~> 7.0'
@ -95,7 +94,6 @@ gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023' gem 'tzinfo-data', '~> 1.2023'
gem 'webauthn', '~> 3.0' gem 'webauthn', '~> 3.0'
gem 'webpacker', '~> 5.4'
gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913' gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913'
gem 'json-ld' gem 'json-ld'
@ -230,3 +228,5 @@ gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1' gem 'hcaptcha', '~> 7.1'
gem 'mail', '~> 2.8' gem 'mail', '~> 2.8'
gem 'vite_rails', '~> 3.0.19'

View File

@ -94,7 +94,7 @@ GEM
ast (2.4.3) ast (2.4.3)
attr_required (1.0.2) attr_required (1.0.2)
aws-eventstream (1.3.2) aws-eventstream (1.3.2)
aws-partitions (1.1087.0) aws-partitions (1.1103.0)
aws-sdk-core (3.215.1) aws-sdk-core (3.215.1)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
@ -109,7 +109,7 @@ GEM
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0) aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-blob (0.5.7) azure-blob (0.5.8)
rexml rexml
base64 (0.2.0) base64 (0.2.0)
bcp47_spec (0.2.1) bcp47_spec (0.2.1)
@ -124,7 +124,7 @@ GEM
binding_of_caller (1.0.1) binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0) debug_inspector (>= 1.2.0)
blurhash (0.1.8) blurhash (0.1.8)
bootsnap (1.18.5) bootsnap (1.18.6)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.0.2) brakeman (7.0.2)
racc racc
@ -148,6 +148,7 @@ GEM
case_transform (0.2) case_transform (0.2)
activesupport activesupport
cbor (0.5.9.8) cbor (0.5.9.8)
cgi (0.4.2)
charlock_holmes (0.7.9) charlock_holmes (0.7.9)
chewy (7.6.0) chewy (7.6.0)
activesupport (>= 5.2) activesupport (>= 5.2)
@ -194,7 +195,7 @@ GEM
devise_pam_authenticatable2 (9.2.0) devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0) devise (>= 4.0.0)
rpam2 (~> 4.0) rpam2 (~> 4.0)
diff-lcs (1.6.1) diff-lcs (1.6.2)
discard (1.4.0) discard (1.4.0)
activerecord (>= 4.2, < 9.0) activerecord (>= 4.2, < 9.0)
docile (1.4.1) docile (1.4.1)
@ -203,6 +204,7 @@ GEM
railties (>= 5) railties (>= 5)
dotenv (3.1.8) dotenv (3.1.8)
drb (2.2.1) drb (2.2.1)
dry-cli (1.2.0)
elasticsearch (7.17.11) elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11) elasticsearch-api (= 7.17.11)
elasticsearch-transport (= 7.17.11) elasticsearch-transport (= 7.17.11)
@ -227,7 +229,7 @@ GEM
fabrication (2.31.0) fabrication (2.31.0)
faker (3.5.1) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.13.0) faraday (2.13.1)
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.5)
json json
logger logger
@ -261,15 +263,16 @@ GEM
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
formatador (1.1.0) formatador (1.1.0)
forwardable (1.3.3)
fugit (1.11.1) fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11) et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-protobuf (4.30.2) google-protobuf (4.31.0)
bigdecimal bigdecimal
rake (>= 13) rake (>= 13)
googleapis-common-protos-types (1.19.0) googleapis-common-protos-types (1.20.0)
google-protobuf (>= 3.18, < 5.a) google-protobuf (>= 3.18, < 5.a)
haml (6.3.0) haml (6.3.0)
temple (>= 0.8.2) temple (>= 0.8.2)
@ -337,7 +340,7 @@ GEM
azure-blob (~> 0.5.2) azure-blob (~> 0.5.2)
hashie (~> 5.0) hashie (~> 5.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.10.2) json (2.12.0)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.16.7) json-jwt (1.16.7)
activesupport (>= 4.2) activesupport (>= 4.2)
@ -381,7 +384,7 @@ GEM
marcel (~> 1.0.1) marcel (~> 1.0.1)
mime-types mime-types
terrapin (>= 0.6.0, < 2.0) terrapin (>= 0.6.0, < 2.0)
language_server-protocol (3.17.0.4) language_server-protocol (3.17.0.5)
launchy (3.1.1) launchy (3.1.1)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) childprocess (~> 5.0)
@ -395,7 +398,11 @@ GEM
rexml rexml
link_header (0.0.8) link_header (0.0.8)
lint_roller (1.1.0) lint_roller (1.1.0)
linzer (0.6.5) linzer (0.7.2)
cgi (~> 0.4.2)
forwardable (~> 1.3, >= 1.3.3)
logger (~> 1.7, >= 1.7.0)
net-http (~> 0.6.0)
openssl (~> 3.0, >= 3.0.0) openssl (~> 3.0, >= 3.0.0)
rack (>= 2.2, < 4.0) rack (>= 2.2, < 4.0)
starry (~> 0.2) starry (~> 0.2)
@ -410,7 +417,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.24.0) loofah (2.24.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@ -426,9 +433,9 @@ GEM
mime-types (3.7.0) mime-types (3.7.0)
logger logger
mime-types-data (~> 3.2025, >= 3.2025.0507) mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0507) mime-types-data (3.2025.0514)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) mini_portile2 (2.8.9)
minitest (5.25.5) minitest (5.25.5)
msgpack (1.8.0) msgpack (1.8.0)
multi_json (1.15.0) multi_json (1.15.0)
@ -503,7 +510,7 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7) opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-action_pack (0.12.0) opentelemetry-instrumentation-action_pack (0.12.1)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-rack (~> 0.21) opentelemetry-instrumentation-rack (~> 0.21)
@ -617,7 +624,7 @@ GEM
activesupport (>= 7.0.0) activesupport (>= 7.0.0)
rack rack
railties (>= 7.0.0) railties (>= 7.0.0)
psych (5.2.3) psych (5.2.6)
date date
stringio stringio
public_suffix (6.0.2) public_suffix (6.0.2)
@ -627,7 +634,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (2.2.14) rack (2.2.16)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-cors (2.0.2) rack-cors (2.0.2)
@ -695,8 +702,6 @@ GEM
psych (>= 4.0.0) psych (>= 4.0.0)
redcarpet (3.6.1) redcarpet (3.6.1)
redis (4.8.1) redis (4.8.1)
redis-namespace (1.11.0)
redis (>= 4)
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.10.0) regexp_parser (2.10.0)
@ -709,7 +714,7 @@ GEM
railties (>= 5.2) railties (>= 5.2)
rexml (3.4.1) rexml (3.4.1)
rotp (6.3.0) rotp (6.3.0)
rouge (4.5.1) rouge (4.5.2)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (3.1.0) rqrcode (3.1.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@ -726,7 +731,7 @@ GEM
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-github (3.0.0) rspec-github (3.0.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
rspec-mocks (3.13.3) rspec-mocks (3.13.4)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (8.0.0) rspec-rails (8.0.0)
@ -743,7 +748,7 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9) sidekiq (>= 5, < 9)
rspec-support (3.13.3) rspec-support (3.13.3)
rubocop (1.75.5) rubocop (1.75.7)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -767,12 +772,12 @@ GEM
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.31.0) rubocop-rails (2.32.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.6.0) rubocop-rspec (3.6.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.72, >= 1.72.1)
@ -780,7 +785,8 @@ GEM
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5) rubocop-rspec (~> 3.5)
ruby-prof (1.7.1) ruby-prof (1.7.2)
base64
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.18.0) ruby-saml (1.18.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
@ -806,7 +812,6 @@ GEM
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
semantic_range (3.1.0)
shoulda-matchers (6.5.0) shoulda-matchers (6.5.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (6.5.12) sidekiq (6.5.12)
@ -892,6 +897,15 @@ GEM
validate_url (1.0.15) validate_url (1.0.15)
activemodel (>= 3.0.0) activemodel (>= 3.0.0)
public_suffix public_suffix
vite_rails (3.0.19)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.9.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
mutex_m
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
webauthn (3.4.0) webauthn (3.4.0)
@ -910,11 +924,6 @@ GEM
addressable (>= 2.8.0) 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.4)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.9.1) webrick (1.9.1)
websocket (1.2.11) websocket (1.2.11)
websocket-driver (0.7.7) websocket-driver (0.7.7)
@ -991,7 +1000,7 @@ DEPENDENCIES
letter_opener (~> 1.8) letter_opener (~> 1.8)
letter_opener_web (~> 3.0) letter_opener_web (~> 3.0)
link_header (~> 0.0) link_header (~> 0.0)
linzer (~> 0.6.1) linzer (~> 0.7.2)
lograge (~> 0.12) lograge (~> 0.12)
mail (~> 2.8) mail (~> 2.8)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
@ -1042,7 +1051,6 @@ DEPENDENCIES
rdf-normalize (~> 0.5) rdf-normalize (~> 0.5)
redcarpet (~> 3.6) redcarpet (~> 3.6)
redis (~> 4.5) redis (~> 4.5)
redis-namespace (~> 1.10)
rqrcode (~> 3.0) rqrcode (~> 3.0)
rspec-github (~> 3.0) rspec-github (~> 3.0)
rspec-rails (~> 8.0) rspec-rails (~> 8.0)
@ -1078,9 +1086,9 @@ DEPENDENCIES
tty-prompt (~> 0.23) tty-prompt (~> 0.23)
twitter-text (~> 3.1.0) twitter-text (~> 3.1.0)
tzinfo-data (~> 1.2023) tzinfo-data (~> 1.2023)
vite_rails (~> 3.0.19)
webauthn (~> 3.0) webauthn (~> 3.0)
webmock (~> 3.18) webmock (~> 3.18)
webpacker (~> 5.4)
webpush! webpush!
xorcist (~> 1.1) xorcist (~> 1.1)
@ -1088,4 +1096,4 @@ RUBY VERSION
ruby 3.4.1p0 ruby 3.4.1p0
BUNDLED WITH BUNDLED WITH
2.6.8 2.6.9

View File

@ -1,4 +1,4 @@
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
stream: env PORT=4000 yarn workspace @mastodon/streaming start stream: env PORT=4000 yarn workspace @mastodon/streaming start
webpack: bin/webpack-dev-server vite: yarn dev

View File

@ -73,10 +73,10 @@ Mastodon is a **free, open-source social network server** based on ActivityPub w
### Requirements ### Requirements
- **PostgreSQL** 12+ - **PostgreSQL** 13+
- **Redis** 4+ - **Redis** 6.2+
- **Ruby** 3.2+ - **Ruby** 3.2+
- **Node.js** 18+ - **Node.js** 20+
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
@ -93,12 +93,12 @@ accepted into Mastodon, you can request to be paid through our [OpenCollective].
## License ## License
Copyright (c) 2016-2024 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md)) Copyright (c) 2016-2025 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md))
Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE): Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE):
``` ```
Copyright (c) 2016-2024 Eugen Rochko & other Mastodon contributors Copyright (c) 2016-2025 Eugen Rochko & other Mastodon contributors
This program is free software: you can redistribute it and/or modify it under This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free the terms of the GNU Affero General Public License as published by the Free

View File

@ -17,10 +17,6 @@
"description": "The secret key base", "description": "The secret key base",
"generator": "secret" "generator": "secret"
}, },
"OTP_SECRET": {
"description": "One-time password secret",
"generator": "secret"
},
"SINGLE_USER_MODE": { "SINGLE_USER_MODE": {
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)", "description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
"value": "false", "value": "false",

View File

@ -7,7 +7,7 @@ module Admin
def index def index
authorize :rule, :index? authorize :rule, :index?
@rules = Rule.ordered @rules = Rule.ordered.includes(:translations)
end end
def new def new
@ -27,7 +27,6 @@ module Admin
if @rule.save if @rule.save
redirect_to admin_rules_path redirect_to admin_rules_path
else else
@rules = Rule.ordered
render :new render :new
end end
end end
@ -50,6 +49,22 @@ module Admin
redirect_to admin_rules_path redirect_to admin_rules_path
end end
def move_up
authorize @rule, :update?
@rule.move!(-1)
redirect_to admin_rules_path
end
def move_down
authorize @rule, :update?
@rule.move!(+1)
redirect_to admin_rules_path
end
private private
def set_rule def set_rule
@ -58,7 +73,7 @@ module Admin
def resource_params def resource_params
params params
.expect(rule: [:text, :hint, :priority]) .expect(rule: [:text, :hint, :priority, translations_attributes: [[:id, :language, :text, :hint, :_destroy]]])
end end
end end
end end

View File

@ -42,37 +42,22 @@ class Api::Fasp::BaseController < ApplicationController
end end
def validate_signature! def validate_signature!
signature_input = request.headers['signature-input']&.encode('UTF-8') raise Error, 'signature-input is missing' if request.headers['signature-input'].blank?
raise Error, 'signature-input is missing' if signature_input.blank?
provider = nil
Linzer.verify!(request.rack_request, no_older_than: 5.minutes) do |keyid|
provider = Fasp::Provider.find(keyid)
Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid)
end
keyid = signature_input.match(KEYID_PATTERN)[1]
provider = Fasp::Provider.find(keyid)
linzer_request = Linzer.new_request(
request.method,
request.original_url,
{},
{
'content-digest' => request.headers['content-digest'],
'signature-input' => signature_input,
'signature' => request.headers['signature'],
}
)
message = Linzer::Message.new(linzer_request)
key = Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid)
signature = Linzer::Signature.build(message.headers)
Linzer.verify(key, message, signature)
@current_provider = provider @current_provider = provider
end end
def sign_response def sign_response
response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:" response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:"
linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] })
message = Linzer::Message.new(linzer_response)
key = Linzer.new_ed25519_key(current_provider.server_private_key_pem) key = Linzer.new_ed25519_key(current_provider.server_private_key_pem)
signature = Linzer.sign(key, message, %w(@status content-digest)) Linzer.sign!(response, key:, components: %w(@status content-digest))
response.headers.merge!(signature.to_h)
end end
def check_fasp_enabled def check_fasp_enabled

View File

@ -18,6 +18,6 @@ class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController
private private
def set_rules def set_rules
@rules = Rule.ordered @rules = Rule.ordered.includes(:translations)
end end
end end

View File

@ -126,7 +126,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def set_rules def set_rules
@rules = Rule.ordered @rules = Rule.ordered.includes(:translations)
end end
def require_rules_acceptance! def require_rules_acceptance!

View File

@ -177,9 +177,7 @@ class Auth::SessionsController < Devise::SessionsController
) )
# Only send a notification email every hour at most # Only send a notification email every hour at most
return if redis.get("2fa_failure_notification:#{user.id}").present? return if redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour, get: true).present?
redis.set("2fa_failure_notification:#{user.id}", '1', ex: 1.hour)
UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later!
end end

View File

@ -12,7 +12,7 @@ module ThemingConcern
def current_skin def current_skin
@current_skin ||= begin @current_skin ||= begin
skins = Themes.instance.skins_for(current_flavour) skins = Themes.instance.skins_for(current_flavour)
[current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) } [current_user&.setting_skin, Setting.skin, 'system', 'application'].find { |skin| skins.include?(skin) }
end end
end end

View File

@ -8,6 +8,7 @@ module WebAppControllerConcern
before_action :redirect_unauthenticated_to_permalinks! before_action :redirect_unauthenticated_to_permalinks!
before_action :set_referer_header before_action :set_referer_header
before_action :redirect_to_tos_interstitial!
content_security_policy do |p| content_security_policy do |p|
policy = ContentSecurityPolicy.new policy = ContentSecurityPolicy.new
@ -45,6 +46,13 @@ module WebAppControllerConcern
protected protected
def redirect_to_tos_interstitial!
return unless current_user&.require_tos_interstitial?
@terms_of_service = TermsOfService.published.first
render 'terms_of_service_interstitial/show', layout: 'auth'
end
def set_referer_header def set_referer_header
response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin') response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin')
end end

View File

@ -4,8 +4,19 @@ class TermsOfServiceController < ApplicationController
include WebAppControllerConcern include WebAppControllerConcern
skip_before_action :require_functional! skip_before_action :require_functional!
skip_before_action :redirect_to_tos_interstitial!
before_action :clear_redirect_interstitial!
def show def show
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end end
private
def clear_redirect_interstitial!
return unless user_signed_in?
current_user.update(require_tos_interstitial: false)
end
end end

View File

@ -248,22 +248,8 @@ module ApplicationHelper
end end
# glitch-soc addition to handle the multiple flavors # glitch-soc addition to handle the multiple flavors
def preload_locale_pack def flavoured_vite_typescript_tag(pack_name, **)
supported_locales = Themes.instance.flavour(current_flavour)['locales'] vite_typescript_tag("#{Themes.instance.flavour(current_flavour)['pack_directory'].delete_prefix('app/javascript/')}/#{pack_name}", **)
preload_pack_asset "locales/#{current_flavour}/#{I18n.locale}-json.js" if supported_locales.include?(I18n.locale.to_s)
end
def flavoured_javascript_pack_tag(pack_name, **)
javascript_pack_tag("flavours/#{current_flavour}/#{pack_name}", **)
end
def flavoured_stylesheet_pack_tag(pack_name, **)
stylesheet_pack_tag("flavours/#{current_flavour}/#{pack_name}", **)
end
def preload_signed_in_js_packs
preload_files = Themes.instance.flavour(current_flavour)&.fetch('signed_in_preload', nil) || []
safe_join(preload_files.map { |entry| preload_pack_asset entry })
end end
private private

View File

@ -4,7 +4,7 @@ module RoutingHelper
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ActionView::Helpers::AssetTagHelper include ActionView::Helpers::AssetTagHelper
include Webpacker::Helper include ViteRails::TagHelpers
included do included do
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
@ -25,7 +25,7 @@ module RoutingHelper
end end
def frontend_asset_path(source, **) def frontend_asset_path(source, **)
asset_pack_path("media/#{source}", **) vite_asset_path(source, **)
end end
def frontend_asset_url(source, **) def frontend_asset_url(source, **)

View File

@ -6,11 +6,13 @@ module ThemeHelper
if theme == 'system' if theme == 'system'
''.html_safe.tap do |tags| ''.html_safe.tap do |tags|
tags << stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') tags << vite_stylesheet_tag("skins/#{flavour}/mastodon-light.scss", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') tags << vite_stylesheet_tag("skins/#{flavour}/application.scss", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
end end
elsif theme == 'default'
vite_stylesheet_tag "skins/#{flavour}/application.scss", media: 'all', crossorigin: 'anonymous'
else else
stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous' vite_stylesheet_tag "skins/#{flavour}/#{theme}.scss", media: 'all', crossorigin: 'anonymous'
end end
end end

View File

@ -1,4 +1,3 @@
import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
@ -273,7 +272,7 @@ async function mountReactComponent(element: Element) {
); );
const { default: Component } = (await import( const { default: Component } = (await import(
`@/mastodon/components/admin/${componentName}` `@/mastodon/components/admin/${componentName}.jsx`
)) as { default: React.ComponentType }; )) as { default: React.ComponentType };
const root = createRoot(element); const root = createRoot(element);

View File

@ -1,11 +1,6 @@
import './public-path'; import { loadLocale } from 'mastodon/locales';
import main from 'mastodon/main'; import main from 'mastodon/main';
import { loadPolyfills } from 'mastodon/polyfills';
import { start } from '../mastodon/common';
import { loadLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills';
start();
loadPolyfills() loadPolyfills()
.then(loadLocale) .then(loadLocale)

View File

@ -1,4 +0,0 @@
/* This file is a hack to have something more reliable than the upstream `common` tag
that is implicitly generated as the common chunk through webpack's `splitChunks` config */
import './public-path';

View File

@ -0,0 +1,3 @@
import { start } from 'mastodon/common';
start();

View File

@ -1,15 +1,11 @@
import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal'; import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
import { start } from '../mastodon/common';
import { Status } from '../mastodon/features/standalone/status'; import { Status } from '../mastodon/features/standalone/status';
import { loadPolyfills } from '../mastodon/polyfills'; import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
start();
function loaded() { function loaded() {
const mountNode = document.getElementById('mastodon-status'); const mountNode = document.getElementById('mastodon-status');

View File

@ -1,4 +1,3 @@
import './public-path';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
ready(() => { ready(() => {

View File

@ -1,4 +0,0 @@
/* Placeholder file to have `inert.scss` compiled by Webpack
This is used by the `wicg-inert` polyfill */
import '../styles/inert.scss';

View File

@ -1,3 +0,0 @@
import '../styles/mailer.scss';
require.context('../icons');

View File

@ -1,23 +0,0 @@
// Dynamically set webpack's loading path depending on a meta header, in order
// to share the same assets regardless of instance configuration.
// See https://webpack.js.org/guides/public-path/#on-the-fly
function removeOuterSlashes(string: string) {
return string.replace(/^\/*/, '').replace(/\/*$/, '');
}
function formatPublicPath(host = '', path = '') {
let formattedHost = removeOuterSlashes(host);
if (formattedHost && !/^http/i.test(formattedHost)) {
formattedHost = `//${formattedHost}`;
}
const formattedPath = removeOuterSlashes(path);
return `${formattedHost}/${formattedPath}/`;
}
const cdnHost = document.querySelector<HTMLMetaElement>('meta[name=cdn-host]');
__webpack_public_path__ = formatPublicPath(
cdnHost ? cdnHost.content : '',
process.env.PUBLIC_OUTPUT_PATH,
);

View File

@ -1,7 +1,5 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import './public-path';
import { IntlMessageFormat } from 'intl-messageformat'; import { IntlMessageFormat } from 'intl-messageformat';
import type { MessageDescriptor, PrimitiveType } from 'react-intl'; import type { MessageDescriptor, PrimitiveType } from 'react-intl';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
@ -10,7 +8,6 @@ import Rails from '@rails/ujs';
import axios from 'axios'; import axios from 'axios';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { start } from '../mastodon/common';
import { timeAgoString } from '../mastodon/components/relative_timestamp'; import { timeAgoString } from '../mastodon/components/relative_timestamp';
import emojify from '../mastodon/features/emoji/emoji'; import emojify from '../mastodon/features/emoji/emoji';
import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions'; import loadKeyboardExtensions from '../mastodon/load_keyboard_extensions';
@ -20,8 +17,6 @@ import ready from '../mastodon/ready';
import 'cocoon-js-vanilla'; import 'cocoon-js-vanilla';
start();
const messages = defineMessages({ const messages = defineMessages({
usernameTaken: { usernameTaken: {
id: 'username.taken', id: 'username.taken',
@ -153,9 +148,7 @@ function loaded() {
const reactComponents = document.querySelectorAll('[data-component]'); const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) { if (reactComponents.length > 0) {
import( import('../mastodon/containers/media_container')
/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container'
)
.then(({ default: MediaContainer }) => { .then(({ default: MediaContainer }) => {
reactComponents.forEach((component) => { reactComponents.forEach((component) => {
Array.from(component.children).forEach((child) => { Array.from(component.children).forEach((child) => {

View File

@ -8,8 +8,6 @@ and performs no other task.
*/ */
import './public-path';
import axios from 'axios'; import axios from 'axios';
interface JRDLink { interface JRDLink {

View File

@ -1,13 +1,9 @@
import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { start } from '../mastodon/common';
import ComposeContainer from '../mastodon/containers/compose_container'; import ComposeContainer from '../mastodon/containers/compose_container';
import { loadPolyfills } from '../mastodon/polyfills'; import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
start();
function loaded() { function loaded() {
const mountNode = document.getElementById('mastodon-compose'); const mountNode = document.getElementById('mastodon-compose');

View File

@ -1,4 +1,3 @@
import './public-path';
import axios from 'axios'; import axios from 'axios';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';

View File

@ -69,6 +69,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog); processStatus(status.reblog);
} }
if (status.quote?.quoted_status) {
processStatus(status.quote.quoted_status);
}
if (status.poll?.id) { if (status.poll?.id) {
pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id]));
} }

View File

@ -23,12 +23,20 @@ export function normalizeFilterResult(result) {
export function normalizeStatus(status, normalOldStatus, settings) { export function normalizeStatus(status, normalOldStatus, settings) {
const normalStatus = { ...status }; const normalStatus = { ...status };
normalStatus.account = status.account.id; normalStatus.account = status.account.id;
if (status.reblog && status.reblog.id) { if (status.reblog && status.reblog.id) {
normalStatus.reblog = status.reblog.id; normalStatus.reblog = status.reblog.id;
} }
if (status.quote?.quoted_status ?? status.quote?.quoted_status_id) {
normalStatus.quote = {
...status.quote,
quoted_status: status.quote.quoted_status?.id ?? status.quote?.quoted_status_id,
};
}
if (status.poll && status.poll.id) { if (status.poll && status.poll.id) {
normalStatus.poll = status.poll.id; normalStatus.poll = status.poll.id;
} }

View File

@ -1,8 +1,6 @@
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
export function start() { export function start() {
require.context('@/images/', true, /\.(jpg|png|svg)$/);
try { try {
Rails.start(); Rails.start();
} catch { } catch {

View File

@ -10,9 +10,12 @@ import {
unblockAccount, unblockAccount,
muteAccount, muteAccount,
unmuteAccount, unmuteAccount,
followAccountSuccess,
} from 'flavours/glitch/actions/accounts'; } from 'flavours/glitch/actions/accounts';
import { showAlertForError } from 'flavours/glitch/actions/alerts';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { apiFollowAccount } from 'flavours/glitch/api/accounts';
import { Avatar } from 'flavours/glitch/components/avatar'; import { Avatar } from 'flavours/glitch/components/avatar';
import { Button } from 'flavours/glitch/components/button'; import { Button } from 'flavours/glitch/components/button';
import { FollowersCounter } from 'flavours/glitch/components/counters'; import { FollowersCounter } from 'flavours/glitch/components/counters';
@ -23,6 +26,8 @@ import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp
import { ShortNumber } from 'flavours/glitch/components/short_number'; import { ShortNumber } from 'flavours/glitch/components/short_number';
import { Skeleton } from 'flavours/glitch/components/skeleton'; import { Skeleton } from 'flavours/glitch/components/skeleton';
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge'; import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
import { useIdentity } from 'flavours/glitch/identity_context';
import { me } from 'flavours/glitch/initial_state';
import type { MenuItem } from 'flavours/glitch/models/dropdown_menu'; import type { MenuItem } from 'flavours/glitch/models/dropdown_menu';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
@ -67,10 +72,12 @@ export const Account: React.FC<{
withBio?: boolean; withBio?: boolean;
}> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => { }> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => {
const intl = useIntl(); const intl = useIntl();
const { signedIn } = useIdentity();
const account = useAppSelector((state) => state.accounts.get(id)); const account = useAppSelector((state) => state.accounts.get(id));
const relationship = useAppSelector((state) => state.relationships.get(id)); const relationship = useAppSelector((state) => state.relationships.get(id));
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const accountUrl = account?.url; const accountUrl = account?.url;
const isRemote = account?.acct !== account?.username;
const handleBlock = useCallback(() => { const handleBlock = useCallback(() => {
if (relationship?.blocking) { if (relationship?.blocking) {
@ -113,37 +120,74 @@ export const Account: React.FC<{
}, },
]; ];
} else if (defaultAction !== 'block') { } else if (defaultAction !== 'block') {
const handleAddToLists = () => { arr = [];
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: id,
},
}),
);
};
arr = [ if (isRemote && accountUrl) {
{ arr.push({
text: intl.formatMessage(messages.openOriginalPage),
href: accountUrl,
});
}
if (signedIn) {
const handleAddToLists = () => {
const openAddToListModal = () => {
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: id,
},
}),
);
};
if (relationship?.following || relationship?.requested || id === me) {
openAddToListModal();
} else {
dispatch(
openModal({
modalType: 'CONFIRM_FOLLOW_TO_LIST',
modalProps: {
accountId: id,
onConfirm: () => {
apiFollowAccount(id)
.then((relationship) => {
dispatch(
followAccountSuccess({
relationship,
alreadyFollowing: false,
}),
);
openAddToListModal();
})
.catch((err: unknown) => {
dispatch(showAlertForError(err));
});
},
},
}),
);
}
};
arr.push({
text: intl.formatMessage(messages.addToLists), text: intl.formatMessage(messages.addToLists),
action: handleAddToLists, action: handleAddToLists,
}, });
];
if (accountUrl) {
arr.unshift(
{
text: intl.formatMessage(messages.openOriginalPage),
href: accountUrl,
},
null,
);
} }
} }
return arr; return arr;
}, [dispatch, intl, id, accountUrl, relationship, defaultAction]); }, [
dispatch,
intl,
id,
accountUrl,
relationship,
defaultAction,
isRemote,
signedIn,
]);
if (hidden) { if (hidden) {
return ( return (

View File

@ -7,10 +7,16 @@ import classNames from 'classnames';
export const AvatarGroup: React.FC<{ export const AvatarGroup: React.FC<{
compact?: boolean; compact?: boolean;
avatarHeight?: number;
children: React.ReactNode; children: React.ReactNode;
}> = ({ children, compact = false }) => ( }> = ({ children, compact = false, avatarHeight }) => (
<div <div
className={classNames('avatar-group', { 'avatar-group--compact': compact })} className={classNames('avatar-group', { 'avatar-group--compact': compact })}
style={
avatarHeight
? ({ '--avatar-height': `${avatarHeight}px` } as React.CSSProperties)
: undefined
}
> >
{children} {children}
</div> </div>

View File

@ -43,3 +43,17 @@ export const FollowersCounter = (
}} }}
/> />
); );
export const FollowersYouKnowCounter = (
displayNumber: React.ReactNode,
pluralReady: number,
) => (
<FormattedMessage
id='account.followers_you_know_counter'
defaultMessage='{counter} you know'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);

View File

@ -8,12 +8,17 @@ import { fetchAccount } from 'flavours/glitch/actions/accounts';
import { AccountBio } from 'flavours/glitch/components/account_bio'; import { AccountBio } from 'flavours/glitch/components/account_bio';
import { AccountFields } from 'flavours/glitch/components/account_fields'; import { AccountFields } from 'flavours/glitch/components/account_fields';
import { Avatar } from 'flavours/glitch/components/avatar'; import { Avatar } from 'flavours/glitch/components/avatar';
import { FollowersCounter } from 'flavours/glitch/components/counters'; import { AvatarGroup } from 'flavours/glitch/components/avatar_group';
import {
FollowersCounter,
FollowersYouKnowCounter,
} from 'flavours/glitch/components/counters';
import { DisplayName } from 'flavours/glitch/components/display_name'; import { DisplayName } from 'flavours/glitch/components/display_name';
import { FollowButton } from 'flavours/glitch/components/follow_button'; import { FollowButton } from 'flavours/glitch/components/follow_button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { Permalink } from 'flavours/glitch/components/permalink'; import { Permalink } from 'flavours/glitch/components/permalink';
import { ShortNumber } from 'flavours/glitch/components/short_number'; import { ShortNumber } from 'flavours/glitch/components/short_number';
import { useFetchFamiliarFollowers } from 'flavours/glitch/features/account_timeline/hooks/familiar_followers';
import { domain } from 'flavours/glitch/initial_state'; import { domain } from 'flavours/glitch/initial_state';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
@ -38,6 +43,8 @@ export const HoverCardAccount = forwardRef<
} }
}, [dispatch, accountId, account]); }, [dispatch, accountId, account]);
const { familiarFollowers } = useFetchFamiliarFollowers({ accountId });
return ( return (
<div <div
ref={ref} ref={ref}
@ -77,11 +84,27 @@ export const HoverCardAccount = forwardRef<
)} )}
</div> </div>
<div className='hover-card__number'> <div className='hover-card__numbers'>
<ShortNumber <ShortNumber
value={account.followers_count} value={account.followers_count}
renderer={FollowersCounter} renderer={FollowersCounter}
/> />
{familiarFollowers.length > 0 && (
<>
&middot;
<div className='hover-card__familiar-followers'>
<ShortNumber
value={familiarFollowers.length}
renderer={FollowersYouKnowCounter}
/>
<AvatarGroup compact>
{familiarFollowers.slice(0, 3).map((account) => (
<Avatar key={account.id} account={account} size={22} />
))}
</AvatarGroup>
</div>
</>
)}
</div> </div>
<FollowButton accountId={accountId} /> <FollowButton accountId={accountId} />

View File

@ -1,17 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { IconLogo } from 'flavours/glitch/components/logo';
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
export const MoreFromAuthor = ({ accountId }) => (
<div className='more-from-author'>
<IconLogo />
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
</div>
);
MoreFromAuthor.propTypes = {
accountId: PropTypes.string.isRequired,
};

View File

@ -0,0 +1,21 @@
import { FormattedMessage } from 'react-intl';
import { IconLogo } from 'flavours/glitch/components/logo';
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
export const MoreFromAuthor: React.FC<{ accountId: string }> = ({
accountId,
}) => (
<FormattedMessage
id='link_preview.more_from_author'
defaultMessage='More from {name}'
values={{ name: <AuthorLink accountId={accountId} /> }}
>
{(chunks) => (
<div className='more-from-author'>
<IconLogo />
{chunks}
</div>
)}
</FormattedMessage>
);

View File

@ -82,6 +82,7 @@ class Status extends ImmutablePureComponent {
id: PropTypes.string, id: PropTypes.string,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.record, account: ImmutablePropTypes.record,
children: PropTypes.node,
previousId: PropTypes.string, previousId: PropTypes.string,
nextInReplyToId: PropTypes.string, nextInReplyToId: PropTypes.string,
rootId: PropTypes.string, rootId: PropTypes.string,
@ -111,6 +112,7 @@ class Status extends ImmutablePureComponent {
withDismiss: PropTypes.bool, withDismiss: PropTypes.bool,
onMoveUp: PropTypes.func, onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func, onMoveDown: PropTypes.func,
isQuotedPost: PropTypes.bool,
getScrollPosition: PropTypes.func, getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func, updateScrollBottom: PropTypes.func,
expanded: PropTypes.bool, expanded: PropTypes.bool,
@ -440,7 +442,7 @@ class Status extends ImmutablePureComponent {
} }
render () { render () {
const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props; const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
const { const {
status, status,
@ -452,6 +454,7 @@ class Status extends ImmutablePureComponent {
onOpenMedia, onOpenMedia,
notification, notification,
history, history,
isQuotedPost,
...other ...other
} = this.props; } = this.props;
let attachments = null; let attachments = null;
@ -618,7 +621,7 @@ class Status extends ImmutablePureComponent {
); );
mediaIcons.push('video-camera'); mediaIcons.push('video-camera');
} }
} else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) { } else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted && !status.get('quote')) {
media.push( media.push(
<Card <Card
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
@ -684,7 +687,17 @@ class Status extends ImmutablePureComponent {
{!skipPrepend && prepend} {!skipPrepend && prepend}
<div <div
className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} className={
classNames('status', `status-${status.get('visibility')}`,
{
'status-reply': !!status.get('in_reply_to_id'),
'status--in-thread': !!rootId,
'status--first-in-thread': previousId && (!connectUp || connectToRoot),
muted: this.props.muted,
'status--is-quote': isQuotedPost,
'status--has-quote': !!status.get('quote'),
})
}
data-id={status.get('id')} data-id={status.get('id')}
> >
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />} {(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
@ -722,6 +735,8 @@ class Status extends ImmutablePureComponent {
{...statusContentProps} {...statusContentProps}
/> />
{children}
{media} {media}
{hashtagBar} {hashtagBar}
</> </>
@ -730,13 +745,15 @@ class Status extends ImmutablePureComponent {
{/* This is a glitch-soc addition to have a placeholder */} {/* This is a glitch-soc addition to have a placeholder */}
{!expanded && <MentionsPlaceholder status={status} />} {!expanded && <MentionsPlaceholder status={status} />}
<StatusActionBar {!isQuotedPost &&
status={status} <StatusActionBar
account={status.get('account')} status={status}
showReplyCount={settings.get('show_reply_count')} account={status.get('account')}
onFilter={matchedFilters ? this.handleFilterClick : null} showReplyCount={settings.get('show_reply_count')}
{...other} onFilter={matchedFilters ? this.handleFilterClick : null}
/> {...other}
/>
}
</div> </div>
</div> </div>
</HotKeys> </HotKeys>

View File

@ -18,8 +18,8 @@ import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react'; import VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';

View File

@ -9,7 +9,7 @@ import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'flavours/glitch/actions/time
import { RegenerationIndicator } from 'flavours/glitch/components/regeneration_indicator'; import { RegenerationIndicator } from 'flavours/glitch/components/regeneration_indicator';
import { InlineFollowSuggestions } from 'flavours/glitch/features/home_timeline/components/inline_follow_suggestions'; import { InlineFollowSuggestions } from 'flavours/glitch/features/home_timeline/components/inline_follow_suggestions';
import StatusContainer from '../containers/status_container'; import { StatusQuoteManager } from '../components/status_quoted';
import { LoadGap } from './load_gap'; import { LoadGap } from './load_gap';
import ScrollableList from './scrollable_list'; import ScrollableList from './scrollable_list';
@ -114,7 +114,7 @@ export default class StatusList extends ImmutablePureComponent {
); );
default: default:
return ( return (
<StatusContainer <StatusQuoteManager
key={statusId} key={statusId}
id={statusId} id={statusId}
onMoveUp={this.handleMoveUp} onMoveUp={this.handleMoveUp}
@ -130,7 +130,7 @@ export default class StatusList extends ImmutablePureComponent {
if (scrollableContent && featuredStatusIds) { if (scrollableContent && featuredStatusIds) {
scrollableContent = featuredStatusIds.map(statusId => ( scrollableContent = featuredStatusIds.map(statusId => (
<StatusContainer <StatusQuoteManager
key={`f-${statusId}`} key={`f-${statusId}`}
id={statusId} id={statusId}
featured featured

View File

@ -0,0 +1,185 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { Map as ImmutableMap } from 'immutable';
import QuoteIcon from '@/images/quote.svg?react';
import ArticleIcon from '@/material-icons/400-24px/article.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import StatusContainer from 'flavours/glitch/containers/status_container';
import type { Status } from 'flavours/glitch/models/status';
import { useAppSelector } from 'flavours/glitch/store';
const MAX_QUOTE_POSTS_NESTING_LEVEL = 1;
const QuoteWrapper: React.FC<{
isError?: boolean;
children: React.ReactElement;
}> = ({ isError, children }) => {
return (
<div
className={classNames('status__quote', {
'status__quote--error': isError,
})}
>
<Icon id='quote' icon={QuoteIcon} className='status__quote-icon' />
{children}
</div>
);
};
const QuoteLink: React.FC<{
status: Status;
}> = ({ status }) => {
const accountId = status.get('account') as string;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const quoteAuthorName = account?.display_name_html;
if (!quoteAuthorName) {
return null;
}
const quoteAuthorElement = (
<span dangerouslySetInnerHTML={{ __html: quoteAuthorName }} />
);
const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`;
return (
<Link to={quoteUrl} className='status__quote-author-button'>
<FormattedMessage
id='status.quote_post_author'
defaultMessage='Post by {name}'
values={{ name: quoteAuthorElement }}
/>
<Icon id='chevron_right' icon={ChevronRightIcon} />
<Icon id='article' icon={ArticleIcon} />
</Link>
);
};
type QuoteMap = ImmutableMap<'state' | 'quoted_status', string | null>;
export const QuotedStatus: React.FC<{
quote: QuoteMap;
contextType?: string;
variant?: 'full' | 'link';
nestingLevel?: number;
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
const quotedStatusId = quote.get('quoted_status');
const state = quote.get('state');
const status = useAppSelector((state) =>
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
);
let quoteError: React.ReactNode = null;
if (state === 'deleted') {
quoteError = (
<FormattedMessage
id='status.quote_error.removed'
defaultMessage='This post was removed by its author.'
/>
);
} else if (state === 'unauthorized') {
quoteError = (
<FormattedMessage
id='status.quote_error.unauthorized'
defaultMessage='This post cannot be displayed as you are not authorized to view it.'
/>
);
} else if (state === 'pending') {
quoteError = (
<FormattedMessage
id='status.quote_error.pending_approval'
defaultMessage='This post is pending approval from the original author.'
/>
);
} else if (state === 'rejected' || state === 'revoked') {
quoteError = (
<FormattedMessage
id='status.quote_error.rejected'
defaultMessage='This post cannot be displayed as the original author does not allow it to be quoted.'
/>
);
} else if (!status || !quotedStatusId) {
quoteError = (
<FormattedMessage
id='status.quote_error.not_found'
defaultMessage='This post cannot be displayed.'
/>
);
}
if (quoteError) {
return <QuoteWrapper isError>{quoteError}</QuoteWrapper>;
}
if (variant === 'link' && status) {
return <QuoteLink status={status} />;
}
const childQuote = status?.get('quote') as QuoteMap | undefined;
const canRenderChildQuote =
childQuote && nestingLevel <= MAX_QUOTE_POSTS_NESTING_LEVEL;
return (
<QuoteWrapper>
{/* @ts-expect-error Status is not yet typed */}
<StatusContainer
isQuotedPost
id={quotedStatusId}
contextType={contextType}
avatarSize={40}
>
{canRenderChildQuote && (
<QuotedStatus
quote={childQuote}
contextType={contextType}
variant={
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
}
nestingLevel={nestingLevel + 1}
/>
)}
</StatusContainer>
</QuoteWrapper>
);
};
interface StatusQuoteManagerProps {
id: string;
contextType?: string;
[key: string]: unknown;
}
/**
* This wrapper component takes a status ID and, if the associated status
* is a quote post, it renders the quote into `StatusContainer` as a child.
* It passes all other props through to `StatusContainer`.
*/
export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
const status = useAppSelector((state) => {
const status = state.statuses.get(props.id);
const reblogId = status?.get('reblog') as string | undefined;
return reblogId ? state.statuses.get(reblogId) : status;
});
const quote = status?.get('quote') as QuoteMap | undefined;
if (quote) {
return (
/* @ts-expect-error Status is not yet typed */
<StatusContainer {...props}>
<QuotedStatus quote={quote} contextType={props.contextType} />
</StatusContainer>
);
}
/* @ts-expect-error Status is not yet typed */
return <StatusContainer {...props} />;
};

View File

@ -1,4 +1,3 @@
import '@/entrypoints/public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
@ -273,7 +272,7 @@ async function mountReactComponent(element: Element) {
); );
const { default: Component } = (await import( const { default: Component } = (await import(
`@/flavours/glitch/components/admin/${componentName}` `@/flavours/glitch/components/admin/${componentName}.jsx`
)) as { default: React.ComponentType }; )) as { default: React.ComponentType };
const root = createRoot(element); const root = createRoot(element);

View File

@ -1,12 +1,7 @@
import '@/entrypoints/public-path';
import { start } from 'flavours/glitch/common';
import { loadLocale } from 'flavours/glitch/locales'; import { loadLocale } from 'flavours/glitch/locales';
import main from 'flavours/glitch/main'; import main from 'flavours/glitch/main';
import { loadPolyfills } from 'flavours/glitch/polyfills'; import { loadPolyfills } from 'flavours/glitch/polyfills';
start();
loadPolyfills() loadPolyfills()
.then(loadLocale) .then(loadLocale)
.then(main) .then(main)

View File

@ -1,7 +0,0 @@
/* This file is a hack to have something more reliable than the upstream `common` tag
that is implicitly generated as the common chunk through webpack's `splitChunks` config */
import '@/entrypoints/public-path';
// This is a hack to ensures that webpack compiles our images.
require.context('../images', true);

View File

@ -0,0 +1,3 @@
import { start } from 'flavours/glitch/common';
start();

View File

@ -1,15 +1,10 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import '@/entrypoints/public-path';
import { start } from 'flavours/glitch/common';
import { Status } from 'flavours/glitch/features/standalone/status'; import { Status } from 'flavours/glitch/features/standalone/status';
import { afterInitialRender } from 'flavours/glitch/hooks/useRenderSignal'; import { afterInitialRender } from 'flavours/glitch/hooks/useRenderSignal';
import { loadPolyfills } from 'flavours/glitch/polyfills'; import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready'; import ready from 'flavours/glitch/ready';
start();
function loaded() { function loaded() {
const mountNode = document.getElementById('mastodon-status'); const mountNode = document.getElementById('mastodon-status');

View File

@ -1,4 +1,3 @@
import '@/entrypoints/public-path';
import ready from 'flavours/glitch/ready'; import ready from 'flavours/glitch/ready';
ready(() => { ready(() => {

View File

@ -1,4 +0,0 @@
/* Placeholder file to have `inert.scss` compiled by Webpack
This is used by the `wicg-inert` polyfill */
import '@/styles/inert.scss';

View File

@ -1,3 +0,0 @@
import '@/styles/mailer.scss';
require.context('@/icons');

View File

@ -1,7 +1,5 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import '@/entrypoints/public-path';
import { IntlMessageFormat } from 'intl-messageformat'; import { IntlMessageFormat } from 'intl-messageformat';
import type { MessageDescriptor, PrimitiveType } from 'react-intl'; import type { MessageDescriptor, PrimitiveType } from 'react-intl';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
@ -10,7 +8,6 @@ import Rails from '@rails/ujs';
import axios from 'axios'; import axios from 'axios';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { start } from 'flavours/glitch/common';
import { timeAgoString } from 'flavours/glitch/components/relative_timestamp'; import { timeAgoString } from 'flavours/glitch/components/relative_timestamp';
import emojify from 'flavours/glitch/features/emoji/emoji'; import emojify from 'flavours/glitch/features/emoji/emoji';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions'; import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
@ -20,8 +17,6 @@ import ready from 'flavours/glitch/ready';
import 'cocoon-js-vanilla'; import 'cocoon-js-vanilla';
start();
const messages = defineMessages({ const messages = defineMessages({
usernameTaken: { usernameTaken: {
id: 'username.taken', id: 'username.taken',
@ -153,9 +148,7 @@ function loaded() {
const reactComponents = document.querySelectorAll('[data-component]'); const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) { if (reactComponents.length > 0) {
import( import('flavours/glitch/containers/media_container')
/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container'
)
.then(({ default: MediaContainer }) => { .then(({ default: MediaContainer }) => {
reactComponents.forEach((component) => { reactComponents.forEach((component) => {
Array.from(component.children).forEach((child) => { Array.from(component.children).forEach((child) => {

View File

@ -8,8 +8,6 @@ and performs no other task.
*/ */
import '@/entrypoints/public-path';
import axios from 'axios'; import axios from 'axios';
interface JRDLink { interface JRDLink {

View File

@ -1,13 +1,9 @@
import '@/entrypoints/public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { start } from 'flavours/glitch/common';
import ComposeContainer from 'flavours/glitch/containers/compose_container'; import ComposeContainer from 'flavours/glitch/containers/compose_container';
import { loadPolyfills } from 'flavours/glitch/polyfills'; import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready'; import ready from 'flavours/glitch/ready';
start();
function loaded() { function loaded() {
const mountNode = document.getElementById('mastodon-compose'); const mountNode = document.getElementById('mastodon-compose');

View File

@ -1,4 +1,3 @@
import '@/entrypoints/public-path';
import axios from 'axios'; import axios from 'axios';
import ready from 'flavours/glitch/ready'; import ready from 'flavours/glitch/ready';

View File

@ -44,6 +44,7 @@ const severityMessages = {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
server: state.getIn(['server', 'server']), server: state.getIn(['server', 'server']),
locale: state.getIn(['meta', 'locale']),
extendedDescription: state.getIn(['server', 'extendedDescription']), extendedDescription: state.getIn(['server', 'extendedDescription']),
domainBlocks: state.getIn(['server', 'domainBlocks']), domainBlocks: state.getIn(['server', 'domainBlocks']),
}); });
@ -91,6 +92,7 @@ class About extends PureComponent {
static propTypes = { static propTypes = {
server: ImmutablePropTypes.map, server: ImmutablePropTypes.map,
locale: ImmutablePropTypes.string,
extendedDescription: ImmutablePropTypes.map, extendedDescription: ImmutablePropTypes.map,
domainBlocks: ImmutablePropTypes.contains({ domainBlocks: ImmutablePropTypes.contains({
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
@ -114,7 +116,7 @@ class About extends PureComponent {
}; };
render () { render () {
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props; const { multiColumn, intl, server, extendedDescription, domainBlocks, locale } = this.props;
const isLoading = server.get('isLoading'); const isLoading = server.get('isLoading');
return ( return (
@ -168,12 +170,15 @@ class About extends PureComponent {
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : ( ) : (
<ol className='rules-list'> <ol className='rules-list'>
{server.get('rules').map(rule => ( {server.get('rules').map(rule => {
<li key={rule.get('id')}> const text = rule.getIn(['translations', locale, 'text']) || rule.getIn(['translations', locale.split('-')[0], 'text']) || rule.get('text');
<div className='rules-list__text'>{rule.get('text')}</div> const hint = rule.getIn(['translations', locale, 'hint']) || rule.getIn(['translations', locale.split('-')[0], 'hint']) || rule.get('hint');
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)} return (
</li> <li key={rule.get('id')}>
))} <div className='rules-list__text'>{text}</div>
{hint.length > 0 && (<div className='rules-list__hint'>{hint}</div>)}
</li>
)})}
</ol> </ol>
))} ))}
</Section> </Section>

View File

@ -14,7 +14,7 @@ import { Account } from 'flavours/glitch/components/account';
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button'; import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { RemoteHint } from 'flavours/glitch/components/remote_hint'; import { RemoteHint } from 'flavours/glitch/components/remote_hint';
import StatusContainer from 'flavours/glitch/containers/status_container'; import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted';
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header'; import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import Column from 'flavours/glitch/features/ui/components/column'; import Column from 'flavours/glitch/features/ui/components/column';
@ -142,9 +142,8 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
/> />
</h4> </h4>
{featuredStatusIds.map((statusId) => ( {featuredStatusIds.map((statusId) => (
<StatusContainer <StatusQuoteManager
key={`f-${statusId}`} key={`f-${statusId}`}
// @ts-expect-error inferred props are wrong
id={statusId} id={statusId}
contextType='account' contextType='account'
/> />

View File

@ -1,15 +1,12 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { fetchAccountsFamiliarFollowers } from '@/flavours/glitch/actions/accounts_familiar_followers';
import { Avatar } from '@/flavours/glitch/components/avatar'; import { Avatar } from '@/flavours/glitch/components/avatar';
import { AvatarGroup } from '@/flavours/glitch/components/avatar_group'; import { AvatarGroup } from '@/flavours/glitch/components/avatar_group';
import type { Account } from '@/flavours/glitch/models/account'; import type { Account } from '@/flavours/glitch/models/account';
import { getAccountFamiliarFollowers } from '@/flavours/glitch/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; import { useFetchFamiliarFollowers } from '../hooks/familiar_followers';
const AccountLink: React.FC<{ account?: Account }> = ({ account }) => { const AccountLink: React.FC<{ account?: Account }> = ({ account }) => {
if (!account) { if (!account) {
@ -64,20 +61,11 @@ const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({
export const FamiliarFollowers: React.FC<{ accountId: string }> = ({ export const FamiliarFollowers: React.FC<{ accountId: string }> = ({
accountId, accountId,
}) => { }) => {
const dispatch = useAppDispatch(); const { familiarFollowers, isLoading } = useFetchFamiliarFollowers({
const familiarFollowers = useAppSelector((state) => accountId,
getAccountFamiliarFollowers(state, accountId), });
);
const hasNoData = familiarFollowers === null; if (isLoading || familiarFollowers.length === 0) {
useEffect(() => {
if (hasNoData) {
void dispatch(fetchAccountsFamiliarFollowers({ id: accountId }));
}
}, [dispatch, accountId, hasNoData]);
if (hasNoData || familiarFollowers.length === 0) {
return null; return null;
} }

View File

@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { fetchAccountsFamiliarFollowers } from '@/flavours/glitch/actions/accounts_familiar_followers';
import { getAccountFamiliarFollowers } from '@/flavours/glitch/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import { me } from 'flavours/glitch/initial_state';
export const useFetchFamiliarFollowers = ({
accountId,
}: {
accountId?: string;
}) => {
const dispatch = useAppDispatch();
const familiarFollowers = useAppSelector((state) =>
accountId ? getAccountFamiliarFollowers(state, accountId) : null,
);
const hasNoData = familiarFollowers === null;
useEffect(() => {
if (hasNoData && accountId && accountId !== me) {
void dispatch(fetchAccountsFamiliarFollowers({ id: accountId }));
}
}, [dispatch, accountId, hasNoData]);
return {
familiarFollowers: hasNoData ? [] : familiarFollowers,
isLoading: hasNoData,
};
};

View File

@ -15,10 +15,6 @@ import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { useSpring, animated } from '@react-spring/web'; import { useSpring, animated } from '@react-spring/web';
import Textarea from 'react-textarea-autosize'; import Textarea from 'react-textarea-autosize';
import { length } from 'stringz'; import { length } from 'stringz';
// eslint-disable-next-line import/extensions
import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
// eslint-disable-next-line import/no-extraneous-dependencies
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
import { showAlertForError } from 'flavours/glitch/actions/alerts'; import { showAlertForError } from 'flavours/glitch/actions/alerts';
import { uploadThumbnail } from 'flavours/glitch/actions/compose'; import { uploadThumbnail } from 'flavours/glitch/actions/compose';
@ -350,9 +346,15 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
fetchTesseract() fetchTesseract()
.then(async ({ createWorker }) => { .then(async ({ createWorker }) => {
const [tesseractWorkerPath, tesseractCorePath] = await Promise.all([
// eslint-disable-next-line import/extensions
import('tesseract.js/dist/worker.min.js?url'),
// eslint-disable-next-line import/no-extraneous-dependencies
import('tesseract.js-core/tesseract-core.wasm.js?url'),
]);
const worker = await createWorker('eng', 1, { const worker = await createWorker('eng', 1, {
workerPath: tesseractWorkerPath as string, workerPath: tesseractWorkerPath.default,
corePath: tesseractCorePath as string, corePath: tesseractCorePath.default,
langPath: `${assetHost}/ocr/lang-data`, langPath: `${assetHost}/ocr/lang-data`,
cacheMethod: 'write', cacheMethod: 'write',
}); });
@ -501,5 +503,4 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
); );
}, },
); );
AltTextModal.displayName = 'AltTextModal'; AltTextModal.displayName = 'AltTextModal';

View File

@ -9,28 +9,27 @@
// to ensure that the prevaled file is regenerated by Babel // to ensure that the prevaled file is regenerated by Babel
// version: 4 // version: 4
const { NimbleEmojiIndex } = require('emoji-mart'); import { NimbleEmojiIndex } from 'emoji-mart';
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data'); import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
import data from './emoji_data.json';
let data = require('./emoji_data.json'); import emojiMap from './emoji_map.json';
const emojiMap = require('./emoji_map.json'); import { unicodeToFilename } from './unicode_to_filename';
const { unicodeToFilename } = require('./unicode_to_filename'); import { unicodeToUnifiedName } from './unicode_to_unified_name';
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
emojiMartUncompress(data); emojiMartUncompress(data);
const emojiMartData = data; const emojiMartData = data;
const emojiIndex = new NimbleEmojiIndex(emojiMartData); const emojiIndex = new NimbleEmojiIndex(emojiMartData);
const excluded = ['®', '©', '™']; const excluded = ['®', '©', '™'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿']; const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {}; const shortcodeMap = {};
const shortCodesToEmojiData = {}; const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = []; const emojisWithoutShortCodes = [];
Object.keys(emojiIndex.emojis).forEach(key => { Object.keys(emojiIndex.emojis).forEach((key) => {
let emoji = emojiIndex.emojis[key]; let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this // Emojis with skin tone modifiers are stored like this
@ -41,22 +40,22 @@ Object.keys(emojiIndex.emojis).forEach(key => {
shortcodeMap[emoji.native] = emoji.id; shortcodeMap[emoji.native] = emoji.id;
}); });
const stripModifiers = unicode => { const stripModifiers = (unicode) => {
skinTones.forEach(tone => { skinTones.forEach((tone) => {
unicode = unicode.replace(tone, ''); unicode = unicode.replace(tone, '');
}); });
return unicode; return unicode;
}; };
Object.keys(emojiMap).forEach(key => { Object.keys(emojiMap).forEach((key) => {
if (excluded.includes(key)) { if (excluded.includes(key)) {
delete emojiMap[key]; delete emojiMap[key];
return; return;
} }
const normalizedKey = stripModifiers(key); const normalizedKey = stripModifiers(key);
let shortcode = shortcodeMap[normalizedKey]; let shortcode = shortcodeMap[normalizedKey];
if (!shortcode) { if (!shortcode) {
shortcode = shortcodeMap[normalizedKey + '\uFE0F']; shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
@ -82,7 +81,7 @@ Object.keys(emojiMap).forEach(key => {
} }
}); });
Object.keys(emojiIndex.emojis).forEach(key => { Object.keys(emojiIndex.emojis).forEach((key) => {
let emoji = emojiIndex.emojis[key]; let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this // Emojis with skin tone modifiers are stored like this
@ -94,9 +93,11 @@ Object.keys(emojiIndex.emojis).forEach(key => {
let { short_names, search, unified } = emojiMartData.emojis[key]; let { short_names, search, unified } = emojiMartData.emojis[key];
if (short_names[0] !== key) { if (short_names[0] !== key) {
throw new Error('The compressor expects the first short_code to be the ' + throw new Error(
'key. It may need to be rewritten if the emoji change such that this ' + 'The compressor expects the first short_code to be the ' +
'is no longer the case.'); 'key. It may need to be rewritten if the emoji change such that this ' +
'is no longer the case.',
);
} }
short_names = short_names.slice(1); // first short name can be inferred from the key short_names = short_names.slice(1); // first short name can be inferred from the key
@ -117,20 +118,22 @@ Object.keys(emojiIndex.emojis).forEach(key => {
// JSON.parse/stringify is to emulate what @preval is doing and avoid any // JSON.parse/stringify is to emulate what @preval is doing and avoid any
// inconsistent behavior in dev mode // inconsistent behavior in dev mode
module.exports = JSON.parse(JSON.stringify([ export default JSON.parse(
shortCodesToEmojiData, JSON.stringify([
/* shortCodesToEmojiData,
* The property `skins` is not found in the current context. /*
* This could potentially lead to issues when interacting with modules or data structures * The property `skins` is not found in the current context.
* that expect the presence of `skins` property. * This could potentially lead to issues when interacting with modules or data structures
* Currently, no definitions or references to `skins` property can be found in: * that expect the presence of `skins` property.
* - {@link node_modules/emoji-mart/dist/utils/data.js} * Currently, no definitions or references to `skins` property can be found in:
* - {@link node_modules/emoji-mart/data/all.json} * - {@link node_modules/emoji-mart/dist/utils/data.js}
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.d.ts#Skins} * - {@link node_modules/emoji-mart/data/all.json}
* Future refactorings or updates should consider adding definitions or handling for `skins` property. * - {@link app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.ts#Skins}
*/ * Future refactorings or updates should consider adding definitions or handling for `skins` property.
emojiMartData.skins, */
emojiMartData.categories, emojiMartData.skins,
emojiMartData.aliases, emojiMartData.categories,
emojisWithoutShortCodes emojiMartData.aliases,
])); emojisWithoutShortCodes,
]),
);

View File

@ -3,9 +3,13 @@
// emojiIndex.search functionality. // emojiIndex.search functionality.
import type { BaseEmoji } from 'emoji-mart'; import type { BaseEmoji } from 'emoji-mart';
import type { Emoji } from 'emoji-mart/dist-es/utils/data'; import type { Emoji } from 'emoji-mart/dist-es/utils/data';
// eslint-disable-next-line import/no-unresolved
import emojiCompressed from 'virtual:mastodon-emoji-compressed';
import type {
Search,
ShortCodesToEmojiData,
} from 'virtual:mastodon-emoji-compressed';
import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name'; import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = Record< type Emojis = Record<

View File

@ -2,11 +2,13 @@
// (i.e. the svg filename) and a shortCode intended to be shown // (i.e. the svg filename) and a shortCode intended to be shown
// as a "title" attribute in an HTML element (aka tooltip). // as a "title" attribute in an HTML element (aka tooltip).
// eslint-disable-next-line import/no-unresolved
import emojiCompressed from 'virtual:mastodon-emoji-compressed';
import type { import type {
FilenameData, FilenameData,
ShortCodesToEmojiDataKey, ShortCodesToEmojiDataKey,
} from './emoji_compressed'; } from 'virtual:mastodon-emoji-compressed';
import emojiCompressed from './emoji_compressed';
import { unicodeToFilename } from './unicode_to_filename'; import { unicodeToFilename } from './unicode_to_filename';
type UnicodeMapping = Record< type UnicodeMapping = Record<

View File

@ -1,6 +1,6 @@
// taken from: // taken from:
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 // https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
exports.unicodeToFilename = (str) => { export const unicodeToFilename = (str) => {
let result = ''; let result = '';
let charCode = 0; let charCode = 0;
let p = 0; let p = 0;

View File

@ -6,7 +6,7 @@ function padLeft(str, num) {
return str; return str;
} }
exports.unicodeToUnifiedName = (str) => { export const unicodeToUnifiedName = (str) => {
let output = ''; let output = '';
for (let i = 0; i < str.length; i += 2) { for (let i = 0; i < str.length; i += 2) {

View File

@ -16,7 +16,7 @@ import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import { Account } from 'flavours/glitch/components/account'; import { Account } from 'flavours/glitch/components/account';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { Permalink } from 'flavours/glitch/components/permalink'; import { Permalink } from 'flavours/glitch/components/permalink';
import StatusContainer from 'flavours/glitch/containers/status_container'; import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import FollowRequestContainer from '../containers/follow_request_container'; import FollowRequestContainer from '../containers/follow_request_container';
@ -159,7 +159,7 @@ class Notification extends ImmutablePureComponent {
renderMention (notification) { renderMention (notification) {
return ( return (
<StatusContainer <StatusQuoteManager
id={notification.get('status')} id={notification.get('status')}
containerId={notification.get('id')} containerId={notification.get('id')}
withDismiss withDismiss
@ -181,7 +181,7 @@ class Notification extends ImmutablePureComponent {
renderFavourite (notification) { renderFavourite (notification) {
return ( return (
<StatusContainer <StatusQuoteManager
containerId={notification.get('id')} containerId={notification.get('id')}
hidden={!!this.props.hidden} hidden={!!this.props.hidden}
id={notification.get('status')} id={notification.get('status')}
@ -206,7 +206,7 @@ class Notification extends ImmutablePureComponent {
renderReblog (notification) { renderReblog (notification) {
return ( return (
<StatusContainer <StatusQuoteManager
containerId={notification.get('id')} containerId={notification.get('id')}
hidden={!!this.props.hidden} hidden={!!this.props.hidden}
id={notification.get('status')} id={notification.get('status')}
@ -231,7 +231,7 @@ class Notification extends ImmutablePureComponent {
renderStatus (notification) { renderStatus (notification) {
return ( return (
<StatusContainer <StatusQuoteManager
containerId={notification.get('id')} containerId={notification.get('id')}
hidden={!!this.props.hidden} hidden={!!this.props.hidden}
id={notification.get('status')} id={notification.get('status')}
@ -256,7 +256,7 @@ class Notification extends ImmutablePureComponent {
renderUpdate (notification) { renderUpdate (notification) {
return ( return (
<StatusContainer <StatusQuoteManager
containerId={notification.get('id')} containerId={notification.get('id')}
hidden={!!this.props.hidden} hidden={!!this.props.hidden}
id={notification.get('status')} id={notification.get('status')}
@ -281,7 +281,7 @@ class Notification extends ImmutablePureComponent {
renderPoll (notification) { renderPoll (notification) {
return ( return (
<StatusContainer <StatusQuoteManager
containerId={notification.get('id')} containerId={notification.get('id')}
hidden={!!this.props.hidden} hidden={!!this.props.hidden}
id={notification.get('status')} id={notification.get('status')}

View File

@ -18,12 +18,14 @@ import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { DisplayedName } from './displayed_name'; import { DisplayedName } from './displayed_name';
import { EmbeddedStatus } from './embedded_status'; import { EmbeddedStatus } from './embedded_status';
const AVATAR_SIZE = 28;
export const AvatarById: React.FC<{ accountId: string }> = ({ accountId }) => { export const AvatarById: React.FC<{ accountId: string }> = ({ accountId }) => {
const account = useAppSelector((state) => state.accounts.get(accountId)); const account = useAppSelector((state) => state.accounts.get(accountId));
if (!account) return null; if (!account) return null;
return <Avatar withLink account={account} size={28} />; return <Avatar withLink account={account} size={AVATAR_SIZE} />;
}; };
export type LabelRenderer = ( export type LabelRenderer = (
@ -108,7 +110,7 @@ export const NotificationGroupWithStatus: React.FC<{
<div className='notification-group__main'> <div className='notification-group__main'>
<div className='notification-group__main__header'> <div className='notification-group__main__header'>
<div className='notification-group__main__header__wrapper'> <div className='notification-group__main__header__wrapper'>
<AvatarGroup> <AvatarGroup avatarHeight={AVATAR_SIZE}>
{accountIds {accountIds
.slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS) .slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS)
.map((id) => ( .map((id) => (
@ -123,7 +125,14 @@ export const NotificationGroupWithStatus: React.FC<{
<div className='notification-group__main__header__label'> <div className='notification-group__main__header__label'>
{label} {label}
{timestamp && <RelativeTimestamp timestamp={timestamp} />} {timestamp && (
<>
<span className='notification-group__main__header__label-separator'>
&middot;
</span>
<RelativeTimestamp timestamp={timestamp} />
</>
)}
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@ import {
} from 'flavours/glitch/actions/statuses'; } from 'flavours/glitch/actions/statuses';
import type { IconProp } from 'flavours/glitch/components/icon'; import type { IconProp } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import Status from 'flavours/glitch/containers/status_container'; import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted';
import { getStatusHidden } from 'flavours/glitch/selectors/filters'; import { getStatusHidden } from 'flavours/glitch/selectors/filters';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
@ -106,8 +106,7 @@ export const NotificationWithStatus: React.FC<{
{label} {label}
</div> </div>
<Status <StatusQuoteManager
// @ts-expect-error -- <Status> is not yet typed
id={statusId} id={statusId}
contextType='notifications' contextType='notifications'
withDismiss withDismiss

View File

@ -12,6 +12,7 @@ import Option from './components/option';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
rules: state.getIn(['server', 'server', 'rules']), rules: state.getIn(['server', 'server', 'rules']),
locale: state.getIn(['meta', 'locale']),
}); });
class Rules extends PureComponent { class Rules extends PureComponent {
@ -19,6 +20,7 @@ class Rules extends PureComponent {
static propTypes = { static propTypes = {
onNextStep: PropTypes.func.isRequired, onNextStep: PropTypes.func.isRequired,
rules: ImmutablePropTypes.list, rules: ImmutablePropTypes.list,
locale: PropTypes.string,
selectedRuleIds: ImmutablePropTypes.set.isRequired, selectedRuleIds: ImmutablePropTypes.set.isRequired,
onToggle: PropTypes.func.isRequired, onToggle: PropTypes.func.isRequired,
}; };
@ -34,7 +36,7 @@ class Rules extends PureComponent {
}; };
render () { render () {
const { rules, selectedRuleIds } = this.props; const { rules, locale, selectedRuleIds } = this.props;
return ( return (
<> <>
@ -49,7 +51,7 @@ class Rules extends PureComponent {
value={item.get('id')} value={item.get('id')}
checked={selectedRuleIds.includes(item.get('id'))} checked={selectedRuleIds.includes(item.get('id'))}
onToggle={this.handleRulesToggle} onToggle={this.handleRulesToggle}
label={item.get('text')} label={item.getIn(['translations', locale, 'text']) || item.getIn(['translations', locale.split('-')[0], 'text']) || item.get('text')}
multiple multiple
/> />
))} ))}

View File

@ -17,7 +17,7 @@ import { ColumnHeader } from 'flavours/glitch/components/column_header';
import { CompatibilityHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import { CompatibilityHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import ScrollableList from 'flavours/glitch/components/scrollable_list'; import ScrollableList from 'flavours/glitch/components/scrollable_list';
import Status from 'flavours/glitch/containers/status_container'; import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted';
import { Search } from 'flavours/glitch/features/compose/components/search'; import { Search } from 'flavours/glitch/features/compose/components/search';
import { useSearchParam } from 'flavours/glitch/hooks/useSearchParam'; import { useSearchParam } from 'flavours/glitch/hooks/useSearchParam';
import type { Hashtag as HashtagType } from 'flavours/glitch/models/tags'; import type { Hashtag as HashtagType } from 'flavours/glitch/models/tags';
@ -53,8 +53,7 @@ const renderHashtags = (hashtags: HashtagType[]) =>
const renderStatuses = (statusIds: string[]) => const renderStatuses = (statusIds: string[]) =>
hidePeek<string>(statusIds).map((id) => ( hidePeek<string>(statusIds).map((id) => (
// @ts-expect-error inferred props are wrong <StatusQuoteManager key={id} id={id} />
<Status key={id} id={id} />
)); ));
type SearchType = 'all' | ApiSearchType; type SearchType = 'all' | ApiSearchType;
@ -190,8 +189,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
onClickMore={handleSelectStatuses} onClickMore={handleSelectStatuses}
> >
{results.statuses.slice(0, INITIAL_DISPLAY).map((id) => ( {results.statuses.slice(0, INITIAL_DISPLAY).map((id) => (
// @ts-expect-error inferred props are wrong <StatusQuoteManager key={id} id={id} />
<Status key={id} id={id} />
))} ))}
</SearchSection> </SearchSection>
)} )}

View File

@ -27,6 +27,7 @@ import { MentionsPlaceholder } from 'flavours/glitch/components/mentions_placeho
import { Permalink } from 'flavours/glitch/components/permalink'; import { Permalink } from 'flavours/glitch/components/permalink';
import { PictureInPicturePlaceholder } from 'flavours/glitch/components/picture_in_picture_placeholder'; import { PictureInPicturePlaceholder } from 'flavours/glitch/components/picture_in_picture_placeholder';
import StatusContent from 'flavours/glitch/components/status_content'; import StatusContent from 'flavours/glitch/components/status_content';
import { QuotedStatus } from 'flavours/glitch/components/status_quoted';
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon'; import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
import { Audio } from 'flavours/glitch/features/audio'; import { Audio } from 'flavours/glitch/features/audio';
import scheduleIdleTask from 'flavours/glitch/features/ui/util/schedule_idle_task'; import scheduleIdleTask from 'flavours/glitch/features/ui/util/schedule_idle_task';
@ -265,7 +266,7 @@ export const DetailedStatus: React.FC<{
); );
mediaIcons.push('video-camera'); mediaIcons.push('video-camera');
} }
} else if (status.get('card')) { } else if (status.get('card') && !status.get('quote')) {
media = ( media = (
<Card <Card
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
@ -355,6 +356,9 @@ export const DetailedStatus: React.FC<{
className={classNames( className={classNames(
'detailed-status', 'detailed-status',
`detailed-status-${status.get('visibility')}`, `detailed-status-${status.get('visibility')}`,
{
'status--has-quote': !!status.get('quote'),
},
)} )}
data-status-by={status.getIn(['account', 'acct'])} data-status-by={status.getIn(['account', 'acct'])}
> >
@ -410,6 +414,10 @@ export const DetailedStatus: React.FC<{
{...(statusContentProps as any)} {...(statusContentProps as any)}
/> />
{status.get('quote') && (
<QuotedStatus quote={status.get('quote')} />
)}
{media} {media}
{hashtagBar} {hashtagBar}
</> </>

View File

@ -6,8 +6,6 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -56,7 +54,7 @@ import {
} from '../../actions/statuses'; } from '../../actions/statuses';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import StatusContainer from '../../containers/status_container'; import { StatusQuoteManager } from '../../components/status_quoted';
import { deleteModal } from '../../initial_state'; import { deleteModal } from '../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { getAncestorsIds, getDescendantsIds } from 'flavours/glitch/selectors/contexts'; import { getAncestorsIds, getDescendantsIds } from 'flavours/glitch/selectors/contexts';
@ -492,7 +490,7 @@ class Status extends ImmutablePureComponent {
const { params: { statusId } } = this.props; const { params: { statusId } } = this.props;
return list.map((id, i) => ( return list.map((id, i) => (
<StatusContainer <StatusQuoteManager
key={id} key={id}
id={id} id={id}
expanded={this.state.threadExpanded} expanded={this.state.threadExpanded}

View File

@ -1,243 +1,243 @@
export function EmojiPicker () { export function EmojiPicker () {
return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'../../emoji/emoji_picker'); return import('../../emoji/emoji_picker');
} }
export function Compose () { export function Compose () {
return import(/* webpackChunkName: "flavours/glitch/async/compose" */'../../compose'); return import('../../compose');
} }
export function Notifications () { export function Notifications () {
return import(/* webpackChunkName: "flavours/glitch/async/notifications" */'../../notifications_v2'); return import('../../notifications_v2');
} }
export function HomeTimeline () { export function HomeTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/home_timeline" */'../../home_timeline'); return import('../../home_timeline');
} }
export function PublicTimeline () { export function PublicTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/public_timeline" */'../../public_timeline'); return import('../../public_timeline');
} }
export function CommunityTimeline () { export function CommunityTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'../../community_timeline'); return import('../../community_timeline');
} }
export function Firehose () { export function Firehose () {
return import(/* webpackChunkName: "flavours/glitch/async/firehose" */'../../firehose'); return import('../../firehose');
} }
export function HashtagTimeline () { export function HashtagTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'../../hashtag_timeline'); return import('../../hashtag_timeline');
} }
export function ListTimeline () { export function ListTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/list_timeline" */'../../list_timeline'); return import('../../list_timeline');
} }
export function Lists () { export function Lists () {
return import(/* webpackChunkName: "flavours/glitch/async/lists" */'../../lists'); return import('../../lists');
} }
export function DirectTimeline() { export function DirectTimeline() {
return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'../../direct_timeline'); return import('../../direct_timeline');
} }
export function Status () { export function Status () {
return import(/* webpackChunkName: "flavours/glitch/async/status" */'../../status'); return import('../../status');
} }
export function GettingStarted () { export function GettingStarted () {
return import(/* webpackChunkName: "flavours/glitch/async/getting_started" */'../../getting_started'); return import('../../getting_started');
} }
export function GettingStartedMisc () { export function GettingStartedMisc () {
return import(/* webpackChunkName: "flavours/glitch/async/getting_started_misc" */'../../getting_started_misc'); return import('../../getting_started_misc');
} }
export function KeyboardShortcuts () { export function KeyboardShortcuts () {
return import(/* webpackChunkName: "flavours/glitch/async/keyboard_shortcuts" */'../../keyboard_shortcuts'); return import('../../keyboard_shortcuts');
} }
export function PinnedStatuses () { export function PinnedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/pinned_statuses" */'../../pinned_statuses'); return import('../../pinned_statuses');
} }
export function AccountTimeline () { export function AccountTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/account_timeline" */'../../account_timeline'); return import('../../account_timeline');
} }
export function AccountGallery () { export function AccountGallery () {
return import(/* webpackChunkName: "flavours/glitch/async/account_gallery" */'../../account_gallery'); return import('../../account_gallery');
} }
export function AccountFeatured() { export function AccountFeatured() {
return import(/* webpackChunkName: "flavours/glitch/async/account_featured" */'../../account_featured'); return import('../../account_featured');
} }
export function Followers () { export function Followers () {
return import(/* webpackChunkName: "flavours/glitch/async/followers" */'../../followers'); return import('../../followers');
} }
export function Following () { export function Following () {
return import(/* webpackChunkName: "flavours/glitch/async/following" */'../../following'); return import('../../following');
} }
export function Reblogs () { export function Reblogs () {
return import(/* webpackChunkName: "flavours/glitch/async/reblogs" */'../../reblogs'); return import('../../reblogs');
} }
export function Favourites () { export function Favourites () {
return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'../../favourites'); return import('../../favourites');
} }
export function FollowRequests () { export function FollowRequests () {
return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'../../follow_requests'); return import('../../follow_requests');
} }
export function FavouritedStatuses () { export function FavouritedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'../../favourited_statuses'); return import('../../favourited_statuses');
} }
export function FollowedTags () { export function FollowedTags () {
return import(/* webpackChunkName: "flavours/glitch/async/followed_tags" */'../../followed_tags'); return import('../../followed_tags');
} }
export function BookmarkedStatuses () { export function BookmarkedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/bookmarked_statuses" */'../../bookmarked_statuses'); return import('../../bookmarked_statuses');
} }
export function Blocks () { export function Blocks () {
return import(/* webpackChunkName: "flavours/glitch/async/blocks" */'../../blocks'); return import('../../blocks');
} }
export function DomainBlocks () { export function DomainBlocks () {
return import(/* webpackChunkName: "flavours/glitch/async/domain_blocks" */'../../domain_blocks'); return import('../../domain_blocks');
} }
export function Mutes () { export function Mutes () {
return import(/* webpackChunkName: "flavours/glitch/async/mutes" */'../../mutes'); return import('../../mutes');
} }
export function MuteModal () { export function MuteModal () {
return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'../components/mute_modal'); return import('../components/mute_modal');
} }
export function BlockModal () { export function BlockModal () {
return import(/* webpackChunkName: "flavours/glitch/async/block_modal" */'../components/block_modal'); return import('../components/block_modal');
} }
export function DomainBlockModal () { export function DomainBlockModal () {
return import(/* webpackChunkName: "flavours/glitch/async/modals/domain_block_modal" */'../components/domain_block_modal'); return import('../components/domain_block_modal');
} }
export function ReportModal () { export function ReportModal () {
return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'../components/report_modal'); return import('../components/report_modal');
} }
export function SettingsModal () { export function SettingsModal () {
return import(/* webpackChunkName: "flavours/glitch/async/settings_modal" */'../../local_settings'); return import('../../local_settings');
} }
export function IgnoreNotificationsModal () { export function IgnoreNotificationsModal () {
return import(/* webpackChunkName: "flavours/glitch/async/ignore_notifications_modal" */'../components/ignore_notifications_modal'); return import('../components/ignore_notifications_modal');
} }
export function MediaGallery () { export function MediaGallery () {
return import(/* webpackChunkName: "flavours/glitch/async/media_gallery" */'../../../components/media_gallery'); return import('../../../components/media_gallery');
} }
export function Video () { export function Video () {
return import(/* webpackChunkName: "flavours/glitch/async/video" */'../../video'); return import('../../video');
} }
export function EmbedModal () { export function EmbedModal () {
return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'../components/embed_modal'); return import('../components/embed_modal');
} }
export function ListAdder () { export function ListAdder () {
return import(/* webpackChunkName: "features/glitch/async/list_adder" */'../../list_adder'); return import('../../list_adder');
} }
export function Tesseract () { export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js'); return import('tesseract.js');
} }
export function Audio () { export function Audio () {
return import(/* webpackChunkName: "features/glitch/async/audio" */'../../audio'); return import('../../audio');
} }
export function Directory () { export function Directory () {
return import(/* webpackChunkName: "features/glitch/async/directory" */'../../directory'); return import('../../directory');
} }
export function OnboardingProfile () { export function OnboardingProfile () {
return import(/* webpackChunkName: "features/glitch/async/onboarding" */'../../onboarding/profile'); return import('../../onboarding/profile');
} }
export function OnboardingFollows () { export function OnboardingFollows () {
return import(/* webpackChunkName: "features/glitch/async/onboarding" */'../../onboarding/follows'); return import('../../onboarding/follows');
} }
export function CompareHistoryModal () { export function CompareHistoryModal () {
return import(/*webpackChunkName: "flavours/glitch/async/compare_history_modal" */'../components/compare_history_modal'); return import('../components/compare_history_modal');
} }
export function Explore () { export function Explore () {
return import(/* webpackChunkName: "flavours/glitch/async/explore" */'../../explore'); return import('../../explore');
} }
export function Search () { export function Search () {
return import(/* webpackChunkName: "features/glitch/async/explore" */'../../search'); return import('../../search');
} }
export function FilterModal () { export function FilterModal () {
return import(/*webpackChunkName: "flavours/glitch/async/filter_modal" */'../components/filter_modal'); return import('../components/filter_modal');
} }
export function InteractionModal () { export function InteractionModal () {
return import(/*webpackChunkName: "flavours/glitch/async/modals/interaction_modal" */'../../interaction_modal'); return import('../../interaction_modal');
} }
export function SubscribedLanguagesModal () { export function SubscribedLanguagesModal () {
return import(/*webpackChunkName: "flavours/glitch/async/modals/subscribed_languages_modal" */'../../subscribed_languages_modal'); return import('../../subscribed_languages_modal');
} }
export function ClosedRegistrationsModal () { export function ClosedRegistrationsModal () {
return import(/*webpackChunkName: "flavours/glitch/async/modals/closed_registrations_modal" */'../../closed_registrations_modal'); return import('../../closed_registrations_modal');
} }
export function About () { export function About () {
return import(/*webpackChunkName: "features/glitch/async/about" */'../../about'); return import('../../about');
} }
export function PrivacyPolicy () { export function PrivacyPolicy () {
return import(/*webpackChunkName: "features/glitch/async/privacy_policy" */'../../privacy_policy'); return import('../../privacy_policy');
} }
export function TermsOfService () { export function TermsOfService () {
return import(/*webpackChunkName: "features/glitch/async/terms_of_service" */'../../terms_of_service'); return import('../../terms_of_service');
} }
export function NotificationRequests () { export function NotificationRequests () {
return import(/*webpackChunkName: "features/glitch/notifications/requests" */'../../notifications/requests'); return import('../../notifications/requests');
} }
export function NotificationRequest () { export function NotificationRequest () {
return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request'); return import('../../notifications/request');
} }
export function LinkTimeline () { export function LinkTimeline () {
return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline'); return import('../../link_timeline');
} }
export function AnnualReportModal () { export function AnnualReportModal () {
return import(/*webpackChunkName: "flavours/glitch/async/modals/annual_report_modal" */'../components/annual_report_modal'); return import('../components/annual_report_modal');
} }
export function ListEdit () { export function ListEdit () {
return import(/*webpackChunkName: "flavours/glitch/async/lists" */'../../lists/new'); return import('../../lists/new');
} }
export function ListMembers () { export function ListMembers () {
return import(/* webpackChunkName: "flavours/glitch/async/lists" */'../../lists/members'); return import('../../lists/members');
} }

View File

@ -350,8 +350,10 @@ export const Video: React.FC<{
const updateProgress = () => { const updateProgress = () => {
nextFrame = requestAnimationFrame(() => { nextFrame = requestAnimationFrame(() => {
if (videoRef.current) { if (videoRef.current) {
const progress =
videoRef.current.currentTime / videoRef.current.duration;
void api.start({ void api.start({
progress: `${(videoRef.current.currentTime / videoRef.current.duration) * 100}%`, progress: isNaN(progress) ? '0%' : `${progress * 100}%`,
immediate: reduceMotion, immediate: reduceMotion,
config: config.stiff, config: config.stiff,
}); });

View File

@ -3,7 +3,7 @@
// can at least log in using KaiOS devices). // can at least log in using KaiOS devices).
function importArrowKeyNavigation() { function importArrowKeyNavigation() {
return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation'); return import('arrow-key-navigation');
} }
export default function loadKeyboardExtensions() { export default function loadKeyboardExtensions() {

View File

@ -5,6 +5,13 @@ import { isLocaleLoaded, setLocale } from './global_locale';
const localeLoadingSemaphore = new Semaphore(1); const localeLoadingSemaphore = new Semaphore(1);
const upstreamLocaleFiles = import.meta.glob<{ default: LocaleData['messages'] }>([
'@/mastodon/locales/*.json',
]);
const localeFiles = import.meta.glob<{ default: LocaleData['messages'] }>([
'./*.json',
]);
export async function loadLocale() { export async function loadLocale() {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en'; const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';
@ -17,21 +24,23 @@ export async function loadLocale() {
// if the locale is already set, then do nothing // if the locale is already set, then do nothing
if (isLocaleLoaded()) return; if (isLocaleLoaded()) return;
const upstreamLocaleData = await import( // If there is no locale file, then fallback to english
/* webpackMode: "lazy" */ const upstreamLocaleFile = Object.hasOwn(upstreamLocaleFiles, `@/mastodon/locales/${locale}.json`)
/* webpackChunkName: "locales/vanilla/[request]" */ ? upstreamLocaleFiles[`/mastodon/locales/${locale}.json`]
/* webpackInclude: /\.json$/ */ : upstreamLocaleFiles['/mastodon/locales/en.json'];
/* webpackPreload: true */
`mastodon/locales/${locale}.json`
) as LocaleData['messages'];
const localeData = await import( if (!upstreamLocaleFile) throw new Error('Could not load the upstream locale JSON file');
/* webpackMode: "lazy" */
/* webpackChunkName: "locales/glitch/[request]" */ const { default: upstreamLocaleData } = await upstreamLocaleFile();
/* webpackInclude: /\.json$/ */
/* webpackPreload: true */ // If there is no locale file, then fallback to english
`flavours/glitch/locales/${locale}.json` const localeFile = Object.hasOwn(localeFiles, `./${locale}.json`)
) as LocaleData['messages']; ? localeFiles[`./${locale}.json`]
: localeFiles['./en.json'];
if (!localeFile) throw new Error('Could not load the locale JSON file');
const { default: localeData } = await localeFile();
setLocale({ messages: { ...upstreamLocaleData, ...localeData }, locale }); setLocale({ messages: { ...upstreamLocaleData, ...localeData }, locale });
}); });

View File

@ -7,17 +7,19 @@ import * as perf from 'flavours/glitch/performance';
import ready from 'flavours/glitch/ready'; import ready from 'flavours/glitch/ready';
import { store } from 'flavours/glitch/store'; import { store } from 'flavours/glitch/store';
import { isProduction } from './utils/environment'; import { isProduction, isDevelopment } from './utils/environment';
/**
* @returns {Promise<void>}
*/
function main() { function main() {
perf.start('main()'); perf.start('main()');
return ready(async () => { return ready(async () => {
const mountNode = document.getElementById('mastodon'); const mountNode = document.getElementById('mastodon');
const props = JSON.parse(mountNode.getAttribute('data-props')); if (!mountNode) {
throw new Error('Mount node not found');
}
const props = JSON.parse(
mountNode.getAttribute('data-props') ?? '{}',
) as Record<string, unknown>;
const root = createRoot(mountNode); const root = createRoot(mountNode);
root.render(<Mastodon {...props} />); root.render(<Mastodon {...props} />);
@ -25,8 +27,10 @@ function main() {
if (isProduction() && me && 'serviceWorker' in navigator) { if (isProduction() && me && 'serviceWorker' in navigator) {
const { Workbox } = await import('workbox-window'); const { Workbox } = await import('workbox-window');
const wb = new Workbox('/sw.js'); const wb = new Workbox(
/** @type {ServiceWorkerRegistration} */ isDevelopment() ? '/packs-dev/dev-sw.js?dev-sw' : '/sw.js',
{ type: 'module', scope: '/' },
);
let registration; let registration;
try { try {
@ -35,8 +39,14 @@ function main() {
console.error(err); console.error(err);
} }
if (registration && 'Notification' in window && Notification.permission === 'granted') { if (
const registerPushNotifications = await import('flavours/glitch/actions/push_notifications'); registration &&
'Notification' in window &&
Notification.permission === 'granted'
) {
const registerPushNotifications = await import(
'flavours/glitch/actions/push_notifications'
);
store.dispatch(registerPushNotifications.register()); store.dispatch(registerPushNotifications.register());
} }
@ -46,4 +56,5 @@ function main() {
}); });
} }
// eslint-disable-next-line import/no-default-export
export default main; export default main;

View File

@ -1,24 +1,11 @@
// //
// Tools for performance debugging, only enabled in development mode. // Tools for performance debugging, only enabled in development mode.
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output. // Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
// Also see config/webpack/loaders/mark.js for the webpack loader marks.
import * as marky from 'marky'; import * as marky from 'marky';
import { isDevelopment } from './utils/environment'; import { isDevelopment } from './utils/environment';
if (isDevelopment()) {
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
performance.setResourceTimingBufferSize(Infinity);
}
// allows us to easily do e.g. ReactPerf.printWasted() while debugging
//window.ReactPerf = require('react-addons-perf');
//window.ReactPerf.start();
}
export function start(name) { export function start(name) {
if (isDevelopment()) { if (isDevelopment()) {
marky.mark(name); marky.mark(name);

View File

@ -2,10 +2,13 @@
// If there are no polyfills, then this is just Promise.resolve() which means // If there are no polyfills, then this is just Promise.resolve() which means
// it will execute in the same tick of the event loop (i.e. near-instant). // it will execute in the same tick of the event loop (i.e. near-instant).
// eslint-disable-next-line import/extensions -- This file is virtual so it thinks it has an extension
import 'vite/modulepreload-polyfill';
import { loadIntlPolyfills } from './intl'; import { loadIntlPolyfills } from './intl';
function importExtraPolyfills() { function importExtraPolyfills() {
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills'); return import('./extra_polyfills');
} }
export function loadPolyfills() { export function loadPolyfills() {

View File

@ -54,11 +54,9 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
return; return;
} }
// Load the polyfill 1st BEFORE loading data // Load the polyfill 1st BEFORE loading data
await import('@formatjs/intl-pluralrules/polyfill-force');
await import( await import(
/* webpackChunkName: "i18n-pluralrules-polyfill" */ '@formatjs/intl-pluralrules/polyfill-force' `../../../../../node_modules/@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}.js`
);
await import(
/* webpackChunkName: "i18n-pluralrules-polyfill-[request]" */ `@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}`
); );
} }
@ -70,11 +68,9 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
// } // }
// // Load the polyfill 1st BEFORE loading data // // Load the polyfill 1st BEFORE loading data
// await import( // await import(
// /* webpackChunkName: "i18n-relativetimeformat-polyfill" */
// '@formatjs/intl-relativetimeformat/polyfill-force' // '@formatjs/intl-relativetimeformat/polyfill-force'
// ); // );
// await import( // await import(
// /* webpackChunkName: "i18n-relativetimeformat-polyfill-[request]" */
// `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}` // `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}`
// ); // );
// } // }

View File

@ -2,8 +2,8 @@
@use 'functions' as *; @use 'functions' as *;
// Commonly used web colors // Commonly used web colors
$black: #000000; // Black $black: #000; // Black
$white: #ffffff; // White $white: #fff; // White
$red-600: #b7253d !default; // Deep Carmine $red-600: #b7253d !default; // Deep Carmine
$red-500: #df405a !default; // Cerise $red-500: #df405a !default; // Cerise
$blurple-600: #563acc; // Iris $blurple-600: #563acc; // Iris

View File

@ -114,15 +114,14 @@ $content-width: 840px;
a { a {
font-size: 14px; font-size: 14px;
display: block; display: flex;
align-items: center;
gap: 6px;
padding: 15px; padding: 15px;
color: $darker-text-color; color: $darker-text-color;
text-decoration: none; text-decoration: none;
transition: all 200ms linear; transition: all 200ms linear;
transition-property: color, background-color; transition-property: color, background-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover { &:hover {
color: $primary-text-color; color: $primary-text-color;
@ -1126,7 +1125,7 @@ a.name-tag,
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 0; margin-bottom: 0;
word-break: break-word; overflow-wrap: anywhere;
} }
&__permissions { &__permissions {
@ -1149,6 +1148,15 @@ a.name-tag,
} }
} }
.rule-actions {
display: flex;
flex-direction: column;
a.table-action-link {
padding-inline-start: 0;
}
}
.dashboard__counters.admin-account-counters { .dashboard__counters.admin-account-counters {
margin-top: 10px; margin-top: 10px;
} }
@ -1451,8 +1459,8 @@ a.sparkline {
inset-inline-start: 10px; inset-inline-start: 10px;
border-radius: 4px; border-radius: 4px;
background: background:
url('~images/warning-stripes.svg') repeat-y left, url('@/images/warning-stripes.svg') repeat-y left,
url('~images/warning-stripes.svg') repeat-y right, url('@/images/warning-stripes.svg') repeat-y right,
var(--background-color); var(--background-color);
} }

View File

@ -2,8 +2,8 @@
@use 'mixins'; @use 'mixins';
@use 'variables'; @use 'variables';
@use 'css_variables'; @use 'css_variables';
@use 'styles/fonts/roboto'; @use '@/styles/fonts/roboto';
@use 'styles/fonts/roboto-mono'; @use '@/styles/fonts/roboto-mono';
@use 'reset'; @use 'reset';
@use 'basics'; @use 'basics';

View File

@ -641,7 +641,7 @@ body > [data-popper-placement] {
align-items: stretch; align-items: stretch;
&__border { &__border {
background: url('~images/warning-stripes.svg') repeat-y; background: url('@/images/warning-stripes.svg') repeat-y;
width: 5px; width: 5px;
flex: 0 0 auto; flex: 0 0 auto;
@ -1480,6 +1480,10 @@ body > [data-popper-placement] {
} }
} }
.status--has-quote .quote-inline {
display: none;
}
.status { .status {
padding: 10px 14px; // glitch: reduced padding padding: 10px 14px; // glitch: reduced padding
min-height: 54px; min-height: 54px;
@ -1550,8 +1554,12 @@ body > [data-popper-placement] {
} }
} }
&--is-quote {
border: none;
}
&--in-thread { &--in-thread {
$thread-margin: 46px + 10px; --thread-margin: calc(46px + 8px);
border-bottom: 0; border-bottom: 0;
@ -1567,16 +1575,16 @@ body > [data-popper-placement] {
.hashtag-bar, .hashtag-bar,
.content-warning, .content-warning,
.filter-warning { .filter-warning {
margin-inline-start: $thread-margin; margin-inline-start: var(--thread-margin);
width: calc(100% - $thread-margin); width: calc(100% - var(--thread-margin));
} }
.more-from-author { .more-from-author {
width: calc(100% - $thread-margin + 2px); width: calc(100% - var(--thread-margin) + 2px);
} }
.status__content__read-more-button { .status__content__read-more-button {
margin-inline-start: $thread-margin; margin-inline-start: var(--thread-margin);
} }
} }
@ -1939,6 +1947,81 @@ body > [data-popper-placement] {
} }
} }
.status__quote {
position: relative;
margin-block-start: 16px;
margin-inline-start: 36px;
border-radius: 8px;
color: var(--nested-card-text);
background: var(--nested-card-background);
border: var(--nested-card-border);
@media screen and (min-width: $mobile-breakpoint) {
margin-inline-start: 56px;
}
}
.status__quote--error {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
font-size: 15px;
}
.status__quote-author-button {
position: relative;
overflow: hidden;
display: inline-flex;
width: auto;
margin-block-start: 10px;
padding: 5px 12px;
align-items: center;
gap: 6px;
font-family: inherit;
font-size: 14px;
font-weight: 700;
line-height: normal;
letter-spacing: 0;
text-decoration: none;
color: $highlight-text-color;
background: var(--nested-card-background);
border: var(--nested-card-border);
border-radius: 4px;
&:active,
&:focus,
&:hover {
border-color: lighten($highlight-text-color, 4%);
color: lighten($highlight-text-color, 4%);
}
&:focus-visible {
outline: $ui-button-icon-focus-outline;
}
}
.status__quote-icon {
position: absolute;
inset-block-start: 18px;
inset-inline-start: -40px;
display: block;
width: 26px;
height: 26px;
padding: 5px;
color: #6a49ba;
z-index: 10;
.status__quote--error & {
inset-block-start: 50%;
transform: translateY(-50%);
}
@media screen and (min-width: $mobile-breakpoint) {
inset-inline-start: -50px;
}
}
.detailed-status__link { .detailed-status__link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -2235,14 +2318,18 @@ a .account__avatar {
.avatar-group { .avatar-group {
display: flex; display: flex;
gap: 8px;
flex-wrap: wrap; --avatar-height: 28px;
&:not(.avatar-group--compact) {
gap: 8px;
flex-wrap: wrap;
height: var(--avatar-height);
overflow-y: clip;
}
} }
.avatar-group--compact { .avatar-group--compact {
gap: 0;
flex-wrap: nowrap;
& > :not(:first-child) { & > :not(:first-child) {
margin-inline-start: -12px; margin-inline-start: -12px;
} }
@ -2371,11 +2458,6 @@ a.account__display-name {
} }
} }
.status__avatar {
width: 46px;
height: 46px;
}
.muted { .muted {
.status__content, .status__content,
.status__content p, .status__content p,
@ -10643,7 +10725,8 @@ noscript {
padding: 15px; padding: 15px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; flex-wrap: wrap;
gap: 4px 8px;
.logo { .logo {
width: 16px; width: 16px;
@ -10737,12 +10820,6 @@ noscript {
overflow: hidden; overflow: hidden;
container-type: inline-size; container-type: inline-size;
@container (width < 350px) {
&__header time {
display: none;
}
}
&__header { &__header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -10755,7 +10832,8 @@ noscript {
&__label { &__label {
display: flex; display: flex;
gap: 8px; flex-wrap: wrap;
gap: 2px 8px;
font-size: 15px; font-size: 15px;
line-height: 22px; line-height: 22px;
color: $darker-text-color; color: $darker-text-color;
@ -10773,6 +10851,13 @@ noscript {
time { time {
color: $dark-text-color; color: $dark-text-color;
} }
@container (width < 350px) {
time,
&-separator {
display: none;
}
}
} }
} }
@ -10830,6 +10915,7 @@ noscript {
line-height: 22px; line-height: 22px;
color: $darker-text-color; color: $darker-text-color;
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
line-clamp: 4;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
max-height: none; max-height: none;
overflow: hidden; overflow: hidden;
@ -10984,7 +11070,15 @@ noscript {
color: inherit; color: inherit;
} }
&__number { &__numbers,
&__familiar-followers {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
&__numbers {
font-size: 15px; font-size: 15px;
line-height: 22px; line-height: 22px;
color: $secondary-text-color; color: $secondary-text-color;
@ -11132,9 +11226,9 @@ noscript {
.content-warning { .content-warning {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
background: rgba($ui-highlight-color, 0.05); background: var(--nested-card-background);
color: $secondary-text-color; color: var(--nested-card-text);
border: 1px solid rgba($ui-highlight-color, 0.15); border: var(--nested-card-border);
border-radius: 8px; border-radius: 8px;
padding: 8px (5px + 8px); padding: 8px (5px + 8px);
position: relative; position: relative;

View File

@ -1,7 +1,7 @@
@use '../functions' as *; @use '../functions' as *;
// Dependent colors // Dependent colors
$black: #000000; $black: #000;
$classic-base-color: hsl(240deg, 16%, 19%); $classic-base-color: hsl(240deg, 16%, 19%);
$classic-primary-color: hsl(240deg, 29%, 70%); $classic-primary-color: hsl(240deg, 29%, 70%);

View File

@ -27,6 +27,10 @@
--rich-text-container-color: rgba(87, 24, 60, 100%); --rich-text-container-color: rgba(87, 24, 60, 100%);
--rich-text-text-color: rgba(255, 175, 212, 100%); --rich-text-text-color: rgba(255, 175, 212, 100%);
--rich-text-decorations-color: rgba(128, 58, 95, 100%); --rich-text-decorations-color: rgba(128, 58, 95, 100%);
--nested-card-background: color(from #{$ui-highlight-color} srgb r g b / 5%);
--nested-card-text: #{$secondary-text-color};
--nested-card-border: 1px solid
color(from #{$ui-highlight-color} srgb r g b / 15%);
--input-placeholder-color: #{$dark-text-color}; --input-placeholder-color: #{$dark-text-color};
--input-background-color: var(--surface-variant-background-color); --input-background-color: var(--surface-variant-background-color);
--on-input-color: #{$secondary-text-color}; --on-input-color: #{$secondary-text-color};

View File

@ -394,7 +394,7 @@ code {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: var(--avatar-border-radius); border-radius: var(--avatar-border-radius);
background: url('images/void.png'); background: url('@/images/void.png');
&[src$='missing.png'] { &[src$='missing.png'] {
visibility: hidden; visibility: hidden;
@ -1156,7 +1156,7 @@ code {
&__type { &__type {
color: $darker-text-color; color: $darker-text-color;
word-break: break-word; overflow-wrap: anywhere;
} }
} }

View File

@ -5,8 +5,8 @@
$lighten-multiplier: -1 $lighten-multiplier: -1
); );
$black: #000000; // Black $black: #000; // Black
$white: #ffffff; // White $white: #fff; // White
$blurple-500: #6364ff; // Brand purple $blurple-500: #6364ff; // Brand purple
$grey-600: hsl(240deg, 8%, 33%); // Trout $grey-600: hsl(240deg, 8%, 33%); // Trout
$grey-100: hsl(240deg, 51%, 90%); // Topaz $grey-100: hsl(240deg, 51%, 90%); // Topaz

View File

@ -44,7 +44,7 @@
content: ''; content: '';
width: 24px; width: 24px;
height: 20px; height: 20px;
mask-image: url('~images/quote.svg'); mask-image: url('@/images/quote.svg');
background-color: var(--rich-text-decorations-color); background-color: var(--rich-text-decorations-color);
position: absolute; position: absolute;
inset-inline-start: 0; inset-inline-start: 0;

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