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:
RAILS_ENV: development
NODE_ENV: development
VITE_RUBY_HOST: 0.0.0.0
BIND: 0.0.0.0
BOOTSNAP_CACHE_DIR: /tmp
REDIS_HOST: redis
@ -22,11 +23,12 @@ services:
ES_PORT: '9200'
LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000
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.
command: sleep infinity
ports:
- '3000:3000'
- '3035:3035'
- '3036:3036'
- '4000:4000'
networks:
- 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)
# -------
SECRET_KEY_BASE=
OTP_SECRET=
# Encryption secrets
# ------------------

View File

@ -25,26 +25,12 @@
'tesseract.js', // 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
'history',
'react-router-dom',
// react-spring: Requires manual upgrade when upgrading react
'@react-spring/web',
],
matchUpdateTypes: ['major'],
dependencyDashboardApproval: true,

View File

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

3
.gitignore vendored
View File

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

2
.nvmrc
View File

@ -1 +1 @@
22.15
22.16

View File

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

View File

@ -70,13 +70,6 @@ Style/OptionalBooleanParameter:
- 'app/workers/domain_block_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).
Style/RedundantConstantBase:
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
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 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
[keepachangelog]: https://keepachangelog.com/en/1.0.0/
[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"]
# 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"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22"

View File

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

View File

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

View File

@ -7,7 +7,7 @@ module Admin
def index
authorize :rule, :index?
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
def new
@ -27,7 +27,6 @@ module Admin
if @rule.save
redirect_to admin_rules_path
else
@rules = Rule.ordered
render :new
end
end
@ -50,6 +49,22 @@ module Admin
redirect_to admin_rules_path
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
def set_rule
@ -58,7 +73,7 @@ module Admin
def resource_params
params
.expect(rule: [:text, :hint, :priority])
.expect(rule: [:text, :hint, :priority, translations_attributes: [[:id, :language, :text, :hint, :_destroy]]])
end
end
end

View File

@ -42,37 +42,22 @@ class Api::Fasp::BaseController < ApplicationController
end
def validate_signature!
signature_input = request.headers['signature-input']&.encode('UTF-8')
raise Error, 'signature-input is missing' if signature_input.blank?
raise Error, 'signature-input is missing' if request.headers['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
end
def sign_response
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)
signature = Linzer.sign(key, message, %w(@status content-digest))
response.headers.merge!(signature.to_h)
Linzer.sign!(response, key:, components: %w(@status content-digest))
end
def check_fasp_enabled

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ module ThemingConcern
def current_skin
@current_skin ||= begin
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

View File

@ -8,6 +8,7 @@ module WebAppControllerConcern
before_action :redirect_unauthenticated_to_permalinks!
before_action :set_referer_header
before_action :redirect_to_tos_interstitial!
content_security_policy do |p|
policy = ContentSecurityPolicy.new
@ -45,6 +46,13 @@ module WebAppControllerConcern
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
response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin')
end

View File

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

View File

@ -248,22 +248,8 @@ module ApplicationHelper
end
# glitch-soc addition to handle the multiple flavors
def preload_locale_pack
supported_locales = Themes.instance.flavour(current_flavour)['locales']
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 })
def flavoured_vite_typescript_tag(pack_name, **)
vite_typescript_tag("#{Themes.instance.flavour(current_flavour)['pack_directory'].delete_prefix('app/javascript/')}/#{pack_name}", **)
end
private

View File

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

View File

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

View File

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

View File

@ -1,11 +1,6 @@
import './public-path';
import { loadLocale } from 'mastodon/locales';
import main from 'mastodon/main';
import { start } from '../mastodon/common';
import { loadLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills';
start();
import { loadPolyfills } from 'mastodon/polyfills';
loadPolyfills()
.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 { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
import { start } from '../mastodon/common';
import { Status } from '../mastodon/features/standalone/status';
import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready';
start();
function loaded() {
const mountNode = document.getElementById('mastodon-status');

View File

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

View File

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

View File

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

View File

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

View File

@ -69,6 +69,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog);
}
if (status.quote?.quoted_status) {
processStatus(status.quote.quoted_status);
}
if (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) {
const normalStatus = { ...status };
normalStatus.account = status.account.id;
if (status.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) {
normalStatus.poll = status.poll.id;
}

View File

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

View File

@ -10,9 +10,12 @@ import {
unblockAccount,
muteAccount,
unmuteAccount,
followAccountSuccess,
} from 'flavours/glitch/actions/accounts';
import { showAlertForError } from 'flavours/glitch/actions/alerts';
import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { apiFollowAccount } from 'flavours/glitch/api/accounts';
import { Avatar } from 'flavours/glitch/components/avatar';
import { Button } from 'flavours/glitch/components/button';
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 { Skeleton } from 'flavours/glitch/components/skeleton';
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 { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
@ -67,10 +72,12 @@ export const Account: React.FC<{
withBio?: boolean;
}> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => {
const intl = useIntl();
const { signedIn } = useIdentity();
const account = useAppSelector((state) => state.accounts.get(id));
const relationship = useAppSelector((state) => state.relationships.get(id));
const dispatch = useAppDispatch();
const accountUrl = account?.url;
const isRemote = account?.acct !== account?.username;
const handleBlock = useCallback(() => {
if (relationship?.blocking) {
@ -113,37 +120,74 @@ export const Account: React.FC<{
},
];
} else if (defaultAction !== 'block') {
const handleAddToLists = () => {
dispatch(
openModal({
modalType: 'LIST_ADDER',
modalProps: {
accountId: id,
},
}),
);
};
arr = [];
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),
action: handleAddToLists,
},
];
if (accountUrl) {
arr.unshift(
{
text: intl.formatMessage(messages.openOriginalPage),
href: accountUrl,
},
null,
);
});
}
}
return arr;
}, [dispatch, intl, id, accountUrl, relationship, defaultAction]);
}, [
dispatch,
intl,
id,
accountUrl,
relationship,
defaultAction,
isRemote,
signedIn,
]);
if (hidden) {
return (

View File

@ -7,10 +7,16 @@ import classNames from 'classnames';
export const AvatarGroup: React.FC<{
compact?: boolean;
avatarHeight?: number;
children: React.ReactNode;
}> = ({ children, compact = false }) => (
}> = ({ children, compact = false, avatarHeight }) => (
<div
className={classNames('avatar-group', { 'avatar-group--compact': compact })}
style={
avatarHeight
? ({ '--avatar-height': `${avatarHeight}px` } as React.CSSProperties)
: undefined
}
>
{children}
</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 { AccountFields } from 'flavours/glitch/components/account_fields';
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 { FollowButton } from 'flavours/glitch/components/follow_button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { Permalink } from 'flavours/glitch/components/permalink';
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 { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
@ -38,6 +43,8 @@ export const HoverCardAccount = forwardRef<
}
}, [dispatch, accountId, account]);
const { familiarFollowers } = useFetchFamiliarFollowers({ accountId });
return (
<div
ref={ref}
@ -77,11 +84,27 @@ export const HoverCardAccount = forwardRef<
)}
</div>
<div className='hover-card__number'>
<div className='hover-card__numbers'>
<ShortNumber
value={account.followers_count}
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>
<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,
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.record,
children: PropTypes.node,
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
@ -111,6 +112,7 @@ class Status extends ImmutablePureComponent {
withDismiss: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
isQuotedPost: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
expanded: PropTypes.bool,
@ -440,7 +442,7 @@ class Status extends ImmutablePureComponent {
}
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 {
status,
@ -452,6 +454,7 @@ class Status extends ImmutablePureComponent {
onOpenMedia,
notification,
history,
isQuotedPost,
...other
} = this.props;
let attachments = null;
@ -618,7 +621,7 @@ class Status extends ImmutablePureComponent {
);
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(
<Card
onOpenMedia={this.handleOpenMedia}
@ -684,7 +687,17 @@ class Status extends ImmutablePureComponent {
{!skipPrepend && prepend}
<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')}
>
{(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}
/>
{children}
{media}
{hashtagBar}
</>
@ -730,13 +745,15 @@ class Status extends ImmutablePureComponent {
{/* This is a glitch-soc addition to have a placeholder */}
{!expanded && <MentionsPlaceholder status={status} />}
<StatusActionBar
status={status}
account={status.get('account')}
showReplyCount={settings.get('show_reply_count')}
onFilter={matchedFilters ? this.handleFilterClick : null}
{...other}
/>
{!isQuotedPost &&
<StatusActionBar
status={status}
account={status.get('account')}
showReplyCount={settings.get('show_reply_count')}
onFilter={matchedFilters ? this.handleFilterClick : null}
{...other}
/>
}
</div>
</div>
</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 VisibilityIcon from '@/material-icons/400-24px/visibility.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
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 { 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 ScrollableList from './scrollable_list';
@ -114,7 +114,7 @@ export default class StatusList extends ImmutablePureComponent {
);
default:
return (
<StatusContainer
<StatusQuoteManager
key={statusId}
id={statusId}
onMoveUp={this.handleMoveUp}
@ -130,7 +130,7 @@ export default class StatusList extends ImmutablePureComponent {
if (scrollableContent && featuredStatusIds) {
scrollableContent = featuredStatusIds.map(statusId => (
<StatusContainer
<StatusQuoteManager
key={`f-${statusId}`}
id={statusId}
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 Rails from '@rails/ujs';
@ -273,7 +272,7 @@ async function mountReactComponent(element: Element) {
);
const { default: Component } = (await import(
`@/flavours/glitch/components/admin/${componentName}`
`@/flavours/glitch/components/admin/${componentName}.jsx`
)) as { default: React.ComponentType };
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 main from 'flavours/glitch/main';
import { loadPolyfills } from 'flavours/glitch/polyfills';
start();
loadPolyfills()
.then(loadLocale)
.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 '@/entrypoints/public-path';
import { start } from 'flavours/glitch/common';
import { Status } from 'flavours/glitch/features/standalone/status';
import { afterInitialRender } from 'flavours/glitch/hooks/useRenderSignal';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready';
start();
function loaded() {
const mountNode = document.getElementById('mastodon-status');

View File

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

View File

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

View File

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

View File

@ -44,6 +44,7 @@ const severityMessages = {
const mapStateToProps = state => ({
server: state.getIn(['server', 'server']),
locale: state.getIn(['meta', 'locale']),
extendedDescription: state.getIn(['server', 'extendedDescription']),
domainBlocks: state.getIn(['server', 'domainBlocks']),
});
@ -91,6 +92,7 @@ class About extends PureComponent {
static propTypes = {
server: ImmutablePropTypes.map,
locale: ImmutablePropTypes.string,
extendedDescription: ImmutablePropTypes.map,
domainBlocks: ImmutablePropTypes.contains({
isLoading: PropTypes.bool,
@ -114,7 +116,7 @@ class About extends PureComponent {
};
render () {
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
const { multiColumn, intl, server, extendedDescription, domainBlocks, locale } = this.props;
const isLoading = server.get('isLoading');
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>
) : (
<ol className='rules-list'>
{server.get('rules').map(rule => (
<li key={rule.get('id')}>
<div className='rules-list__text'>{rule.get('text')}</div>
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
</li>
))}
{server.get('rules').map(rule => {
const text = rule.getIn(['translations', locale, 'text']) || rule.getIn(['translations', locale.split('-')[0], 'text']) || rule.get('text');
const hint = rule.getIn(['translations', locale, 'hint']) || rule.getIn(['translations', locale.split('-')[0], 'hint']) || rule.get('hint');
return (
<li key={rule.get('id')}>
<div className='rules-list__text'>{text}</div>
{hint.length > 0 && (<div className='rules-list__hint'>{hint}</div>)}
</li>
)})}
</ol>
))}
</Section>

View File

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

View File

@ -1,15 +1,12 @@
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { fetchAccountsFamiliarFollowers } from '@/flavours/glitch/actions/accounts_familiar_followers';
import { Avatar } from '@/flavours/glitch/components/avatar';
import { AvatarGroup } from '@/flavours/glitch/components/avatar_group';
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 }) => {
if (!account) {
@ -64,20 +61,11 @@ const FamiliarFollowersReadout: React.FC<{ familiarFollowers: Account[] }> = ({
export const FamiliarFollowers: React.FC<{ accountId: string }> = ({
accountId,
}) => {
const dispatch = useAppDispatch();
const familiarFollowers = useAppSelector((state) =>
getAccountFamiliarFollowers(state, accountId),
);
const { familiarFollowers, isLoading } = useFetchFamiliarFollowers({
accountId,
});
const hasNoData = familiarFollowers === null;
useEffect(() => {
if (hasNoData) {
void dispatch(fetchAccountsFamiliarFollowers({ id: accountId }));
}
}, [dispatch, accountId, hasNoData]);
if (hasNoData || familiarFollowers.length === 0) {
if (isLoading || familiarFollowers.length === 0) {
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 Textarea from 'react-textarea-autosize';
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 { uploadThumbnail } from 'flavours/glitch/actions/compose';
@ -350,9 +346,15 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
fetchTesseract()
.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, {
workerPath: tesseractWorkerPath as string,
corePath: tesseractCorePath as string,
workerPath: tesseractWorkerPath.default,
corePath: tesseractCorePath.default,
langPath: `${assetHost}/ocr/lang-data`,
cacheMethod: 'write',
});
@ -501,5 +503,4 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
);
},
);
AltTextModal.displayName = 'AltTextModal';

View File

@ -9,28 +9,27 @@
// to ensure that the prevaled file is regenerated by Babel
// version: 4
const { NimbleEmojiIndex } = require('emoji-mart');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
import { NimbleEmojiIndex } from 'emoji-mart';
import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
let data = require('./emoji_data.json');
const emojiMap = require('./emoji_map.json');
const { unicodeToFilename } = require('./unicode_to_filename');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
import data from './emoji_data.json';
import emojiMap from './emoji_map.json';
import { unicodeToFilename } from './unicode_to_filename';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
emojiMartUncompress(data);
const emojiMartData = data;
const emojiIndex = new NimbleEmojiIndex(emojiMartData);
const excluded = ['®', '©', '™'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {};
const excluded = ['®', '©', '™'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {};
const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = [];
Object.keys(emojiIndex.emojis).forEach(key => {
Object.keys(emojiIndex.emojis).forEach((key) => {
let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this
@ -41,22 +40,22 @@ Object.keys(emojiIndex.emojis).forEach(key => {
shortcodeMap[emoji.native] = emoji.id;
});
const stripModifiers = unicode => {
skinTones.forEach(tone => {
const stripModifiers = (unicode) => {
skinTones.forEach((tone) => {
unicode = unicode.replace(tone, '');
});
return unicode;
};
Object.keys(emojiMap).forEach(key => {
Object.keys(emojiMap).forEach((key) => {
if (excluded.includes(key)) {
delete emojiMap[key];
return;
}
const normalizedKey = stripModifiers(key);
let shortcode = shortcodeMap[normalizedKey];
let shortcode = shortcodeMap[normalizedKey];
if (!shortcode) {
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];
// 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];
if (short_names[0] !== key) {
throw new Error('The compressor expects the first short_code to be the ' +
'key. It may need to be rewritten if the emoji change such that this ' +
'is no longer the case.');
throw new Error(
'The compressor expects the first short_code to be the ' +
'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
@ -117,20 +118,22 @@ Object.keys(emojiIndex.emojis).forEach(key => {
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
// inconsistent behavior in dev mode
module.exports = JSON.parse(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
* that expect the presence of `skins` property.
* Currently, no definitions or references to `skins` property can be found in:
* - {@link node_modules/emoji-mart/dist/utils/data.js}
* - {@link node_modules/emoji-mart/data/all.json}
* - {@link app/javascript/mastodon/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.aliases,
emojisWithoutShortCodes
]));
export default JSON.parse(
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
* that expect the presence of `skins` property.
* Currently, no definitions or references to `skins` property can be found in:
* - {@link node_modules/emoji-mart/dist/utils/data.js}
* - {@link node_modules/emoji-mart/data/all.json}
* - {@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.aliases,
emojisWithoutShortCodes,
]),
);

View File

@ -3,9 +3,13 @@
// emojiIndex.search functionality.
import type { BaseEmoji } from 'emoji-mart';
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';
type Emojis = Record<

View File

@ -2,11 +2,13 @@
// (i.e. the svg filename) and a shortCode intended to be shown
// 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 {
FilenameData,
ShortCodesToEmojiDataKey,
} from './emoji_compressed';
import emojiCompressed from './emoji_compressed';
} from 'virtual:mastodon-emoji-compressed';
import { unicodeToFilename } from './unicode_to_filename';
type UnicodeMapping = Record<

View File

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

View File

@ -6,7 +6,7 @@ function padLeft(str, num) {
return str;
}
exports.unicodeToUnifiedName = (str) => {
export const unicodeToUnifiedName = (str) => {
let output = '';
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 { Icon } from 'flavours/glitch/components/icon';
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 FollowRequestContainer from '../containers/follow_request_container';
@ -159,7 +159,7 @@ class Notification extends ImmutablePureComponent {
renderMention (notification) {
return (
<StatusContainer
<StatusQuoteManager
id={notification.get('status')}
containerId={notification.get('id')}
withDismiss
@ -181,7 +181,7 @@ class Notification extends ImmutablePureComponent {
renderFavourite (notification) {
return (
<StatusContainer
<StatusQuoteManager
containerId={notification.get('id')}
hidden={!!this.props.hidden}
id={notification.get('status')}
@ -206,7 +206,7 @@ class Notification extends ImmutablePureComponent {
renderReblog (notification) {
return (
<StatusContainer
<StatusQuoteManager
containerId={notification.get('id')}
hidden={!!this.props.hidden}
id={notification.get('status')}
@ -231,7 +231,7 @@ class Notification extends ImmutablePureComponent {
renderStatus (notification) {
return (
<StatusContainer
<StatusQuoteManager
containerId={notification.get('id')}
hidden={!!this.props.hidden}
id={notification.get('status')}
@ -256,7 +256,7 @@ class Notification extends ImmutablePureComponent {
renderUpdate (notification) {
return (
<StatusContainer
<StatusQuoteManager
containerId={notification.get('id')}
hidden={!!this.props.hidden}
id={notification.get('status')}
@ -281,7 +281,7 @@ class Notification extends ImmutablePureComponent {
renderPoll (notification) {
return (
<StatusContainer
<StatusQuoteManager
containerId={notification.get('id')}
hidden={!!this.props.hidden}
id={notification.get('status')}

View File

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

View File

@ -15,7 +15,7 @@ import {
} from 'flavours/glitch/actions/statuses';
import type { IconProp } 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 { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
@ -106,8 +106,7 @@ export const NotificationWithStatus: React.FC<{
{label}
</div>
<Status
// @ts-expect-error -- <Status> is not yet typed
<StatusQuoteManager
id={statusId}
contextType='notifications'
withDismiss

View File

@ -12,6 +12,7 @@ import Option from './components/option';
const mapStateToProps = state => ({
rules: state.getIn(['server', 'server', 'rules']),
locale: state.getIn(['meta', 'locale']),
});
class Rules extends PureComponent {
@ -19,6 +20,7 @@ class Rules extends PureComponent {
static propTypes = {
onNextStep: PropTypes.func.isRequired,
rules: ImmutablePropTypes.list,
locale: PropTypes.string,
selectedRuleIds: ImmutablePropTypes.set.isRequired,
onToggle: PropTypes.func.isRequired,
};
@ -34,7 +36,7 @@ class Rules extends PureComponent {
};
render () {
const { rules, selectedRuleIds } = this.props;
const { rules, locale, selectedRuleIds } = this.props;
return (
<>
@ -49,7 +51,7 @@ class Rules extends PureComponent {
value={item.get('id')}
checked={selectedRuleIds.includes(item.get('id'))}
onToggle={this.handleRulesToggle}
label={item.get('text')}
label={item.getIn(['translations', locale, 'text']) || item.getIn(['translations', locale.split('-')[0], 'text']) || item.get('text')}
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 { Icon } from 'flavours/glitch/components/icon';
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 { useSearchParam } from 'flavours/glitch/hooks/useSearchParam';
import type { Hashtag as HashtagType } from 'flavours/glitch/models/tags';
@ -53,8 +53,7 @@ const renderHashtags = (hashtags: HashtagType[]) =>
const renderStatuses = (statusIds: string[]) =>
hidePeek<string>(statusIds).map((id) => (
// @ts-expect-error inferred props are wrong
<Status key={id} id={id} />
<StatusQuoteManager key={id} id={id} />
));
type SearchType = 'all' | ApiSearchType;
@ -190,8 +189,7 @@ export const SearchResults: React.FC<{ multiColumn: boolean }> = ({
onClickMore={handleSelectStatuses}
>
{results.statuses.slice(0, INITIAL_DISPLAY).map((id) => (
// @ts-expect-error inferred props are wrong
<Status key={id} id={id} />
<StatusQuoteManager key={id} id={id} />
))}
</SearchSection>
)}

View File

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

View File

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

View File

@ -1,243 +1,243 @@
export function EmojiPicker () {
return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'../../emoji/emoji_picker');
return import('../../emoji/emoji_picker');
}
export function Compose () {
return import(/* webpackChunkName: "flavours/glitch/async/compose" */'../../compose');
return import('../../compose');
}
export function Notifications () {
return import(/* webpackChunkName: "flavours/glitch/async/notifications" */'../../notifications_v2');
return import('../../notifications_v2');
}
export function HomeTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/home_timeline" */'../../home_timeline');
return import('../../home_timeline');
}
export function PublicTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/public_timeline" */'../../public_timeline');
return import('../../public_timeline');
}
export function CommunityTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'../../community_timeline');
return import('../../community_timeline');
}
export function Firehose () {
return import(/* webpackChunkName: "flavours/glitch/async/firehose" */'../../firehose');
return import('../../firehose');
}
export function HashtagTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'../../hashtag_timeline');
return import('../../hashtag_timeline');
}
export function ListTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/list_timeline" */'../../list_timeline');
return import('../../list_timeline');
}
export function Lists () {
return import(/* webpackChunkName: "flavours/glitch/async/lists" */'../../lists');
return import('../../lists');
}
export function DirectTimeline() {
return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'../../direct_timeline');
return import('../../direct_timeline');
}
export function Status () {
return import(/* webpackChunkName: "flavours/glitch/async/status" */'../../status');
return import('../../status');
}
export function GettingStarted () {
return import(/* webpackChunkName: "flavours/glitch/async/getting_started" */'../../getting_started');
return import('../../getting_started');
}
export function GettingStartedMisc () {
return import(/* webpackChunkName: "flavours/glitch/async/getting_started_misc" */'../../getting_started_misc');
return import('../../getting_started_misc');
}
export function KeyboardShortcuts () {
return import(/* webpackChunkName: "flavours/glitch/async/keyboard_shortcuts" */'../../keyboard_shortcuts');
return import('../../keyboard_shortcuts');
}
export function PinnedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/pinned_statuses" */'../../pinned_statuses');
return import('../../pinned_statuses');
}
export function AccountTimeline () {
return import(/* webpackChunkName: "flavours/glitch/async/account_timeline" */'../../account_timeline');
return import('../../account_timeline');
}
export function AccountGallery () {
return import(/* webpackChunkName: "flavours/glitch/async/account_gallery" */'../../account_gallery');
return import('../../account_gallery');
}
export function AccountFeatured() {
return import(/* webpackChunkName: "flavours/glitch/async/account_featured" */'../../account_featured');
return import('../../account_featured');
}
export function Followers () {
return import(/* webpackChunkName: "flavours/glitch/async/followers" */'../../followers');
return import('../../followers');
}
export function Following () {
return import(/* webpackChunkName: "flavours/glitch/async/following" */'../../following');
return import('../../following');
}
export function Reblogs () {
return import(/* webpackChunkName: "flavours/glitch/async/reblogs" */'../../reblogs');
return import('../../reblogs');
}
export function Favourites () {
return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'../../favourites');
return import('../../favourites');
}
export function FollowRequests () {
return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'../../follow_requests');
return import('../../follow_requests');
}
export function FavouritedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'../../favourited_statuses');
return import('../../favourited_statuses');
}
export function FollowedTags () {
return import(/* webpackChunkName: "flavours/glitch/async/followed_tags" */'../../followed_tags');
return import('../../followed_tags');
}
export function BookmarkedStatuses () {
return import(/* webpackChunkName: "flavours/glitch/async/bookmarked_statuses" */'../../bookmarked_statuses');
return import('../../bookmarked_statuses');
}
export function Blocks () {
return import(/* webpackChunkName: "flavours/glitch/async/blocks" */'../../blocks');
return import('../../blocks');
}
export function DomainBlocks () {
return import(/* webpackChunkName: "flavours/glitch/async/domain_blocks" */'../../domain_blocks');
return import('../../domain_blocks');
}
export function Mutes () {
return import(/* webpackChunkName: "flavours/glitch/async/mutes" */'../../mutes');
return import('../../mutes');
}
export function MuteModal () {
return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'../components/mute_modal');
return import('../components/mute_modal');
}
export function BlockModal () {
return import(/* webpackChunkName: "flavours/glitch/async/block_modal" */'../components/block_modal');
return import('../components/block_modal');
}
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 () {
return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'../components/report_modal');
return import('../components/report_modal');
}
export function SettingsModal () {
return import(/* webpackChunkName: "flavours/glitch/async/settings_modal" */'../../local_settings');
return import('../../local_settings');
}
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 () {
return import(/* webpackChunkName: "flavours/glitch/async/media_gallery" */'../../../components/media_gallery');
return import('../../../components/media_gallery');
}
export function Video () {
return import(/* webpackChunkName: "flavours/glitch/async/video" */'../../video');
return import('../../video');
}
export function EmbedModal () {
return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'../components/embed_modal');
return import('../components/embed_modal');
}
export function ListAdder () {
return import(/* webpackChunkName: "features/glitch/async/list_adder" */'../../list_adder');
return import('../../list_adder');
}
export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
return import('tesseract.js');
}
export function Audio () {
return import(/* webpackChunkName: "features/glitch/async/audio" */'../../audio');
return import('../../audio');
}
export function Directory () {
return import(/* webpackChunkName: "features/glitch/async/directory" */'../../directory');
return import('../../directory');
}
export function OnboardingProfile () {
return import(/* webpackChunkName: "features/glitch/async/onboarding" */'../../onboarding/profile');
return import('../../onboarding/profile');
}
export function OnboardingFollows () {
return import(/* webpackChunkName: "features/glitch/async/onboarding" */'../../onboarding/follows');
return import('../../onboarding/follows');
}
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 () {
return import(/* webpackChunkName: "flavours/glitch/async/explore" */'../../explore');
return import('../../explore');
}
export function Search () {
return import(/* webpackChunkName: "features/glitch/async/explore" */'../../search');
return import('../../search');
}
export function FilterModal () {
return import(/*webpackChunkName: "flavours/glitch/async/filter_modal" */'../components/filter_modal');
return import('../components/filter_modal');
}
export function InteractionModal () {
return import(/*webpackChunkName: "flavours/glitch/async/modals/interaction_modal" */'../../interaction_modal');
return import('../../interaction_modal');
}
export function SubscribedLanguagesModal () {
return import(/*webpackChunkName: "flavours/glitch/async/modals/subscribed_languages_modal" */'../../subscribed_languages_modal');
return import('../../subscribed_languages_modal');
}
export function ClosedRegistrationsModal () {
return import(/*webpackChunkName: "flavours/glitch/async/modals/closed_registrations_modal" */'../../closed_registrations_modal');
return import('../../closed_registrations_modal');
}
export function About () {
return import(/*webpackChunkName: "features/glitch/async/about" */'../../about');
return import('../../about');
}
export function PrivacyPolicy () {
return import(/*webpackChunkName: "features/glitch/async/privacy_policy" */'../../privacy_policy');
return import('../../privacy_policy');
}
export function TermsOfService () {
return import(/*webpackChunkName: "features/glitch/async/terms_of_service" */'../../terms_of_service');
return import('../../terms_of_service');
}
export function NotificationRequests () {
return import(/*webpackChunkName: "features/glitch/notifications/requests" */'../../notifications/requests');
return import('../../notifications/requests');
}
export function NotificationRequest () {
return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request');
return import('../../notifications/request');
}
export function LinkTimeline () {
return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline');
return import('../../link_timeline');
}
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 () {
return import(/*webpackChunkName: "flavours/glitch/async/lists" */'../../lists/new');
return import('../../lists/new');
}
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 = () => {
nextFrame = requestAnimationFrame(() => {
if (videoRef.current) {
const progress =
videoRef.current.currentTime / videoRef.current.duration;
void api.start({
progress: `${(videoRef.current.currentTime / videoRef.current.duration) * 100}%`,
progress: isNaN(progress) ? '0%' : `${progress * 100}%`,
immediate: reduceMotion,
config: config.stiff,
});

View File

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

View File

@ -5,6 +5,13 @@ import { isLocaleLoaded, setLocale } from './global_locale';
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() {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
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 (isLocaleLoaded()) return;
const upstreamLocaleData = await import(
/* webpackMode: "lazy" */
/* webpackChunkName: "locales/vanilla/[request]" */
/* webpackInclude: /\.json$/ */
/* webpackPreload: true */
`mastodon/locales/${locale}.json`
) as LocaleData['messages'];
// If there is no locale file, then fallback to english
const upstreamLocaleFile = Object.hasOwn(upstreamLocaleFiles, `@/mastodon/locales/${locale}.json`)
? upstreamLocaleFiles[`/mastodon/locales/${locale}.json`]
: upstreamLocaleFiles['/mastodon/locales/en.json'];
const localeData = await import(
/* webpackMode: "lazy" */
/* webpackChunkName: "locales/glitch/[request]" */
/* webpackInclude: /\.json$/ */
/* webpackPreload: true */
`flavours/glitch/locales/${locale}.json`
) as LocaleData['messages'];
if (!upstreamLocaleFile) throw new Error('Could not load the upstream locale JSON file');
const { default: upstreamLocaleData } = await upstreamLocaleFile();
// If there is no locale file, then fallback to english
const localeFile = Object.hasOwn(localeFiles, `./${locale}.json`)
? 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 });
});

View File

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

View File

@ -1,24 +1,11 @@
//
// Tools for performance debugging, only enabled in development mode.
// 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 { 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) {
if (isDevelopment()) {
marky.mark(name);

View File

@ -2,10 +2,13 @@
// 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).
// 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';
function importExtraPolyfills() {
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
return import('./extra_polyfills');
}
export function loadPolyfills() {

View File

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

View File

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

View File

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

View File

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

View File

@ -641,7 +641,7 @@ body > [data-popper-placement] {
align-items: stretch;
&__border {
background: url('~images/warning-stripes.svg') repeat-y;
background: url('@/images/warning-stripes.svg') repeat-y;
width: 5px;
flex: 0 0 auto;
@ -1480,6 +1480,10 @@ body > [data-popper-placement] {
}
}
.status--has-quote .quote-inline {
display: none;
}
.status {
padding: 10px 14px; // glitch: reduced padding
min-height: 54px;
@ -1550,8 +1554,12 @@ body > [data-popper-placement] {
}
}
&--is-quote {
border: none;
}
&--in-thread {
$thread-margin: 46px + 10px;
--thread-margin: calc(46px + 8px);
border-bottom: 0;
@ -1567,16 +1575,16 @@ body > [data-popper-placement] {
.hashtag-bar,
.content-warning,
.filter-warning {
margin-inline-start: $thread-margin;
width: calc(100% - $thread-margin);
margin-inline-start: var(--thread-margin);
width: calc(100% - var(--thread-margin));
}
.more-from-author {
width: calc(100% - $thread-margin + 2px);
width: calc(100% - var(--thread-margin) + 2px);
}
.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 {
display: inline-flex;
align-items: center;
@ -2235,14 +2318,18 @@ a .account__avatar {
.avatar-group {
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 {
gap: 0;
flex-wrap: nowrap;
& > :not(:first-child) {
margin-inline-start: -12px;
}
@ -2371,11 +2458,6 @@ a.account__display-name {
}
}
.status__avatar {
width: 46px;
height: 46px;
}
.muted {
.status__content,
.status__content p,
@ -10643,7 +10725,8 @@ noscript {
padding: 15px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
gap: 4px 8px;
.logo {
width: 16px;
@ -10737,12 +10820,6 @@ noscript {
overflow: hidden;
container-type: inline-size;
@container (width < 350px) {
&__header time {
display: none;
}
}
&__header {
display: flex;
flex-direction: column;
@ -10755,7 +10832,8 @@ noscript {
&__label {
display: flex;
gap: 8px;
flex-wrap: wrap;
gap: 2px 8px;
font-size: 15px;
line-height: 22px;
color: $darker-text-color;
@ -10773,6 +10851,13 @@ noscript {
time {
color: $dark-text-color;
}
@container (width < 350px) {
time,
&-separator {
display: none;
}
}
}
}
@ -10830,6 +10915,7 @@ noscript {
line-height: 22px;
color: $darker-text-color;
-webkit-line-clamp: 4;
line-clamp: 4;
-webkit-box-orient: vertical;
max-height: none;
overflow: hidden;
@ -10984,7 +11070,15 @@ noscript {
color: inherit;
}
&__number {
&__numbers,
&__familiar-followers {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
&__numbers {
font-size: 15px;
line-height: 22px;
color: $secondary-text-color;
@ -11132,9 +11226,9 @@ noscript {
.content-warning {
display: block;
box-sizing: border-box;
background: rgba($ui-highlight-color, 0.05);
color: $secondary-text-color;
border: 1px solid rgba($ui-highlight-color, 0.15);
background: var(--nested-card-background);
color: var(--nested-card-text);
border: var(--nested-card-border);
border-radius: 8px;
padding: 8px (5px + 8px);
position: relative;

View File

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

View File

@ -27,6 +27,10 @@
--rich-text-container-color: rgba(87, 24, 60, 100%);
--rich-text-text-color: rgba(255, 175, 212, 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-background-color: var(--surface-variant-background-color);
--on-input-color: #{$secondary-text-color};

View File

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

View File

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

View File

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

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