mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-11 20:21:10 +00:00
Compare commits
15 Commits
77a8ef2654
...
866aeeeb4e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
866aeeeb4e | ||
![]() |
f2b85d4696 | ||
![]() |
b68f93fc47 | ||
![]() |
653d8965c6 | ||
![]() |
ccc22cce76 | ||
![]() |
3475cfec36 | ||
![]() |
c07b0db3ab | ||
![]() |
14120381e2 | ||
![]() |
823cd8fb4d | ||
![]() |
698962c185 | ||
![]() |
fbe9728f36 | ||
![]() |
3bbf3e9709 | ||
![]() |
79931bf3ae | ||
![]() |
22e2e7f02b | ||
![]() |
41d00bc28b |
|
@ -9,6 +9,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
RAILS_ENV: development
|
RAILS_ENV: development
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
|
VITE_RUBY_HOST: 0.0.0.0
|
||||||
BIND: 0.0.0.0
|
BIND: 0.0.0.0
|
||||||
BOOTSNAP_CACHE_DIR: /tmp
|
BOOTSNAP_CACHE_DIR: /tmp
|
||||||
REDIS_HOST: redis
|
REDIS_HOST: redis
|
||||||
|
@ -27,6 +28,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
- '3035:3035'
|
- '3035:3035'
|
||||||
|
- '3036:3036'
|
||||||
- '4000:4000'
|
- '4000:4000'
|
||||||
networks:
|
networks:
|
||||||
- external_network
|
- external_network
|
||||||
|
|
17
.github/renovate.json5
vendored
17
.github/renovate.json5
vendored
|
@ -25,23 +25,6 @@
|
||||||
'tesseract.js', // Requires code changes
|
'tesseract.js', // Requires code changes
|
||||||
'react-hotkeys', // Requires code changes
|
'react-hotkeys', // Requires code changes
|
||||||
|
|
||||||
// Requires Webpacker upgrade or replacement
|
|
||||||
'@svgr/webpack',
|
|
||||||
'@types/webpack',
|
|
||||||
'babel-loader',
|
|
||||||
'compression-webpack-plugin',
|
|
||||||
'css-loader',
|
|
||||||
'imports-loader',
|
|
||||||
'mini-css-extract-plugin',
|
|
||||||
'postcss-loader',
|
|
||||||
'sass-loader',
|
|
||||||
'terser-webpack-plugin',
|
|
||||||
'webpack',
|
|
||||||
'webpack-assets-manifest',
|
|
||||||
'webpack-bundle-analyzer',
|
|
||||||
'webpack-dev-server',
|
|
||||||
'webpack-cli',
|
|
||||||
|
|
||||||
// react-router: Requires manual upgrade
|
// react-router: Requires manual upgrade
|
||||||
'history',
|
'history',
|
||||||
'react-router-dom',
|
'react-router-dom',
|
||||||
|
|
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
|
@ -40,4 +40,4 @@ jobs:
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|
||||||
- name: JavaScript testing
|
- name: JavaScript testing
|
||||||
run: yarn jest --reporters github-actions summary
|
run: yarn test:js
|
||||||
|
|
4
.github/workflows/test-ruby.yml
vendored
4
.github/workflows/test-ruby.yml
vendored
|
@ -49,7 +49,7 @@ jobs:
|
||||||
public/assets
|
public/assets
|
||||||
public/packs
|
public/packs
|
||||||
public/packs-test
|
public/packs-test
|
||||||
tmp/cache/webpacker
|
tmp/cache/vite
|
||||||
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
||||||
|
@ -63,7 +63,7 @@ jobs:
|
||||||
|
|
||||||
- name: Archive asset artifacts
|
- name: Archive asset artifacts
|
||||||
run: |
|
run: |
|
||||||
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs*
|
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: matrix.mode == 'test'
|
if: matrix.mode == 'test'
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -21,10 +21,11 @@
|
||||||
/public/system
|
/public/system
|
||||||
/public/assets
|
/public/assets
|
||||||
/public/packs
|
/public/packs
|
||||||
|
/public/packs-dev
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
/node_modules/
|
node_modules/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
# Ignore Vagrant files
|
# Ignore Vagrant files
|
||||||
|
|
|
@ -18,10 +18,6 @@
|
||||||
!/log/.keep
|
!/log/.keep
|
||||||
/tmp
|
/tmp
|
||||||
/coverage
|
/coverage
|
||||||
/public/system
|
|
||||||
/public/assets
|
|
||||||
/public/packs
|
|
||||||
/public/packs-test
|
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
.env.development
|
.env.development
|
||||||
|
@ -60,6 +56,7 @@ docker-compose.override.yml
|
||||||
/public/packs
|
/public/packs
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
/public/system
|
/public/system
|
||||||
|
/public/vite*
|
||||||
|
|
||||||
# Ignore emoji map file
|
# Ignore emoji map file
|
||||||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -2,9 +2,34 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [4.3.8] - 2025-05-06
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
- Check scheme on account, profile, and media URLs ([GHSA-x2rc-v5wx-g3m5](https://github.com/mastodon/mastodon/security/advisories/GHSA-x2rc-v5wx-g3m5))
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add warning for REDIS_NAMESPACE deprecation at startup (#34581 by @ClearlyClaire)
|
||||||
|
- Add built-in context for interaction policies (#34574 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change activity distribution error handling to skip retrying for deleted accounts (#33617 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove double-query for signed query strings (#34610 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549 by @ClearlyClaire)
|
||||||
|
- Fix sign-up e-mail confirmation page reloading on error or redirect (#34548 by @ClearlyClaire)
|
||||||
|
|
||||||
## [4.3.7] - 2025-04-02
|
## [4.3.7] - 2025-04-02
|
||||||
|
|
||||||
### Add
|
### Added
|
||||||
|
|
||||||
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
|
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
|
||||||
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)
|
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)
|
||||||
|
|
|
@ -66,7 +66,7 @@ Example:
|
||||||
Pull requests that do not pass automated checks on CI may not be reviewed. In
|
Pull requests that do not pass automated checks on CI may not be reviewed. In
|
||||||
particular, please keep in mind:
|
particular, please keep in mind:
|
||||||
|
|
||||||
- Unit and integration tests (rspec, jest)
|
- Unit and integration tests (rspec, vitest)
|
||||||
- Code style rules (rubocop, eslint)
|
- Code style rules (rubocop, eslint)
|
||||||
- Normalization of locale files (i18n-tasks)
|
- Normalization of locale files (i18n-tasks)
|
||||||
- Relevant accessibility or performance concerns
|
- Relevant accessibility or performance concerns
|
||||||
|
|
|
@ -307,6 +307,7 @@ RUN \
|
||||||
ldconfig; \
|
ldconfig; \
|
||||||
# Use Ruby on Rails to create Mastodon assets
|
# Use Ruby on Rails to create Mastodon assets
|
||||||
SECRET_KEY_BASE_DUMMY=1 \
|
SECRET_KEY_BASE_DUMMY=1 \
|
||||||
|
# Do not run `yarn` when precompiling assets, we already ran it before
|
||||||
bundle exec rails assets:precompile; \
|
bundle exec rails assets:precompile; \
|
||||||
# Cleanup temporary files
|
# Cleanup temporary files
|
||||||
rm -fr /opt/mastodon/tmp;
|
rm -fr /opt/mastodon/tmp;
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -95,7 +95,6 @@ gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2023'
|
gem 'tzinfo-data', '~> 1.2023'
|
||||||
gem 'webauthn', '~> 3.0'
|
gem 'webauthn', '~> 3.0'
|
||||||
gem 'webpacker', '~> 5.4'
|
|
||||||
gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913'
|
gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913'
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
|
@ -230,3 +229,5 @@ gem 'rubyzip', '~> 2.3'
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
|
||||||
gem 'mail', '~> 2.8'
|
gem 'mail', '~> 2.8'
|
||||||
|
|
||||||
|
gem 'vite_rails', '~> 3.0.19'
|
||||||
|
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -203,6 +203,7 @@ GEM
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (3.1.8)
|
dotenv (3.1.8)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
|
dry-cli (1.2.0)
|
||||||
elasticsearch (7.17.11)
|
elasticsearch (7.17.11)
|
||||||
elasticsearch-api (= 7.17.11)
|
elasticsearch-api (= 7.17.11)
|
||||||
elasticsearch-transport (= 7.17.11)
|
elasticsearch-transport (= 7.17.11)
|
||||||
|
@ -806,7 +807,6 @@ GEM
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
semantic_range (3.1.0)
|
|
||||||
shoulda-matchers (6.5.0)
|
shoulda-matchers (6.5.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (6.5.12)
|
sidekiq (6.5.12)
|
||||||
|
@ -892,6 +892,15 @@ GEM
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
public_suffix
|
public_suffix
|
||||||
|
vite_rails (3.0.19)
|
||||||
|
railties (>= 5.1, < 9)
|
||||||
|
vite_ruby (~> 3.0, >= 3.2.2)
|
||||||
|
vite_ruby (3.9.2)
|
||||||
|
dry-cli (>= 0.7, < 2)
|
||||||
|
logger (~> 1.6)
|
||||||
|
mutex_m
|
||||||
|
rack-proxy (~> 0.6, >= 0.6.1)
|
||||||
|
zeitwerk (~> 2.2)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
webauthn (3.4.0)
|
webauthn (3.4.0)
|
||||||
|
@ -910,11 +919,6 @@ GEM
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webpacker (5.4.4)
|
|
||||||
activesupport (>= 5.2)
|
|
||||||
rack-proxy (>= 0.6.1)
|
|
||||||
railties (>= 5.2)
|
|
||||||
semantic_range (>= 2.3.0)
|
|
||||||
webrick (1.9.1)
|
webrick (1.9.1)
|
||||||
websocket (1.2.11)
|
websocket (1.2.11)
|
||||||
websocket-driver (0.7.7)
|
websocket-driver (0.7.7)
|
||||||
|
@ -1078,9 +1082,9 @@ DEPENDENCIES
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
twitter-text (~> 3.1.0)
|
twitter-text (~> 3.1.0)
|
||||||
tzinfo-data (~> 1.2023)
|
tzinfo-data (~> 1.2023)
|
||||||
|
vite_rails (~> 3.0.19)
|
||||||
webauthn (~> 3.0)
|
webauthn (~> 3.0)
|
||||||
webmock (~> 3.18)
|
webmock (~> 3.18)
|
||||||
webpacker (~> 5.4)
|
|
||||||
webpush!
|
webpush!
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
|
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
|
||||||
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
|
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
|
||||||
stream: env PORT=4000 yarn workspace @mastodon/streaming start
|
stream: env PORT=4000 yarn workspace @mastodon/streaming start
|
||||||
webpack: bin/webpack-dev-server
|
vite: yarn dev
|
||||||
|
|
|
@ -4,7 +4,7 @@ module RoutingHelper
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include ActionView::Helpers::AssetTagHelper
|
include ActionView::Helpers::AssetTagHelper
|
||||||
include Webpacker::Helper
|
include ViteRails::TagHelpers
|
||||||
|
|
||||||
included do
|
included do
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
@ -25,7 +25,7 @@ module RoutingHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def frontend_asset_path(source, **)
|
def frontend_asset_path(source, **)
|
||||||
asset_pack_path("media/#{source}", **)
|
vite_asset_path(source, **)
|
||||||
end
|
end
|
||||||
|
|
||||||
def frontend_asset_url(source, **)
|
def frontend_asset_url(source, **)
|
||||||
|
|
|
@ -4,11 +4,14 @@ module ThemeHelper
|
||||||
def theme_style_tags(theme)
|
def theme_style_tags(theme)
|
||||||
if theme == 'system'
|
if theme == 'system'
|
||||||
''.html_safe.tap do |tags|
|
''.html_safe.tap do |tags|
|
||||||
tags << stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
tags << vite_stylesheet_tag('styles/mastodon-light.scss', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||||
tags << stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
tags << vite_stylesheet_tag('styles/application.scss', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||||
end
|
end
|
||||||
|
# TODO: Determine why default doesn't map correctly.
|
||||||
|
elsif theme == 'default'
|
||||||
|
vite_stylesheet_tag 'styles/application.scss', media: 'all', crossorigin: 'anonymous'
|
||||||
else
|
else
|
||||||
stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous'
|
vite_stylesheet_tag "styles/#{theme}.scss", media: 'all', crossorigin: 'anonymous'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import './public-path';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
|
@ -273,7 +272,7 @@ async function mountReactComponent(element: Element) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const { default: Component } = (await import(
|
const { default: Component } = (await import(
|
||||||
`@/mastodon/components/admin/${componentName}`
|
`@/mastodon/components/admin/${componentName}.jsx`
|
||||||
)) as { default: React.ComponentType };
|
)) as { default: React.ComponentType };
|
||||||
|
|
||||||
const root = createRoot(element);
|
const root = createRoot(element);
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import './public-path';
|
import { start } from 'mastodon/common';
|
||||||
|
import { loadLocale } from 'mastodon/locales';
|
||||||
import main from 'mastodon/main';
|
import main from 'mastodon/main';
|
||||||
|
import { loadPolyfills } from 'mastodon/polyfills';
|
||||||
import { start } from '../mastodon/common';
|
|
||||||
import { loadLocale } from '../mastodon/locales';
|
|
||||||
import { loadPolyfills } from '../mastodon/polyfills';
|
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import './public-path';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
|
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import './public-path';
|
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
|
|
8
app/javascript/entrypoints/index.html
Normal file
8
app/javascript/entrypoints/index.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="module" src="./application.ts"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mastodon"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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 { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import './public-path';
|
|
||||||
|
|
||||||
import { IntlMessageFormat } from 'intl-messageformat';
|
import { IntlMessageFormat } from 'intl-messageformat';
|
||||||
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
@ -18,8 +16,6 @@ import { loadLocale, getLocale } from '../mastodon/locales';
|
||||||
import { loadPolyfills } from '../mastodon/polyfills';
|
import { loadPolyfills } from '../mastodon/polyfills';
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
import 'cocoon-js-vanilla';
|
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -153,9 +149,7 @@ function loaded() {
|
||||||
const reactComponents = document.querySelectorAll('[data-component]');
|
const reactComponents = document.querySelectorAll('[data-component]');
|
||||||
|
|
||||||
if (reactComponents.length > 0) {
|
if (reactComponents.length > 0) {
|
||||||
import(
|
import('../mastodon/containers/media_container')
|
||||||
/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container'
|
|
||||||
)
|
|
||||||
.then(({ default: MediaContainer }) => {
|
.then(({ default: MediaContainer }) => {
|
||||||
reactComponents.forEach((component) => {
|
reactComponents.forEach((component) => {
|
||||||
Array.from(component.children).forEach((child) => {
|
Array.from(component.children).forEach((child) => {
|
||||||
|
|
|
@ -8,8 +8,6 @@ and performs no other task.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './public-path';
|
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
interface JRDLink {
|
interface JRDLink {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import './public-path';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { start } from '../mastodon/common';
|
import { start } from '../mastodon/common';
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import './public-path';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
|
@ -77,6 +77,17 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
||||||
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
|
||||||
|
|
||||||
|
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
|
||||||
|
normalStatus.url = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalStatus.url ||= normalStatus.uri;
|
||||||
|
|
||||||
|
normalStatus.media_attachments.forEach(item => {
|
||||||
|
if (item.remote_url && !(item.remote_url.startsWith('http://') || item.remote_url.startsWith('https://')))
|
||||||
|
item.remote_url = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalOldStatus) {
|
if (normalOldStatus) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
|
|
||||||
export function start() {
|
export function start() {
|
||||||
require.context('../images/', true, /\.(jpg|png|svg)$/);
|
// TODO: Find alternative to this
|
||||||
|
// require.context('../images/', true, /\.(jpg|png|svg)$/);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Rails.start();
|
Rails.start();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
|
exports[`<AutosuggestEmoji /> > renders emoji with custom url 1`] = `
|
||||||
<div
|
<div
|
||||||
className="autosuggest-emoji"
|
className="autosuggest-emoji"
|
||||||
>
|
>
|
||||||
|
@ -17,7 +17,7 @@ exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
|
exports[`<AutosuggestEmoji /> > renders native emoji 1`] = `
|
||||||
<div
|
<div
|
||||||
className="autosuggest-emoji"
|
className="autosuggest-emoji"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
className="account__avatar account__avatar--loading"
|
className="account__avatar account__avatar--loading"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
|
@ -21,7 +21,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
exports[`<Avatar /> > Still > renders a still avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
className="account__avatar account__avatar--loading"
|
className="account__avatar account__avatar--loading"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<AvatarOverlay renders a overlay avatar 1`] = `
|
exports[`<AvatarOverlay > renders a overlay avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
className="account__avatar-overlay"
|
className="account__avatar-overlay"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
|
exports[`<Button /> > adds class "button-secondary" if props.secondary given 1`] = `
|
||||||
<button
|
<button
|
||||||
className="button button-secondary"
|
className="button button-secondary"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -8,7 +8,7 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> renders a button element 1`] = `
|
exports[`<Button /> > renders a button element 1`] = `
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -16,7 +16,7 @@ exports[`<Button /> renders a button element 1`] = `
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
|
exports[`<Button /> > renders a disabled attribute if props.disabled given 1`] = `
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
@ -25,7 +25,7 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> renders class="button--block" if props.block given 1`] = `
|
exports[`<Button /> > renders class="button--block" if props.block given 1`] = `
|
||||||
<button
|
<button
|
||||||
className="button button--block"
|
className="button button--block"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -33,7 +33,7 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
|
||||||
/>
|
/>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> renders the children 1`] = `
|
exports[`<Button /> > renders the children 1`] = `
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -45,7 +45,7 @@ exports[`<Button /> renders the children 1`] = `
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> renders the given text 1`] = `
|
exports[`<Button /> > renders the given text 1`] = `
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -55,7 +55,7 @@ exports[`<Button /> renders the given text 1`] = `
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> renders the props.text instead of children 1`] = `
|
exports[`<Button /> > renders the props.text instead of children 1`] = `
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<DisplayName /> renders display name + account name 1`] = `
|
exports[`<DisplayName /> > renders display name + account name 1`] = `
|
||||||
<span
|
<span
|
||||||
className="display-name"
|
className="display-name"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe('<Button />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles click events using the given handler', () => {
|
it('handles click events using the given handler', () => {
|
||||||
const handler = jest.fn();
|
const handler = vi.fn();
|
||||||
render(<Button onClick={handler}>button</Button>);
|
render(<Button onClick={handler}>button</Button>);
|
||||||
fireEvent.click(screen.getByText('button'));
|
fireEvent.click(screen.getByText('button'));
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ describe('<Button />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not handle click events if props.disabled given', () => {
|
it('does not handle click events if props.disabled given', () => {
|
||||||
const handler = jest.fn();
|
const handler = vi.fn();
|
||||||
render(<Button onClick={handler} disabled>button</Button>);
|
render(<Button onClick={handler} disabled>button</Button>);
|
||||||
fireEvent.click(screen.getByText('button'));
|
fireEvent.click(screen.getByText('button'));
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg';
|
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||||
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
|
|
|
@ -15,10 +15,6 @@ import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
import { useSpring, animated } from '@react-spring/web';
|
import { useSpring, animated } from '@react-spring/web';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
// eslint-disable-next-line import/extensions
|
|
||||||
import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
|
|
||||||
|
|
||||||
import { showAlertForError } from 'mastodon/actions/alerts';
|
import { showAlertForError } from 'mastodon/actions/alerts';
|
||||||
import { uploadThumbnail } from 'mastodon/actions/compose';
|
import { uploadThumbnail } from 'mastodon/actions/compose';
|
||||||
|
@ -350,9 +346,15 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
|
||||||
|
|
||||||
fetchTesseract()
|
fetchTesseract()
|
||||||
.then(async ({ createWorker }) => {
|
.then(async ({ createWorker }) => {
|
||||||
|
const [tesseractWorkerPath, tesseractCorePath] = await Promise.all([
|
||||||
|
// eslint-disable-next-line import/extensions
|
||||||
|
import('tesseract.js/dist/worker.min.js?url'),
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import('tesseract.js-core/tesseract-core.wasm.js?url'),
|
||||||
|
]);
|
||||||
const worker = await createWorker('eng', 1, {
|
const worker = await createWorker('eng', 1, {
|
||||||
workerPath: tesseractWorkerPath as string,
|
workerPath: tesseractWorkerPath.default,
|
||||||
corePath: tesseractCorePath as string,
|
corePath: tesseractCorePath.default,
|
||||||
langPath: `${assetHost}/ocr/lang-data`,
|
langPath: `${assetHost}/ocr/lang-data`,
|
||||||
cacheMethod: 'write',
|
cacheMethod: 'write',
|
||||||
});
|
});
|
||||||
|
@ -501,5 +503,4 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
AltTextModal.displayName = 'AltTextModal';
|
AltTextModal.displayName = 'AltTextModal';
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
|
import emojiCompressed from '@/mastodon/features/emoji/emoji_compressed.mjs';
|
||||||
import { assetHost } from 'mastodon/utils/config';
|
import { assetHost } from 'mastodon/utils/config';
|
||||||
|
|
||||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||||
|
|
|
@ -9,28 +9,27 @@
|
||||||
// to ensure that the prevaled file is regenerated by Babel
|
// to ensure that the prevaled file is regenerated by Babel
|
||||||
// version: 4
|
// version: 4
|
||||||
|
|
||||||
const { NimbleEmojiIndex } = require('emoji-mart');
|
import { NimbleEmojiIndex } from 'emoji-mart';
|
||||||
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
|
||||||
|
|
||||||
|
import data from './emoji_data.json';
|
||||||
let data = require('./emoji_data.json');
|
import emojiMap from './emoji_map.json';
|
||||||
const emojiMap = require('./emoji_map.json');
|
import { unicodeToFilename } from './unicode_to_filename';
|
||||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
|
||||||
|
|
||||||
emojiMartUncompress(data);
|
emojiMartUncompress(data);
|
||||||
|
|
||||||
const emojiMartData = data;
|
const emojiMartData = data;
|
||||||
const emojiIndex = new NimbleEmojiIndex(emojiMartData);
|
const emojiIndex = new NimbleEmojiIndex(emojiMartData);
|
||||||
|
|
||||||
const excluded = ['®', '©', '™'];
|
const excluded = ['®', '©', '™'];
|
||||||
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
|
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
|
||||||
const shortcodeMap = {};
|
const shortcodeMap = {};
|
||||||
|
|
||||||
const shortCodesToEmojiData = {};
|
const shortCodesToEmojiData = {};
|
||||||
const emojisWithoutShortCodes = [];
|
const emojisWithoutShortCodes = [];
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiIndex.emojis).forEach((key) => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiIndex.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
// Emojis with skin tone modifiers are stored like this
|
||||||
|
@ -41,22 +40,22 @@ Object.keys(emojiIndex.emojis).forEach(key => {
|
||||||
shortcodeMap[emoji.native] = emoji.id;
|
shortcodeMap[emoji.native] = emoji.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
const stripModifiers = unicode => {
|
const stripModifiers = (unicode) => {
|
||||||
skinTones.forEach(tone => {
|
skinTones.forEach((tone) => {
|
||||||
unicode = unicode.replace(tone, '');
|
unicode = unicode.replace(tone, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
return unicode;
|
return unicode;
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(emojiMap).forEach(key => {
|
Object.keys(emojiMap).forEach((key) => {
|
||||||
if (excluded.includes(key)) {
|
if (excluded.includes(key)) {
|
||||||
delete emojiMap[key];
|
delete emojiMap[key];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedKey = stripModifiers(key);
|
const normalizedKey = stripModifiers(key);
|
||||||
let shortcode = shortcodeMap[normalizedKey];
|
let shortcode = shortcodeMap[normalizedKey];
|
||||||
|
|
||||||
if (!shortcode) {
|
if (!shortcode) {
|
||||||
shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
|
shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
|
||||||
|
@ -82,7 +81,7 @@ Object.keys(emojiMap).forEach(key => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiIndex.emojis).forEach((key) => {
|
||||||
let emoji = emojiIndex.emojis[key];
|
let emoji = emojiIndex.emojis[key];
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
// Emojis with skin tone modifiers are stored like this
|
||||||
|
@ -94,9 +93,11 @@ Object.keys(emojiIndex.emojis).forEach(key => {
|
||||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||||
|
|
||||||
if (short_names[0] !== key) {
|
if (short_names[0] !== key) {
|
||||||
throw new Error('The compressor expects the first short_code to be the ' +
|
throw new Error(
|
||||||
'key. It may need to be rewritten if the emoji change such that this ' +
|
'The compressor expects the first short_code to be the ' +
|
||||||
'is no longer the case.');
|
'key. It may need to be rewritten if the emoji change such that this ' +
|
||||||
|
'is no longer the case.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
short_names = short_names.slice(1); // first short name can be inferred from the key
|
short_names = short_names.slice(1); // first short name can be inferred from the key
|
||||||
|
@ -117,21 +118,23 @@ Object.keys(emojiIndex.emojis).forEach(key => {
|
||||||
|
|
||||||
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
|
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
|
||||||
// inconsistent behavior in dev mode
|
// inconsistent behavior in dev mode
|
||||||
module.exports = JSON.parse(JSON.stringify([
|
export default JSON.parse(
|
||||||
shortCodesToEmojiData,
|
JSON.stringify([
|
||||||
/*
|
shortCodesToEmojiData,
|
||||||
* The property `skins` is not found in the current context.
|
/*
|
||||||
* This could potentially lead to issues when interacting with modules or data structures
|
* The property `skins` is not found in the current context.
|
||||||
* that expect the presence of `skins` property.
|
* This could potentially lead to issues when interacting with modules or data structures
|
||||||
* Currently, no definitions or references to `skins` property can be found in:
|
* that expect the presence of `skins` property.
|
||||||
* - {@link node_modules/emoji-mart/dist/utils/data.js}
|
* Currently, no definitions or references to `skins` property can be found in:
|
||||||
* - {@link node_modules/emoji-mart/data/all.json}
|
* - {@link node_modules/emoji-mart/dist/utils/data.js}
|
||||||
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.d.ts#Skins}
|
* - {@link node_modules/emoji-mart/data/all.json}
|
||||||
* Future refactorings or updates should consider adding definitions or handling for `skins` property.
|
* - {@link app/javascript/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.skins,
|
||||||
emojiMartData.aliases,
|
emojiMartData.categories,
|
||||||
emojisWithoutShortCodes,
|
emojiMartData.aliases,
|
||||||
emojiMartData
|
emojisWithoutShortCodes,
|
||||||
]));
|
emojiMartData,
|
||||||
|
]),
|
||||||
|
);
|
|
@ -4,8 +4,8 @@
|
||||||
import type { BaseEmoji } from 'emoji-mart';
|
import type { BaseEmoji } from 'emoji-mart';
|
||||||
import type { Emoji } from 'emoji-mart/dist-es/utils/data';
|
import type { Emoji } from 'emoji-mart/dist-es/utils/data';
|
||||||
|
|
||||||
import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
|
import type { Search, ShortCodesToEmojiData } from './emoji_compressed.mjs';
|
||||||
import emojiCompressed from './emoji_compressed';
|
import emojiCompressed from './emoji_compressed.mjs';
|
||||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||||
|
|
||||||
type Emojis = Record<
|
type Emojis = Record<
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
import type {
|
import type {
|
||||||
FilenameData,
|
FilenameData,
|
||||||
ShortCodesToEmojiDataKey,
|
ShortCodesToEmojiDataKey,
|
||||||
} from './emoji_compressed';
|
} from './emoji_compressed.mjs';
|
||||||
import emojiCompressed from './emoji_compressed';
|
import emojiCompressed from './emoji_compressed.mjs';
|
||||||
import { unicodeToFilename } from './unicode_to_filename';
|
import { unicodeToFilename } from './unicode_to_filename';
|
||||||
|
|
||||||
type UnicodeMapping = Record<
|
type UnicodeMapping = Record<
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// taken from:
|
// taken from:
|
||||||
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
||||||
exports.unicodeToFilename = (str) => {
|
export const unicodeToFilename = (str) => {
|
||||||
let result = '';
|
let result = '';
|
||||||
let charCode = 0;
|
let charCode = 0;
|
||||||
let p = 0;
|
let p = 0;
|
||||||
|
|
|
@ -6,7 +6,7 @@ function padLeft(str, num) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.unicodeToUnifiedName = (str) => {
|
export const unicodeToUnifiedName = (str) => {
|
||||||
let output = '';
|
let output = '';
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
for (let i = 0; i < str.length; i += 2) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ const fakeIcon = () => <span />;
|
||||||
describe('<Column />', () => {
|
describe('<Column />', () => {
|
||||||
describe('<ColumnHeader /> click handler', () => {
|
describe('<ColumnHeader /> click handler', () => {
|
||||||
it('runs the scroll animation if the column contains scrollable content', () => {
|
it('runs the scroll animation if the column contains scrollable content', () => {
|
||||||
const scrollToMock = jest.fn();
|
const scrollToMock = vi.fn();
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<Column heading='notifications' icon='notifications' iconComponent={fakeIcon}>
|
<Column heading='notifications' icon='notifications' iconComponent={fakeIcon}>
|
||||||
<div className='scrollable' />
|
<div className='scrollable' />
|
||||||
|
|
|
@ -1,235 +1,235 @@
|
||||||
export function EmojiPicker () {
|
export function EmojiPicker () {
|
||||||
return import(/* webpackChunkName: "emoji_picker" */'../../emoji/emoji_picker');
|
return import('../../emoji/emoji_picker');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Compose () {
|
export function Compose () {
|
||||||
return import(/* webpackChunkName: "features/compose" */'../../compose');
|
return import('../../compose');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Notifications () {
|
export function Notifications () {
|
||||||
return import(/* webpackChunkName: "features/notifications" */'../../notifications_v2');
|
return import('../../notifications_v2');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HomeTimeline () {
|
export function HomeTimeline () {
|
||||||
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
|
return import('../../home_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PublicTimeline () {
|
export function PublicTimeline () {
|
||||||
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
|
return import('../../public_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommunityTimeline () {
|
export function CommunityTimeline () {
|
||||||
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
|
return import('../../community_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Firehose () {
|
export function Firehose () {
|
||||||
return import(/* webpackChunkName: "features/firehose" */'../../firehose');
|
return import('../../firehose');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HashtagTimeline () {
|
export function HashtagTimeline () {
|
||||||
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
return import('../../hashtag_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DirectTimeline() {
|
export function DirectTimeline() {
|
||||||
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
|
return import('../../direct_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListTimeline () {
|
export function ListTimeline () {
|
||||||
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
|
return import('../../list_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Lists () {
|
export function Lists () {
|
||||||
return import(/* webpackChunkName: "features/lists" */'../../lists');
|
return import('../../lists');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Status () {
|
export function Status () {
|
||||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
return import('../../status');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GettingStarted () {
|
export function GettingStarted () {
|
||||||
return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
|
return import('../../getting_started');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeyboardShortcuts () {
|
export function KeyboardShortcuts () {
|
||||||
return import(/* webpackChunkName: "features/keyboard_shortcuts" */'../../keyboard_shortcuts');
|
return import('../../keyboard_shortcuts');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PinnedStatuses () {
|
export function PinnedStatuses () {
|
||||||
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
|
return import('../../pinned_statuses');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountTimeline () {
|
export function AccountTimeline () {
|
||||||
return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline');
|
return import('../../account_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountGallery () {
|
export function AccountGallery () {
|
||||||
return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
|
return import('../../account_gallery');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AccountFeatured() {
|
export function AccountFeatured() {
|
||||||
return import(/* webpackChunkName: "features/account_featured" */'../../account_featured');
|
return import('../../account_featured');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Followers () {
|
export function Followers () {
|
||||||
return import(/* webpackChunkName: "features/followers" */'../../followers');
|
return import('../../followers');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Following () {
|
export function Following () {
|
||||||
return import(/* webpackChunkName: "features/following" */'../../following');
|
return import('../../following');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Reblogs () {
|
export function Reblogs () {
|
||||||
return import(/* webpackChunkName: "features/reblogs" */'../../reblogs');
|
return import('../../reblogs');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Favourites () {
|
export function Favourites () {
|
||||||
return import(/* webpackChunkName: "features/favourites" */'../../favourites');
|
return import('../../favourites');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FollowRequests () {
|
export function FollowRequests () {
|
||||||
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
|
return import('../../follow_requests');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FavouritedStatuses () {
|
export function FavouritedStatuses () {
|
||||||
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses');
|
return import('../../favourited_statuses');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FollowedTags () {
|
export function FollowedTags () {
|
||||||
return import(/* webpackChunkName: "features/followed_tags" */'../../followed_tags');
|
return import('../../followed_tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BookmarkedStatuses () {
|
export function BookmarkedStatuses () {
|
||||||
return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses');
|
return import('../../bookmarked_statuses');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Blocks () {
|
export function Blocks () {
|
||||||
return import(/* webpackChunkName: "features/blocks" */'../../blocks');
|
return import('../../blocks');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DomainBlocks () {
|
export function DomainBlocks () {
|
||||||
return import(/* webpackChunkName: "features/domain_blocks" */'../../domain_blocks');
|
return import('../../domain_blocks');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Mutes () {
|
export function Mutes () {
|
||||||
return import(/* webpackChunkName: "features/mutes" */'../../mutes');
|
return import('../../mutes');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MuteModal () {
|
export function MuteModal () {
|
||||||
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
|
return import('../components/mute_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlockModal () {
|
export function BlockModal () {
|
||||||
return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal');
|
return import('../components/block_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DomainBlockModal () {
|
export function DomainBlockModal () {
|
||||||
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/domain_block_modal');
|
return import('../components/domain_block_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportModal () {
|
export function ReportModal () {
|
||||||
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
return import('../components/report_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IgnoreNotificationsModal () {
|
export function IgnoreNotificationsModal () {
|
||||||
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/ignore_notifications_modal');
|
return import('../components/ignore_notifications_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MediaGallery () {
|
export function MediaGallery () {
|
||||||
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
|
return import('../../../components/media_gallery');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Video () {
|
export function Video () {
|
||||||
return import(/* webpackChunkName: "features/video" */'../../video');
|
return import('../../video');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmbedModal () {
|
export function EmbedModal () {
|
||||||
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
|
return import('../components/embed_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListAdder () {
|
export function ListAdder () {
|
||||||
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
|
return import('../../list_adder');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tesseract () {
|
export function Tesseract () {
|
||||||
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
return import('tesseract.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Audio () {
|
export function Audio () {
|
||||||
return import(/* webpackChunkName: "features/audio" */'../../audio');
|
return import('../../audio');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Directory () {
|
export function Directory () {
|
||||||
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
return import('../../directory');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingProfile () {
|
export function OnboardingProfile () {
|
||||||
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/profile');
|
return import('../../onboarding/profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingFollows () {
|
export function OnboardingFollows () {
|
||||||
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/follows');
|
return import('../../onboarding/follows');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CompareHistoryModal () {
|
export function CompareHistoryModal () {
|
||||||
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
|
return import('../components/compare_history_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Explore () {
|
export function Explore () {
|
||||||
return import(/* webpackChunkName: "features/explore" */'../../explore');
|
return import('../../explore');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Search () {
|
export function Search () {
|
||||||
return import(/* webpackChunkName: "features/explore" */'../../search');
|
return import('../../search');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilterModal () {
|
export function FilterModal () {
|
||||||
return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal');
|
return import('../components/filter_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InteractionModal () {
|
export function InteractionModal () {
|
||||||
return import(/*webpackChunkName: "modals/interaction_modal" */'../../interaction_modal');
|
return import('../../interaction_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SubscribedLanguagesModal () {
|
export function SubscribedLanguagesModal () {
|
||||||
return import(/*webpackChunkName: "modals/subscribed_languages_modal" */'../../subscribed_languages_modal');
|
return import('../../subscribed_languages_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ClosedRegistrationsModal () {
|
export function ClosedRegistrationsModal () {
|
||||||
return import(/*webpackChunkName: "modals/closed_registrations_modal" */'../../closed_registrations_modal');
|
return import('../../closed_registrations_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function About () {
|
export function About () {
|
||||||
return import(/*webpackChunkName: "features/about" */'../../about');
|
return import('../../about');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrivacyPolicy () {
|
export function PrivacyPolicy () {
|
||||||
return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy');
|
return import('../../privacy_policy');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TermsOfService () {
|
export function TermsOfService () {
|
||||||
return import(/*webpackChunkName: "features/terms_of_service" */'../../terms_of_service');
|
return import('../../terms_of_service');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationRequests () {
|
export function NotificationRequests () {
|
||||||
return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests');
|
return import('../../notifications/requests');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationRequest () {
|
export function NotificationRequest () {
|
||||||
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
|
return import('../../notifications/request');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinkTimeline () {
|
export function LinkTimeline () {
|
||||||
return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
|
return import('../../link_timeline');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AnnualReportModal () {
|
export function AnnualReportModal () {
|
||||||
return import(/*webpackChunkName: "modals/annual_report_modal" */'../components/annual_report_modal');
|
return import('../components/annual_report_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListEdit () {
|
export function ListEdit () {
|
||||||
return import(/*webpackChunkName: "features/lists" */'../../lists/new');
|
return import('../../lists/new');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ListMembers () {
|
export function ListMembers () {
|
||||||
return import(/* webpackChunkName: "features/lists" */'../../lists/members');
|
return import('../../lists/members');
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// can at least log in using KaiOS devices).
|
// can at least log in using KaiOS devices).
|
||||||
|
|
||||||
function importArrowKeyNavigation() {
|
function importArrowKeyNavigation() {
|
||||||
return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation');
|
return import('arrow-key-navigation');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function loadKeyboardExtensions() {
|
export default function loadKeyboardExtensions() {
|
||||||
|
|
|
@ -5,6 +5,10 @@ import { isLocaleLoaded, setLocale } from './global_locale';
|
||||||
|
|
||||||
const localeLoadingSemaphore = new Semaphore(1);
|
const localeLoadingSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
|
const localeFiles = import.meta.glob<{ default: LocaleData['messages'] }>([
|
||||||
|
'./*.json',
|
||||||
|
]);
|
||||||
|
|
||||||
export async function loadLocale() {
|
export async function loadLocale() {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
|
||||||
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';
|
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';
|
||||||
|
@ -17,13 +21,14 @@ export async function loadLocale() {
|
||||||
// if the locale is already set, then do nothing
|
// if the locale is already set, then do nothing
|
||||||
if (isLocaleLoaded()) return;
|
if (isLocaleLoaded()) return;
|
||||||
|
|
||||||
const localeData = (await import(
|
// If there is no locale file, then fallback to english
|
||||||
/* webpackMode: "lazy" */
|
const localeFile = Object.hasOwn(localeFiles, `./${locale}.json`)
|
||||||
/* webpackChunkName: "locale/[request]" */
|
? localeFiles[`./${locale}.json`]
|
||||||
/* webpackInclude: /\.json$/ */
|
: localeFiles['./en.json'];
|
||||||
/* webpackPreload: true */
|
|
||||||
`mastodon/locales/${locale}.json`
|
if (!localeFile) throw new Error('Could not load the locale JSON file');
|
||||||
)) as LocaleData['messages'];
|
|
||||||
|
const { default: localeData } = await localeFile();
|
||||||
|
|
||||||
setLocale({ messages: localeData, locale });
|
setLocale({ messages: localeData, locale });
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,17 +7,19 @@ import * as perf from 'mastodon/performance';
|
||||||
import ready from 'mastodon/ready';
|
import ready from 'mastodon/ready';
|
||||||
import { store } from 'mastodon/store';
|
import { store } from 'mastodon/store';
|
||||||
|
|
||||||
import { isProduction } from './utils/environment';
|
import { isProduction, isDevelopment } from './utils/environment';
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
function main() {
|
function main() {
|
||||||
perf.start('main()');
|
perf.start('main()');
|
||||||
|
|
||||||
return ready(async () => {
|
return ready(async () => {
|
||||||
const mountNode = document.getElementById('mastodon');
|
const mountNode = document.getElementById('mastodon');
|
||||||
const props = JSON.parse(mountNode.getAttribute('data-props'));
|
if (!mountNode) {
|
||||||
|
throw new Error('Mount node not found');
|
||||||
|
}
|
||||||
|
const props = JSON.parse(
|
||||||
|
mountNode.getAttribute('data-props') ?? '{}',
|
||||||
|
) as Record<string, unknown>;
|
||||||
|
|
||||||
const root = createRoot(mountNode);
|
const root = createRoot(mountNode);
|
||||||
root.render(<Mastodon {...props} />);
|
root.render(<Mastodon {...props} />);
|
||||||
|
@ -25,8 +27,10 @@ function main() {
|
||||||
|
|
||||||
if (isProduction() && me && 'serviceWorker' in navigator) {
|
if (isProduction() && me && 'serviceWorker' in navigator) {
|
||||||
const { Workbox } = await import('workbox-window');
|
const { Workbox } = await import('workbox-window');
|
||||||
const wb = new Workbox('/sw.js');
|
const wb = new Workbox(
|
||||||
/** @type {ServiceWorkerRegistration} */
|
isDevelopment() ? '/packs-dev/dev-sw.js?dev-sw' : '/sw.js',
|
||||||
|
{ type: 'module', scope: '/' },
|
||||||
|
);
|
||||||
let registration;
|
let registration;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -35,8 +39,14 @@ function main() {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registration && 'Notification' in window && Notification.permission === 'granted') {
|
if (
|
||||||
const registerPushNotifications = await import('mastodon/actions/push_notifications');
|
registration &&
|
||||||
|
'Notification' in window &&
|
||||||
|
Notification.permission === 'granted'
|
||||||
|
) {
|
||||||
|
const registerPushNotifications = await import(
|
||||||
|
'mastodon/actions/push_notifications'
|
||||||
|
);
|
||||||
|
|
||||||
store.dispatch(registerPushNotifications.register());
|
store.dispatch(registerPushNotifications.register());
|
||||||
}
|
}
|
||||||
|
@ -46,4 +56,5 @@ function main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default main;
|
export default main;
|
|
@ -144,5 +144,10 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
),
|
),
|
||||||
note_emojified: emojify(accountJSON.note, emojiMap),
|
note_emojified: emojify(accountJSON.note, emojiMap),
|
||||||
note_plain: unescapeHTML(accountJSON.note),
|
note_plain: unescapeHTML(accountJSON.note),
|
||||||
|
url:
|
||||||
|
accountJSON.url.startsWith('http://') ||
|
||||||
|
accountJSON.url.startsWith('https://')
|
||||||
|
? accountJSON.url
|
||||||
|
: accountJSON.uri,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
//
|
//
|
||||||
// Tools for performance debugging, only enabled in development mode.
|
// Tools for performance debugging, only enabled in development mode.
|
||||||
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
|
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
|
||||||
// Also see config/webpack/loaders/mark.js for the webpack loader marks.
|
|
||||||
|
|
||||||
import * as marky from 'marky';
|
import * as marky from 'marky';
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,13 @@
|
||||||
// If there are no polyfills, then this is just Promise.resolve() which means
|
// If there are no polyfills, then this is just Promise.resolve() which means
|
||||||
// it will execute in the same tick of the event loop (i.e. near-instant).
|
// it will execute in the same tick of the event loop (i.e. near-instant).
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/extensions -- This file is virtual so it thinks it has an extension
|
||||||
|
import 'vite/modulepreload-polyfill';
|
||||||
|
|
||||||
import { loadIntlPolyfills } from './intl';
|
import { loadIntlPolyfills } from './intl';
|
||||||
|
|
||||||
function importExtraPolyfills() {
|
function importExtraPolyfills() {
|
||||||
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
|
return import('./extra_polyfills');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPolyfills() {
|
export function loadPolyfills() {
|
||||||
|
|
|
@ -54,11 +54,9 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Load the polyfill 1st BEFORE loading data
|
// Load the polyfill 1st BEFORE loading data
|
||||||
|
await import('@formatjs/intl-pluralrules/polyfill-force');
|
||||||
await import(
|
await import(
|
||||||
/* webpackChunkName: "i18n-pluralrules-polyfill" */ '@formatjs/intl-pluralrules/polyfill-force'
|
`../../../../node_modules/@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}.js`
|
||||||
);
|
|
||||||
await import(
|
|
||||||
/* webpackChunkName: "i18n-pluralrules-polyfill-[request]" */ `@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,11 +68,9 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
|
||||||
// }
|
// }
|
||||||
// // Load the polyfill 1st BEFORE loading data
|
// // Load the polyfill 1st BEFORE loading data
|
||||||
// await import(
|
// await import(
|
||||||
// /* webpackChunkName: "i18n-relativetimeformat-polyfill" */
|
|
||||||
// '@formatjs/intl-relativetimeformat/polyfill-force'
|
// '@formatjs/intl-relativetimeformat/polyfill-force'
|
||||||
// );
|
// );
|
||||||
// await import(
|
// await import(
|
||||||
// /* webpackChunkName: "i18n-relativetimeformat-polyfill-[request]" */
|
|
||||||
// `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}`
|
// `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}`
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ExpirationPlugin } from 'workbox-expiration';
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
import { precacheAndRoute } from 'workbox-precaching';
|
// import { precacheAndRoute } from 'workbox-precaching';
|
||||||
import { registerRoute } from 'workbox-routing';
|
import { registerRoute } from 'workbox-routing';
|
||||||
import { CacheFirst } from 'workbox-strategies';
|
import { CacheFirst } from 'workbox-strategies';
|
||||||
|
|
||||||
|
@ -15,10 +15,10 @@ function fetchRoot() {
|
||||||
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
||||||
}
|
}
|
||||||
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
// precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
registerRoute(
|
registerRoute(
|
||||||
/locale_.*\.js$/,
|
/intl\/.*\.js$/,
|
||||||
new CacheFirst({
|
new CacheFirst({
|
||||||
cacheName: `${CACHE_NAME_PREFIX}locales`,
|
cacheName: `${CACHE_NAME_PREFIX}locales`,
|
||||||
plugins: [
|
plugins: [
|
|
@ -1,41 +0,0 @@
|
||||||
/* @preval */
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const { defineMessages } = require('react-intl');
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
mentioned_you: { id: 'notification.mentioned_you', defaultMessage: '{name} mentioned you' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const filtered = {};
|
|
||||||
const filenames = fs.readdirSync(path.resolve(__dirname, '../locales'));
|
|
||||||
|
|
||||||
filenames.forEach(filename => {
|
|
||||||
if (!filename.match(/\.json$/)) return;
|
|
||||||
|
|
||||||
const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8');
|
|
||||||
const full = JSON.parse(content);
|
|
||||||
const locale = filename.split('.')[0];
|
|
||||||
|
|
||||||
filtered[locale] = {
|
|
||||||
'notification.favourite': full['notification.favourite'] || '',
|
|
||||||
'notification.follow': full['notification.follow'] || '',
|
|
||||||
'notification.follow_request': full['notification.follow_request'] || '',
|
|
||||||
'notification.mention': full[messages.mentioned_you.id] || '',
|
|
||||||
'notification.reblog': full['notification.reblog'] || '',
|
|
||||||
'notification.poll': full['notification.poll'] || '',
|
|
||||||
'notification.status': full['notification.status'] || '',
|
|
||||||
'notification.update': full['notification.update'] || '',
|
|
||||||
'notification.admin.sign_up': full['notification.admin.sign_up'] || '',
|
|
||||||
|
|
||||||
'status.show_more': full['status.show_more'] || '',
|
|
||||||
'status.reblog': full['status.reblog'] || '',
|
|
||||||
'status.favourite': full['status.favourite'] || '',
|
|
||||||
|
|
||||||
'notifications.group': full['notifications.group'] || '',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = JSON.parse(JSON.stringify(filtered));
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { IntlMessageFormat } from 'intl-messageformat';
|
import { IntlMessageFormat } from 'intl-messageformat';
|
||||||
|
|
||||||
import { unescape } from 'lodash';
|
import { unescape } from 'lodash';
|
||||||
|
// see config/vite/plugins/sw-locales
|
||||||
import locales from './web_push_locales';
|
// it needs to be updated when new locale keys are used in this file
|
||||||
|
// eslint-disable-next-line import/no-unresolved
|
||||||
|
import locales from "virtual:mastodon-sw-locales";
|
||||||
|
|
||||||
const MAX_NOTIFICATIONS = 5;
|
const MAX_NOTIFICATIONS = 5;
|
||||||
const GROUP_TAG = 'tag';
|
const GROUP_TAG = 'tag';
|
||||||
|
|
|
@ -3,17 +3,17 @@ import { IntlProvider } from 'react-intl';
|
||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
|
|
||||||
import type { RenderOptions } from '@testing-library/react';
|
import type { RenderOptions } from '@testing-library/react';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import { render as rtlRender } from '@testing-library/react';
|
import { render as rtlRender } from '@testing-library/react';
|
||||||
|
|
||||||
import { IdentityContext } from './identity_context';
|
import { IdentityContext } from './identity_context';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeAll(() => {
|
||||||
global.requestIdleCallback = jest
|
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
|
||||||
.fn()
|
// @ts-expect-error IdleRequestCallback expects an argument of type IdleDeadline,
|
||||||
.mockImplementation((fn: () => void) => {
|
// but that doesn't exist in this environment.
|
||||||
fn();
|
cb();
|
||||||
});
|
return 0;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function render(
|
function render(
|
||||||
|
@ -46,7 +46,6 @@ function render(
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-export everything
|
// re-export everything
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
export * from '@testing-library/react';
|
export * from '@testing-library/react';
|
||||||
|
|
||||||
// override render method
|
// override render method
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
import '@testing-library/jest-dom';
|
|
|
@ -1,7 +1,11 @@
|
||||||
export function isDevelopment() {
|
export function isDevelopment() {
|
||||||
return process.env.NODE_ENV === 'development';
|
if (typeof process !== 'undefined')
|
||||||
|
return process.env.NODE_ENV === 'development';
|
||||||
|
else return import.meta.env.DEV;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isProduction() {
|
export function isProduction() {
|
||||||
return process.env.NODE_ENV === 'production';
|
if (typeof process !== 'undefined')
|
||||||
|
return process.env.NODE_ENV === 'production';
|
||||||
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
|
@ -393,7 +393,7 @@ code {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: var(--avatar-border-radius);
|
border-radius: var(--avatar-border-radius);
|
||||||
background: url('images/void.png');
|
background: url('@/images/void.png');
|
||||||
|
|
||||||
&[src$='missing.png'] {
|
&[src$='missing.png'] {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|
19
app/javascript/types/image.d.ts
vendored
19
app/javascript/types/image.d.ts
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
|
|
||||||
/* eslint-disable import/no-default-export */
|
/* eslint-disable import/no-default-export */
|
||||||
declare module '*.avif' {
|
declare module '*.avif' {
|
||||||
const path: string;
|
const path: string;
|
||||||
|
@ -19,23 +21,6 @@ declare module '*.png' {
|
||||||
export default path;
|
export default path;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.svg' {
|
|
||||||
const path: string;
|
|
||||||
export default path;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.svg?react' {
|
|
||||||
import type React from 'react';
|
|
||||||
|
|
||||||
interface SVGPropsWithTitle extends React.SVGProps<SVGSVGElement> {
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReactComponent: React.FC<SVGPropsWithTitle>;
|
|
||||||
|
|
||||||
export default ReactComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.webp' {
|
declare module '*.webp' {
|
||||||
const path: string;
|
const path: string;
|
||||||
export default path;
|
export default path;
|
||||||
|
|
|
@ -15,13 +15,15 @@ class ActivityPub::Parser::MediaAttachmentParser
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_url
|
def remote_url
|
||||||
Addressable::URI.parse(@json['url'])&.normalize&.to_s
|
url = Addressable::URI.parse(@json['url'])&.normalize&.to_s
|
||||||
|
url unless unsupported_uri_scheme?(url)
|
||||||
rescue Addressable::URI::InvalidURIError
|
rescue Addressable::URI::InvalidURIError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def thumbnail_remote_url
|
def thumbnail_remote_url
|
||||||
Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
|
url = Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
|
||||||
|
url unless unsupported_uri_scheme?(url)
|
||||||
rescue Addressable::URI::InvalidURIError
|
rescue Addressable::URI::InvalidURIError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,10 @@ class ActivityPub::Parser::StatusParser
|
||||||
end
|
end
|
||||||
|
|
||||||
def url
|
def url
|
||||||
url_to_href(@object['url'], 'text/html') if @object['url'].present?
|
return if @object['url'].blank?
|
||||||
|
|
||||||
|
url = url_to_href(@object['url'], 'text/html')
|
||||||
|
url unless unsupported_uri_scheme?(url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def text
|
def text
|
||||||
|
@ -95,11 +98,11 @@ class ActivityPub::Parser::StatusParser
|
||||||
end
|
end
|
||||||
|
|
||||||
def favourites_count
|
def favourites_count
|
||||||
@object.dig('likes', 'totalItems') if @object.is_a?(Hash)
|
@object['likes']['totalItems'] if @object.is_a?(Hash) && @object['likes'].is_a?(Hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reblogs_count
|
def reblogs_count
|
||||||
@object.dig('shares', 'totalItems') if @object.is_a?(Hash)
|
@object['shares']['totalItems'] if @object.is_a?(Hash) && @object['shares'].is_a?(Hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote_policy
|
def quote_policy
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'singleton'
|
||||||
|
|
||||||
class ActivityPub::TagManager
|
class ActivityPub::TagManager
|
||||||
include Singleton
|
include Singleton
|
||||||
|
include JsonLdHelper
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
|
||||||
CONTEXT = 'https://www.w3.org/ns/activitystreams'
|
CONTEXT = 'https://www.w3.org/ns/activitystreams'
|
||||||
|
@ -17,7 +18,7 @@ class ActivityPub::TagManager
|
||||||
end
|
end
|
||||||
|
|
||||||
def url_for(target)
|
def url_for(target)
|
||||||
return target.url if target.respond_to?(:local?) && !target.local?
|
return unsupported_uri_scheme?(target.url) ? nil : target.url if target.respond_to?(:local?) && !target.local?
|
||||||
|
|
||||||
return unless target.respond_to?(:object_type)
|
return unless target.respond_to?(:object_type)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('auth.login')
|
= t('auth.login')
|
||||||
|
|
||||||
= javascript_pack_tag 'two_factor_authentication', crossorigin: 'anonymous'
|
= vite_typescript_tag 'two_factor_authentication.ts', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- if webauthn_enabled?
|
- if webauthn_enabled?
|
||||||
= render partial: 'auth/sessions/two_factor/webauthn_form', locals: { hidden: @scheme_type != 'webauthn' }
|
= render partial: 'auth/sessions/two_factor/webauthn_form', locals: { hidden: @scheme_type != 'webauthn' }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('auth.setup.title')
|
= t('auth.setup.title')
|
||||||
|
|
||||||
= javascript_pack_tag 'sign_up', crossorigin: 'anonymous'
|
= vite_typescript_tag 'sign_up.ts', crossorigin: 'anonymous'
|
||||||
|
|
||||||
= simple_form_for(@user, url: auth_setup_path) do |f|
|
= simple_form_for(@user, url: auth_setup_path) do |f|
|
||||||
= render 'auth/shared/progress', stage: 'confirm'
|
= render 'auth/shared/progress', stage: 'confirm'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
= vite_typescript_tag 'admin.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :body_classes, 'admin'
|
- content_for :body_classes, 'admin'
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,12 @@
|
||||||
%title= html_title
|
%title= html_title
|
||||||
|
|
||||||
= theme_style_tags current_theme
|
= theme_style_tags current_theme
|
||||||
|
= vite_client_tag
|
||||||
|
= vite_react_refresh_tag
|
||||||
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
||||||
= stylesheet_pack_tag 'inert', media: 'all', crossorigin: 'anonymous', id: 'inert-style'
|
= vite_stylesheet_tag 'styles/inert.scss', media: 'all', id: 'inert-style'
|
||||||
|
|
||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
-# = preload_pack_asset "locale/#{I18n.locale}-json.js"
|
||||||
= preload_pack_asset "locale/#{I18n.locale}-json.js"
|
|
||||||
= csrf_meta_tags unless skip_csrf_meta_tags?
|
= csrf_meta_tags unless skip_csrf_meta_tags?
|
||||||
%meta{ name: 'style-nonce', content: request.content_security_policy_nonce }
|
%meta{ name: 'style-nonce', content: request.content_security_policy_nonce }
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :content do
|
- content_for :content do
|
||||||
.container-alt
|
.container-alt
|
||||||
|
|
|
@ -11,11 +11,12 @@
|
||||||
- if storage_host?
|
- if storage_host?
|
||||||
%link{ rel: 'dns-prefetch', href: storage_host }/
|
%link{ rel: 'dns-prefetch', href: storage_host }/
|
||||||
|
|
||||||
|
= vite_client_tag
|
||||||
|
= vite_react_refresh_tag
|
||||||
= theme_style_tags 'mastodon-light'
|
= theme_style_tags 'mastodon-light'
|
||||||
= javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
|
-# = preload_pack_asset "locale/#{I18n.locale}-json.js"
|
||||||
= preload_pack_asset "locale/#{I18n.locale}-json.js"
|
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'embed', integrity: true, crossorigin: 'anonymous'
|
= vite_typescript_tag 'embed.tsx', integrity: true, crossorigin: 'anonymous'
|
||||||
%body.embed
|
%body.embed
|
||||||
= yield
|
= yield
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
%meta{ charset: 'utf-8' }/
|
%meta{ charset: 'utf-8' }/
|
||||||
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
||||||
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
||||||
|
= vite_client_tag
|
||||||
|
= vite_react_refresh_tag
|
||||||
= theme_style_tags Setting.default_settings['theme']
|
= theme_style_tags Setting.default_settings['theme']
|
||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
= vite_typescript_tag 'error.ts', crossorigin: 'anonymous'
|
||||||
= javascript_pack_tag 'error', crossorigin: 'anonymous'
|
|
||||||
%body.error
|
%body.error
|
||||||
.dialog
|
.dialog
|
||||||
.dialog__illustration
|
.dialog__illustration
|
||||||
|
|
|
@ -3,6 +3,4 @@
|
||||||
%head
|
%head
|
||||||
%meta{ charset: 'utf-8' }/
|
%meta{ charset: 'utf-8' }/
|
||||||
|
|
||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
|
||||||
|
|
||||||
= yield :header_tags
|
= yield :header_tags
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
</o:OfficeDocumentSettings>
|
</o:OfficeDocumentSettings>
|
||||||
</xml>
|
</xml>
|
||||||
= stylesheet_pack_tag 'mailer'
|
= vite_stylesheet_tag 'styles/mailer.scss'
|
||||||
%body
|
%body
|
||||||
.email{ dir: locale_direction }
|
.email{ dir: locale_direction }
|
||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :body_classes, 'modal-layout compose-standalone'
|
- content_for :body_classes, 'modal-layout compose-standalone'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :body_classes, 'player'
|
- content_for :body_classes, 'player'
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
= t('settings.relationships')
|
= t('settings.relationships')
|
||||||
|
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
= vite_typescript_tag 'admin.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
.filters
|
.filters
|
||||||
.filter-subset
|
.filter-subset
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
%meta{ name: 'robots', content: 'noindex' }/
|
%meta{ name: 'robots', content: 'noindex' }/
|
||||||
|
|
||||||
= javascript_pack_tag 'remote_interaction_helper', crossorigin: 'anonymous'
|
= vite_typescript_tag 'remote_interaction_helper.ts', crossorigin: 'anonymous'
|
||||||
|
|
|
@ -13,4 +13,4 @@
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('webauthn_credentials.add'), class: 'js-webauthn', type: :submit
|
= f.button :button, t('webauthn_credentials.add'), class: 'js-webauthn', type: :submit
|
||||||
|
|
||||||
= javascript_pack_tag 'two_factor_authentication', crossorigin: 'anonymous'
|
= vite_typescript_tag 'two_factor_authentication.ts', crossorigin: 'anonymous'
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
- content_for :body_classes, 'app-body'
|
- content_for :body_classes, 'app-body'
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
- if user_signed_in?
|
- if user_signed_in?
|
||||||
= preload_pack_asset 'features/compose.js'
|
-#
|
||||||
= preload_pack_asset 'features/home_timeline.js'
|
= preload_pack_asset 'features/compose.js'
|
||||||
= preload_pack_asset 'features/notifications.js'
|
= preload_pack_asset 'features/home_timeline.js'
|
||||||
|
= preload_pack_asset 'features/notifications.js'
|
||||||
%meta{ name: 'initialPath', content: request.path }
|
%meta{ name: 'initialPath', content: request.path }
|
||||||
|
|
||||||
%meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
|
%meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
|
||||||
|
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'application', crossorigin: 'anonymous'
|
= vite_typescript_tag 'application.ts', crossorigin: 'anonymous'
|
||||||
|
|
||||||
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
||||||
%noscript
|
%noscript
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'share', crossorigin: 'anonymous'
|
= vite_typescript_tag 'share.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
#mastodon-compose{ data: { props: Oj.dump(default_props) } }
|
#mastodon-compose{ data: { props: Oj.dump(default_props) } }
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
module.exports = (api) => {
|
|
||||||
const env = api.env();
|
|
||||||
|
|
||||||
const reactOptions = {
|
|
||||||
development: false,
|
|
||||||
runtime: 'automatic',
|
|
||||||
};
|
|
||||||
|
|
||||||
const envOptions = {
|
|
||||||
useBuiltIns: "usage",
|
|
||||||
corejs: { version: "3.30" },
|
|
||||||
debug: false,
|
|
||||||
include: [
|
|
||||||
'transform-numeric-separator',
|
|
||||||
'transform-optional-chaining',
|
|
||||||
'transform-nullish-coalescing-operator',
|
|
||||||
'transform-class-properties',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const plugins = [
|
|
||||||
['formatjs'],
|
|
||||||
'preval',
|
|
||||||
];
|
|
||||||
|
|
||||||
switch (env) {
|
|
||||||
case 'production':
|
|
||||||
plugins.push(...[
|
|
||||||
'lodash',
|
|
||||||
[
|
|
||||||
'transform-react-remove-prop-types',
|
|
||||||
{
|
|
||||||
mode: 'remove',
|
|
||||||
removeImport: true,
|
|
||||||
additionalLibraries: [
|
|
||||||
'react-immutable-proptypes',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'@babel/transform-react-inline-elements',
|
|
||||||
[
|
|
||||||
'@babel/transform-runtime',
|
|
||||||
{
|
|
||||||
helpers: true,
|
|
||||||
regenerator: false,
|
|
||||||
useESModules: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'development':
|
|
||||||
reactOptions.development = true;
|
|
||||||
envOptions.debug = true;
|
|
||||||
|
|
||||||
// We need Babel to not inject polyfills in dev, as this breaks `preval` files
|
|
||||||
envOptions.useBuiltIns = false;
|
|
||||||
envOptions.corejs = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
presets: [
|
|
||||||
'@babel/preset-typescript',
|
|
||||||
['@babel/react', reactOptions],
|
|
||||||
['@babel/env', envOptions],
|
|
||||||
],
|
|
||||||
plugins,
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
test: [/tesseract\.js/, /fuzzysort\.js/],
|
|
||||||
presets: [
|
|
||||||
['@babel/env', { ...envOptions, modules: 'commonjs' }],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
27
bin/vite
Executable file
27
bin/vite
Executable file
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
#
|
||||||
|
# This file was generated by Bundler.
|
||||||
|
#
|
||||||
|
# The application 'vite' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
||||||
|
|
||||||
|
bundle_binstub = File.expand_path("bundle", __dir__)
|
||||||
|
|
||||||
|
if File.file?(bundle_binstub)
|
||||||
|
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
||||||
|
load(bundle_binstub)
|
||||||
|
else
|
||||||
|
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
||||||
|
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
load Gem.bin_path("vite_ruby", "vite")
|
19
bin/webpack
19
bin/webpack
|
@ -1,19 +0,0 @@
|
||||||
#!/usr/bin/env ruby
|
|
||||||
|
|
||||||
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
|
|
||||||
ENV["NODE_ENV"] ||= "development"
|
|
||||||
|
|
||||||
require "pathname"
|
|
||||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
|
||||||
Pathname.new(__FILE__).realpath)
|
|
||||||
|
|
||||||
require "rubygems"
|
|
||||||
require "bundler/setup"
|
|
||||||
|
|
||||||
require "webpacker"
|
|
||||||
require "webpacker/webpack_runner"
|
|
||||||
|
|
||||||
APP_ROOT = File.expand_path("..", __dir__)
|
|
||||||
Dir.chdir(APP_ROOT) do
|
|
||||||
Webpacker::WebpackRunner.run(ARGV)
|
|
||||||
end
|
|
|
@ -1,19 +0,0 @@
|
||||||
#!/usr/bin/env ruby
|
|
||||||
|
|
||||||
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
|
|
||||||
ENV["NODE_ENV"] ||= "development"
|
|
||||||
|
|
||||||
require "pathname"
|
|
||||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
|
||||||
Pathname.new(__FILE__).realpath)
|
|
||||||
|
|
||||||
require "rubygems"
|
|
||||||
require "bundler/setup"
|
|
||||||
|
|
||||||
require "webpacker"
|
|
||||||
require "webpacker/dev_server_runner"
|
|
||||||
|
|
||||||
APP_ROOT = File.expand_path("..", __dir__)
|
|
||||||
Dir.chdir(APP_ROOT) do
|
|
||||||
Webpacker::DevServerRunner.run(ARGV)
|
|
||||||
end
|
|
|
@ -46,14 +46,13 @@ require_relative '../lib/chewy/settings_extensions'
|
||||||
require_relative '../lib/chewy/index_extensions'
|
require_relative '../lib/chewy/index_extensions'
|
||||||
require_relative '../lib/chewy/strategy/mastodon'
|
require_relative '../lib/chewy/strategy/mastodon'
|
||||||
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
||||||
require_relative '../lib/webpacker/manifest_extensions'
|
|
||||||
require_relative '../lib/webpacker/helper_extensions'
|
|
||||||
require_relative '../lib/rails/engine_extensions'
|
require_relative '../lib/rails/engine_extensions'
|
||||||
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
||||||
require_relative '../lib/stoplight/redis_data_store_extensions'
|
require_relative '../lib/stoplight/redis_data_store_extensions'
|
||||||
require_relative '../lib/active_record/database_tasks_extensions'
|
require_relative '../lib/active_record/database_tasks_extensions'
|
||||||
require_relative '../lib/active_record/batches'
|
require_relative '../lib/active_record/batches'
|
||||||
require_relative '../lib/simple_navigation/item_extensions'
|
require_relative '../lib/simple_navigation/item_extensions'
|
||||||
|
require_relative '../lib/vite_ruby/sri_extensions'
|
||||||
|
|
||||||
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
|
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ Rails.application.config.content_security_policy do |p|
|
||||||
p.frame_ancestors :none
|
p.frame_ancestors :none
|
||||||
p.font_src :self, assets_host
|
p.font_src :self, assets_host
|
||||||
p.img_src :self, :data, :blob, *media_hosts
|
p.img_src :self, :data, :blob, *media_hosts
|
||||||
p.style_src :self, assets_host
|
|
||||||
p.media_src :self, :data, *media_hosts
|
p.media_src :self, :data, *media_hosts
|
||||||
p.manifest_src :self, assets_host
|
p.manifest_src :self, assets_host
|
||||||
|
|
||||||
|
@ -32,16 +31,18 @@ Rails.application.config.content_security_policy do |p|
|
||||||
p.worker_src :self, :blob, assets_host
|
p.worker_src :self, :blob, assets_host
|
||||||
|
|
||||||
if Rails.env.development?
|
if Rails.env.development?
|
||||||
webpacker_public_host = ENV.fetch('WEBPACKER_DEV_SERVER_PUBLIC', Webpacker.config.dev_server[:public])
|
# Hacky solution to force CSP to correctly allow localhost, even if ViteRuby is bound to 0.0.0.0.
|
||||||
front_end_build_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{webpacker_public_host}" }
|
front_end_build_urls = %w(ws http).map { |protocol| "#{protocol}#{ViteRuby.config.https ? 's' : ''}://localhost:#{ViteRuby.config.port}" }
|
||||||
|
|
||||||
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url, *front_end_build_urls
|
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url, *front_end_build_urls
|
||||||
p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
|
p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
|
||||||
p.frame_src :self, :https, :http
|
p.frame_src :self, :https, :http
|
||||||
|
p.style_src :self, assets_host, :unsafe_inline
|
||||||
else
|
else
|
||||||
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url
|
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url
|
||||||
p.script_src :self, assets_host, "'wasm-unsafe-eval'"
|
p.script_src :self, assets_host, "'wasm-unsafe-eval'"
|
||||||
p.frame_src :self, :https
|
p.frame_src :self, :https
|
||||||
|
p.style_src :self, assets_host
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
20
config/vite.json
Normal file
20
config/vite.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"all": {
|
||||||
|
"sourceCodeDir": "app/javascript",
|
||||||
|
"additionalEntrypoints": ["~/{fonts,icons,images}/**/*", "~/styles/*.scss"],
|
||||||
|
"watchAdditionalPaths": []
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"publicOutputDir": "packs"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"autoBuild": true,
|
||||||
|
"publicOutputDir": "packs-dev",
|
||||||
|
"port": 3036
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"autoBuild": true,
|
||||||
|
"publicOutputDir": "packs-test",
|
||||||
|
"port": 3037
|
||||||
|
}
|
||||||
|
}
|
85
config/vite/plugin-manifest-sri.ts
Normal file
85
config/vite/plugin-manifest-sri.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
import type { Plugin, Manifest } from 'vite';
|
||||||
|
|
||||||
|
export type Algorithm = 'sha256' | 'sha384' | 'sha512';
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
/**
|
||||||
|
* Which hashing algorithms to use when calculate the integrity hash for each
|
||||||
|
* asset in the manifest.
|
||||||
|
* @default ['sha384']
|
||||||
|
*/
|
||||||
|
algorithms?: Algorithm[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'vite' {
|
||||||
|
interface ManifestChunk {
|
||||||
|
integrity: string;
|
||||||
|
assetIntegrity: Record<string, string>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function manifestSRI(options: Options = {}): Plugin {
|
||||||
|
const { algorithms = ['sha384'] } = options;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-manifest-sri',
|
||||||
|
apply: 'build',
|
||||||
|
enforce: 'post',
|
||||||
|
async writeBundle({ dir }) {
|
||||||
|
await augmentManifest('manifest.json', algorithms, dir ?? '');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function augmentManifest(
|
||||||
|
manifestPath: string,
|
||||||
|
algorithms: string[],
|
||||||
|
outDir: string,
|
||||||
|
) {
|
||||||
|
const resolveInOutDir = (path: string) => resolve(outDir, path);
|
||||||
|
manifestPath = resolveInOutDir(manifestPath);
|
||||||
|
|
||||||
|
const manifest: Manifest | undefined = await fs
|
||||||
|
.readFile(manifestPath, 'utf-8')
|
||||||
|
.then((file) => JSON.parse(file) as Manifest);
|
||||||
|
|
||||||
|
if (!manifest) {
|
||||||
|
throw new Error(`Manifest file not found at ${manifestPath}`);
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
Object.values(manifest).map(async (chunk) => {
|
||||||
|
chunk.integrity = integrityForAsset(
|
||||||
|
await fs.readFile(resolveInOutDir(chunk.file)),
|
||||||
|
algorithms,
|
||||||
|
);
|
||||||
|
if (!chunk.assets && !chunk.css) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chunk.assetIntegrity = {};
|
||||||
|
await Promise.all(
|
||||||
|
(chunk.assets ?? []).concat(chunk.css ?? []).map(async (asset) => {
|
||||||
|
chunk.assetIntegrity[asset] = integrityForAsset(
|
||||||
|
await fs.readFile(resolveInOutDir(asset)),
|
||||||
|
algorithms,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function integrityForAsset(source: Buffer, algorithms: string[]) {
|
||||||
|
return algorithms
|
||||||
|
.map((algorithm) => calculateIntegrityHash(source, algorithm))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateIntegrityHash(source: Buffer, algorithm: string) {
|
||||||
|
const hash = createHash(algorithm).update(source).digest().toString('base64');
|
||||||
|
return `${algorithm.toLowerCase()}-${hash}`;
|
||||||
|
}
|
82
config/vite/plugin-sw-locales.ts
Normal file
82
config/vite/plugin-sw-locales.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/* This plugin provides the `virtual:mastodon-sw-locales` import
|
||||||
|
which exports translations for every locales, but only with the
|
||||||
|
keys defined below.
|
||||||
|
This is used by the notifications code in the service-worker, to
|
||||||
|
provide localised texts without having to load all the translations
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import type { Plugin, ResolvedConfig } from 'vite';
|
||||||
|
|
||||||
|
const KEEP_KEYS = [
|
||||||
|
'notification.favourite',
|
||||||
|
'notification.follow',
|
||||||
|
'notification.follow_request',
|
||||||
|
'notification.mention',
|
||||||
|
'notification.reblog',
|
||||||
|
'notification.poll',
|
||||||
|
'notification.status',
|
||||||
|
'notification.update',
|
||||||
|
'notification.admin.sign_up',
|
||||||
|
'status.show_more',
|
||||||
|
'status.reblog',
|
||||||
|
'status.favourite',
|
||||||
|
'notifications.group',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function MastodonServiceWorkerLocales(): Plugin {
|
||||||
|
const virtualModuleId = 'virtual:mastodon-sw-locales';
|
||||||
|
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||||
|
|
||||||
|
let config: ResolvedConfig;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'mastodon-sw-locales',
|
||||||
|
configResolved(resolvedConfig) {
|
||||||
|
config = resolvedConfig;
|
||||||
|
},
|
||||||
|
resolveId(id) {
|
||||||
|
if (id === virtualModuleId) {
|
||||||
|
return resolvedVirtualModuleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
load(id) {
|
||||||
|
if (id === resolvedVirtualModuleId) {
|
||||||
|
const filteredLocales: Record<string, Record<string, string>> = {};
|
||||||
|
const localesPath = path.resolve(config.root, 'mastodon/locales');
|
||||||
|
|
||||||
|
const filenames = fs.readdirSync(localesPath);
|
||||||
|
|
||||||
|
filenames
|
||||||
|
.filter((filename) => /[a-zA-Z-]+\.json$/.exec(filename))
|
||||||
|
.forEach((filename) => {
|
||||||
|
const content = fs.readFileSync(
|
||||||
|
path.resolve(localesPath, filename),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const full = JSON.parse(content) as Record<string, string>;
|
||||||
|
const locale = filename.split('.')[0];
|
||||||
|
|
||||||
|
if (!locale)
|
||||||
|
throw new Error('Could not parse locale from filename');
|
||||||
|
|
||||||
|
const filteredLocale: Record<string, string> = {};
|
||||||
|
|
||||||
|
Object.entries(full).forEach(([key, value]) => {
|
||||||
|
if (KEEP_KEYS.includes(key)) filteredLocale[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredLocales[locale] = filteredLocale;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `const locales = ${JSON.stringify(filteredLocales)}; \n export default locales;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
// Common configuration for webpacker loaded from config/webpacker.yml
|
|
||||||
|
|
||||||
const { readFileSync } = require('fs');
|
|
||||||
const { resolve } = require('path');
|
|
||||||
const { env } = require('process');
|
|
||||||
|
|
||||||
const { load } = require('js-yaml');
|
|
||||||
|
|
||||||
const configPath = resolve('config', 'webpacker.yml');
|
|
||||||
const settings = load(readFileSync(configPath), 'utf8')[env.RAILS_ENV || env.NODE_ENV];
|
|
||||||
|
|
||||||
const themePath = resolve('config', 'themes.yml');
|
|
||||||
const themes = load(readFileSync(themePath), 'utf8');
|
|
||||||
|
|
||||||
const output = {
|
|
||||||
path: resolve('public', settings.public_output_path),
|
|
||||||
publicPath: `/${settings.public_output_path}/`,
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
settings,
|
|
||||||
themes,
|
|
||||||
env: {
|
|
||||||
NODE_ENV: env.NODE_ENV,
|
|
||||||
PUBLIC_OUTPUT_PATH: settings.public_output_path,
|
|
||||||
},
|
|
||||||
output,
|
|
||||||
};
|
|
|
@ -1,62 +0,0 @@
|
||||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
|
||||||
|
|
||||||
const { merge } = require('webpack-merge');
|
|
||||||
|
|
||||||
const { settings, output } = require('./configuration');
|
|
||||||
const sharedConfig = require('./shared');
|
|
||||||
|
|
||||||
const watchOptions = {};
|
|
||||||
|
|
||||||
if (process.env.VAGRANT) {
|
|
||||||
// If we are in Vagrant, we can't rely on inotify to update us with changed
|
|
||||||
// files, so we must poll instead. Here, we poll every second to see if
|
|
||||||
// anything has changed.
|
|
||||||
watchOptions.poll = 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = merge(sharedConfig, {
|
|
||||||
mode: 'development',
|
|
||||||
cache: true,
|
|
||||||
devtool: 'cheap-module-eval-source-map',
|
|
||||||
|
|
||||||
stats: {
|
|
||||||
errorDetails: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
output: {
|
|
||||||
pathinfo: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
devServer: {
|
|
||||||
clientLogLevel: 'none',
|
|
||||||
compress: settings.dev_server.compress,
|
|
||||||
quiet: settings.dev_server.quiet,
|
|
||||||
disableHostCheck: settings.dev_server.disable_host_check,
|
|
||||||
host: settings.dev_server.host,
|
|
||||||
port: settings.dev_server.port,
|
|
||||||
https: settings.dev_server.https,
|
|
||||||
hot: settings.dev_server.hmr,
|
|
||||||
contentBase: output.path,
|
|
||||||
inline: settings.dev_server.inline,
|
|
||||||
useLocalIp: settings.dev_server.use_local_ip,
|
|
||||||
public: settings.dev_server.public,
|
|
||||||
publicPath: output.publicPath,
|
|
||||||
historyApiFallback: {
|
|
||||||
disableDotRule: true,
|
|
||||||
},
|
|
||||||
headers: settings.dev_server.headers,
|
|
||||||
overlay: settings.dev_server.overlay,
|
|
||||||
stats: {
|
|
||||||
entrypoints: false,
|
|
||||||
errorDetails: false,
|
|
||||||
modules: false,
|
|
||||||
moduleTrace: false,
|
|
||||||
},
|
|
||||||
watchOptions: Object.assign(
|
|
||||||
{},
|
|
||||||
settings.dev_server.watch_options,
|
|
||||||
watchOptions,
|
|
||||||
),
|
|
||||||
writeToDisk: filePath => /ocr/.test(filePath),
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,74 +0,0 @@
|
||||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
|
||||||
|
|
||||||
const { createHash } = require('crypto');
|
|
||||||
const { readFileSync } = require('fs');
|
|
||||||
const { resolve } = require('path');
|
|
||||||
|
|
||||||
const CompressionPlugin = require('compression-webpack-plugin');
|
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
|
||||||
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
|
||||||
const { merge } = require('webpack-merge');
|
|
||||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
|
||||||
|
|
||||||
const sharedConfig = require('./shared');
|
|
||||||
|
|
||||||
const root = resolve(__dirname, '..', '..');
|
|
||||||
|
|
||||||
module.exports = merge(sharedConfig, {
|
|
||||||
mode: 'production',
|
|
||||||
devtool: 'source-map',
|
|
||||||
stats: 'normal',
|
|
||||||
bail: true,
|
|
||||||
optimization: {
|
|
||||||
minimize: true,
|
|
||||||
minimizer: [
|
|
||||||
new TerserPlugin({
|
|
||||||
cache: true,
|
|
||||||
parallel: true,
|
|
||||||
sourceMap: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new CompressionPlugin({
|
|
||||||
filename: '[path][base].gz[query]',
|
|
||||||
cache: true,
|
|
||||||
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
|
||||||
}),
|
|
||||||
new CompressionPlugin({
|
|
||||||
filename: '[path][base].br[query]',
|
|
||||||
algorithm: 'brotliCompress',
|
|
||||||
cache: true,
|
|
||||||
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
|
|
||||||
}),
|
|
||||||
new BundleAnalyzerPlugin({ // generates report.html
|
|
||||||
analyzerMode: 'static',
|
|
||||||
openAnalyzer: false,
|
|
||||||
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
|
|
||||||
}),
|
|
||||||
new InjectManifest({
|
|
||||||
additionalManifestEntries: ['1f602.svg', 'sheet_15_1.png'].map((filename) => {
|
|
||||||
const path = resolve(root, 'public', 'emoji', filename);
|
|
||||||
const body = readFileSync(path);
|
|
||||||
const md5 = createHash('md5');
|
|
||||||
|
|
||||||
md5.update(body);
|
|
||||||
|
|
||||||
return {
|
|
||||||
revision: md5.digest('hex'),
|
|
||||||
url: `/emoji/${filename}`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
exclude: [
|
|
||||||
/(?:base|extra)_polyfills-.*\.js$/,
|
|
||||||
/locale_.*\.js$/,
|
|
||||||
/mailer-.*\.(?:css|js)$/,
|
|
||||||
],
|
|
||||||
include: [/\.js$/, /\.css$/],
|
|
||||||
maximumFileSizeToCacheInBytes: 2 * 1_024 * 1_024, // 2 MiB
|
|
||||||
swDest: resolve(root, 'public', 'packs', 'sw.js'),
|
|
||||||
swSrc: resolve(root, 'app', 'javascript', 'mastodon', 'service_worker', 'entry.js'),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
|
@ -1,28 +0,0 @@
|
||||||
const { join, resolve } = require('path');
|
|
||||||
|
|
||||||
const { env, settings } = require('../configuration');
|
|
||||||
|
|
||||||
// Those modules contain modern ES code that need to be transpiled for Webpack to process it
|
|
||||||
const nodeModulesToProcess = [
|
|
||||||
'@reduxjs', 'fuzzysort', 'toygrad', '@react-spring'
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
test: /\.(js|jsx|mjs|ts|tsx)$/,
|
|
||||||
include: [
|
|
||||||
settings.source_path,
|
|
||||||
...settings.resolved_paths,
|
|
||||||
...nodeModulesToProcess.map(p => resolve(`node_modules/${p}`)),
|
|
||||||
].map(p => resolve(p)),
|
|
||||||
exclude: new RegExp('node_modules\\/(?!(' + nodeModulesToProcess.join('|')+')\\/).*'),
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
cacheDirectory: join(settings.cache_path, 'babel-loader'),
|
|
||||||
cacheCompression: env.NODE_ENV === 'production',
|
|
||||||
compact: env.NODE_ENV === 'production',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
test: /\.s?css$/i,
|
|
||||||
use: [
|
|
||||||
MiniCssExtractPlugin.loader,
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
sourceMap: true,
|
|
||||||
importLoaders: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'postcss-loader',
|
|
||||||
options: {
|
|
||||||
sourceMap: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loader: 'sass-loader',
|
|
||||||
options: {
|
|
||||||
implementation: require('sass'),
|
|
||||||
sourceMap: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
const { join } = require('path');
|
|
||||||
|
|
||||||
const { settings } = require('../configuration');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
test: new RegExp(`(${settings.static_assets_extensions.join('|')})$`, 'i'),
|
|
||||||
exclude: [/material-icons/, /svg-icons/],
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name(file) {
|
|
||||||
if (file.includes(settings.source_path)) {
|
|
||||||
return 'media/[path][name]-[hash].[ext]';
|
|
||||||
}
|
|
||||||
return 'media/[folder]/[name]-[hash:8].[ext]';
|
|
||||||
},
|
|
||||||
context: join(settings.source_path),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,16 +0,0 @@
|
||||||
const babel = require('./babel');
|
|
||||||
const css = require('./css');
|
|
||||||
const file = require('./file');
|
|
||||||
const materialIcons = require('./material_icons');
|
|
||||||
const tesseract = require('./tesseract');
|
|
||||||
|
|
||||||
// Webpack loaders are processed in reverse order
|
|
||||||
// https://webpack.js.org/concepts/loaders/#loader-features
|
|
||||||
// Lastly, process static files using file loader
|
|
||||||
module.exports = {
|
|
||||||
materialIcons,
|
|
||||||
file,
|
|
||||||
tesseract,
|
|
||||||
css,
|
|
||||||
babel,
|
|
||||||
};
|
|
|
@ -1,8 +0,0 @@
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
module.exports = {};
|
|
||||||
} else {
|
|
||||||
module.exports = {
|
|
||||||
test: /\.js$/,
|
|
||||||
loader: 'mark-loader',
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
test: /\.svg$/,
|
|
||||||
include: [/material-icons/, /svg-icons/],
|
|
||||||
issuer: /\.[jt]sx?$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: '@svgr/webpack',
|
|
||||||
options: {
|
|
||||||
svgo: false,
|
|
||||||
titleProp: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,13 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
test: [
|
|
||||||
/tesseract\.js\/dist\/worker\.min\.js$/,
|
|
||||||
/tesseract\.js\/dist\/worker\.min\.js\.map$/,
|
|
||||||
/tesseract\.js-core\/tesseract-core\.wasm\.js$/,
|
|
||||||
],
|
|
||||||
use: {
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: 'ocr/[name]-[hash].[ext]',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,113 +0,0 @@
|
||||||
// Note: You must restart bin/webpack-dev-server for changes to take effect
|
|
||||||
|
|
||||||
const { basename, dirname, join, relative, resolve } = require('path');
|
|
||||||
|
|
||||||
const CircularDependencyPlugin = require('circular-dependency-plugin');
|
|
||||||
const { sync } = require('glob');
|
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
const extname = require('path-complete-extname');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const AssetsManifestPlugin = require('webpack-assets-manifest');
|
|
||||||
|
|
||||||
const { env, settings, themes, output } = require('./configuration');
|
|
||||||
const rules = require('./rules');
|
|
||||||
|
|
||||||
const extensionGlob = `**/*{${settings.extensions.join(',')}}*`;
|
|
||||||
const entryPath = join(settings.source_path, settings.source_entry_path);
|
|
||||||
const packPaths = sync(join(entryPath, extensionGlob));
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: Object.assign(
|
|
||||||
packPaths.reduce((map, entry) => {
|
|
||||||
const localMap = map;
|
|
||||||
const namespace = relative(join(entryPath), dirname(entry));
|
|
||||||
localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry);
|
|
||||||
return localMap;
|
|
||||||
}, {}),
|
|
||||||
Object.keys(themes).reduce((themePaths, name) => {
|
|
||||||
themePaths[name] = resolve(join(settings.source_path, themes[name]));
|
|
||||||
return themePaths;
|
|
||||||
}, {}),
|
|
||||||
),
|
|
||||||
|
|
||||||
output: {
|
|
||||||
filename: 'js/[name]-[chunkhash].js',
|
|
||||||
chunkFilename: 'js/[name]-[chunkhash].chunk.js',
|
|
||||||
hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
|
|
||||||
hashFunction: 'sha256',
|
|
||||||
crossOriginLoading: 'anonymous',
|
|
||||||
path: output.path,
|
|
||||||
publicPath: output.publicPath,
|
|
||||||
},
|
|
||||||
|
|
||||||
optimization: {
|
|
||||||
runtimeChunk: {
|
|
||||||
name: 'common',
|
|
||||||
},
|
|
||||||
splitChunks: {
|
|
||||||
cacheGroups: {
|
|
||||||
default: false,
|
|
||||||
vendors: false,
|
|
||||||
common: {
|
|
||||||
name: 'common',
|
|
||||||
chunks: 'all',
|
|
||||||
minChunks: 2,
|
|
||||||
minSize: 0,
|
|
||||||
test: /^(?!.*[\\/]node_modules[\\/]react-intl[\\/]).+$/,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
occurrenceOrder: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
module: {
|
|
||||||
rules: Object.keys(rules).map(key => rules[key]),
|
|
||||||
strictExportPresence: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
|
|
||||||
new webpack.NormalModuleReplacementPlugin(
|
|
||||||
/^history\//, (resource) => {
|
|
||||||
// temporary fix for https://github.com/ReactTraining/react-router/issues/5576
|
|
||||||
// to reduce bundle size
|
|
||||||
resource.request = resource.request.replace(/^history/, 'history/es');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: 'css/[name]-[contenthash:8].css',
|
|
||||||
chunkFilename: 'css/[name]-[contenthash:8].chunk.css',
|
|
||||||
}),
|
|
||||||
new AssetsManifestPlugin({
|
|
||||||
integrity: true,
|
|
||||||
integrityHashes: ['sha256'],
|
|
||||||
entrypoints: true,
|
|
||||||
writeToDisk: true,
|
|
||||||
publicPath: true,
|
|
||||||
}),
|
|
||||||
new CircularDependencyPlugin({
|
|
||||||
failOnError: true,
|
|
||||||
})
|
|
||||||
],
|
|
||||||
|
|
||||||
resolve: {
|
|
||||||
extensions: settings.extensions,
|
|
||||||
modules: [
|
|
||||||
resolve(settings.source_path),
|
|
||||||
'node_modules',
|
|
||||||
],
|
|
||||||
alias: {
|
|
||||||
"@": resolve(settings.source_path),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resolveLoader: {
|
|
||||||
modules: ['node_modules'],
|
|
||||||
},
|
|
||||||
|
|
||||||
node: {
|
|
||||||
// Called by http-link-header in an API we never use, increases
|
|
||||||
// bundle size unnecessarily
|
|
||||||
Buffer: false,
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,94 +0,0 @@
|
||||||
# Note: You must restart bin/webpack-dev-server for changes to take effect
|
|
||||||
|
|
||||||
default: &default
|
|
||||||
source_path: app/javascript
|
|
||||||
source_entry_path: entrypoints
|
|
||||||
public_root_path: public
|
|
||||||
public_output_path: packs
|
|
||||||
cache_path: tmp/cache/webpacker
|
|
||||||
check_yarn_integrity: false
|
|
||||||
webpack_compile_output: false
|
|
||||||
|
|
||||||
# Additional paths webpack should lookup modules
|
|
||||||
# ['app/assets', 'engine/foo/app/assets']
|
|
||||||
resolved_paths: []
|
|
||||||
|
|
||||||
# Cache manifest.json for performance
|
|
||||||
cache_manifest: true
|
|
||||||
|
|
||||||
# Extract and emit a css file
|
|
||||||
extract_css: true
|
|
||||||
|
|
||||||
static_assets_extensions:
|
|
||||||
- .jpg
|
|
||||||
- .jpeg
|
|
||||||
- .png
|
|
||||||
- .tiff
|
|
||||||
- .ico
|
|
||||||
- .svg
|
|
||||||
- .eot
|
|
||||||
- .otf
|
|
||||||
- .ttf
|
|
||||||
- .woff
|
|
||||||
- .woff2
|
|
||||||
|
|
||||||
extensions:
|
|
||||||
- .mjs
|
|
||||||
- .js
|
|
||||||
- .jsx
|
|
||||||
- .ts
|
|
||||||
- .tsx
|
|
||||||
- .sass
|
|
||||||
- .scss
|
|
||||||
- .css
|
|
||||||
- .module.sass
|
|
||||||
- .module.scss
|
|
||||||
- .module.css
|
|
||||||
- .png
|
|
||||||
- .svg
|
|
||||||
- .gif
|
|
||||||
- .jpeg
|
|
||||||
- .jpg
|
|
||||||
|
|
||||||
development:
|
|
||||||
<<: *default
|
|
||||||
|
|
||||||
compile: true
|
|
||||||
|
|
||||||
# Reload manifest in development environment so we pick up changes
|
|
||||||
cache_manifest: false
|
|
||||||
|
|
||||||
# Reference: https://webpack.js.org/configuration/dev-server/
|
|
||||||
dev_server:
|
|
||||||
https: false
|
|
||||||
host: 0.0.0.0
|
|
||||||
port: 3035
|
|
||||||
public: localhost:3035
|
|
||||||
hmr: false
|
|
||||||
# Inline should be set to true if using HMR
|
|
||||||
inline: true
|
|
||||||
overlay: true
|
|
||||||
compress: true
|
|
||||||
disable_host_check: true
|
|
||||||
use_local_ip: false
|
|
||||||
quiet: false
|
|
||||||
headers:
|
|
||||||
'Access-Control-Allow-Origin': '*'
|
|
||||||
watch_options:
|
|
||||||
ignored: '**/node_modules/**'
|
|
||||||
|
|
||||||
test:
|
|
||||||
<<: *default
|
|
||||||
|
|
||||||
# CI precompiles packs prior to running the tests.
|
|
||||||
# Also avoids race conditions in parallel_tests.
|
|
||||||
compile: false
|
|
||||||
|
|
||||||
# Compile test packs to a separate directory
|
|
||||||
public_output_path: packs-test
|
|
||||||
|
|
||||||
production:
|
|
||||||
<<: *default
|
|
||||||
|
|
||||||
# Production depends on precompilation of packs prior to booting for performance.
|
|
||||||
compile: false
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user