Merge pull request #3074 from ClearlyClaire/glitch-soc/merge-upstream
[Breaking changes] Merge upstram changes up to a13756148d353c7479f68e65a210f6d88d26c785
This commit is contained in:
commit
c9c2fbb27d
@ -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
|
||||
|
||||
@ -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
|
||||
# ------------------
|
||||
|
||||
20
.github/renovate.json5
vendored
20
.github/renovate.json5
vendored
@ -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,
|
||||
|
||||
18
.github/workflows/test-ruby.yml
vendored
18
.github/workflows/test-ruby.yml
vendored
@ -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
3
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1 +1 @@
|
||||
3.4.3
|
||||
3.4.4
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
6
Gemfile
6
Gemfile
@ -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'
|
||||
|
||||
76
Gemfile.lock
76
Gemfile.lock
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
10
README.md
10
README.md
@ -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
|
||||
|
||||
4
app.json
4
app.json
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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!
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, **)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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';
|
||||
3
app/javascript/entrypoints/common.ts
Normal file
3
app/javascript/entrypoints/common.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { start } from 'mastodon/common';
|
||||
|
||||
start();
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import './public-path';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
ready(() => {
|
||||
|
||||
@ -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';
|
||||
@ -1,3 +0,0 @@
|
||||
import '../styles/mailer.scss';
|
||||
|
||||
require.context('../icons');
|
||||
@ -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,
|
||||
);
|
||||
@ -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) => {
|
||||
|
||||
@ -8,8 +8,6 @@ and performs no other task.
|
||||
|
||||
*/
|
||||
|
||||
import './public-path';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
interface JRDLink {
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import './public-path';
|
||||
import axios from 'axios';
|
||||
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
@ -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]));
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import Rails from '@rails/ujs';
|
||||
|
||||
export function start() {
|
||||
require.context('@/images/', true, /\.(jpg|png|svg)$/);
|
||||
|
||||
try {
|
||||
Rails.start();
|
||||
} catch {
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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 && (
|
||||
<>
|
||||
·
|
||||
<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} />
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
|
||||
185
app/javascript/flavours/glitch/components/status_quoted.tsx
Normal file
185
app/javascript/flavours/glitch/components/status_quoted.tsx
Normal 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} />;
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
3
app/javascript/flavours/glitch/entrypoints/common.ts
Normal file
3
app/javascript/flavours/glitch/entrypoints/common.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { start } from 'flavours/glitch/common';
|
||||
|
||||
start();
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import '@/entrypoints/public-path';
|
||||
import ready from 'flavours/glitch/ready';
|
||||
|
||||
ready(() => {
|
||||
|
||||
@ -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';
|
||||
@ -1,3 +0,0 @@
|
||||
import '@/styles/mailer.scss';
|
||||
|
||||
require.context('@/icons');
|
||||
@ -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) => {
|
||||
|
||||
@ -8,8 +8,6 @@ and performs no other task.
|
||||
|
||||
*/
|
||||
|
||||
import '@/entrypoints/public-path';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
interface JRDLink {
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import '@/entrypoints/public-path';
|
||||
import axios from 'axios';
|
||||
|
||||
import ready from 'flavours/glitch/ready';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
]),
|
||||
);
|
||||
@ -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<
|
||||
|
||||
@ -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<
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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'>
|
||||
·
|
||||
</span>
|
||||
<RelativeTimestamp timestamp={timestamp} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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}
|
||||
</>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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}`
|
||||
// );
|
||||
// }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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%);
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user