diff --git a/.config/ci.yml b/.config/ci.yml index 44092d36623b4ff32fec95a83473623eccac2e2f..d20ede8d3575dff504363d7bd2b10d5c94da879a 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -167,8 +167,18 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) -maxNoteLength: 3000 +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) +#maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +#maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +#maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +#maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml new file mode 100644 index 0000000000000000000000000000000000000000..d8013a1c95050cfd19430c7a53af874a2496a339 --- /dev/null +++ b/.config/cypress-devcontainer.yml @@ -0,0 +1,224 @@ +#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â” +# Misskey configuration +#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â” + +# ┌─────┠+#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: 'http://misskey.local' + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# ┌───────────────────────┠+#───┘ Port and TLS settings └─────────────────────────────────── + +# +# Misskey requires a reverse proxy to support HTTPS connections. +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to set up a reverse proxy. (e.g. nginx) +# An encrypted connection with HTTPS is highly recommended +# because tokens may be transferred in GET requests. + +# The port that your Misskey server should listen on. +port: 61812 + +# ┌──────────────────────────┠+#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + host: db + port: 5432 + + # Database name + db: misskey + + # Auth + user: postgres + pass: postgres + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +dbReplications: false + +# You can configure any number of replicas here +#dbSlaves: +# - +# host: +# port: +# db: +# user: +# pass: +# - +# host: +# port: +# db: +# user: +# pass: + +# ┌─────────────────────┠+#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: redis + port: 6379 + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + #pass: example-pass + #prefix: example-prefix + #db: 1 + +#redisForPubsub: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + +#redisForJobQueue: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + +#redisForTimelines: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + +#redisForReactions: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + +# ┌───────────────────────────┠+#───┘ MeiliSearch configuration └───────────────────────────── + +#meilisearch: +# host: meilisearch +# port: 7700 +# apiKey: '' +# ssl: true +# index: '' + +# ┌───────────────┠+#───┘ ID generation └─────────────────────────────────────────── + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# aidx ... Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: 'aidx' + +# ┌────────────────┠+#───┘ Error tracking └────────────────────────────────────────── + +# Sentry is available for error tracking. +# See the Sentry documentation for more details on options. + +#sentryForBackend: +# enableNodeProfiling: true +# options: +# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' + +#sentryForFrontend: +# options: +# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' + +# ┌─────────────────────┠+#───┘ Other configuration └───────────────────────────────────── + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 32 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) +#maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +#maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +#maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +#maxRemoteAltTextLength: 100000 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +proxyBypassHosts: + - api.deepl.com + - api-free.deepl.com + - www.recaptcha.net + - hcaptcha.com + - challenges.cloudflare.com + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +#mediaProxy: https://example.com/proxy + +# Proxy remote files (default: true) +proxyRemoteFiles: true + +# Sign to ActivityPub GET request (default: true) +signToActivityPubGet: true + +allowedPrivateNetworks: [ + '127.0.0.1/32' +] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index de95f1b21a2d736aa675dbec4ff3e7449e01dd7d..5fac3dc41e29dfa112c53f109583f7e369cb531c 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -163,6 +163,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForReactions: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── @@ -242,8 +250,18 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) -maxNoteLength: 3000 +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) +#maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +#maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +#maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +#maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/example.yml b/.config/example.yml index 21e85b7b8904566136155c4fbed59f6d83d6076a..0062b6670c0f7b65a36f247410f205b6dd5e158b 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -172,6 +172,16 @@ redis: # # You can specify more ioredis options... # #username: example-username +#redisForReactions: +# host: localhost +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 +# # You can specify more ioredis options... +# #username: example-username + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── @@ -251,8 +261,18 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) -maxNoteLength: 3000 +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) +#maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +#maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +#maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +#maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index beefcfd0a2d5094528d1ec859c7f28a6999e8dad..3eb4fc28794be38e42b27b269b9f140939c00e0b 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -103,6 +103,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForReactions: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index 55fb1e6fa6873c51aea3b54ca25c233b6eeae520..e02a533c15913117b0b6f24068ee592b82ccb7b8 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -3,6 +3,8 @@ set -xe sudo chown node node_modules +sudo apt-get update +sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb git config --global --add safe.directory /workspace git submodule update --init corepack install @@ -12,3 +14,4 @@ pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml pnpm build pnpm migrate +pnpm exec cypress install diff --git a/.gitignore b/.gitignore index 758d36cea4fcc603dafa2743f04f8ca6f84c3b04..7cc7354a4a01d721e3621799f55ed9e6168c6d61 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ coverage !/.config/example.yml !/.config/docker_example.yml !/.config/docker_example.env +!/.config/cypress-devcontainer.yml docker-compose.yml compose.yml .devcontainer/compose.yml @@ -47,6 +48,7 @@ compose.yml /build built built-test +js-built /data /.cache-loader /db @@ -66,8 +68,9 @@ temp tsdoc-metadata.json misskey-assets -# Sharkey -/packages/megalodon/lib +# Vite temporary files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* # blender backups *.blend1 @@ -78,3 +81,6 @@ misskey-assets # VSCode addon .favorites.json + +# Sharkey +/packages/megalodon/lib diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2e773eddf9aecc1e3f9efa3322a75f27b33a6550..4db8bda32ec026bad7fe3ec15b74f8b40fe34d5e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,9 +20,9 @@ testCommit: - pnpm install --frozen-lockfile - pnpm run build - pnpm run migrate - - pnpm run --filter='!megalodon' --workspace-concurrency=1 test - - pnpm run --filter=backend lint - - pnpm run --filter=frontend eslint + - pnpm run --filter='!megalodon' test + - pnpm run --filter=backend --filter=misskey-js lint + - pnpm run --filter=frontend --filter=frontend-embed eslint cache: key: test policy: pull-push diff --git a/CHANGELOG.md b/CHANGELOG.md index 0986b6eae32c1206d2771e5e266f8ace94cb468d..cf0437e51a8d0f702e91a3147e7fd49da4a62bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,56 @@ +## 2024.9.0 + +### General +- Feat: ノートå˜ä½“・ユーザーã®ãƒŽãƒ¼ãƒˆãƒ»ã‚¯ãƒªãƒƒãƒ—ã®ãƒŽãƒ¼ãƒˆã®åŸ‹ã‚è¾¼ã¿æ©Ÿèƒ½ + - 埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚„ウェブサイトã¸ã®å®Ÿè£…方法ã®è©³ç´°ã¯ https://misskey-hub.net/docs/for-users/features/embed/ ã‚’ã”覧ãã ã•ã„ +- Feat: パスã‚ーã§ãƒã‚°ã‚¤ãƒ³ãƒœã‚¿ãƒ³ã‚’実装 (#14574) +- Feat: フォãƒãƒ¼ã•ã‚ŒãŸéš›ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’è¨å®šã§ãるよã†ã« +- Feat: 連åˆã‚’ホワイトリスト制ã«ã§ãるよã†ã« +- Feat: UserWebhookã¨SystemWebhookã®ãƒ†ã‚¹ãƒˆé€ä¿¡æ©Ÿèƒ½ã‚’è¿½åŠ (#14445) +- Feat: モデレーターã¯ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã‹ã‹ã‚らãšãƒ•ã‚¡ã‚¤ãƒ«ãŒæ·»ä»˜ã•ã‚Œã¦ã„るノートを検索ã§ãるよã†ã« + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) +- Feat: データエクスãƒãƒ¼ãƒˆãŒå®Œäº†ã—ãŸéš›ã«é€šçŸ¥ã‚’発行ã™ã‚‹ã‚ˆã†ã« +- Enhance: ユーザーã«ã‚ˆã‚‹ã‚³ãƒ³ãƒ†ãƒ³ãƒ„インãƒãƒ¼ãƒˆã®å¯å¦ã‚’ãƒãƒ¼ãƒ«ãƒãƒªã‚·ãƒ¼ã§åˆ¶å¾¡ã§ãるよã†ã« +- Enhance: ä¾å˜é–¢ä¿‚ã®æ›´æ–° +- Enhance: l10nã®æ›´æ–° + +### Client +- Enhance: サイズ制é™ã‚’超éŽã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã—よã†ã¨ã—ãŸéš›ã«ã‚¨ãƒ©ãƒ¼ã‚’出ã™ã‚ˆã†ã« +- Enhance: アイコンデコレーション管ç†ç”»é¢ã«ãƒ—ãƒ¬ãƒ“ãƒ¥ãƒ¼ã‚’è¿½åŠ +- Enhance: コントãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«å†…ã®ãƒ•ã‚¡ã‚¤ãƒ«ä¸€è¦§ã§ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãªãƒ•ã‚¡ã‚¤ãƒ«ã‚’区別ã—ã‚„ã™ã +- Enhance: Scratchpadã«UIã‚¤ãƒ³ã‚¹ãƒšã‚¯ã‚¿ãƒ¼ã‚’è¿½åŠ +- Enhance: Play編集画é¢ã®é …ç›®ã®ä¸¦ã³ã‚’å°‘ã—リデザイン +- Enhance: å„種メニューをドãƒãƒ¯ãƒ¼è¡¨ç¤ºã™ã‚‹ã‹ã©ã†ã‹è¨å®šå¯èƒ½ã« +- Enhance: AiScriptã®Mk:C:containerã®ã‚ªãƒ—ションã«`borderStyle`ã¨`borderRadius`ã‚’è¿½åŠ +- Enhance: CWã§ã‚‚絵文å—をクリックã—ã¦ãƒ¡ãƒ‹ãƒ¥ãƒ¼ã‚’表示ã§ãるよã†ã« +- Fix: サーãƒãƒ¼ãƒ¡ãƒˆãƒªã‚¯ã‚¹ãŒ2ã¤ä»¥ä¸Šã‚ã‚‹ã¨ãƒªãƒãƒ¼ãƒ‰ç›´å¾Œã®è¡¨ç¤ºãŒãŠã‹ã—ããªã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: コントãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«å†…ã®Ap requests内ã®ãƒãƒ£ãƒ¼ãƒˆã®è¡¨ç¤ºãŒãŠã‹ã—ã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: 月ã®é•ã†åŒã˜æ—¥ã¯ã‚»ãƒ‘レータãŒè¡¨ç¤ºã•ã‚Œãªã„ã®ã‚’ä¿®æ£ +- Fix: タッãƒç”»é¢ã§ãƒ¬ãƒ³ã‚¸ã‚¹ãƒ©ã‚¤ãƒ€ãƒ¼ã‚’æ“作ã™ã‚‹ã¨ãƒ„ールãƒãƒƒãƒ—ãŒè¤‡æ•°è¡¨ç¤ºã•ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://github.com/taiyme/misskey/pull/265) +- Fix: 縦横比ãŒæ¥µç«¯ãªã‚«ã‚¹ã‚¿ãƒ 絵文å—を表示ã™ã‚‹éš›ã«ãƒ¬ã‚¤ã‚¢ã‚¦ãƒˆãŒå´©ã‚Œã‚‹ç®‡æ‰€ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725) +- Fix: è¨å®šå¤‰æ›´æ™‚ã®ãƒªãƒãƒ¼ãƒ‰ç¢ºèªãƒ€ã‚¤ã‚¢ãƒã‚°ãŒè¤‡æ•°å€‹è¡¨ç¤ºã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: ファイルã®è©³ç´°ãƒšãƒ¼ã‚¸ã®ãƒ•ã‚¡ã‚¤ãƒ«ã®èª¬æ˜Žã§æ”¹è¡ŒãŒæ£ã—ã表示ã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110) +- Fix: 一部画é¢ã®ãƒšãƒ¼ã‚¸ãƒãƒ¼ã‚·ãƒ§ãƒ³ãŒå‹•ä½œã—ã«ãããªã£ã¦ã„ãŸã®ã‚’ä¿®æ£ ( #12766 , #11449 ) + +### Server +- Feat: Misskey® Reactions Boost Technologyâ„¢ (RBT)ã«ã‚ˆã‚Šã€ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®ä½œæˆè² è·ã‚’低減ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã« +- Fix: アンテナã®æ›¸ãè¾¼ã¿æ™‚ã«ã‚ーワードãŒä¸Žãˆã‚‰ã‚Œãªã‹ã£ãŸå ´åˆã®ã‚¨ãƒ©ãƒ¼ã‚’ApiErrorã¨ã—ã¦æŠ•ã’るよã†ã« + - ã“ã®å¤‰æ›´ã«ã‚ˆã‚Šã€å…¬å¼ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰ã§ã¯å…¥åŠ›ã®ä¸å‚™ãŒå†…部エラーã¨ã—ã¦å ±å‘Šã•ã‚Œã‚‹ä»£ã‚ã‚Šã«ä¸€èˆ¬çš„ãªã‚¨ãƒ©ãƒ¼ãƒ€ã‚¤ã‚¢ãƒã‚°ã§å ±å‘Šã•ã‚Œã¾ã™ +- Fix: ファイルãŒã‚µã‚¤ã‚ºã®åˆ¶é™ã‚’超ãˆã¦ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã•ã‚ŒãŸéš›ã«ã‚¨ãƒ©ãƒ¼ã‚’è¿”ã•ãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: 外部ページを解æžã™ã‚‹éš›ã«ã€ãƒšãƒ¼ã‚¸ã«ç´ã¥ã‘られãŸé–¢é€£ãƒªã‚½ãƒ¼ã‚¹ã‚‚èªã¿è¾¼ã¾ã‚Œã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) +- Fix: Continue importing from file if single emoji import fails +- Fix: `Retry-After`ヘッダーãŒé€ä¿¡ã•ã‚Œãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624) +- Fix: サーãƒãƒ¼ã‚µã‚¤ãƒ‰ã®DOM解æžå®Œäº†æ™‚ã«ãƒªã‚½ãƒ¼ã‚¹ã‚’開放ã™ã‚‹ã‚ˆã†ã« + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634) +- Fix: `<link rel="alternate">`を追ã£ã¦ç…§ä¼šã™ã‚‹ã®ã¯OKレスãƒãƒ³ã‚¹ãŒè¿”å´ã•ã‚ŒãŸå ´åˆã®ã¿ã« + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633) +- Fix: メールã«ã‚¹ã‚¿ã‚¤ãƒ«ãŒé©ç”¨ã•ã‚Œã¦ã„ãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ + ## 2024.8.0 ### General diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a50b550b3a482cc42b8ccf916bf6cb706707ef1c..f2e48ec61ddbc4314b701234187c49a84ff7a140 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -529,7 +529,8 @@ enumã®åˆ—挙ã®å†…容ã®å‰Šé™¤ã¯ã€ãã®å€¤ã‚’ã‚‚ã¤ãƒ¬ã‚³ãƒ¼ãƒ‰ã‚’å…¨ã¦å‰Š ### Migration作æˆæ–¹æ³• packages/backendã§: ```sh -pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name> +pnpm run build +pnpm dlx typeorm migration:generate -d ormconfig.js -o migration/<migration name> ``` - 生æˆå¾Œã€ãƒ•ã‚¡ã‚¤ãƒ«ã‚’migration下ã«ç§»ã—ã¦ãã ã•ã„ @@ -573,6 +574,26 @@ marginã¯ãã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã‚’使ã†å´ãŒè¨å®šã™ã‚‹ ### indexã¨ã„ã†ãƒ•ã‚¡ã‚¤ãƒ«åを使ã†ãª ESMã§ã¯ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚¤ãƒ³ãƒãƒ¼ãƒˆã¯å»ƒæ¢ã•ã‚Œã¦ã„ã‚‹ã®ã¨ã€ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚¤ãƒ³ãƒãƒ¼ãƒˆã›ãšã¨ã‚‚ファイルå㌠index ã ã¨ä½•æ•…ã‹ä¸€éƒ¨ã®ãƒ©ã‚¤ãƒ–ラリ?ã§ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚¤ãƒ³ãƒãƒ¼ãƒˆã ã¨è¦‹åšã•ã‚Œã¦ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ +## CSS Recipe + +### Lighten CSS vars + +``` css +color: hsl(from var(--accent) h s calc(l + 10)); +``` + +### Darken CSS vars + +``` css +color: hsl(from var(--accent) h s calc(l - 10)); +``` + +### Add alpha to CSS vars + +``` css +color: color(from var(--accent) srgb r g b / 0.5); +``` + ## Merging from Misskey into Sharkey Make sure you have both remotes in the same clone (`git remote add misskey @@ -590,15 +611,11 @@ seems to do a decent job) *after that commit*, do all the extra work, on the same branch: * copy all changes (commit after each step): - * in `packages/backend/src/core/NoteCreateService.ts`, from `create` to - `import` (and vice versa if `git` got confused!) * in `packages/backend/src/core/activitypub/models/ApNoteService.ts`, from `createNote` to `updateNote` * from `packages/backend/src/core/NoteCreateService.ts` to `packages/backend/src/core/NoteEditService.vue` - * in `packages/backend/src/core/activitypub/models/ApNoteService.ts`, - from `createNote` to `updateNote` * from `packages/backend/src/server/api/endpoints/notes/create.ts` to `packages/backend/src/server/api/endpoints/notes/edit.ts` * from `packages/frontend/src/components/MkNote*.vue` to @@ -614,9 +631,9 @@ seems to do a decent job) * check the changes against our `develop` (`git diff develop`) and against Misskey (`git diff misskey/develop`) * re-generate `misskey-js` (`pnpm build-misskey-js-with-types`) and commit -* build the frontend: `rm -rf built/; NODE_ENV=development pnpm --filter=frontend - build` (the `development` tells it to keep some of the original - filenames in the built files) +* build the frontend: `rm -rf built/; NODE_ENV=development pnpm + --filter=frontend --filter=frontend-embed build` (the `development` + tells it to keep some of the original filenames in the built files) * make sure there aren't any new `ti-*` classes (Tabler Icons), and replace them with appropriate `ph-*` ones (Phosphor Icons): `grep -rP '["'\'']ti[ -](?!fw)' -- built/` should show you what to change. diff --git a/Dockerfile b/Dockerfile index 288e97481c298e53a41cb32c1ff8227aa6d6bd5a..acef95deab2bd193b4bc31e0c848acbded0a43ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,9 +23,10 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ RUN pnpm build RUN node scripts/trim-deps.mjs RUN mv packages/frontend/assets sharkey-assets +RUN mv packages/frontend-embed/assets sharkey-embed-assets RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm prune -RUN rm -r node_modules packages/frontend packages/sw +RUN rm -r node_modules packages/frontend packages/frontend-shared packages/frontend-embed packages/sw RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --prod --frozen-lockfile --aggregate-output RUN rm -rf .git @@ -64,6 +65,7 @@ COPY --chown=sharkey:sharkey --from=build /sharkey/packages/megalodon/lib ./pack COPY --chown=sharkey:sharkey --from=build /sharkey/fluent-emojis ./fluent-emojis COPY --chown=sharkey:sharkey --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-assets ./packages/frontend/assets +COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-embed-assets ./packages/frontend-embed/assets COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md new file mode 100644 index 0000000000000000000000000000000000000000..8bebd4eb34790dffea739fde66b1daa7864116c8 --- /dev/null +++ b/UPGRADE_NOTES.md @@ -0,0 +1,41 @@ +# Upgrade Notes + +## 2024.9.0 + +### Following Feed + +When upgrading an existing instance to version 2024.9.0, the Following Feed will initially be empty. +The feed will gradually fill as new posts federate, but it may be desirable to back-fill the feed with existing data. +This database script will populate the feed with the latest post of each type for all users, ensuring that data is fully populated after the update. +Run this after migrations but before starting the instance. +Warning: the script may take a long time to execute! + +```postgresql +INSERT INTO latest_note (user_id, note_id, is_public, is_reply, is_quote) +SELECT + "userId" as user_id, + id as note_id, + visibility = 'public' AS is_public, + "replyId" IS NOT NULL AS is_reply, + ( + "renoteId" IS NOT NULL + AND ( + text IS NOT NULL + OR cw IS NOT NULL + OR "replyId" IS NOT NULL + OR "hasPoll" + OR "fileIds" != '{}' + ) + ) AS is_quote +FROM note +WHERE ( -- Exclude pure renotes (boosts) + "renoteId" IS NULL + OR text IS NOT NULL + OR cw IS NOT NULL + OR "replyId" IS NOT NULL + OR "hasPoll" + OR "fileIds" != '{}' + ) +ORDER BY id DESC -- This part is very important: it ensures that we only load the *latest* notes of each type. Do not remove it! +ON CONFLICT DO NOTHING; -- Any conflicts are guaranteed to be older notes that we can ignore. +``` diff --git a/chart/files/default.yml b/chart/files/default.yml index aab7ed6ce12edd997908bdaafb14d28b8fa30d08..97201aad662dc62d6b2b224b430a87f78b23f53f 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -124,6 +124,14 @@ redis: # #prefix: example-prefix # #db: 1 +#redisForReactions: +# host: redis +# port: 6379 +# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 +# #pass: example-pass +# #prefix: example-prefix +# #db: 1 + # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── diff --git a/compose_example.yml b/compose_example.yml index 15df128eff4dee0aaac3a7e9dd272ce2dc1d2947..0db8b04dc6e2958a0f98474857a63e8909c49f7c 100644 --- a/compose_example.yml +++ b/compose_example.yml @@ -53,7 +53,7 @@ services: # restart: always # image: mcaptcha/mcaptcha:latest # networks: -# shonks: +# shonk: # aliases: # - localhost # ports: @@ -63,6 +63,8 @@ services: # environment: # PORT: 7493 # MCAPTCHA_redis_URL: "redis://mcaptcha_redis/" +# MCAPTCHA_allow_registration: true +# MCAPTCHA_server_DOMAIN: "example.tld" # depends_on: # db: # condition: service_healthy @@ -72,7 +74,7 @@ services: # mcaptcha_redis: # image: mcaptcha/cache:latest # networks: -# - shonks +# - shonk # healthcheck: # test: "redis-cli ping" # interval: 5s diff --git a/crowdin.yml b/crowdin.yml index 774ddc7a630f46ee114a12e31eff3099f378150a..0525ac7b0b54b2a1fdf33a536e2ea13554e3f8cd 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,4 +1,4 @@ files: - - source: /locales/ja-JP.yml - translation: /locales/%locale%.yml + - source: /sharkey-locales/en-US.yml + translation: /sharkey-locales/%locale%.yml update_option: update_as_unapproved diff --git a/eslint/locale.js b/eslint/locale.js new file mode 100644 index 0000000000000000000000000000000000000000..dbb807b71421e06c66763cb735cd3be8c4182ac5 --- /dev/null +++ b/eslint/locale.js @@ -0,0 +1,251 @@ +/* + * SPDX-FileCopyrightText: dakkar and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* This is a ESLint rule to report use of the `i18n.ts` and `i18n.tsx` + * objects that reference translation items that don't actually exist + * in the lexicon (the `locale/` files) + */ + +/* given a MemberExpression node, collects all the member names + * + * e.g. for a bit of code like `foo=one.two.three`, `collectMembers` + * called on the node for `three` would return `['one', 'two', + * 'three']` + */ +function collectMembers(node) { + if (!node) return []; + if (node.type !== 'MemberExpression') return []; + // this is something like `foo[bar]` + if (node.computed) return []; + return [ node.property.name, ...collectMembers(node.parent) ]; +} + +/* given an object and an array of names, recursively descends the + * object via those names + * + * e.g. `walkDown({one:{two:{three:15}}},['one','two','three'])` would + * return 15 + */ +function walkDown(locale, path) { + if (!locale) return null; + if (!path || path.length === 0 || !path[0]) return locale; + return walkDown(locale[path[0]], path.slice(1)); +} + +/* given a MemberExpression node, returns its attached CallExpression + * node if present + * + * e.g. for a bit of code like `foo=one.two.three()`, + * `findCallExpression` called on the node for `three` would return + * the node for function call (which is the parent of the `one` and + * `two` nodes, and holds the nodes for the argument list) + * + * if the code had been `foo=one.two.three`, `findCallExpression` + * would have returned null, because there's no function call attached + * to the MemberExpressions + */ +function findCallExpression(node) { + if (!node.parent) return null; + + // the second half of this guard protects from cases like + // `foo(one.two.three)` where the CallExpression is parent of the + // MemberExpressions, but via `arguments`, not `callee` + if (node.parent.type === 'CallExpression' && node.parent.callee === node) return node.parent; + if (node.parent.type === 'MemberExpression') return findCallExpression(node.parent); + return null; +} + +// same, but for Vue expressions (`<I18n :src="i18n.ts.foo">`) +function findVueExpression(node) { + if (!node.parent) return null; + + if (node.parent.type.match(/^VExpr/) && node.parent.expression === node) return node.parent; + if (node.parent.type === 'MemberExpression') return findVueExpression(node.parent); + return null; +} + +function areArgumentsOneObject(node) { + return node.arguments.length === 1 && + node.arguments[0].type === 'ObjectExpression'; +} + +// only call if `areArgumentsOneObject(node)` is true +function getArgumentObjectProperties(node) { + return new Set(node.arguments[0].properties.map( + p => { + if (p.key && p.key.type === 'Identifier') return p.key.name; + return null; + }, + )); +} + +function getTranslationParameters(translation) { + return new Set(Array.from(translation.matchAll(/\{(\w+)\}/g)).map( m => m[1] )); +} + +function setDifference(a,b) { + const result = []; + for (const element of a.values()) { + if (!b.has(element)) { + result.push(element); + } + } + + return result; +} + +/* the actual rule body + */ +function theRuleBody(context,node) { + // we get the locale/translations via the options; it's the data + // that goes into a specific language's JSON file, see + // `scripts/build-assets.mjs` + const locale = context.options[0]; + + // sometimes we get MemberExpression nodes that have a + // *descendent* with the right identifier: skip them, we'll get + // the right ones as well + if (node.object?.name !== 'i18n') { + return; + } + + // `method` is going to be `'ts'` or `'tsx'`, `path` is going to + // be the various translation steps/names + const [ method, ...path ] = collectMembers(node); + const pathStr = `i18n.${method}.${path.join('.')}`; + + // does that path point to a real translation? + const translation = walkDown(locale, path); + if (!translation) { + context.report({ + node, + message: `translation missing for ${pathStr}`, + }); + return; + } + + // we hit something weird, assume the programmers know what + // they're doing (this is usually some complicated slicing of + // the translation structure) + if (typeof(translation) !== 'string') return; + + const callExpression = findCallExpression(node); + const vueExpression = findVueExpression(node); + + // some more checks on how the translation is called + if (method === 'ts') { + // the `<I18n> component gets parametric translations via + // `i18n.ts.*`, but we error out elsewhere + if (translation.match(/\{/) && !vueExpression) { + context.report({ + node, + message: `translation for ${pathStr} is parametric, but called via 'ts'`, + }); + return; + } + + if (callExpression) { + context.report({ + node, + message: `translation for ${pathStr} is not parametric, but is called as a function`, + }); + } + } + + if (method === 'tsx') { + if (!translation.match(/\{/)) { + context.report({ + node, + message: `translation for ${pathStr} is not parametric, but called via 'tsx'`, + }); + return; + } + + if (!callExpression && !vueExpression) { + context.report({ + node, + message: `translation for ${pathStr} is parametric, but not called as a function`, + }); + return; + } + + // we're not currently checking arguments when used via the + // `<I18n>` component, because it's too complicated (also, it + // would have to be done inside the `if (method === 'ts')`) + if (!callExpression) return; + + if (!areArgumentsOneObject(callExpression)) { + context.report({ + node, + message: `translation for ${pathStr} should be called with a single object as argument`, + }); + return; + } + + const translationParameters = getTranslationParameters(translation); + const parameterCount = translationParameters.size; + const callArguments = getArgumentObjectProperties(callExpression); + const argumentCount = callArguments.size; + + if (parameterCount !== argumentCount) { + context.report({ + node, + message: `translation for ${pathStr} has ${parameterCount} parameters, but is called with ${argumentCount} arguments`, + }); + } + + // node 20 doesn't have `Set.difference`... + const extraArguments = setDifference(callArguments, translationParameters); + const missingArguments = setDifference(translationParameters, callArguments); + + if (extraArguments.length > 0) { + context.report({ + node, + message: `translation for ${pathStr} passes unused arguments ${extraArguments.join(' ')}`, + }); + } + + if (missingArguments.length > 0) { + context.report({ + node, + message: `translation for ${pathStr} does not pass arguments ${missingArguments.join(' ')}`, + }); + } + } +} + +function theRule(context) { + // we get the locale/translations via the options; it's the data + // that goes into a specific language's JSON file, see + // `scripts/build-assets.mjs` + const locale = context.options[0]; + + // for all object member access that have an identifier 'i18n'... + return context.getSourceCode().parserServices.defineTemplateBodyVisitor( + { + // this is for <template> bits, needs work + 'MemberExpression:has(Identifier[name=i18n])': (node) => theRuleBody(context, node), + }, + { + // this is for normal code + 'MemberExpression:has(Identifier[name=i18n])': (node) => theRuleBody(context, node), + }, + ); +} + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'assert that all translations used are present in the locale files', + }, + schema: [ + // here we declare that we need the locale/translation as a + // generic object + { type: 'object', additionalProperties: true }, + ], + }, + create: theRule, +}; diff --git a/eslint/locale.test.js b/eslint/locale.test.js new file mode 100644 index 0000000000000000000000000000000000000000..2b69672d27b80c11c763f9182b6750d4a09328ee --- /dev/null +++ b/eslint/locale.test.js @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: dakkar and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only +*/ + +const {RuleTester} = require("eslint"); +const localeRule = require("./locale"); + +const locale = { foo: { bar: 'ok', baz: 'good {x}' }, top: '123' }; + +const ruleTester = new RuleTester({ + languageOptions: { + parser: require('vue-eslint-parser'), + ecmaVersion: 2015, + }, +}); + +function testCase(code,errors) { + return { code, errors, options: [ locale ], filename: 'test.ts' }; +} +function testCaseVue(code,errors) { + return { code, errors, options: [ locale ], filename: 'test.vue' }; +} + +ruleTester.run( + 'sharkey-locale', + localeRule, + { + valid: [ + testCase('i18n.ts.foo.bar'), + testCase('i18n.ts.top'), + testCase('i18n.tsx.foo.baz({x:1})'), + testCase('whatever.i18n.ts.blah.blah'), + testCase('whatever.i18n.tsx.does.not.matter'), + testCase('whatever(i18n.ts.foo.bar)'), + testCaseVue('<template><p>{{ i18n.ts.foo.bar }}</p></template>'), + testCaseVue('<template><I18n :src="i18n.ts.foo.baz"/></template>'), + // we don't detect the problem here, but should still accept it + testCase('i18n.ts.foo["something"]'), + testCase('i18n.ts.foo[something]'), + ], + invalid: [ + testCase('i18n.ts.not', 1), + testCase('i18n.tsx.deep.not', 1), + testCase('i18n.tsx.deep.not({x:12})', 1), + testCase('i18n.tsx.top({x:1})', 1), + testCase('i18n.ts.foo.baz', 1), + testCase('i18n.tsx.foo.baz', 1), + testCase('i18n.tsx.foo.baz({y:2})', 2), + testCaseVue('<template><p>{{ i18n.ts.not }}</p></template>', 1), + testCaseVue('<template><I18n :src="i18n.ts.not"/></template>', 1), + ], + }, +); diff --git a/idea/MkDisableSection.vue b/idea/MkDisableSection.vue new file mode 100644 index 0000000000000000000000000000000000000000..d177886569a209174e9272e77717a768e4f21de8 --- /dev/null +++ b/idea/MkDisableSection.vue @@ -0,0 +1,41 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[$style.root]"> + <div :inert="disabled" :class="[{ [$style.disabled]: disabled }]"> + <slot></slot> + </div> + <div v-if="disabled" :class="[$style.cover]"></div> +</div> +</template> + +<script lang="ts" setup> +defineProps<{ + disabled?: boolean; +}>(); +</script> + +<style lang="scss" module> +.root { + position: relative; +} + +.disabled { + opacity: 0.7; +} + +.cover { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: not-allowed; + --color: color(from var(--error) srgb r g b / 0.25); + background-size: auto auto; + background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px); +} +</style> diff --git a/idea/README.md b/idea/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f64d16800a98502e377f9d9085146942f7a87d29 --- /dev/null +++ b/idea/README.md @@ -0,0 +1 @@ +使ã‚ã‚Œãªããªã£ãŸã‘ã©æ¶ˆã™ã®ã¯å‹¿ä½“ãªã„(å°†æ¥ä½¿ãˆã‚‹ã‹ã‚‚ã—ã‚Œãªã„)コードを入れã¦ãŠãã¨ã“ diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index b6bfbfa68281d05b84fc19ae1a3371a722b37eb8..de24ad4bb96317d3b962d56adb79cbce68f416b3 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -626,10 +626,7 @@ abuseReported: "Ø£Ùرسل البلاغ، شكرًا لك" reporter: "المÙبلّغ" reporteeOrigin: "أصل البلاغ" reporterOrigin: "أصل المÙبلّغ" -forwardReport: "وجّه البلاغ إلى المثيل البعيد" -forwardReportIsAnonymous: "ÙÙŠ المثيل البعيد سيظهر المبلّغ ÙƒØساب مجهول." send: "أرسل" -abuseMarkAsResolved: "علّم البلاغ كمØلول" openInNewTab: "اÙØªØ ÙÙŠ لسان جديد" defaultNavigationBehaviour: "سلوك الملاØØ© الاÙتراضي" editTheseSettingsMayBreakAccount: "تعديل هذه الإعدادات قد يسبب عطبًا Ù„Øسابك" @@ -1255,7 +1252,6 @@ _theme: buttonBg: "خلÙية الأزرار" buttonHoverBg: "خلÙية الأزرار (عند التمرير Ùوقها)" inputBorder: "Øوا٠Øقل الإدخال" - listItemHoverBg: "خلÙية عناصر القائمة (عند التمرير Ùوقها)" driveFolderBg: "خلÙية مجلد قرص التخزين" messageBg: "خلÙية المØادثة" _sfx: @@ -1533,6 +1529,7 @@ _notification: reaction: "التÙاعل" receiveFollowRequest: "طلبات المتابعة" followRequestAccepted: "طلبات المتابعة المقبولة" + login: "Ù„Ùج" app: "إشعارات التطبيقات المرتبطة" _actions: followBack: "تابعك بالمثل" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 6fb51ea5d8606930dbb07c4bce80c4b1e83b86df..0e761b074393c75ba5c78599473442cad673d7e7 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -451,7 +451,6 @@ or: "অথবা" language: "à¦à¦¾à¦·à¦¾" uiLanguage: "UI à¦à¦° à¦à¦¾à¦·à¦¾" aboutX: "{x} সমà§à¦ªà¦°à§à¦•à§‡" -disableDrawer: "ডà§à¦°à¦¯à¦¼à¦¾à¦° মেনৠপà§à¦°à¦¦à¦°à§à¦¶à¦¨ করবেন না" noHistory: "কোনো ইতিহাস নেই" signinHistory: "পà§à¦°à¦¬à§‡à¦¶ করার ইতিহাস" doing: "পà§à¦°à¦•à§à¦°à¦¿à§Ÿà¦¾ করছে..." @@ -625,10 +624,7 @@ abuseReported: "আপনার অà¦à¦¿à¦¯à§‹à¦—টি দাখিল কর reporter: "অà¦à¦¿à¦¯à§‹à¦—কারী" reporteeOrigin: "অà¦à¦¿à¦¯à§‹à¦—টির উৎস" reporterOrigin: "অà¦à¦¿à¦¯à§‹à¦—কারীর উৎস" -forwardReport: "রিমোট ইনà§à¦¸à¦¤à§à¦¯à¦¾à¦¨à§à¦¸à§‡ অà¦à¦¿à¦¯à§‹à¦—টি পাঠান" -forwardReportIsAnonymous: "আপনার তথà§à¦¯ রিমোট ইনà§à¦¸à¦¤à§à¦¯à¦¾à¦¨à§à¦¸à§‡ পাঠানো হবে না à¦à¦¬à¦‚ à¦à¦•à¦Ÿà¦¿ বেনামী সিসà§à¦Ÿà§‡à¦® অà§à¦¯à¦¾à¦•à¦¾à¦‰à¦¨à§à¦Ÿ হিসাবে পà§à¦°à¦¦à¦°à§à¦¶à¦¿à¦¤ হবে।" send: "পাঠান" -abuseMarkAsResolved: "অà¦à¦¿à¦¯à§‹à¦—টিকে সমাধাকৃত হিসাবে চিহà§à¦¨à¦¿à¦¤ করà§à¦¨" openInNewTab: "নতà§à¦¨ টà§à¦¯à¦¾à¦¬à§‡ খà§à¦²à§à¦¨" openInSideView: "সাইড à¦à¦¿à¦‰à¦¤à§‡ খà§à¦²à§à¦¨" defaultNavigationBehaviour: "ডিফলà§à¦Ÿ নেà¦à¦¿à¦—েশন" @@ -1021,7 +1017,6 @@ _theme: buttonBg: "বাটনের পটà¦à§‚মি" buttonHoverBg: "বাটনের পটà¦à§‚মি (হà¦à¦¾à¦°)" inputBorder: "ইনপà§à¦Ÿ ফিলà§à¦¡à§‡à¦° বরà§à¦¡à¦¾à¦°" - listItemHoverBg: "লিসà§à¦Ÿ আইটেমের পটà¦à§‚মি (হোà¦à¦¾à¦°)" driveFolderBg: "ডà§à¦°à¦¾à¦‡à¦ ফোলà§à¦¡à¦¾à¦°à§‡à¦° পটà¦à§‚মি" wallpaperOverlay: "ওয়ালপেপার ওà¦à¦¾à¦°à¦²à§‡" badge: "বà§à¦¯à¦¾à¦œ" @@ -1314,6 +1309,7 @@ _notification: pollEnded: "পোল শেষ" receiveFollowRequest: "পà§à¦°à¦¾à¦ªà§à¦¤ অনà§à¦¸à¦°à¦£à§‡à¦° অনà§à¦°à§‹à¦§à¦¸à¦®à§‚হ" followRequestAccepted: "গৃহীত অনà§à¦¸à¦°à¦£à§‡à¦° অনà§à¦°à§‹à¦§à¦¸à¦®à§‚হ" + login: "পà§à¦°à¦¬à§‡à¦¶ করà§à¦¨" app: "লিঙà§à¦• করা অà§à¦¯à¦¾à¦ª থেকে বিজà§à¦žà¦ªà§à¦¤à¦¿" _actions: followBack: "ফলো বà§à¦¯à¦¾à¦• করেছে" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 0b2acf8f05e75926e6cc9a7be93a8d9901856809..b9f3fecc76f9618b4b9301df593f83aa82b117d5 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -8,6 +8,8 @@ search: "Cercar" notifications: "Notificacions" username: "Nom d'usuari" password: "Contrasenya" +initialPasswordForSetup: "Contrasenya inicial per la configuració inicial" +initialPasswordIsIncorrect: "La contrasenya no és correcta." forgotPassword: "Contrasenya oblidada" fetchingAsApObject: "Cercant en el Fediverse..." ok: "OK" @@ -60,6 +62,7 @@ copyFileId: "Copiar ID d'arxiu" copyFolderId: "Copiar ID de carpeta" copyProfileUrl: "Copiar URL del perfil" searchUser: "Cercar un usuari" +searchThisUsersNotes: "Cerca les publicacions de l'usuari" reply: "Respondre" loadMore: "Carregar més" showMore: "Veure més" @@ -108,11 +111,14 @@ enterEmoji: "Introduir un emoji" renote: "Impulsa" unrenote: "Anul·la l'impuls" renoted: "S'ha impulsat" +renotedToX: "Impulsat per {name}." cantRenote: "No es pot impulsar aquesta publicació" cantReRenote: "No es pot impulsar l'impuls." quote: "Cita" inChannelRenote: "Renotar només al Canal" inChannelQuote: "Citar només al Canal" +renoteToChannel: "Impulsa a un canal" +renoteToOtherChannel: "Impulsa a un altre canal" pinnedNote: "Nota fixada" pinned: "Fixar al perfil" you: "Tu" @@ -151,6 +157,7 @@ editList: "Editar llista" selectChannel: "Selecciona un canal" selectAntenna: "Tria una antena" editAntenna: "Modificar antena" +createAntenna: "Crea una antena" selectWidget: "Triar un giny" editWidgets: "Editar ginys" editWidgetsExit: "Fet" @@ -177,6 +184,10 @@ addAccount: "Afegeix un compte" reloadAccountsList: "Recarregar la llista de contactes" loginFailed: "S'ha produït un error al accedir." showOnRemote: "Navega més en el perfil original" +continueOnRemote: "Veure perfil original" +chooseServerOnMisskeyHub: "Escull un servidor des del Hub de Misskey" +specifyServerHost: "Especifica un servidor directament" +inputHostName: "Introdueix el domini" general: "General" wallpaper: "Fons de Pantalla" setWallpaper: "Defineix el fons de pantalla" @@ -187,6 +198,7 @@ followConfirm: "Està s segur que vols deixar de seguir {name}?" proxyAccount: "Compte de proxy" proxyAccountDescription: "Un compte proxy és un compte que actua com a seguidor remot per als usuaris en determinades condicions. Per exemple, quan un usuari afegeix un usuari remot a la llista, l'activitat de l'usuari remot no es lliurarà al servidor si cap usuari local segueix aquest usuari, de manera que el compte proxy el seguirà ." host: "Amfitrió" +selectSelf: "Escollir manualment" selectUser: "Selecciona usuari/a" recipient: "Destinatari" annotation: "Comentaris" @@ -202,6 +214,7 @@ perDay: "Per dia" stopActivityDelivery: "Deixa d'enviar activitats" blockThisInstance: "Deixa d'enviar activitats" silenceThisInstance: "Silencia aquesta instà ncia " +mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instà ncia " operations: "Accions" software: "Programari" version: "Versió" @@ -223,6 +236,10 @@ blockedInstances: "Instà ncies bloquejades" blockedInstancesDescription: "Llista els enllaços d'amfitrió de les instà ncies que vols bloquejar separades per un salt de pà gina. Les instà ncies llistades no podran comunicar-se amb aquesta instà ncia." silencedInstances: "Instà ncies silenciades" silencedInstancesDescription: "Llista els enllaços d'amfitrió de les instà ncies que vols silenciar. Tots els comptes de les instà ncies llistades s'establiran com silenciades i només podran fer sol·licitacions de seguiment, i no podran mencionar als comptes locals si no els segueixen. Això no afectarà les instà ncies bloquejades." +mediaSilencedInstances: "Instà ncies amb els arxius silenciats" +mediaSilencedInstancesDescription: "Llista els noms dels servidors que vulguis silenciar els arxius, un servidor per lÃnia. Tots els comptes que pertanyin als servidors llistats seran tractats com sensibles i no podran fer servir emojis personalitzats. Això no tindrà efecte sobre els servidors blocats." +federationAllowedHosts: "Llista de servidors federats" +federationAllowedHostsDescription: "Llista dels servidors amb els quals es federa." muteAndBlock: "Silencia i bloca" mutedUsers: "Usuaris silenciats" blockedUsers: "Usuaris bloquejats" @@ -313,6 +330,7 @@ selectFile: "Selecciona fitxers" selectFiles: "Selecciona fitxers" selectFolder: "Selecció de carpeta" selectFolders: "Selecció de carpeta" +fileNotSelected: "Cap fitxer seleccionat" renameFile: "Canvia el nom del fitxer" folderName: "Nom de la carpeta" createFolder: "Crea una carpeta" @@ -320,6 +338,7 @@ renameFolder: "Canvia el nom de la carpeta" deleteFolder: "Elimina la carpeta" folder: "Carpeta " addFile: "Afegeix un fitxer" +showFile: "Mostrar fitxer" emptyDrive: "La teva unitat és buida" emptyFolder: "La carpeta està buida" unableToDelete: "No es pot eliminar" @@ -434,6 +453,7 @@ totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d' moderator: "Moderador/a" moderation: "Moderació" moderationNote: "Nota de moderació " +moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors." addModerationNote: "Afegir una nota de moderació " moderationLogs: "Registre de moderació " nUsersMentioned: "{n} usuaris mencionats" @@ -468,10 +488,12 @@ retype: "Torneu a introduir-la" noteOf: "Publicació de: {user}" quoteAttached: "Frase adjunta" quoteQuestion: "Vols annexar-la com a cita?" +attachAsFileQuestion: "El text copiat és massa llarg. Vols adjuntar-lo com un fitxer de text?" noMessagesYet: "Encara no hi ha missatges" newMessageExists: "Has rebut un nou missatge" onlyOneFileCanBeAttached: "Només pots adjuntar un fitxer a un missatge" signinRequired: "Si us plau, Registra't o inicia la sessió abans de continuar" +signinOrContinueOnRemote: "Per continuar necessites moure el teu servidor o registrar-te / iniciar sessió en aquest servidor." invitations: "Convida" invitationCode: "Codi d'invitació" checking: "Comprovació en curs..." @@ -493,7 +515,10 @@ uiLanguage: "Idioma de l'interfÃcie" aboutX: "Respecte a {x}" emojiStyle: "Estil d'emoji" native: "Nadiu" -disableDrawer: "No mostrar els menús en calaixos" +menuStyle: "Estil de menú" +style: "Estil" +drawer: "Calaix" +popup: "Emergent" showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor" showReactionsCount: "Mostra el nombre de reaccions a les publicacions" noHistory: "No hi ha un registre previ" @@ -540,10 +565,10 @@ objectStorageRegion: "Regió " objectStorageRegionDesc: "Especifica una regió com 'xx-east-1'. Si el teu servei no diferència regions has de posar 'us-east-1'. Deixa'l buit si fas servir variables d'entorn o un arxiu de configuració d'AWS." objectStorageUseSSL: "Fes servir SSL" objectStorageUseSSLDesc: "Desactiva'l si no tens pensat fer servir HTTPS per les connexions de l'API" -objectStorageUseProxy: "Connectar-se mitjançant un Proxy" +objectStorageUseProxy: "Connectar-se mitjançant un Proxy" objectStorageUseProxyDesc: "Desactiva'l si no farà s servir un Proxy per les connexions de l'API" objectStorageSetPublicRead: "Configurar les pujades com públiques " -s3ForcePathStyleDesc: "Si s3ForcePathStyle es troba activat el nom del dipòsit s'ha d'incloure a l'adreça URL en comtes del nom del host. Potser que necessitis activar-ho quan facis servir, per exemple, Minio a un servidor propi." +s3ForcePathStyleDesc: "Si s3ForcePathStyle es troba activat el nom del cubell s'haurà d'especificar com a part de l'adreça URL en comptes del nom del servidor. Podria ser que necessitis activar aquesta opció quan facis servir serveis com ara l'allotjament a un servidor propi." serverLogs: "Registres del servidor" deleteAll: "Elimina-ho tot" showFixedPostForm: "Mostrar el formulari per escriure a l'inici de la lÃnia de temps" @@ -576,6 +601,8 @@ ascendingOrder: "Ascendent" descendingOrder: "Descendent" scratchpad: "Bloc de proves" scratchpadDescription: "El bloc de proves proporciona un entorn experimental per AiScript. Pot escriure i verificar els resultats que interactuen amb Misskey." +uiInspector: "Inspector de la interfÃcie" +uiInspectorDescription: "Podeu visualitzar una llista d'elements UI presents en la memòria. Els components de la interfÃcie d'usuari són generats per les funcions Ui:C:." output: "Sortida" script: "Script" disablePagesScript: "Desactivar AiScript a les pà gines " @@ -692,10 +719,7 @@ abuseReported: "La teva denúncia s'ha enviat. Moltes grà cies." reporter: "Denunciant " reporteeOrigin: "Origen de la denúncia " reporterOrigin: "Origen del denunciant" -forwardReport: "Transferir la denúncia a una instà ncia remota" -forwardReportIsAnonymous: "En lloc del teu compte, es farà servir un compte anònim com a denunciant al servidor remot." send: "Envia" -abuseMarkAsResolved: "Marca la denúncia com a resolta" openInNewTab: "Obre a una pestanya nova" openInSideView: "Obre a una vista lateral" defaultNavigationBehaviour: "Navegació per defecte" @@ -832,6 +856,7 @@ administration: "Administració" accounts: "Comptes" switch: "Canvia" noMaintainerInformationWarning: "La informació de l'administrador no s'ha configurat" +noInquiryUrlWarning: "No s'ha desat l'URL de consulta." noBotProtectionWarning: "La protecció contra bots no s'ha configurat." configure: "Configurar" postToGallery: "Crear una nova publicació a la galeria" @@ -896,6 +921,7 @@ followersVisibility: "Visibilitat dels seguidors" continueThread: "Veure la continuació del fil" deleteAccountConfirm: "Això eliminarà el teu compte irreversiblement. Procedir?" incorrectPassword: "Contrasenya incorrecta." +incorrectTotp: "La contrasenya no és correcta, o ha caducat." voteConfirm: "Confirma el teu vot \"{choice}\"" hide: "Amagar" useDrawerReactionPickerForMobile: "Mostrar el selector de reaccions com un calaix al mòbil " @@ -1021,6 +1047,7 @@ thisPostMayBeAnnoyingHome: "Publicar a la lÃnia de temps d'Inici" thisPostMayBeAnnoyingCancel: "Cancel·lar " thisPostMayBeAnnoyingIgnore: "Publicar de totes maneres" collapseRenotes: "Col·lapsar les renotes que ja has vist" +collapseRenotesDescription: "Col·lapse les notes a les quals ja has reaccionat o que ja has renotat" internalServerError: "Error intern del servidor" internalServerErrorDescription: "El servidor ha fallat de manera inexplicable." copyErrorInfo: "Copiar la informació de l'error " @@ -1094,6 +1121,8 @@ preservedUsernames: "Noms d'usuaris reservats" preservedUsernamesDescription: "Llistat de noms d'usuaris que no es poden fer servir separats per salts de linia. Aquests noms d'usuaris no estaran disponibles quan es creï un compte d'usuari normal, però els administradors els poden fer servir per crear comptes manualment. Per altre banda els comptes ja creats amb aquests noms d'usuari no es veure'n afectats." createNoteFromTheFile: "Compon una nota des d'aquest fitxer" archive: "Arxiu" +archived: "Arxivat" +unarchive: "Desarxivar" channelArchiveConfirmTitle: "Vols arxivar {name}?" channelArchiveConfirmDescription: "Un Canal arxivat no apareixerà a la llista de canals o als resultats de cerca. Tampoc es poden afegir noves entrades." thisChannelArchived: "Aquest Canal ha sigut arxivat." @@ -1104,6 +1133,9 @@ preventAiLearning: "Descartar l'ús d'aprenentatge automà tic (IA Generativa)" preventAiLearningDescription: "Demanar els indexadors no fer servir els texts, imatges, etc. en cap conjunt de dades per alimentar l'aprenentatge automà tic (IA Predictiva/ Generativa). Això s'aconsegueix afegint la etiqueta \"noai\" com a resposta HTML al contingut corresponent. Prevenir aquest ús totalment pot ser que no sigui aconseguit, ja que molts indexadors poden obviar aquesta etiqueta." options: "Opcions" specifyUser: "Especificar usuari" +lookupConfirm: "Vols fer una cerca?" +openTagPageConfirm: "Vols obrir una pà gina d'etiquetes?" +specifyHost: "Especifica un servidor" failedToPreviewUrl: "Vista prèvia no disponible" update: "Actualitzar" rolesThatCanBeUsedThisEmojiAsReaction: "Rols que poden fer servir aquest emoji com a reacció " @@ -1146,7 +1178,7 @@ currentAnnouncements: "Informes actuals" pastAnnouncements: "Informes passats" youHaveUnreadAnnouncements: "Tens informes per llegir." useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." -replies: "Respostes" +replies: "Respondre" renotes: "Impulsa" loadReplies: "Mostrar les respostes" loadConversation: "Mostrar la conversació " @@ -1172,7 +1204,10 @@ confirmShowRepliesAll: "Aquesta opció no té marxa enrere. Vols mostrar les tev confirmHideRepliesAll: "Aquesta opció no té marxa enrere. Vols ocultar les teves respostes a tots els usuaris que segueixes a la lÃnia de temps?" externalServices: "Serveis externs" sourceCode: "Codi font" +sourceCodeIsNotYetProvided: "El codi font encara no es troba disponible. Contacta amb l'administrador per solucionar aquest problema." repositoryUrl: "URL del repositori" +repositoryUrlDescription: "Si està s fent servir Misskey tal com és (sense cap canvi al codi font), introdueix https://github.com/misskey-dev/misskey" +repositoryUrlOrTarballRequired: "Si no ofereixes cap repositori, publica un fitxer tarball. Dona una ullada a .config/example.yml per a més informació." feedback: "Opinió" feedbackUrl: "URL per a opinar" impressum: "Impressum" @@ -1211,6 +1246,7 @@ showReplay: "Veure reproducció" replay: "Reproduir" replaying: "Reproduint" endReplay: "Tanca la redifusió" +copyReplayData: "Copia les dades de la resposta" ranking: "Classificació" lastNDays: "Últims {n} dies" backToTitle: "Torna al tÃtol" @@ -1224,12 +1260,60 @@ gameRetry: "Torna a provar" notUsePleaseLeaveBlank: "Si no voleu usar-ho, deixeu-ho en blanc" useTotp: "Usa una contrasenya d'un sol ús" useBackupCode: "Usa un codi de recuperació" +launchApp: "Inicia l'aplicació " +useNativeUIForVideoAudioPlayer: "Fes servir la UI del navegador quan reprodueixis vÃdeo i à udio " +keepOriginalFilename: "Desa el nom del fitxer original" +keepOriginalFilenameDescription: "Si desactives aquesta opció els noms dels fitxers se substituiran per una cadena aleatòria quan carreguis nous fitxers de forma automà tica." +noDescription: "No hi ha una descripció " +alwaysConfirmFollow: "Confirma sempre els seguiments" +inquiry: "Contacte" +tryAgain: "Intenta-ho més tard." +confirmWhenRevealingSensitiveMedia: "Confirmació quan revelis contingut sensible " +sensitiveMediaRevealConfirm: "Aquest contingut potser sensible. Segur que ho vols revelar?" +createdLists: "Llistes creades " +createdAntennas: "Antenes creades" +fromX: "De {x}" +genEmbedCode: "Obtenir el codi per incrustar" +noteOfThisUser: "Notes d'aquest usuari" +clipNoteLimitExceeded: "No es poden afegir més notes a aquest clip." +performance: "Rendiment" +modified: "Modificat" +discard: "Descarta" +thereAreNChanges: "Hi ha(n) {n} canvi(s)" +signinWithPasskey: "Inicia sessió amb Passkey" +unknownWebAuthnKey: "Passkey desconeguda" +passkeyVerificationFailed: "La verificació a fallat" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya." +messageToFollower: "Missatge als meus seguidors" +target: "Assumpte " +testCaptchaWarning: "És una caracterÃstica dissenyada per a la prova de CAPTCHA. <strong>No l'utilitzes en l'entorn real.</strong>" +_abuseUserReport: + forward: "Reenviar " + forwardDescription: "Reenvia l'informe a una altra instà ncia com un compte del sistema anònima." + resolve: "Solució " + accept: "Acceptar " + reject: "Rebutjar" + resolveTutorial: "Si l'informe és legÃtim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legÃtim selecciona \"Rebutjar\" per resoldre'l negativament." _delivery: + status: "Estat d'entrega " stop: "Suspés" + resume: "Torna a enviar" _type: none: "S'està publicant" + manuallySuspended: "Suspendre manualment" + goneSuspended: "Servidor suspès perquè el servidor s'ha esborrat" + autoSuspendedForNotResponding: "Servidor suspès perquè el servidor no respon" _bubbleGame: howToPlay: "Com es juga" + hold: "Mantenir" + _score: + score: "Puntuació " + scoreYen: "Diners guanyats" + highScore: "Millor puntuació " + maxChain: "Nombre mà xim de combos" + yen: "{yen}Ien" + estimatedQty: "{qty}peces" + scoreSweets: "{onigiriQtyWithUnit}ongiris" _howToPlay: section1: "Ajusta la posició i deixa caure l'objecte dintre la caixa." section2: "Quan dos objectes del mateix tipus es toquen, canviaran en un objecte diferent i guanyares punts." @@ -1281,10 +1365,10 @@ _initialTutorial: _reaction: title: "Què són les Reaccions?" description: "Es poden reaccionar a les Notes amb diferents emoticones. Les reaccions et permeten expressar matisos que hi són més enllà d'un simple m'agrada." - letsTryReacting: "Es poden afegir reaccions fent clic al botó '{reaction}'. Prova reaccionant a aquesta nota!" + letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!" reactToContinue: "Afegeix una reacció per continuar." reactNotification: "Rebrà s notificacions en temps real quan un usuari reaccioni a les teves notes." - reactDone: "Pots desfer una reacció fent clic al botó '{undo}'." + reactDone: "Pots desfer una reacció fent clic al botó '-'." _timeline: title: "El concepte de les lÃnies de temps" description1: "Misskey mostra diferents lÃnies de temps basades en l'ús (algunes poden no estar disponibles depenent de la polÃtica del servidor)" @@ -1344,6 +1428,10 @@ _serverSettings: fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les lÃnies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat." fanoutTimelineDbFallback: "Carregar de la base de dades" fanoutTimelineDbFallbackDescription: "Quan s'activa, la lÃnia de temps fa servir la base de dades per consultes adicionals si la lÃnia de temps no es troba a la memòria cau. Si és desactiva la cà rrega del servidor és veure reduïda, però també és reduirà el nombre de lÃnies de temps que és poden obtenir." + reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les lÃnies de temps reduint la cà rrega de la base. Com a contrapunt, augmentarà l'ús de memòria de RedÃs. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat." + inquiryUrl: "URL de consulta " + inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pà gina web amb el contacte d'informació." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un perÃode de temps, aquesta opció es desactiva automà ticament per evitar el correu brossa." _accountMigration: moveFrom: "Migrar un altre compte a aquest" moveFromSub: "Crear un à lies per un altre compte" @@ -1651,6 +1739,7 @@ _role: gtlAvailable: "Pot veure la lÃnia de temps global" ltlAvailable: "Pot veure la lÃnia de temps local" canPublicNote: "Pot enviar notes públiques" + mentionMax: "Nombre mà xim de mencions a una nota" canInvite: "Pot crear invitacions a la instà ncia " inviteLimit: "LÃmit d'invitacions " inviteLimitCycle: "Temps de refresc de les invitacions" @@ -1659,6 +1748,7 @@ _role: canManageAvatarDecorations: "Gestiona les decoracions dels avatars " driveCapacity: "Capacitat del disc" alwaysMarkNsfw: "Marca sempre els fitxers com a sensibles" + canUpdateBioMedia: "Permet l'edició d'una icona o un bà ner" pinMax: "Nombre mà xim de notes fixades" antennaMax: "Nombre mà xim d'antenes" wordMuteMax: "Nombre mà xim de carà cters permesos a les paraules silenciades" @@ -1673,9 +1763,20 @@ _role: canSearchNotes: "Pot cercar notes" canUseTranslator: "Pot fer servir el traductor" avatarDecorationLimit: "Nombre mà xim de decoracions que es poden aplicar els avatars" + canImportAntennas: "Autoritza la importació d'antenes " + canImportBlocking: "Autoritza la importació de bloquejats" + canImportFollowing: "Autoritza la importació de seguidors" + canImportMuting: "Autoritza la importació de silenciats" + canImportUserLists: "Autoritza la importació de llistes d'usuaris " _condition: + roleAssignedTo: "Assignat a rols manuals" isLocal: "Usuari local" isRemote: "Usuari remot" + isCat: "Usuaris gats" + isBot: "Usuaris bots" + isSuspended: "Usuari suspès" + isLocked: "Comptes privats" + isExplorable: "Fes que el compte aparegui a les cerques" createdLessThan: "Han passat menys de X a passat des de la creació del compte" createdMoreThan: "Han passat més de X des de la creació del compte" followersLessThanOrEq: "Té menys de X seguidors" @@ -1739,12 +1840,13 @@ _email: _follow: title: "t'ha seguit" _receiveFollowRequest: - title: "Has rebut una sol·licitud de seguiment" + title: "Has rebut una sol·licitud de seguiment" _plugin: install: "Instal·lar un afegit " installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança." manage: "Gestionar els afegits" viewSource: "Veure l'origen " + viewLog: "Mostra el registre" _preferencesBackups: list: "Llista de còpies de seguretat" saveNew: "Fer una còpia de seguretat nova" @@ -1774,6 +1876,8 @@ _aboutMisskey: contributors: "Col·laboradors principals" allContributors: "Tots els col·laboradors " source: "Codi font" + original: "Original" + thisIsModifiedVersion: "En {name} fa servir una versió modificada de Misskey." translation: "Tradueix Misskey" donate: "Fes un donatiu a Misskey" morePatrons: "També agraïm el suport d'altres col·laboradors que no surten en aquesta llista. Grà cies! 🥰" @@ -1881,7 +1985,6 @@ _theme: buttonBg: "Fons botó " buttonHoverBg: "Fons botó (en passar-hi per sobre)" inputBorder: "Contorn del cap d'introducció " - listItemHoverBg: "Fons dels elements d'una llista" driveFolderBg: "Fons de la carpeta Disc" wallpaperOverlay: "Superposició del fons de pantalla " badge: "InsÃgnia " @@ -1901,6 +2004,7 @@ _soundSettings: driveFileTypeWarnDescription: "Seleccionar un fitxer d'à udio " driveFileDurationWarn: "L'à udio és massa llarg" driveFileDurationWarnDescription: "Els à udios molt llargs pot interrompre l'ús de Misskey. Vols continuar?" + driveFileError: "El so no es pot carregar. Canvia la configuració" _ago: future: "Futur " justNow: "Ara mateix" @@ -1953,6 +2057,7 @@ _2fa: backupCodesDescription: "Si l'aplicació d'autenticació no es pot utilitzar, es pot accedir al compte utilitzant els següents codis de còpia de seguretat. Assegura't de mantenir aquests codis en un lloc segur. Cada codi es pot utilitzar només una vegada." backupCodeUsedWarning: "Es va utilitzar un codi de còpia de seguretat. Si l'aplicació de certificació està disponible, reconfigura l'aplicació d'autenticació tan aviat com sigui possible." backupCodesExhaustedWarning: "Es van utilitzar tots els codis de còpia de seguretat. Si no es pot utilitzar l'aplicació d'autenticació, ja no es pot accedir al compte. Torna a registrar l'aplicació d'autenticació." + moreDetailedGuideHere: "Aquà tens una guia al detall" _permissions: "read:account": "Veure la informació del compte." "write:account": "Editar la informació del compte." @@ -2026,22 +2131,73 @@ _permissions: "read:admin:emoji": "Veure emojis" "write:admin:queue": "Gestionar la cua de feines" "read:admin:queue": "Veure la cua de feines" + "write:admin:promo": "Gestiona les notes promocionals" + "write:admin:drive": "Gestiona el disc de l'usuari" + "read:admin:drive": "Veure la informació del disc de l'usuari" + "read:admin:stream": "Fes servir l'API sobre Websocket per l'administració" + "write:admin:ad": "Gestiona la publicitat" + "read:admin:ad": "Veure anuncis" + "write:invite-codes": "Crear codis d'invitació" + "read:invite-codes": "Obtenir codis d'invitació" + "write:clip-favorite": "Gestionar els clips favorits" + "read:clip-favorite": "Veure clips favorits" + "read:federation": "Veure dades de federació" + "write:report-abuse": "Informar d'un abús" +_auth: + shareAccessTitle: "Concedeix permisos a l'aplicació" + shareAccess: "Vols que {name} pugui accedir al vostre compte?" + shareAccessAsk: "Segur que vols que aquesta aplicació pugui accedir al vostre compte?" + permission: "{name} demana els següents permisos" + permissionAsk: "Aquesta aplicació demana els següents permisos" + pleaseGoBack: "Si us plau, torna a l'aplicació" + callback: "Tornant a l'aplicació" + denied: "Accés denegat" + pleaseLogin: "Si us plau, identificat per autoritzar l'aplicació." _antennaSources: all: "Totes les publicacions" homeTimeline: "Publicacions dels usuaris seguits" users: "Publicacions d'usuaris especÃfics" userList: "Publicacions d'una llista d'usuaris" + userBlacklist: "Totes les notes excepte les d'un o alguns usuaris especificats" +_weekday: + sunday: "Diumenge" + monday: "Dilluns" + tuesday: "Dimarts" + wednesday: "Dimecres" + thursday: "Dijous" + friday: "Divendres" + saturday: "Dissabte" _widgets: profile: "Perfil" instanceInfo: "Informació del fitxer d'instal·lació" + memo: "Notes adhesives" notifications: "Notificacions" timeline: "LÃnia de temps" + calendar: "Calendari" + trends: "Tendència" + clock: "Rellotge" + rss: "Lector RSS" + rssTicker: "RSS ticker" activity: "Activitat" + photos: "Fotografies" + digitalClock: "Rellotge digital" + unixClock: "Rellotge UNIX" federation: "Federació" + instanceCloud: "Núvol d'instà ncies" + postForm: "Formulari de publicació" + slideshow: "Presentació" button: "Botó " + onlineUsers: "Usuaris actius" jobQueue: "Cua de tasques" + serverMetric: "Mètriques del servidor" + aiscript: "Consola AiScript" + aiscriptApp: "Aplicació AiScript" + aichan: "Ai" + userList: "Llistat d'usuaris" _userList: chooseList: "Tria una llista" + clicker: "Clicker" + birthdayFollowings: "Usuaris que fan l'aniversari avui" _cw: hide: "Amagar" show: "Carregar més" @@ -2105,27 +2261,79 @@ _profile: changeBanner: "Canviar el bà ner " verifiedLinkDescription: "Escrivint una adreça URL que enllaci a aquest perfil, una icona de propietat verificada es mostrarà al costat del camp." avatarDecorationMax: "Pot afegir un mà xim de {max} decoracions." + followedMessage: "Missatge als nous seguidors" + followedMessageDescription: "Es pot configurar un missatge curt que es mostra a l'altra persona quan comença a seguir-te." + followedMessageDescriptionForLockedAccount: "Si comencen a seguir-te es mostra un missatge de quan es permet aquesta sol·licitud. " _exportOrImport: allNotes: "Totes les publicacions" + favoritedNotes: "Notes preferides" clips: "Retalls" followingList: "Seguint" muteList: "Silencia" blockingList: "Bloqueja" userLists: "Llistes" + excludeMutingUsers: "Exclou usuaris silenciats" + excludeInactiveUsers: "Exclou usuaris inactius" + withReplies: "Inclou a la lÃnia de temps les respostes d'usuaris importats" _charts: federation: "Federació" + apRequest: "Peticions" + usersIncDec: "Diferència entre el nombre d'usuaris" + usersTotal: "Nombre total d'usuaris" + activeUsers: "Usuaris actius" + notesIncDec: "Diferència entre el nombre de notes" + localNotesIncDec: "Diferencia en el nombre de notes locals" + remoteNotesIncDec: "Diferencia en el nombre de notes remotes" + notesTotal: "Nombre total de notes" + filesIncDec: "Diferencia en el nombre de fitxers" + filesTotal: "Nombre total de fitxers" + storageUsageIncDec: "Diferencia en l'emmagatzematge usat" + storageUsageTotal: "Emmagatzematge usat" +_instanceCharts: + requests: "Peticions" + users: "Diferència entre el nombre d'usuaris" + usersTotal: "Usuaris totals acumulats" + notes: "Diferència entre el nombre de notes" + notesTotal: "Notes totals acumulades" + ff: "Diferència en nombre d'usuaris seguits / seguidors" + ffTotal: "Nombre total acumulat d'usuaris seguits / seguidors" + cacheSize: "Diferència a la mida de la memòria cau" + cacheSizeTotal: "Total acumulat de la mida de la memòria cau" + files: "Diferència al nombre d'arxius" + filesTotal: "Nombre acumulatiu de fitxers" _timelines: home: "Inici" local: "Local" social: "Social" global: "Global" _play: + new: "Crear un guió" + edit: "Editar guió" + created: "Guió creat" + updated: "Guió editat" + deleted: "Guió esborrat" + pageSetting: "Configuració del guió" + editThisPage: "Edita aquest guió" viewSource: "Veure l'origen " + my: "Els meus guions" + liked: "Guions que m'han agradat" featured: "Popular" title: "TÃtol " script: "Script" summary: "Descripció" + visibilityDescription: "" _pages: + newPage: "pa" + editPage: "Editar la pà gina" + readPage: "Veure el codi font d'aquesta pà gina" + created: "La pà gina ha sigut creada correctament" + updated: "La pà gina s'ha editat correctament" + deleted: "La pà gina s'ha esborrat sense problemes" + pageSetting: "Configuració de la pà gina" + nameAlreadyExists: "L'adreça URL de la pà gina ja existeix" + invalidNameTitle: "L'adreça URL de la pà gina no és và lida" + invalidNameText: "Assegurat que el tÃtol de la pà gina no és buit" + editThisPage: "Editar la pà gina" viewSource: "Veure l'origen " viewPage: "Veure les teves pà gines " like: "M'agrada " @@ -2148,6 +2356,7 @@ _pages: eyeCatchingImageSet: "Escull una miniatura" eyeCatchingImageRemove: "Esborrar la miniatura" chooseBlock: "Afegeix un bloc" + enterSectionTitle: "Escriu el tÃtol de la secció" selectType: "Seleccionar tipus" contentBlocks: "Contingut" inputBlocks: "Entrada " @@ -2158,6 +2367,8 @@ _pages: section: "Secció " image: "Imatges" button: "Botó " + dynamic: "Blocs dinà mics" + dynamicDescription: "Aquest bloc és antic. Ara en endavant fes servir {play}" note: "Incorporar una Nota" _note: id: "ID de la publicació" @@ -2187,29 +2398,54 @@ _notification: sendTestNotification: "Enviar notificació de prova" notificationWillBeDisplayedLikeThis: "Les notificacions és veure'n aixà " reactedBySomeUsers: "Han reaccionat {n} usuaris" + likedBySomeUsers: "A {n} usuaris els hi agrada la teva nota" renotedBySomeUsers: "L'han impulsat {n} usuaris" + followedBySomeUsers: "Et segueixen {n} usuaris" + flushNotification: "Netejar notificacions" + exportOfXCompleted: "Completada l'exportació de {n}" _types: all: "Tots" + note: "Notes noves" follow: "Seguint" mention: "Menció" + reply: "Respostes" renote: "Renotar" quote: "Citar" reaction: "Reaccions" + pollEnded: "Enquesta terminada" + receiveFollowRequest: "Rebuda una petició de seguiment" + followRequestAccepted: "Petició de seguiment acceptada" + roleAssigned: "Rol donat" + achievementEarned: "Assoliment desbloquejat" + exportCompleted: "Exportació completada" + login: "Iniciar sessió" + test: "Prova la notificació" + app: "Notificacions d'aplicacions" _actions: followBack: "t'ha seguit també" reply: "Respondre" renote: "Renotar" _deck: + alwaysShowMainColumn: "Mostrar sempre la columna principal" columnAlign: "Alinea les columnes" addColumn: "Afig una columna" + newNoteNotificationSettings: "Configuració de notificacions per a notes noves" + configureColumn: "Configuració de columnes" swapLeft: "Mou a l’esquerra" swapRight: "Mou a la dreta" swapUp: "Mou cap amunt" swapDown: "Mou cap avall" + stackLeft: "Pila a la columna esquerra" popRight: "Col·loca a la dreta" profile: "Perfil" newProfile: "Perfil nou" deleteProfile: "Elimina el perfil" + introduction: "Crea la interfÃcie perfecta posant les columnes allà on vulguis!" + introduction2: "Fes clic al botó + de la dreta per afegir noves columnes sempre que vulguis." + widgetsIntroduction: "Selecciona \"Editar ginys\" a la columna del menú i afegeix un." + useSimpleUiForNonRootPages: "Usa una interfÃcie senzilla per a les pà gines navegades" + usedAsMinWidthWhenFlexible: "L'amplada mÃnima es farà servir quan \"Ajust automà tic de l'amplada\" estigui activat" + flexible: "Ajust automà tic de l'amplada" _columns: main: "Principal" widgets: "Ginys" @@ -2220,21 +2456,82 @@ _deck: channel: "Canals" mentions: "Mencions" direct: "Publicacions directes" + roleTimeline: "LÃnia de temps dels rols" +_dialog: + charactersExceeded: "Has arribat al mà xim de carà cters! Actualment és {current} de {max}" + charactersBelow: "Ets per sota del mÃnim de carà cters! Actualment és {current} de {min}" +_disabledTimeline: + title: "LÃnia de tems desactivada" + description: "No pots fer servir aquesta lÃnia de temps amb els teus rols actuals." +_drivecleaner: + orderBySizeDesc: "Mida del fitxer descendent" + orderByCreatedAtAsc: "Data ascendent" _webhookSettings: + createWebhook: "Crear un Webhook" + modifyWebhook: "Modificar un Webhook" name: "Nom" + secret: "Secret" + trigger: "Activador" active: "Activat" + _events: + follow: "Quan se segueix a un usuari" + followed: "Quan et segueixen" + note: "Quan es publica una nota" + reply: "Quan es rep una resposta" + renote: "Quan es renoti" + reaction: "Quan es rep una reacció " + mention: "Quan et mencionen" + _systemEvents: + abuseReport: "Quan reps un nou informe de moderació " + abuseReportResolved: "Quan resols un informe de moderació " + userCreated: "Quan es crea un usuari" + deleteConfirm: "Segur que vols esborrar el webhook?" + testRemarks: "Si feu clic al botó a la dreta de l'interruptor, podeu enviar un webhook de prova amb dades dummy." _abuseReport: _notificationRecipient: + createRecipient: "Afegeix un destinatari a l'informe de moderació " + modifyRecipient: "Editar un destinatari en l'informe de moderació " + recipientType: "Tipus de notificació " _recipientType: mail: "Correu electrònic" + webhook: "Webhook" + _captions: + mail: "Enviar un correu electrònic a tots els moderadors quan es rep un informe de moderació " + webhook: "Enviar una notificació al SystemWebhook quan es rebi o es resolgui un informe de moderació " + keywords: "Paraules clau" + notifiedUser: "Usuaris que s'han de notificar " + notifiedWebhook: "Webhook que s'ha de fer servir" + deleteConfirm: "Segur que vols esborrar el destinatari de l'informe de moderació?" _moderationLogTypes: + createRole: "Rol creat" + deleteRole: "Rol esborrat" + updateRole: "Rol actualitzat" + assignRole: "Assignat al rol" + unassignRole: "Esborrat del rol" suspend: "Suspèn" + unsuspend: "Suspensió treta" + addCustomEmoji: "Afegit emoji personalitzat" + updateCustomEmoji: "Actualitzat emoji personalitzat" + deleteCustomEmoji: "Esborrat emoji personalitzat" + updateServerSettings: "Configuració del servidor actualitzada" + updateUserNote: "Nota de moderació actualitzada" + deleteDriveFile: "Fitxer esborrat" + deleteNote: "Nota esborrada" + createGlobalAnnouncement: "Anunci global creat" + createUserAnnouncement: "Anunci individual creat" + updateGlobalAnnouncement: "Anunci global actualitzat" + updateUserAnnouncement: "Anunci individual actualitzat " + deleteGlobalAnnouncement: "Anunci global esborrat" + deleteUserAnnouncement: "Anunci individual esborrat " resetPassword: "Restableix la contrasenya" suspendRemoteInstance: "Servidor remot suspès " unsuspendRemoteInstance: "S'ha tret la suspensió del servidor remot" + updateRemoteInstanceNote: "Nota de moderació de la instà ncia remota actualitzada" markSensitiveDriveFile: "Fitxer marcat com a sensible" unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer" resolveAbuseReport: "Informe resolt" + forwardAbuseReport: "Informe reenviat" + updateAbuseReportNote: "Nota de moderació d'un informe actualitzat" createInvitation: "Crear codi d'invitació " createAd: "Anunci creat" deleteAd: "Anunci esborrat" @@ -2244,6 +2541,16 @@ _moderationLogTypes: deleteAvatarDecoration: "S'ha esborrat la decoració de l'avatar " unsetUserAvatar: "Esborrar l'avatar d'aquest usuari" unsetUserBanner: "Esborrar el bà ner d'aquest usuari" + createSystemWebhook: "Crear un SystemWebhook" + updateSystemWebhook: "Actualitzar SystemWebhook" + deleteSystemWebhook: "Esborrar SystemWebhook" + createAbuseReportNotificationRecipient: "Crear un destinatari per l'informe de moderació " + updateAbuseReportNotificationRecipient: "Actualitzar destinatari per l'informe de moderació " + deleteAbuseReportNotificationRecipient: "Esborrar destinatari de l'informe de moderació " + deleteAccount: "Esborrar el compte " + deletePage: "Esborrar la pà gina" + deleteFlash: "Esborrar el guió" + deleteGalleryPost: "Esborrar la publicació de la galeria" _fileViewer: title: "Detall del fitxer" type: "Tipus de fitxer" @@ -2270,5 +2577,54 @@ _externalResourceInstaller: _errors: _invalidParams: title: "Parà metres no và lids " + description: "No hi ha suficient informació per carregar les dades del lloc extern. Confirma l'URL que hi ha escrita." + _resourceTypeNotSupported: + title: "El recurs extern no està suportat." + description: "Aquesta mena de recurs no està suportat. Contacta amb l'administrador." + _failedToFetch: + title: "Ha fallat l'obtenció de dades" + fetchErrorDescription: "Ha aparegut un error comunicant-se amb el lloc extern. Si després d'intentar-ho un altre cop no es resol, contacta amb l'administrador." + parseErrorDescription: "Ha aparegut un error processant les dades carregades del lloc extern. Contacta amb l'administrador." + _hashUnmatched: + title: "Ha fallat la verificació de les dades" + description: "Ha aparegut un error verificant les dades obtingudes. Com a mesura de seguretat la instal·lació no pot continuar. Contacta amb l'administrador." + _pluginParseFailed: + title: "Error d'AiScript" + description: "Les dades sol·licitades s'han obtingut correctament, però hem trobat un error durant el processament d'AiScript. Contacta amb l'autor de l'afegit. Detalls de l'error es pot veure a la consola JavaScript." + _pluginInstallFailed: + title: "La instal·lació de l'afegit a fallat" + description: "Ha aparegut un error durant la instal·lació de l'afegit. Intenta-ho una altra vegada. El detall de l'error es pot veure a la consola JavaScript." + _themeParseFailed: + title: "Ha fallat el processament del tema" + description: "Les dades sol·licitades s'han obtingut correctament, però hem trobat un error durant el processament del tema. Contacta amb l'autor de l'afegit. Detalls de l'error es pot veure a la consola JavaScript." + _themeInstallFailed: + title: "La instal·lació del tema a fallat" + description: "Ha aparegut un error durant la instal·lació del tema. Intenta-ho una altra vegada. El detall de l'error es pot veure a la consola JavaScript." +_dataSaver: + _media: + title: "Carregant multimèdia " + description: "Desactiva la cà rrega automà tica d'imatges i vÃdeos. Les imatges i els vÃdeos amagats es carregaran quan es faci clic a sobre." + _avatar: + title: "Avatars animats" + description: "Detenir l'animació dels avatars animats. Les imatges animades solen tenir un pes més gran que les imatges normals, reduint el trà fic disponible." + _urlPreview: + title: "Miniatures vista prèvia de l'URL" + description: "Les imatges en miniatura que serveixen com a vista prèvia de les URLs no es tornaran a carregar." + _code: + title: "Ressaltat del codi " _reversi: total: "Total" +_embedCodeGen: + title: "Personalitza el codi per incrustar" + header: "Mostrar la capçalera" + autoload: "Carregar automà ticament (no recomanat)" + maxHeight: "Alçada mà xima" + maxHeightDescription: "0 anul·la la configuració mà xima. Per evitar que continuï creixent verticalment, especifiqui qualsevol valor." + maxHeightWarn: "El lÃmit mà xim d'alçada és nul (0). Si això no és un canvi previst, estableix el mà xim d'alçada a un cert valor." + previewIsNotActual: "La visualització és diferent de la que es mostra quan s'implanta." + rounded: "Angle recte" + border: "Afegeix un marc al contenidor" + applyToPreview: "Aplica a la vista prèvia" + generateCode: "Crea el codi per incrustar" + codeGenerated: "Codi generat" + codeGeneratedDescription: "Si us plau, enganxeu el codi generat al lloc web." diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 0983e05ad9fd56c92bae9d771ecc2f56ab72477b..caf6d6e16349d2a77aa5955797ac75732d2a6c4b 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -471,7 +471,6 @@ uiLanguage: "Jazyk uživatelského rozhranÃ" aboutX: "O {x}" emojiStyle: "Styl emoji" native: "VýchozÃ" -disableDrawer: "NepoužÃvat Å¡uplÃkové menu" showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom pÅ™i nabÄ›hnutà myÅ¡i" noHistory: "Žádná historie" signinHistory: "Historie pÅ™ihlášenÃ" @@ -658,10 +657,7 @@ abuseReported: "Nahlášenà bylo odesláno. DÄ›kujeme pÅ™evelice." reporter: "Nahlásil" reporteeOrigin: "Původ nahlášenÃ" reporterOrigin: "Původ nahlasovaÄe" -forwardReport: "PÅ™eposlat nahlášenà do vzdálené instance" -forwardReportIsAnonymous: "MÃsto vaÅ¡eho úÄtu se ve vzdálené instanci zobrazà anonymnà systémový úÄet jako nahlaÅ¡ovaÄ." send: "Odeslat" -abuseMarkAsResolved: "OznaÄit nahlášenà jako vyÅ™eÅ¡ené" openInNewTab: "OtevÅ™Ãt v nové kartÄ›" openInSideView: "OtevÅ™Ãt v boÄnÃm panelu" defaultNavigationBehaviour: "Výchozà chovánà navigace" @@ -1093,7 +1089,7 @@ doYouAgree: "SouhlasÃte?" beSureToReadThisAsItIsImportant: "PÅ™eÄtÄ›te si prosÃm tyto důležité informace." iHaveReadXCarefullyAndAgree: "PÅ™eÄetl jsem si text \"{x}\" a souhlasÃm s nÃm." icon: "Avatar" -replies: "OdpovÄ›di" +replies: "OdpovÄ›dÄ›t" renotes: "PÅ™eposlat" sourceCode: "Zdrojový kód" flip: "OtoÄit" @@ -1633,7 +1629,6 @@ _theme: buttonBg: "Pozadà tlaÄÃtka" buttonHoverBg: "Pozadà tlaÄÃtka (Hover)" inputBorder: "OhraniÄenà vstupnÃho pole" - listItemHoverBg: "Pozadà položky seznamu (Hover)" driveFolderBg: "Pozadà složky disku" wallpaperOverlay: "PÅ™ekrytà tapety" badge: "Odznak" @@ -1963,6 +1958,7 @@ _notification: receiveFollowRequest: "Obdržené žádosti o sledovánÃ" followRequestAccepted: "PÅ™ijaté žádosti o sledovánÃ" achievementEarned: "ÚspÄ›ch odemÄen" + login: "PÅ™ihlásit se" app: "Oznámenà z propojených aplikacÃ" _actions: followBack: "vás zaÄal sledovat zpÄ›t" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 8c7d372c3f420672ff33e39c01ed7cb4e9bab14d..4e2bd069340a7d161f29ed24629df01eb0493a70 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1,8 +1,8 @@ --- _lang_: "Deutsch" headlineMisskey: "Ein durch Notizen verbundenes Netzwerk" -introMisskey: "Willkommen! Sharkey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Notizen“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. ðŸ‘\nEine neue Welt wartet auf dich! 🚀" -poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform <b>Sharkey</b> betriebenen Dienste die auf Misskey basiert ist (meist als \"Misskey-Instanz\" bezeichnet)." +introMisskey: "Willkommen! Misskey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Notizen“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. ðŸ‘\nEine neue Welt wartet auf dich! 🚀" +poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform <b>Misskey</b> betriebenen Dienste." monthAndDay: "{day}.{month}." search: "Suchen" notifications: "Benachrichtigungen" @@ -15,7 +15,7 @@ gotIt: "Verstanden!" cancel: "Abbrechen" noThankYou: "Nein, danke" enterUsername: "Benutzername eingeben" -renotedBy: "Geboostet von {user}" +renotedBy: "Renote von {user}" noNotes: "Keine Notizen gefunden" noNotifications: "Keine Benachrichtigungen gefunden" instance: "Instanz" @@ -105,13 +105,13 @@ followRequests: "Follow-Anfragen" unfollow: "Entfolgen" followRequestPending: "Follow-Anfrage ausstehend" enterEmoji: "Gib ein Emoji ein" -renote: "Boost" -unrenote: "Boost zurücknehmen" -renoted: "Boost getätigt." -cantRenote: "Boosten dieses Beitrags nicht möglich." -cantReRenote: "Boosten von einen Boost nicht möglich." +renote: "Renote" +unrenote: "Renote zurücknehmen" +renoted: "Renote getätigt." +cantRenote: "Renote dieses Beitrags nicht möglich." +cantReRenote: "Renote einer Renote nicht möglich." quote: "Zitieren" -inChannelRenote: "Kanal-interner Boost" +inChannelRenote: "Kanal-interner Renote" inChannelQuote: "Kanal-internes Zitat" pinnedNote: "Angeheftete Notiz" pinned: "Angeheftet" @@ -166,7 +166,7 @@ youCanCleanRemoteFilesCache: "Klicke auf den 🗑ï¸-Knopf der Dateiverwaltungsa cacheRemoteSensitiveFiles: "Sensitive Dateien von fremden Instanzen im Cache speichern" cacheRemoteSensitiveFilesDescription: "Ist diese Einstellung deaktiviert, so werden sensitive Dateien fremder Instanzen direkt von dort ohne Zwischenspeicherung geladen." flagAsBot: "Als Bot markieren" -flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Sharkeys interne Systeme dieses Benutzerkonto als Bot behandeln." +flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Misskeys interne Systeme dieses Benutzerkonto als Bot behandeln." flagAsCat: "Als Katze markieren" flagAsCatDescription: "Aktiviere diese Option, um dieses Benutzerkonto als Katze zu markieren." flagShowTimelineReplies: "Antworten in der Chronik anzeigen" @@ -229,7 +229,7 @@ noUsers: "Keine Benutzer gefunden" editProfile: "Profil bearbeiten" noteDeleteConfirm: "Möchtest du diese Notiz wirklich löschen?" pinLimitExceeded: "Du kannst nicht noch mehr Notizen anheften." -intro: "Sharkey ist installiert! Lass uns nun ein Administratorkonto einrichten." +intro: "Misskey ist installiert! Lass uns nun ein Administratorkonto einrichten." done: "Fertig" processing: "In Bearbeitung …" preview: "Vorschau" @@ -422,7 +422,7 @@ exploreFediverse: "Das Fediverse erkunden" popularTags: "Beliebte Schlagwörter" userList: "Liste" about: "Ãœber" -aboutMisskey: "Ãœber Sharkey" +aboutMisskey: "Ãœber Misskey" administrator: "Administrator" token: "Token" 2fa: "Zwei-Faktor-Authentifizierung" @@ -491,7 +491,6 @@ uiLanguage: "Sprache der Benutzeroberfläche" aboutX: "Ãœber {x}" emojiStyle: "Emoji-Stil" native: "Nativ" -disableDrawer: "Keine ausfahrbaren Menüs verwenden" showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen" noHistory: "Kein Verlauf gefunden" signinHistory: "Anmeldungsverlauf" @@ -572,7 +571,7 @@ sort: "Sortieren" ascendingOrder: "Aufsteigende Reihenfolge" descendingOrder: "Absteigende Reihenfolge" scratchpad: "Testumgebung" -scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Sharkey überprüfen." +scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Misskey überprüfen." output: "Ausgabe" script: "Skript" disablePagesScript: "AiScript auf Seiten deaktivieren" @@ -687,10 +686,7 @@ abuseReported: "Deine Meldung wurde versendet. Vielen Dank." reporter: "Melder" reporteeOrigin: "Herkunft des Gemeldeten" reporterOrigin: "Herkunft des Meldenden" -forwardReport: "Meldung an fremde Instanz weiterleiten" -forwardReportIsAnonymous: "Anstatt deines Benutzerkontos wird bei der fremden Instanz ein anonymes Systemkonto als Melder angezeigt." send: "Senden" -abuseMarkAsResolved: "Meldung als gelöst markieren" openInNewTab: "In neuem Tab öffnen" openInSideView: "In Seitenansicht öffnen" defaultNavigationBehaviour: "Standardnavigationsverhalten" @@ -709,14 +705,14 @@ unclip: "Aus Clip entfernen" confirmToUnclipAlreadyClippedNote: "Diese Notiz ist bereits im \"{name}\" Clip enthalten. Möchtest du sie aus diesem Clip entfernen?" public: "Öffentlich" private: "Privat" -i18nInfo: "Sharkey wird durch freiwillige Helfer in viele verschiedene Sprachen übersetzt. Auf {link} kannst du mithelfen." +i18nInfo: "Misskey wird durch freiwillige Helfer in viele verschiedene Sprachen übersetzt. Auf {link} kannst du mithelfen." manageAccessTokens: "Zugriffstokens verwalten" accountInfo: "Benutzerkonto-Informationen" notesCount: "Anzahl der Notizen" repliesCount: "Anzahl gesendeter Antworten" renotesCount: "Anzahl getätigter Renotes" repliedCount: "Anzahl erhaltener Antworten" -renotedCount: "Anzahl erhaltener Boosts" +renotedCount: "Anzahl erhaltener Renotes" followingCount: "Anzahl gefolgter Benutzer" followersCount: "Anzahl an Followern" sentReactionsCount: "Anzahl gesendeter Reaktionen" @@ -763,7 +759,7 @@ onlineUsersCount: "{n} Benutzer sind online" nUsers: "{n} Benutzer" nNotes: "{n} Notizen" sendErrorReports: "Fehlerberichte senden" -sendErrorReportsDescription: "Ist diese Option aktiviert, so werden beim Auftreten von Fehlern detaillierte Fehlerinformationen an Sharkey weitergegeben, was zur Verbesserung der Qualität von Sharkey beiträgt.\nEnthalten in diesen Informationen sind u.a. die Version deines Betriebssystems, welchen Browser du verwendest und ein Verlauf deiner Aktivitäten innerhalb Sharkey." +sendErrorReportsDescription: "Ist diese Option aktiviert, so werden beim Auftreten von Fehlern detaillierte Fehlerinformationen an Misskey weitergegeben, was zur Verbesserung der Qualität von Misskey beiträgt.\nEnthalten in diesen Informationen sind u.a. die Version deines Betriebssystems, welchen Browser du verwendest und ein Verlauf deiner Aktivitäten innerhalb Misskey." myTheme: "Mein Farbschema" backgroundColor: "Hintergrundfarbe" accentColor: "Akzentfarbe" @@ -857,7 +853,7 @@ hashtags: "Hashtags" troubleshooting: "Problembehandlung" useBlurEffect: "Weichzeichnungseffekt in der Benutzeroberfläche verwenden" learnMore: "Mehr erfahren" -misskeyUpdated: "Sharkey wurde aktualisiert!" +misskeyUpdated: "Misskey wurde aktualisiert!" whatIsNew: "Änderungen anzeigen" translate: "Ãœbersetzen" translatedFrom: "Aus {x} übersetzt" @@ -984,9 +980,8 @@ numberOfLikes: "\"Gefällt mir\"-Anzahl" show: "Anzeigen" neverShow: "Nicht wieder anzeigen" remindMeLater: "Vielleicht später" -didYouLikeMisskey: "Gefällt dir Sharkey?" -pleaseDonate: "Sharkey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" -pleaseDonateInstance: "Du kannst {host} auch direkt unterstützen, indem du an deine Instanz Administration spendest." +didYouLikeMisskey: "Gefällt dir Misskey?" +pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" roles: "Rollen" role: "Rolle" noRole: "Rolle nicht gefunden" @@ -1006,7 +1001,7 @@ permissionDeniedError: "Aktion verweigert" permissionDeniedErrorDescription: "Dieses Benutzerkonto besitzt nicht die Berechtigung, um diese Aktion auszuführen." preset: "Vorlage" selectFromPresets: "Aus Vorlagen wählen" -achievements: "Erfolge" +achievements: "Errungenschaften" gotInvalidResponseError: "Ungültige Antwort des Servers" gotInvalidResponseErrorDescription: "Eventuell ist der Server momentan nicht erreichbar oder untergeht Wartungsarbeiten. Bitte versuche es später noch einmal." thisPostMayBeAnnoying: "Dieser Beitrag stört eventuell andere Benutzer." @@ -1101,7 +1096,7 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Diese Rollen müssen öffe cancelReactionConfirm: "Möchtest du deine Reaktion wirklich löschen?" changeReactionConfirm: "Möchtest du deine Reaktion wirklich ändern?" later: "Später" -goToMisskey: "Zu Sharkey" +goToMisskey: "Zu Misskey" additionalEmojiDictionary: "Zusätzliche Emoji-Wörterbücher" installed: "Installiert" branding: "Branding" @@ -1166,8 +1161,6 @@ impressumDescription: "In manchen Ländern, wie Deutschland und dessen Umgebung, privacyPolicy: "Datenschutzerklärung" privacyPolicyUrl: "Datenschutzerklärungs-URL" tosAndPrivacyPolicy: "Nutzungsbedingungen und Datenschutzerklärung" -donation: "Spenden" -donationUrl: "Spenden-URL" avatarDecorations: "Profilbilddekoration" attach: "Anbringen" detach: "Entfernen" @@ -1271,7 +1264,7 @@ _accountMigration: moveTo: "Dieses Konto zu einem neuen migrieren" moveToLabel: "Umzugsziel:" moveCannotBeUndone: "Die Migration eines Benutzerkontos ist unwiderruflich." - moveAccountDescription: "Hierdurch wird dein Konto zu einem anderen migriert.\n ・Follower von diesem Konto werden automatisch auf das neue Konto migriert\n ・Dieses Konto wird allen Nutzern, denen es derzeit folgt, nicht mehr folgen\n ・Mit diesem Konto können keine neuen Notizen usw. erstellt werden\n\nWährend die Migration der Follower automatisch erfolgt, muss die Migration der Konten, denen du folgst, manuell vorbereitet werden. Exportiere hierzu die Liste der gefolgten Nutzer über das Einstellungsmenu, und importiere diese Liste im neuen Konto. Das gleiche Verfahren gilt für erstellte Listen und stummgeschaltete oder blockierte Nutzer.\n\n(Diese Erklärung gilt für Sharkey v13.12.0 oder später. Die Funktionsweise andere ActivityPub-Software, beispielsweise Mastodon, kann hiervon abweichen.)" + moveAccountDescription: "Hierdurch wird dein Konto zu einem anderen migriert.\n ・Follower von diesem Konto werden automatisch auf das neue Konto migriert\n ・Dieses Konto wird allen Nutzern, denen es derzeit folgt, nicht mehr folgen\n ・Mit diesem Konto können keine neuen Notizen usw. erstellt werden\n\nWährend die Migration der Follower automatisch erfolgt, muss die Migration der Konten, denen du folgst, manuell vorbereitet werden. Exportiere hierzu die Liste der gefolgten Nutzer über das Einstellungsmenu, und importiere diese Liste im neuen Konto. Das gleiche Verfahren gilt für erstellte Listen und stummgeschaltete oder blockierte Nutzer.\n\n(Diese Erklärung gilt für Misskey v13.12.0 oder später. Die Funktionsweise andere ActivityPub-Software, beispielsweise Mastodon, kann hiervon abweichen.)" moveAccountHowTo: "Um ein Konto zu migrieren, erstelle zuerst auf dem Umzugsziel einen Alias für dieses Konto.\nGib dann das Umzugsziel in folgendem Format ein: @username@server.example.com" startMigration: "Migrieren" migrationConfirm: "Dieses Konto wirklich zu {account} umziehen? Sobald der Umzug beginnt, kann er nicht rückgängig gemacht werden, und dieses Konto nicht wieder im ursprünglichen Zustand verwendet werden." @@ -1282,26 +1275,26 @@ _achievements: earnedAt: "Freigeschaltet am" _types: _notes1: - title: "Hallo Sharkey!" + title: "Hallo Misskey!" description: "Sende deine erste Notiz" - flavor: "Hab eine schöne Zeit mit Sharkey!" + flavor: "Hab eine schöne Zeit mit Misskey!" _notes10: - title: "Notizblog" + title: "Ein paar Notizen" description: "10 Notizen gesendet" _notes100: - title: "Schreiberling" + title: "Viele Notizen" description: "100 Notizen gesendet" _notes500: - title: "Autor" + title: "Ãœberschüttet mit Notizen" description: "500 Notizen gesendet" _notes1000: - title: "Bestseller" + title: "Berg an Notizen" description: "1.000 Notizen gesendet" _notes5000: - title: "Dicker Wälzer" + title: "Ãœberquellende Notizen" description: "5.000 Notizen gesendet" _notes10000: - title: "Verlagshaus" + title: "Supernotiz" description: "10.000 Notizen gesendet" _notes20000: title: "Brauche... mehr... Notizen..." @@ -1328,28 +1321,28 @@ _achievements: title: "Notizversum" description: "90.000 Notizen gesendet" _notes100000: - title: "ALL YOUR NOTES ARE BELONG TO US" + title: "ALL YOUR NOTE ARE BELONG TO US" description: "100.000 Notizen gesendet" flavor: "Du hast wirklich viel zu sagen." _login3: - title: "Neuling â… " + title: "Anfänger â… " description: "An 3 Tagen eingeloggt" flavor: "Nenn' mich ab heute Misskist" _login7: - title: "Neuling â…¡" + title: "Anfänger â…¡" description: "An 7 Tagen eingeloggt" flavor: "Na, eingewöht?" _login15: - title: "Neuling â…¢" + title: "Anfänger â…¢" description: "An 15 Tagen eingeloggt" _login30: - title: "Fedizen â… " + title: "Misskist â… " description: "An 30 Tagen eingeloggt" _login60: - title: "Fedizen â…¡" + title: "Misskist â…¡" description: "An 60 Tagen eingeloggt" _login100: - title: "Fedizen â…¢" + title: "Misskist â…¢" description: "An 100 Tagen eingeloggt" flavor: "Violent Misskist" _login200: @@ -1362,14 +1355,14 @@ _achievements: title: "Stammbesucher â…¢" description: "An 400 Tagen eingeloggt" _login500: - title: "Alter Hase â… " + title: "Veteran â… " description: "An 500 Tagen eingeloggt" flavor: "Meine Kameraden, ich liebe sie, die Notizen." _login600: - title: "Alter Hase â…¡" + title: "Veteran â…¡" description: "An 600 Tagen eingeloggt" _login700: - title: "Alter Hase â…¢" + title: "Veteran â…¢" description: "An 700 Tagen eingeloggt" _login800: title: "Meister der Notizen â… " @@ -1378,39 +1371,39 @@ _achievements: title: "Meister der Notizen â…¡" description: "An 900 Tagen eingeloggt" _login1000: - title: "Wie die Zeit vergeht" + title: "Meister der Notizen â…¢" description: "An 1000 Tagen eingeloggt" - flavor: "Danke, dass du Sharkey nutzt!" + flavor: "Danke, dass du Misskey nutzt!" _noteClipped1: - title: "Das merk ich mir" + title: "Muss... clippen..." description: "Die erste Notiz geclippt" _noteFavorited1: title: "Sternengucker" description: "Eine Notiz als Favorit markiert" _myNoteFavorited1: - title: "Hilfreich" + title: "Sternensucher" description: "Ein anderer Benutzer hat eine deiner Notizen als Favoriten markiert" _profileFilled: title: "Perfekte Vorbereitung" description: "Fülle dein Profil aus" _markedAsCat: - title: "Das Königreich der Katzen" - description: "Markiere dein Profil als Katze" - flavor: "Einen Namen bekommst du später~ " + title: "Ich der Kater" + description: "Markiere dein Konto als Katze" + flavor: "Einen Namen bekommst du später. " _following1: - title: "Immer auf dem neusten Stand" + title: "Das Folgen beginnt" description: "Du folgst deiner ersten Person" _following10: title: "Folge ihnen... folge ihnen..." description: "Du folgst über 10 Leuten" _following50: - title: "Lieblingsposter" + title: "Viele Freunde" description: "Du folgst über 50 Leuten" _following100: - title: "Die Top 100" + title: "100 Freunde" description: "Du folgst über 100 Leuten" _following300: - title: "Folgen, folgen, folgen!" + title: "Freundeüberschuss" description: "Du folgst über 300 Leuten" _followers1: title: "Der erste Follower" @@ -1425,7 +1418,7 @@ _achievements: title: "Beliebt" description: "Die Anzahl deiner Follower hat 100 überschritten" _followers300: - title: "Teil des Schiffs, Teil der Crew" + title: "Eine geordnete Reihe, bitte!" description: "Die Anzahl deiner Follower hat 300 überschritten" _followers500: title: "Funkmast" @@ -1434,31 +1427,31 @@ _achievements: title: "Influencer" description: "Die Anzahl deiner Follower hat 1000 überschritten" _collectAchievements30: - title: "Sammler der Erfolge" + title: "Sammler der Errungenschaften" description: "Schalte 30 Errungenschaften frei" _viewAchievements3min: - title: "Ausstellung" + title: "Fan von Errungenschaften" description: "Schau dir die Liste deiner Errungenschaften für mindestens 3 Minuten an" _iLoveMisskey: - title: "I Love Sharkey" - description: "Sende \"I ⤠#Sharkey\"" - flavor: "Danke, dass du Sharkey verwendest! - vom Entwicklerteam" + title: "I Love Misskey" + description: "Sende \"I ⤠#Misskey\"" + flavor: "Danke, dass du Misskey verwendest! - vom Entwicklerteam" _foundTreasure: title: "Schatzsuche" description: "Du hast einen verborgenen Schatz gefunden" _client30min: title: "Kurze Pause" - description: "Habe Sharkey für mindestens 30 Minuten geöffnet" + description: "Habe Misskey für mindestens 30 Minuten geöffnet" _client60min: - title: "Munter mit Sharkey" - description: "Habe Sharkey für mindestens 60 Minuten geöffnet" + title: "Munter mit Misskey" + description: "Habe Misskey für mindestens 60 Minuten geöffnet" _noteDeletedWithin1min: - title: "Katze auf der Tastatur" + title: "Ups" description: "Lösche eine Notiz innerhalb von 1 Minute nachdem sie gesendet wurde" _postedAtLateNight: title: "Nachtaktiv" description: "Sende mitten in der Nacht eine Notiz" - flavor: "Echte Katzen sind nunmal nachtaktiv~" + flavor: "Geh bald schlafen." _postedAt0min0sec: title: "Zeitansage" description: "Sende um 00:00 eine Notiz" @@ -1473,7 +1466,7 @@ _achievements: title: "Analyst" description: "Schau dir die Messwerte der Instanz an" _outputHelloWorldOnScratchpad: - title: "Junghacker" + title: "Hallo Welt!" description: "Gib \"hello world\" in der Testumgebung aus" _open3windows: title: "Splitscreen" @@ -1482,25 +1475,25 @@ _achievements: title: "Zyklischer Verweis" description: "Versuche, in Drive einen Zirkelbezug von Ordnern herzustellen" _reactWithoutRead: - title: "Erst reagieren, dann lesen" + title: "Hast du das wirklich gelesen?" description: "Reagiere auf eine Notiz mit mindestens 100 Zeichen innerhalb von 3 Sekunden der Erstellung der Notiz" _clickedClickHere: title: "Klicke hier" description: "Du hast hier geklickt" _justPlainLucky: - title: "Glück 100" + title: "Pures Glück" description: "Kann alle 10 Sekunden mit einer Warscheinlichkeit von 0.005% erhalten werden" _setNameToSyuilo: title: "Gottkomplex" description: "Setze deinen Namen auf \"syuilo\"" _passedSinceAccountCreated1: - title: "Davon erzähle ich meinen Enkeln" + title: "Einjahresjubiläum" description: "Seit der Erstellung deines Kontos ist 1 Jahr vergangen" _passedSinceAccountCreated2: - title: "Diese Software hat mich zur Katze gemacht" + title: "Zweijahresjubiläum" description: "Seit der Erstellung deines Kontos sind 2 Jahre vergangen" _passedSinceAccountCreated3: - title: "Goldene Zeit" + title: "Dreijahresjubiläum" description: "Seit der Erstellung deines Kontos sind 3 Jahre vergangen" _loggedInOnBirthday: title: "Alles Gute Zum Geburtstag" @@ -1508,15 +1501,15 @@ _achievements: _loggedInOnNewYearsDay: title: "Frohes Neujahr" description: "Logge dich am Neujahrstag ein" - flavor: "Auf ein weiteres tolles Jahr in diesem Universum!" + flavor: "Auf ein weiteres tolles Jahr in dieser Instanz" _cookieClicked: - title: "Kreise klicken für Omas" + title: "Ein Spiel, in dem du auf einen Keks klickst" description: "Den Keks geklickt" flavor: "Bist du hier richtig?" _brainDiver: title: "Brain Diver" description: "Sende den Link zu Brain Diver" - flavor: "Sharkey-Sharkey La-Tu-Ma" + flavor: "Misskey-Misskey La-Tu-Ma" _smashTestNotificationButton: title: "Testüberfluss" description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne" @@ -1681,12 +1674,12 @@ _registry: domain: "Domain" createKey: "Schlüssel erstellen" _aboutMisskey: - about: "Sharkey ist Open-Source-Software basiert auf Misskey welche von syuilo seit 2014 entwickelt wird." + about: "Misskey ist Open-Source-Software, welche von syuilo seit 2014 entwickelt wird." contributors: "Hauptmitwirkende" allContributors: "Alle Mitwirkenden" source: "Quellcode" - translation: "Sharkey übersetzen" - donate: "An Sharkey spenden" + translation: "Misskey übersetzen" + donate: "An Misskey spenden" morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰" patrons: "UnterstützerInnen" projectMembers: "Projektmitglieder" @@ -1791,7 +1784,6 @@ _theme: buttonBg: "Hintergrund von Schaltflächen" buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)" inputBorder: "Rahmen von Eingabefeldern" - listItemHoverBg: "Hintergrund von Listeneinträgen (Mouseover)" driveFolderBg: "Hintergrund von Drive-Ordnern" wallpaperOverlay: "Hintergrundbild-Overlay" badge: "Wappen" @@ -2116,7 +2108,7 @@ _notification: youGotMention: "{name} hat dich erwähnt" youGotReply: "{name} hat dir geantwortet" youGotQuote: "{name} hat dich zitiert" - youRenoted: "Boost deiner Notiz von {name}" + youRenoted: "Renote deiner Notiz von {name}" youWereFollowed: "ist dir gefolgt" youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" @@ -2145,6 +2137,7 @@ _notification: receiveFollowRequest: "Erhaltene Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen" achievementEarned: "Errungenschaft freigeschaltet" + login: "Anmelden" app: "Benachrichtigungen von Apps" _actions: followBack: "folgt dir nun auch" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 5eca348e186f883ff54dcaf9e69f8a5465696aa5..4657842ca5bf73ceffa4a13e51b2a36572bc189e 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -378,6 +378,7 @@ _notification: renote: "Κοινοποίηση σημειώματος" quote: "ΠαÏάθεση" reaction: "ΑντιδÏάσεις" + login: "ΣÏνδεση" _actions: reply: "Απάντηση" renote: "Κοινοποίηση σημειώματος" diff --git a/locales/en-US.yml b/locales/en-US.yml index 6d24bb5b41de15df1cede6ec5ef4348270d0d474..6ea7fb4f8ddefe2f67e90d1763f610dec830f1aa 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1,13 +1,16 @@ --- _lang_: "English" headlineMisskey: "A network connected by notes" -introMisskey: "Welcome! Sharkey is an open source, decentralized microblogging service.\nCreate \"notes\" to share your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. ðŸ‘\nLet's explore a new world! 🚀" -poweredByMisskeyDescription: "{name} is one of the services powered by the open source platform <b>Sharkey</b> which is based on Misskey (referred to as a \"Misskey instance\")." +introMisskey: "Welcome! Misskey is an open source, decentralized microblogging service.\nCreate \"notes\" to share your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. ðŸ‘\nLet's explore a new world! 🚀" +poweredByMisskeyDescription: "{name} is one of the services powered by the open source platform <b>Misskey</b> (referred to as a \"Misskey instance\")." monthAndDay: "{month}/{day}" search: "Search" notifications: "Notifications" username: "Username" password: "Password" +initialPasswordForSetup: "Initial password for setup" +initialPasswordIsIncorrect: "Initial password for setup is incorrect" +initialPasswordForSetupDescription: "Use the password you entered in the configuration file if you installed Misskey yourself.\n If you are using a Misskey hosting service, use the password provided.\n If you have not set a password, leave it blank to continue." forgotPassword: "Forgot password" fetchingAsApObject: "Fetching from the Fediverse..." ok: "OK" @@ -15,7 +18,7 @@ gotIt: "Got it!" cancel: "Cancel" noThankYou: "Not now" enterUsername: "Enter username" -renotedBy: "Boosted by {user}" +renotedBy: "Renoted by {user}" noNotes: "No notes" noNotifications: "No notifications" instance: "Instance" @@ -34,7 +37,6 @@ signup: "Sign Up" uploading: "Uploading..." save: "Save" users: "Users" -approvals: "Approvals" addUser: "Add a user" favorite: "Add to favorites" favorites: "Favorites" @@ -46,16 +48,15 @@ pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents" copyLink: "Copy link" -copyLinkRenote: "Copy boost link" +copyLinkRenote: "Copy renote link" delete: "Delete" deleteAndEdit: "Delete and edit" -deleteAndEditConfirm: "Are you sure you want to redraft this note? This means you will lose all reactions, boosts, and replies to it." +deleteAndEditConfirm: "Are you sure you want to redraft this note? This means you will lose all reactions, renotes, and replies to it." addToList: "Add to list" addToAntenna: "Add to antenna" sendMessage: "Send a message" copyRSS: "Copy RSS" copyUsername: "Copy username" -openRemoteProfile: "Open remote profile" copyUserId: "Copy user ID" copyNoteId: "Copy note ID" copyFileId: "Copy file ID" @@ -108,16 +109,14 @@ followRequests: "Follow requests" unfollow: "Unfollow" followRequestPending: "Follow request pending" enterEmoji: "Enter an emoji" -renote: "Boost" -unrenote: "Remove boost" -renoted: "Boosted." -quoted: "Quoted." -rmboost: "Unboosted." -renotedToX: "Boosted to {name}" -cantRenote: "This post can't be boosted." -cantReRenote: "A boost can't be boosted." +renote: "Renote" +unrenote: "Remove renote" +renoted: "Renoted." +renotedToX: "Renoted to {name}." +cantRenote: "This post can't be renoted." +cantReRenote: "A renote can't be renoted." quote: "Quote" -inChannelRenote: "Channel-only Boost" +inChannelRenote: "Channel-only Renote" inChannelQuote: "Channel-only Quote" renoteToChannel: "Renote to channel" renoteToOtherChannel: "Renote to other channel" @@ -144,19 +143,15 @@ unmarkAsSensitive: "Unmark as sensitive" enterFileName: "Enter filename" mute: "Mute" unmute: "Unmute" -renoteMute: "Mute Boosts" -renoteUnmute: "Unmute Boosts" +renoteMute: "Mute Renotes" +renoteUnmute: "Unmute Renotes" block: "Block" unblock: "Unblock" -markAsNSFW: "Mark all media from user as NSFW" suspend: "Suspend" unsuspend: "Unsuspend" blockConfirm: "Are you sure that you want to block this account?" unblockConfirm: "Are you sure that you want to unblock this account?" -nsfwConfirm: "Are you sure that you want to mark all media from this account as NSFW?" -unNsfwConfirm: "Are you sure that you want to unmark all media from this account as NSFW?" suspendConfirm: "Are you sure that you want to suspend this account?" -approveConfirm: "Are you sure that you want to approve this account?" unsuspendConfirm: "Are you sure that you want to unsuspend this account?" selectList: "Select a list" editList: "Edit list" @@ -180,11 +175,9 @@ youCanCleanRemoteFilesCache: "You can clear the cache by clicking the ðŸ—‘ï¸ bu cacheRemoteSensitiveFiles: "Cache sensitive remote files" cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching." flagAsBot: "Mark this account as a bot" -flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Sharkey's internal systems to treat this account as a bot." +flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot." flagAsCat: "Mark this account as a cat" flagAsCatDescription: "Enable this option to mark this account as a cat." -flagSpeakAsCat: "Speak as a cat" -flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode." flagShowTimelineReplies: "Show replies in timeline" flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on." autoAcceptFollowed: "Automatically approve follow requests from users you're following" @@ -192,8 +185,8 @@ addAccount: "Add account" reloadAccountsList: "Reload account list" loginFailed: "Failed to sign in" showOnRemote: "View on remote instance" -continueOnRemote: "Continue on remote instance" -chooseServerOnMisskeyHub: "Choose a instance from Misskey Hub" +continueOnRemote: "Continue on a remote server" +chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub" specifyServerHost: "Specify a server host directly" inputHostName: "Enter the domain" general: "General" @@ -222,7 +215,7 @@ perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" silenceThisInstance: "Silence this instance" -mediaSilenceThisInstance: "Silence media from this instance" +mediaSilenceThisInstance: "Media-silence this server" operations: "Operations" software: "Software" version: "Version" @@ -243,9 +236,11 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote blockedInstances: "Blocked Instances" blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance." silencedInstances: "Silenced instances" -silencedInstancesDescription: "List the host names of the instances that you want to silence, separated by a new line. All accounts belonging to the listed instances will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances." -mediaSilencedInstances: "Media-silenced instances" -mediaSilencedInstancesDescription: "List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances." +silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked servers." +mediaSilencedInstances: "Media-silenced servers" +mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers." +federationAllowedHosts: "Federation allowed servers" +federationAllowedHostsDescription: "Specify the hostnames of the servers you want to allow federation separated by line breaks." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" @@ -253,7 +248,7 @@ noUsers: "There are no users" editProfile: "Edit profile" noteDeleteConfirm: "Are you sure you want to delete this note?" pinLimitExceeded: "You cannot pin any more notes" -intro: "Installation of Sharkey has been finished! Please create an admin user." +intro: "Installation of Misskey has been finished! Please create an admin user." done: "Done" processing: "Processing..." preview: "Preview" @@ -331,13 +326,12 @@ lightThemes: "Light themes" darkThemes: "Dark themes" syncDeviceDarkMode: "Sync Dark Mode with your device settings" drive: "Drive" -driveSearchbarPlaceholder: "Search drive" fileName: "Filename" selectFile: "Select a file" selectFiles: "Select files" selectFolder: "Select a folder" selectFolders: "Select folders" -fileNotSelected: "No file selected" +fileNotSelected: "" renameFile: "Rename file" folderName: "Folder name" createFolder: "Create a folder" @@ -345,6 +339,7 @@ renameFolder: "Rename this folder" deleteFolder: "Delete this folder" folder: "Folder" addFile: "Add a file" +showFile: "Show files" emptyDrive: "Your Drive is empty" emptyFolder: "This folder is empty" unableToDelete: "Unable to delete" @@ -357,7 +352,6 @@ copyUrl: "Copy URL" rename: "Rename" avatar: "Avatar" banner: "Banner" -background: "Background" displayOfSensitiveMedia: "Display of sensitive media" whenServerDisconnected: "When losing connection to the server" disconnectedFromServer: "Connection to server has been lost" @@ -431,7 +425,7 @@ antennaKeywordsDescription: "Separate with spaces for an AND condition or with l notifyAntenna: "Notify about new notes" withFileAntenna: "Only notes with files" enableServiceworker: "Enable Push-Notifications for your Browser" -antennaUsersDescription: "List one username per line. Use \"*@instance.com\" to specify all users of an instance" +antennaUsersDescription: "List one username per line" caseSensitive: "Case sensitive" withReplies: "Include replies" connectedTo: "Following account(s) are connected" @@ -450,7 +444,7 @@ exploreFediverse: "Explore the Fediverse" popularTags: "Popular tags" userList: "Lists" about: "About" -aboutMisskey: "About Sharkey" +aboutMisskey: "About Misskey" administrator: "Administrator" token: "Token" 2fa: "Two-factor authentication" @@ -460,6 +454,7 @@ totpDescription: "Use an authenticator app to enter one-time passwords" moderator: "Moderator" moderation: "Moderation" moderationNote: "Moderation note" +moderationNoteDescription: "You can fill in notes that will be shared only among moderators." addModerationNote: "Add moderation note" moderationLogs: "Moderation logs" nUsersMentioned: "Mentioned by {n} users" @@ -492,16 +487,14 @@ enable: "Enable" next: "Next" retype: "Enter again" noteOf: "Note by {user}" -expandAllCws: "Show content for all replies" -collapseAllCws: "Hide content for all replies" quoteAttached: "Quote" quoteQuestion: "Append as quote?" -attachAsFileQuestion: "The text in clipboard is long. Would you like to attach it as a text file?" +attachAsFileQuestion: "The text in clipboard is long. Would you want to attach it as text file?" noMessagesYet: "No messages yet" newMessageExists: "There are new messages" onlyOneFileCanBeAttached: "You can only attach one file to a message" signinRequired: "Please register or sign in before continuing" -signinOrContinueOnRemote: "To continue, you need to go to your instance to perform this action or sign up / log in to the instance you are trying to interact with." +signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server." invitations: "Invites" invitationCode: "Invitation code" checking: "Checking..." @@ -523,9 +516,12 @@ uiLanguage: "User interface language" aboutX: "About {x}" emojiStyle: "Emoji style" native: "Native" -disableDrawer: "Don't use drawer-style menus" +menuStyle: "Menu style" +style: "Style" +drawer: "Drawer" +popup: "Pop up" showNoteActionsOnlyHover: "Only show note actions on hover" -showReactionsCount: "Show the number of reactions in notes" +showReactionsCount: "See the number of reactions in notes" noHistory: "No history available" signinHistory: "Login history" enableAdvancedMfm: "Enable advanced MFM" @@ -538,12 +534,10 @@ createAccount: "Create account" existingAccount: "Existing account" regenerate: "Regenerate" fontSize: "Font size" -cornerRadius: "Corner roundness" mediaListWithOneImageAppearance: "Height of media lists with one image only" limitTo: "Limit to {x}" noFollowRequests: "You don't have any pending follow requests" openImageInNewTab: "Open images in new tab" -warnForMissingAltText: "Warn you when you forget to put alt text" dashboard: "Dashboard" local: "Local" remote: "Remote" @@ -576,8 +570,6 @@ objectStorageUseProxy: "Connect over Proxy" objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections" objectStorageSetPublicRead: "Set \"public-read\" on upload" s3ForcePathStyleDesc: "If s3ForcePathStyle is enabled, the bucket name has to included in the path of the URL as opposed to the hostname of the URL. You may need to enable this setting when using services such as a self-hosted Minio instance." -deeplFreeMode: "Use DeepLX-JS (No Auth Key)" -deeplFreeModeDescription: "Need Help? Check our documentation to know how to setup DeepLX-JS." serverLogs: "Server logs" deleteAll: "Delete all" showFixedPostForm: "Display the posting form at the top of the timeline" @@ -593,7 +585,7 @@ popout: "Pop-out" volume: "Volume" masterVolume: "Master volume" notUseSound: "Disable sound" -useSoundOnlyWhenActive: "Output sounds only if Sharkey is active." +useSoundOnlyWhenActive: "Output sounds only if Misskey is active." details: "Details" chooseEmoji: "Select an emoji" unableToProcess: "The operation could not be completed" @@ -609,7 +601,9 @@ sort: "Sorting order" ascendingOrder: "Ascending" descendingOrder: "Descending" scratchpad: "Scratchpad" -scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Sharkey in it." +scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Misskey in it." +uiInspector: "UI inspector" +uiInspectorDescription: "You can see the UI component server list on memory. UI component will be generated by Ui:C: function." output: "Output" script: "Script" disablePagesScript: "Disable AiScript on Pages" @@ -707,11 +701,6 @@ channel: "Channels" create: "Create" notificationSetting: "Notification settings" notificationSettingDesc: "Select the types of notification to display." -enableFaviconNotificationDot: "Enable favicon notification dot" -verifyNotificationDotWorkingButton: "Check if the notification dot works on your instance" -notificationDotNotWorking: "Unfortunately, this instance does not support the notification dot feature at this time." -notificationDotWorking: "The notification dot is functioning properly on this instance." -notificationDotNotWorkingAdvice: "If the notification dot doesn't work, ask an admin to check our documentation {link}" useGlobalSetting: "Use global settings" useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made." other: "Other" @@ -724,17 +713,14 @@ behavior: "Behavior" sample: "Sample" abuseReports: "Reports" reportAbuse: "Report" -reportAbuseRenote: "Report boost" +reportAbuseRenote: "Report renote" reportAbuseOf: "Report {name}" fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL." abuseReported: "Your report has been sent. Thank you very much." reporter: "Reporter" reporteeOrigin: "Reportee Origin" reporterOrigin: "Reporter Origin" -forwardReport: "Forward report to remote instance" -forwardReportIsAnonymous: "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance." send: "Send" -abuseMarkAsResolved: "Mark report as resolved" openInNewTab: "Open in new tab" openInSideView: "Open in side view" defaultNavigationBehaviour: "Default navigation behavior" @@ -753,14 +739,14 @@ unclip: "Unclip" confirmToUnclipAlreadyClippedNote: "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?" public: "Public" private: "Private" -i18nInfo: "Sharkey is being translated into various languages by volunteers. You can help at {link}." +i18nInfo: "Misskey is being translated into various languages by volunteers. You can help at {link}." manageAccessTokens: "Manage access tokens" accountInfo: "Account Info" notesCount: "Number of notes" repliesCount: "Number of replies sent" -renotesCount: "Number of boosts sent" +renotesCount: "Number of renotes sent" repliedCount: "Number of replies received" -renotedCount: "Number of boosts received" +renotedCount: "Number of renotes received" followingCount: "Number of followed accounts" followersCount: "Number of followers" sentReactionsCount: "Number of sent reactions" @@ -776,11 +762,6 @@ noCrawleDescription: "Ask search engines to not index your profile page, notes, lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved." alwaysMarkSensitive: "Mark as sensitive by default" loadRawImages: "Load original images instead of showing thumbnails" -showTickerOnReplies: "Show instance ticker on replies" -searchEngine: "Search Engine For Search MFM" -searchEngineOther: "Other" -searchEngineCustomURIDescription: "The custom URI must be input in the format like \"https://www.google.com/search?q=\\{query}\" or \"https://www.google.com/search?q=%s\"." -searchEngineCusomURI: "Custom URI" disableShowingAnimatedImages: "Don't play animated images" highlightSensitiveMedia: "Highlight sensitive media" verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." @@ -798,8 +779,6 @@ thisIsExperimentalFeature: "This is an experimental feature. Its functionality i developer: "Developer" makeExplorable: "Make account visible in \"Explore\"" makeExplorableDescription: "If you turn this off, your account will not show up in the \"Explore\" section." -makeIndexable: "Make public notes not indexable" -makeIndexableDescription: "Stop note search from indexing your public notes." showGapBetweenNotesInTimeline: "Show a gap between posts on the timeline" duplicate: "Duplicate" left: "Left" @@ -814,7 +793,7 @@ onlineUsersCount: "{n} users are online" nUsers: "{n} Users" nNotes: "{n} Notes" sendErrorReports: "Send error reports" -sendErrorReportsDescription: "When turned on, detailed error information will be shared with Sharkey when a problem occurs, helping to improve the quality of Sharkey.\nThis will include information such the version of your OS, what browser you're using, your activity in Sharkey, etc." +sendErrorReportsDescription: "When turned on, detailed error information will be shared with Misskey when a problem occurs, helping to improve the quality of Misskey.\nThis will include information such the version of your OS, what browser you're using, your activity in Misskey, etc." myTheme: "My theme" backgroundColor: "Background color" accentColor: "Accent color" @@ -878,7 +857,7 @@ administration: "Management" accounts: "Accounts" switch: "Switch" noMaintainerInformationWarning: "Maintainer information is not configured." -noInquiryUrlWarning: "Contact URL is not set." +noInquiryUrlWarning: "Inquiry URL isn’t set" noBotProtectionWarning: "Bot protection is not configured." configure: "Configure" postToGallery: "Create new gallery post" @@ -909,12 +888,12 @@ hashtags: "Hashtags" troubleshooting: "Troubleshooting" useBlurEffect: "Use blur effects in the UI" learnMore: "Learn more" -misskeyUpdated: "Sharkey has been updated!" +misskeyUpdated: "Misskey has been updated!" whatIsNew: "Show changes" translate: "Translate" translatedFrom: "Translated from {x}" accountDeletionInProgress: "Account deletion is currently in progress" -usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later." +usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later." aiChanMode: "Ai Mode" devMode: "Developer mode" keepCw: "Keep content warnings" @@ -929,7 +908,6 @@ itsOff: "Disabled" on: "On" off: "Off" emailRequiredForSignup: "Require email address for sign-up" -approvalRequiredForSignup: "Require approval for new users" unread: "Unread" filter: "Filter" controlPanel: "Control Panel" @@ -944,8 +922,8 @@ followersVisibility: "Visibility of followers" continueThread: "View thread continuation" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" incorrectPassword: "Incorrect password." +incorrectTotp: "The one-time password is incorrect or has expired." voteConfirm: "Confirm your vote for \"{choice}\"?" -voteConfirmMulti: "Confirm your vote for \"{choice}\"?\n You can choose more options after confirmation." hide: "Hide" useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile" welcomeBackWithName: "Welcome back, {name}" @@ -981,7 +959,6 @@ recentNHours: "Last {n} hours" recentNDays: "Last {n} days" noEmailServerWarning: "Email server not configured." thereIsUnresolvedAbuseReportWarning: "There are unsolved reports." -pendingUserApprovals: "There are users awaiting approval." recommended: "Recommended" check: "Check" driveCapOverrideLabel: "Change the drive capacity for this user" @@ -990,20 +967,9 @@ requireAdminForView: "You must log in with an administrator account to view this isSystemAccount: "An account created and automatically operated by the system." typeToConfirm: "Please enter {x} to confirm" deleteAccount: "Delete account" -approveAccount: "Approve" -denyAccount: "Deny & Delete" -approved: "Approved" -notApproved: "Not Approved" -approvalStatus: "Approval Status" document: "Documentation" numberOfPageCache: "Number of cached pages" numberOfPageCacheDescription: "Increasing this number will improve convenience for but cause more load as more memory usage on the user's device." -numberOfReplies: "Number of replies in a thread" -numberOfRepliesDescription: "Increasing this number will display more replies. Setting this too high can cause replies to be cramped and unreadable." -boostSettings: "Boost Settings" -showVisibilitySelectorOnBoost: "Show Visibility Selector" -showVisibilitySelectorOnBoostDescription: "Shows the visiblity selector if enabled when clicking boost, if disabled it will use the default visiblity defined below and the selector will not show up." -visibilityOnBoost: "Default boost visibility" logoutConfirm: "Really log out?" lastActiveDate: "Last used at" statusbar: "Status bar" @@ -1048,14 +1014,12 @@ cannotLoad: "Unable to load" numberOfProfileView: "Profile views" like: "Like" unlike: "Unlike" -defaultLike: "Default like emoji" numberOfLikes: "Likes" show: "Show" neverShow: "Don't show again" remindMeLater: "Maybe later" -didYouLikeMisskey: "Have you taken a liking to Sharkey?" -pleaseDonate: "{host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue!" -pleaseDonateInstance: "You can also support {host} directly by donating to your instance administration." +didYouLikeMisskey: "Have you taken a liking to Misskey?" +pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!" correspondingSourceIsAvailable: "The corresponding source code is available at {anchor}" roles: "Roles" role: "Role" @@ -1083,16 +1047,8 @@ thisPostMayBeAnnoying: "This note may annoy others." thisPostMayBeAnnoyingHome: "Post to home timeline" thisPostMayBeAnnoyingCancel: "Cancel" thisPostMayBeAnnoyingIgnore: "Post anyway" -thisPostIsMissingAltTextCancel: "Cancel" -thisPostIsMissingAltTextIgnore: "Post anyway" -thisPostIsMissingAltText: "One of the files attached to this post is missing alt text. Please ensure all the attachments have alt text." -collapseRenotes: "Collapse boosts you've already seen" -collapseRenotesDescription: "Collapse boosts that you have boosted or reacted to" -collapseNotesRepliedTo: "Collapse notes replied to" -collapseFiles: "Collapse files" -uncollapseCW: "Uncollapse CWs on notes" -expandLongNote: "Always expand long notes" -autoloadConversation: "Load conversation on replies" +collapseRenotes: "Collapse renotes you've already seen" +collapseRenotesDescription: "Collapse notes that you've reacted to or renoted before." internalServerError: "Internal Server Error" internalServerErrorDescription: "The server has run into an unexpected error." copyErrorInfo: "Copy error details" @@ -1103,7 +1059,6 @@ disableFederationConfirm: "Really disable federation?" disableFederationConfirmWarn: "Even if defederated, posts will continue to be public unless set otherwise. You usually do not need to do this." disableFederationOk: "Disable" invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up." -approvalRequiredToRegister: "This instance is only accepting users who specify a reason for registration." emailNotSupported: "This instance does not support sending emails" postToTheChannel: "Post to channel" cannotBeChangedLater: "This cannot be changed later." @@ -1134,7 +1089,7 @@ enableChartsForRemoteUser: "Generate remote user data charts" enableChartsForFederatedInstances: "Generate remote instance data charts" showClipButtonInNoteFooter: "Add \"Clip\" to note action menu" reactionsDisplaySize: "Reaction display size" -limitWidthOfReaction: "Limits the maximum width of reactions and display them in reduced size." +limitWidthOfReaction: "Limit the maximum width of reactions and display them in reduced size." noteIdOrUrl: "Note ID or URL" video: "Video" videos: "Videos" @@ -1146,11 +1101,10 @@ accountMoved: "This user has moved to a new account:" accountMovedShort: "This account has been migrated." operationForbidden: "Operation forbidden" forceShowAds: "Always show ads" -oneko: "Cat friend :3" addMemo: "Add memo" editMemo: "Edit memo" reactionsList: "Reactions" -renotesList: "Boosts" +renotesList: "Renotes" notificationDisplay: "Notifications" leftTop: "Top left" rightTop: "Top right" @@ -1180,9 +1134,9 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)" preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored." options: "Options" specifyUser: "Specific user" -lookupConfirm: "Are you sure that you want to look this up?" -openTagPageConfirm: "Are you sure you want to open this hashtags page?" -specifyHost: "Specify a host" +lookupConfirm: "Do you want to look up?" +openTagPageConfirm: "Do you want to open a hashtag page?" +specifyHost: "Specific host" failedToPreviewUrl: "Could not preview" update: "Update" rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" @@ -1191,15 +1145,11 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "These roles must be public cancelReactionConfirm: "Really delete your reaction?" changeReactionConfirm: "Really change your reaction?" later: "Later" -goToMisskey: "To Sharkey" +goToMisskey: "To Misskey" additionalEmojiDictionary: "Additional emoji dictionaries" installed: "Installed" branding: "Branding" enableServerMachineStats: "Publish server hardware stats" -enableAchievements: "Enable Achievements" -turnOffAchievements: "Turning this off will disable the achievement system" -enableBotTrending: "Populate Hashtags with Bots" -turnOffBotTrending: "Turning this off will stop Bots from populating Hashtags" enableIdenticonGeneration: "Enable user identicon generation" turnOffToImprovePerformance: "Turning this off can increase performance." createInviteCode: "Generate invite" @@ -1229,21 +1179,19 @@ currentAnnouncements: "Current announcements" pastAnnouncements: "Past announcements" youHaveUnreadAnnouncements: "There are unread announcements." useSecurityKey: "Please follow your browser's or device's instructions to use your security- or passkey." -replies: "Replies" -renotes: "Boosts" +replies: "Reply" +renotes: "Renotes" loadReplies: "Show replies" loadConversation: "Show conversation" pinnedList: "Pinned list" keepScreenOn: "Keep screen on" -clickToOpen: "Click to open notes" -showBots: "Show bots in timeline" verifiedLink: "Link ownership has been verified" notifyNotes: "Notify about new notes" unnotifyNotes: "Stop notifying about new notes" authentication: "Authentication" authenticationRequiredToContinue: "Please authenticate to continue" dateAndTime: "Timestamp" -showRenotes: "Show boosts" +showRenotes: "Show renotes" edited: "Edited" notificationRecieveConfig: "Notification Settings" mutualFollow: "Mutual follow" @@ -1257,9 +1205,9 @@ confirmShowRepliesAll: "This operation is irreversible. Would you really like to confirmHideRepliesAll: "This operation is irreversible. Would you really like to hide replies to others from everyone you follow in your timeline?" externalServices: "External Services" sourceCode: "Source code" -sourceCodeIsNotYetProvided: "The source code is not yet available. Please contact your administrator to fix this problem." +sourceCodeIsNotYetProvided: "Source code is not yet available. Contact the administrator to fix this problem." repositoryUrl: "Repository URL" -repositoryUrlDescription: "If there is a repository where the source code is publicly available, enter its URL. If you are using Sharkey as-is (without any changes to the source code), enter https://activitypub.software/TransFem-org/Sharkey/." +repositoryUrlDescription: "If you are using Misskey as is (without any changes to the source code), enter https://github.com/misskey-dev/misskey" repositoryUrlOrTarballRequired: "If you have not published a repository, you must provide a tarball instead. See .config/example.yml for more information." feedback: "Feedback" feedbackUrl: "Feedback URL" @@ -1269,8 +1217,6 @@ impressumDescription: "In some countries, like germany, the inclusion of operato privacyPolicy: "Privacy Policy" privacyPolicyUrl: "Privacy Policy URL" tosAndPrivacyPolicy: "Terms of Service and Privacy Policy" -donation: "Donate" -donationUrl: "Donation URL" avatarDecorations: "Avatar decorations" attach: "Attach" detach: "Remove" @@ -1290,7 +1236,7 @@ code: "Code" reloadRequiredToApplySettings: "Reloading is required to apply the settings." remainingN: "Remaining: {n}" overwriteContentConfirm: "Are you sure you want to overwrite the current content?" -seasonalScreenEffect: "Seasonal screen effects" +seasonalScreenEffect: "Seasonal Screen Effect" decorate: "Decorate" addMfmFunction: "Add MFM" enableQuickAddMfmFunction: "Show advanced MFM picker" @@ -1319,18 +1265,39 @@ launchApp: "Launch the app" useNativeUIForVideoAudioPlayer: "Use UI of browser when play video and audio" keepOriginalFilename: "Keep original file name" keepOriginalFilenameDescription: "If you turn off this setting, files names will be replaced with random string automatically when you upload files." -noDescription: "No description" +noDescription: "There is no explanation" alwaysConfirmFollow: "Always confirm when following" inquiry: "Contact" tryAgain: "Please try again later" confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media" -sensitiveMediaRevealConfirm: "This media might be sensitive. Are you sure you want to reveal it?" +sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?" createdLists: "Created lists" createdAntennas: "Created antennas" +fromX: "From {x}" +genEmbedCode: "Generate embed code" +noteOfThisUser: "Notes by this user" +clipNoteLimitExceeded: "No more notes can be added to this clip." +performance: "Performance" +modified: "Modified" +discard: "Discard" +thereAreNChanges: "There are {n} change(s)" +signinWithPasskey: "Sign in with Passkey" +unknownWebAuthnKey: "Unknown Passkey" +passkeyVerificationFailed: "Passkey verification has failed." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled." +messageToFollower: "Message to followers" +target: "Target" +_abuseUserReport: + forward: "Forward" + forwardDescription: "Forward the report to a remote server as an anonymous system account." + resolve: "Resolve" + accept: "Accept" + reject: "Reject" + resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative." _delivery: status: "Delivery status" - stop: "Suspend delivery" - resume: "Resume delivery" + stop: "Suspended" + resume: "Delivery resume" _type: none: "Publishing" manuallySuspended: "Manually suspended" @@ -1376,7 +1343,7 @@ _initialAccountSetting: pushNotificationDescription: "Enabling push notifications will allow you to receive notifications from {name} directly on your device." initialAccountSettingCompleted: "Profile setup complete!" haveFun: "Enjoy {name}!" - youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Sharkey) or you can exit the setup here and start using it immediately." + youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Misskey) or you can exit the setup here and start using it immediately." startTutorial: "Start Tutorial" skipAreYouSure: "Really skip profile setup?" laterAreYouSure: "Really do profile setup later?" @@ -1387,10 +1354,10 @@ _initialTutorial: skipAreYouSure: "Quit Tutorial?" _landing: title: "Welcome to the Tutorial" - description: "Here, you can learn the basics of using Sharkey and its features." + description: "Here, you can learn the basics of using Misskey and its features." _note: title: "What is a Note?" - description: "Posts on Sharkey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time." + description: "Posts on Misskey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time." reply: "Click on this button to reply to a message. It's also possible to reply to replies, continuing the conversation like a thread." renote: "You can share that note to your own timeline. You can also quote them with your comments." reaction: "You can add reactions to the Note. More details will be explained on the next page." @@ -1398,13 +1365,13 @@ _initialTutorial: _reaction: title: "What are Reactions?" description: "Notes can be reacted to with various emojis. Reactions allow you to express nuances that may not be conveyed with just a 'like.'" - letsTryReacting: "Reactions can be added by clicking the '{reaction}' button on the note. Try reacting to this sample note!" + letsTryReacting: "Reactions can be added by clicking the '+' button on the note. Try reacting to this sample note!" reactToContinue: "Add a reaction to proceed." reactNotification: "You'll receive real-time notifications when someone reacts to your note." - reactDone: "You can undo a reaction by pressing the '{undo}' button." + reactDone: "You can undo a reaction by pressing the '-' button." _timeline: title: "The Concept of Timelines" - description1: "Sharkey provides multiple timelines based on usage (some may not be available depending on the server's policies)." + description1: "Misskey provides multiple timelines based on usage (some may not be available depending on the server's policies)." home: "You can view notes from accounts you follow." local: "You can view notes from all users on this server." social: "Notes from the Home and Local timelines will be displayed." @@ -1413,12 +1380,12 @@ _initialTutorial: description3: "Additionally, there are list timelines and channel timelines. For more details, please refer to {link}." _postNote: title: "Note Posting Settings" - description1: "When posting a note on Sharkey, various options are available. The posting form looks like this." + description1: "When posting a note on Misskey, various options are available. The posting form looks like this." _visibility: description: "You can limit who can view your note." public: "Your note will be visible for all users." - home: "Public only on the Home timeline. People visiting your profile, via followers, and through boosts can see it." - followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be boosted by others." + home: "Public only on the Home timeline. People visiting your profile, via followers, and through renotes can see it." + followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be renoted by others." direct: "Visible only to specified users, and the recipient will be notified. It can be used as an alternative to direct messaging." doNotSendConfidencialOnDirect1: "Be careful when sending sensitive information!" doNotSendConfidencialOnDirect2: "Administrators of the server can see what you write. Be careful with sensitive information when sending direct notes to users on untrusted servers." @@ -1440,8 +1407,8 @@ _initialTutorial: sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines." doItToContinue: "Mark the attachment file as sensitive to proceed." _done: - title: "The tutorial is complete! 🎉" - description: "The functions introduced here are just a small part. For a more detailed understanding of using Sharkey, please refer to {link}." + title: "You've completed the tutorial! 🎉" + description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}." _timelineDescription: home: "In the Home timeline, you can see notes from accounts you follow." local: "In the Local timeline, you can see notes from all users on this server." @@ -1461,8 +1428,9 @@ _serverSettings: fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability." fanoutTimelineDbFallback: "Fallback to database" fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved." - inquiryUrl: "Contact URL" - inquiryUrlDescription: "Specify the URL of a web page that contains a contact form or the instance operators' contact information." + reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase." + inquiryUrl: "Inquiry URL" + inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." _accountMigration: moveFrom: "Migrate another account to this one" moveFromSub: "Create alias to another account" @@ -1471,7 +1439,7 @@ _accountMigration: moveTo: "Migrate this account to a different one" moveToLabel: "Account to move to:" moveCannotBeUndone: "Account migration cannot be undone." - moveAccountDescription: "This will migrate your account to a different one.\n ・Followers from this account will automatically be migrated to the new account\n ・This account will unfollow all users it is currently following\n ・You will be unable to create new notes etc. on this account\n\nWhile migration of followers is automatic, you must manually prepare some steps to migrate the list of users you are following. To do so, carry out a follows export that you will later import on the new account in the settings menu. The same procedure applies to your lists as well as your muted and blocked users.\n\n(This explanation applies to Sharkey v13.12.0 and later. Other ActivityPub software, such as Mastodon, might function differently.)" + moveAccountDescription: "This will migrate your account to a different one.\n ・Followers from this account will automatically be migrated to the new account\n ・This account will unfollow all users it is currently following\n ・You will be unable to create new notes etc. on this account\n\nWhile migration of followers is automatic, you must manually prepare some steps to migrate the list of users you are following. To do so, carry out a follows export that you will later import on the new account in the settings menu. The same procedure applies to your lists as well as your muted and blocked users.\n\n(This explanation applies to Misskey v13.12.0 and later. Other ActivityPub software, such as Mastodon, might function differently.)" moveAccountHowTo: "To migrate, first create an alias for this account on the account to move to.\nAfter you have created the alias, enter the account to move to in the following format: @username@server.example.com" startMigration: "Migrate" migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore." @@ -1482,9 +1450,9 @@ _achievements: earnedAt: "Unlocked at" _types: _notes1: - title: "just setting up my shonk" + title: "just setting up my msky" description: "Post your first note" - flavor: "Have a good time with Sharkey!" + flavor: "Have a good time with Misskey!" _notes10: title: "Some notes" description: "Post 10 notes" @@ -1580,7 +1548,7 @@ _achievements: _login1000: title: "Master of Notes III" description: "Log in for a total of 1,000 days" - flavor: "Thank you for using Sharkey!" + flavor: "Thank you for using Misskey!" _noteClipped1: title: "Must... clip..." description: "Clip your first note" @@ -1640,18 +1608,18 @@ _achievements: title: "Likes Achievements" description: "Look at your list of achievements for at least 3 minutes" _iLoveMisskey: - title: "I Love Sharkey" - description: "Post \"I ⤠#Sharkey\"" - flavor: "Sharkey's development team greatly appreciates your support!" + title: "I Love Misskey" + description: "Post \"I ⤠#Misskey\"" + flavor: "Misskey's development team greatly appreciates your support!" _foundTreasure: title: "Treasure Hunt" description: "You've found the hidden treasure" _client30min: title: "Short break" - description: "Keep Sharkey opened for at least 30 minutes" + description: "Keep Misskey opened for at least 30 minutes" _client60min: - title: "No \"Miss\" in Sharkey" - description: "Keep Sharkey opened for at least 60 minutes" + title: "No \"Miss\" in Misskey" + description: "Keep Misskey opened for at least 60 minutes" _noteDeletedWithin1min: title: "Nevermind" description: "Delete a note within a minute of posting it" @@ -1716,12 +1684,12 @@ _achievements: _brainDiver: title: "Brain Diver" description: "Post the link to Brain Diver" - flavor: "Sharkey-Sharkey La-Tu-Ma" + flavor: "Misskey-Misskey La-Tu-Ma" _smashTestNotificationButton: title: "Test overflow" description: "Trigger the notification test repeatedly within an extremely short time" _tutorialCompleted: - title: "Sharkey Elementary Course Diploma" + title: "Misskey Elementary Course Diploma" description: "Tutorial completed" _bubbleGameExplodingHead: title: "🤯" @@ -1768,10 +1736,8 @@ _role: high: "High" _options: gtlAvailable: "Can view the global timeline" - btlAvailable: "Can view the bubble timeline" ltlAvailable: "Can view the local timeline" canPublicNote: "Can send public notes" - canImportNotes: "Can import notes" mentionMax: "Maximum number of mentions in a note" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" @@ -1781,7 +1747,7 @@ _role: canManageAvatarDecorations: "Manage avatar decorations" driveCapacity: "Drive capacity" alwaysMarkNsfw: "Always mark files as NSFW" - canUpdateBioMedia: "Allow users to edit their avatar or banner" + canUpdateBioMedia: "Can edit an icon or a banner image" pinMax: "Maximum number of pinned notes" antennaMax: "Maximum number of antennas" wordMuteMax: "Maximum number of characters allowed in word mutes" @@ -1796,6 +1762,11 @@ _role: canSearchNotes: "Usage of note search" canUseTranslator: "Translator usage" avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" + canImportAntennas: "Allow importing antennas" + canImportBlocking: "Allow importing blocking" + canImportFollowing: "Allow importing following" + canImportMuting: "Allow importing muting" + canImportUserLists: "Allow importing lists" _condition: roleAssignedTo: "Assigned to manual roles" isLocal: "Local user" @@ -1803,8 +1774,8 @@ _role: isCat: "Cat Users" isBot: "Bot Users" isSuspended: "Suspended user" - isLocked: "Private account" - isExplorable: "Account is discoverable" + isLocked: "Private accounts" + isExplorable: "Effective user of \"make an account discoverable\"" createdLessThan: "Less than X has passed since account creation" createdMoreThan: "More than X has passed since account creation" followersLessThanOrEq: "Has X or fewer followers" @@ -1830,7 +1801,7 @@ _emailUnavailable: disposable: "Disposable email addresses may not be used" mx: "This email server is invalid" smtp: "This email server is not responding" - banned: "This email address is banned" + banned: "You cannot register with this email address" _ffVisibility: public: "Public" followers: "Visible to followers only" @@ -1839,8 +1810,6 @@ _signup: almostThere: "Almost there" emailAddressInfo: "Please enter your email address. It will not be made public." emailSent: "A confirmation email has been sent to your email address ({email}). Please click the included link to complete account creation." - approvalPending: "Your account has been created and is awaiting approval." - reasonInfo: "Please enter a reason as to why you want to join the instance." _accountDelete: accountDelete: "Delete account" mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded." @@ -1902,20 +1871,17 @@ _registry: domain: "Domain" createKey: "Create key" _aboutMisskey: - about: "Sharkey is open-source software based on Misskey which has been in development by syuilo since 2014." + about: "Misskey is open-source software being developed by syuilo since 2014." contributors: "Main contributors" allContributors: "All contributors" source: "Source code" - original: "Misskey original" - original_sharkey: "Sharkey original" - thisIsModifiedVersion: "{name} uses a modified version of the original Sharkey." - translation: "Translate Sharkey" + original: "Original" + thisIsModifiedVersion: "{name} uses a modified version of the original Misskey." + translation: "Translate Misskey" donate: "Donate to Misskey" - donate_sharkey: "Donate to Sharkey" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰" patrons: "Patrons" projectMembers: "Project members" - testers: "Testers" _displayOfSensitiveMedia: respect: "Hide media marked as sensitive" ignore: "Display media marked as sensitive" @@ -1928,7 +1894,6 @@ _serverDisconnectedBehavior: reload: "Automatically reload" dialog: "Show warning dialog" quiet: "Show unobtrusive warning" - disabled: "Disable warning" _channel: create: "Create channel" edit: "Edit channel" @@ -1941,7 +1906,7 @@ _channel: notesCount: "{n} Notes" nameAndDescription: "Name and description" nameOnly: "Name only" - allowRenoteToExternal: "Allow boosts and quote outside the channel" + allowRenoteToExternal: "Allow renote and quote outside the channel" _menuDisplay: sideFull: "Side" sideIcon: "Side (Icons)" @@ -1952,7 +1917,7 @@ _wordMute: muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Surround keywords with slashes to use regular expressions." _instanceMute: - instanceMuteDescription: "This will mute any notes/boosts from the listed instances, including those of users replying to a user from a muted instance." + instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance." instanceMuteDescription2: "Separate with newlines" title: "Hides notes from listed instances." heading: "List of instances to be muted" @@ -2004,7 +1969,7 @@ _theme: hashtag: "Hashtag" mention: "Mention" mentionMe: "Mentions (Me)" - renote: "Boost" + renote: "Renote" modalBg: "Modal background" divider: "Divider" scrollbarHandle: "Scrollbar handle" @@ -2019,7 +1984,6 @@ _theme: buttonBg: "Button background" buttonHoverBg: "Button background (Hover)" inputBorder: "Input field border" - listItemHoverBg: "List item background (Hover)" driveFolderBg: "Drive folder background" wallpaperOverlay: "Wallpaper overlay" badge: "Badge" @@ -2038,8 +2002,8 @@ _soundSettings: driveFileTypeWarn: "This file is not supported" driveFileTypeWarnDescription: "Select an audio file" driveFileDurationWarn: "The audio is too long." - driveFileDurationWarnDescription: "Long audio may disrupt using Sharkey. Still continue?" - driveFileError: "The audio couldn't be loaded. Please make sure you selected an audio file." + driveFileDurationWarnDescription: "Long audio may disrupt using Misskey. Still continue?" + driveFileError: "It couldn't load the sound. Please change the setting." _ago: future: "Future" justNow: "Just now" @@ -2092,7 +2056,7 @@ _2fa: backupCodesDescription: "You can use these codes to gain access to your account in case of becoming unable to use your two-factor authentificator app. Each can only be used once. Please keep them in a safe place." backupCodeUsedWarning: "A backup code has been used. Please reconfigure two-factor authentification as soon as possible if you are no longer able to use it." backupCodesExhaustedWarning: "All backup codes have been used. Should you lose access to your two-factor authentification app, you will be unable to access this account. Please reconfigure two-factor authentification." - moreDetailedGuideHere: "Click here for a detailed guide" + moreDetailedGuideHere: "Here is detailed guide" _permissions: "read:account": "View your account information" "write:account": "Edit your account information" @@ -2232,7 +2196,6 @@ _widgets: _userList: chooseList: "Select a list" clicker: "Clicker" - search: "Search" birthdayFollowings: "Users who celebrate their birthday today" _cw: hide: "Hide" @@ -2261,7 +2224,6 @@ _poll: remainingHours: "{h} hour(s) {m} minute(s) remaining" remainingMinutes: "{m} minute(s) {s} second(s) remaining" remainingSeconds: "{s} second(s) remaining" - multiple: "Multiple choices" _visibility: public: "Public" publicDescription: "Your note will be visible for all users" @@ -2296,13 +2258,11 @@ _profile: metadataContent: "Content" changeAvatar: "Change avatar" changeBanner: "Change banner" - updateBanner: "Update banner" - removeBanner: "Remove banner" - changeBackground: "Change background" - updateBackground: "Update background" - removeBackground: "Remove background" verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field." avatarDecorationMax: "You can add up to {max} decorations." + followedMessage: "Message when you are followed" + followedMessageDescription: "You can set a short message to be displayed to the recipient when they follow you." + followedMessageDescriptionForLockedAccount: "If you have set up that follow requests require approval, this will be displayed when you grant a follow request." _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" @@ -2345,7 +2305,6 @@ _timelines: local: "Local" social: "Social" global: "Global" - bubble: "Bubble" _play: new: "Create Play" edit: "Edit Play" @@ -2408,7 +2367,7 @@ _pages: image: "Images" button: "Button" dynamic: "Dynamic Blocks" - dynamicDescription: "This block type has been removed. Please use {play} from now on." + dynamicDescription: "This block has been abolished. Please use {play} from now on." note: "Embedded note" _note: id: "Note ID" @@ -2423,7 +2382,7 @@ _notification: youGotMention: "{name} mentioned you" youGotReply: "{name} replied to you" youGotQuote: "{name} quoted you" - youRenoted: "Boost from {name}" + youRenoted: "Renote from {name}" youWereFollowed: "followed you" youReceivedFollowRequest: "You've received a follow request" yourFollowRequestAccepted: "Your follow request was accepted" @@ -2439,17 +2398,18 @@ _notification: notificationWillBeDisplayedLikeThis: "Notifications look like this" reactedBySomeUsers: "{n} users reacted" likedBySomeUsers: "{n} users liked your note" - renotedBySomeUsers: "Boosted by {n} users" + renotedBySomeUsers: "Renote from {n} users" followedBySomeUsers: "Followed by {n} users" flushNotification: "Clear notifications" - edited: "Note got edited" + exportOfXCompleted: "Export of {x} has been completed" + login: "Someone logged in" _types: all: "All" note: "New notes" follow: "New followers" mention: "Mentions" reply: "Replies" - renote: "Boosts" + renote: "Renotes" quote: "Quotes" reaction: "Reactions" pollEnded: "Polls ending" @@ -2457,12 +2417,14 @@ _notification: followRequestAccepted: "Accepted follow requests" roleAssigned: "Role given" achievementEarned: "Achievement unlocked" + exportCompleted: "The export has been completed" + login: "Sign In" + test: "Notification test" app: "Notifications from linked apps" - edited: "Edits" _actions: followBack: "followed you back" reply: "Reply" - renote: "Boost" + renote: "Renote" _deck: alwaysShowMainColumn: "Always show main column" columnAlign: "Align columns" @@ -2516,25 +2478,26 @@ _webhookSettings: followed: "When being followed" note: "When posting a note" reply: "When receiving a reply" - renote: "When boosted" + renote: "When renoted" reaction: "When receiving a reaction" mention: "When being mentioned" _systemEvents: - abuseReport: "When received a new abuse report" - abuseReportResolved: "When resolved abuse reports" + abuseReport: "When received a new report" + abuseReportResolved: "When resolved report" userCreated: "When user is created" deleteConfirm: "Are you sure you want to delete the Webhook?" + testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data." _abuseReport: _notificationRecipient: - createRecipient: "Add a recipient for abuse reports" - modifyRecipient: "Edit a recipient for abuse reports" + createRecipient: "Add a recipient for reports" + modifyRecipient: "Edit a recipient for reports" recipientType: "Notification type" _recipientType: mail: "Email" webhook: "Webhook" _captions: - mail: "Send an email to the moderators when an abuse report is received." - webhook: "Send a notification to the SystemWebhook when an abuse report is received or resolved." + mail: "Send the email to moderators' email addresses when you receive reports." + webhook: "Send a notification to System Webhook when you receive or resolve reports." keywords: "Keywords" notifiedUser: "Users to notify" notifiedWebhook: "Webhook to use" @@ -2545,7 +2508,6 @@ _moderationLogTypes: updateRole: "Role updated" assignRole: "Assigned to role" unassignRole: "Removed from role" - approve: "Approved" suspend: "Suspended" unsuspend: "Unsuspended" addCustomEmoji: "Custom emoji added" @@ -2568,6 +2530,8 @@ _moderationLogTypes: markSensitiveDriveFile: "File marked as sensitive" unmarkSensitiveDriveFile: "File unmarked as sensitive" resolveAbuseReport: "Report resolved" + forwardAbuseReport: "Report forwarded" + updateAbuseReportNote: "Moderation note of a report updated" createInvitation: "Invite generated" createAd: "Ad created" deleteAd: "Ad deleted" @@ -2575,100 +2539,18 @@ _moderationLogTypes: createAvatarDecoration: "Avatar decoration created" updateAvatarDecoration: "Avatar decoration updated" deleteAvatarDecoration: "Avatar decoration deleted" - unsetUserAvatar: "Unset this user's avatar" - unsetUserBanner: "Unset this user's banner" - createSystemWebhook: "Create SystemWebhook" - updateSystemWebhook: "Update SystemWebhook" - deleteSystemWebhook: "Delete SystemWebhook" - createAbuseReportNotificationRecipient: "Create a recipient for abuse reports" - updateAbuseReportNotificationRecipient: "Update recipients for abuse reports" - deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports" - deleteAccount: "Delete the account" - deletePage: "Delete the page" - deleteFlash: "Delete Play" - deleteGalleryPost: "Delete the gallery post" - -_mfm: - uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks" - intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax." - dummy: "Sharkey expands the world of the Fediverse" - mention: "Mention" - mentionDescription: "You can specify a user by using an At-Symbol and a username." - hashtag: "Hashtag" - hashtagDescription: "You can specify a hashtag using a number sign and text." - url: "URL" - urlDescription: "URLs can be displayed." - link: "Link" - linkDescription: "Specific parts of text can be displayed as a URL." - bold: "Bold" - boldDescription: "Highlights letters by making them thicker." - small: "Small" - smallDescription: "Displays content small and thin." - center: "Center" - centerDescription: "Displays content centered." - inlineCode: "Code (Inline)" - inlineCodeDescription: "Displays inline syntax highlighting for (program) code." - blockCode: "Code (Block)" - blockCodeDescription: "Displays syntax highlighting for multi-line (program) code in a block." - inlineMath: "Math (Inline)" - inlineMathDescription: "Display math formulas (KaTeX) in-line" - blockMath: "Math (Block)" - blockMathDescription: "Display math formulas (KaTeX) in a block" - quote: "Quote" - quoteDescription: "Displays content as a quote." - emoji: "Custom Emoji" - emojiDescription: "By surrounding a custom emoji name with colons, custom emoji can be displayed." - search: "Search" - searchDescription: "Displays a search box with pre-entered text." - flip: "Flip" - flipDescription: "Flips content horizontally or vertically." - jelly: "Animation (Jelly)" - jellyDescription: "Gives content a jelly-like animation." - tada: "Animation (Tada)" - tadaDescription: "Gives content a \"Tada!\"-like animation." - jump: "Animation (Jump)" - jumpDescription: "Gives content a jumping animation." - bounce: "Animation (Bounce)" - bounceDescription: "Gives content a bouncy animation." - shake: "Animation (Shake)" - shakeDescription: "Gives content a shaking animation." - twitch: "Animation (Twitch)" - twitchDescription: "Gives content a strongly twitching animation." - spin: "Animation (Spin)" - spinDescription: "Gives content a spinning animation." - x2: "Big" - x2Description: "Displays content bigger." - x3: "Very big" - x3Description: "Displays content even bigger." - x4: "Unbelievably big" - x4Description: "Displays content even bigger than bigger than big." - blur: "Blur" - blurDescription: "Blurs content. It will be displayed clearly when hovered over." - font: "Font" - fontDescription: "Sets the font to display content in." - rainbow: "Rainbow" - rainbowDescription: "Makes the content appear in rainbow colors." - sparkle: "Sparkle" - sparkleDescription: "Gives content a sparkling particle effect." - rotate: "Rotate" - rotateDescription: "Turns content by a specified angle." - position: "Position" - positionDescription: "Move content by a specified amount." - crop: "Crop" - cropDescription: "Crop content." - followMouse: "Follow Mouse" - followMouseDescription: "Content will follow the mouse. On mobile it will follow wherever the user taps." - scale: "Scale" - scaleDescription: "Scale content by a specified amount." - foreground: "Foreground color" - foregroundDescription: "Change the foreground color of text." - fade: 'Fade' - fadeDescription: 'Fade text in and out.' - background: "Background color" - backgroundDescription: "Change the background color of text." - plain: "Plain" - plainDescription: "Deactivates the effects of all MFM contained within this MFM effect." - + unsetUserAvatar: "User avatar unset" + unsetUserBanner: "User banner unset" + createSystemWebhook: "System Webhook created" + updateSystemWebhook: "System Webhook updated" + deleteSystemWebhook: "System Webhook deleted" + createAbuseReportNotificationRecipient: "Recipient for reports created" + updateAbuseReportNotificationRecipient: "Recipient for reports updated" + deleteAbuseReportNotificationRecipient: "Recipient for reports deleted" + deleteAccount: "Account deleted" + deletePage: "Page deleted" + deleteFlash: "Play deleted" + deleteGalleryPost: "Gallery post deleted" _fileViewer: title: "File details" type: "File type" @@ -2718,27 +2600,13 @@ _externalResourceInstaller: _themeInstallFailed: title: "Failed to install theme" description: "A problem occurred during theme installation. Please try again. Error details can be viewed in the Javascript console." - -_animatedMFM: - play: "Play MFM Animation" - stop: "Stop MFM Animation" - _alert: - text: "Animated MFMs could include flashing lights and fast moving text/emojis." - confirm: "Animate" - -_dataRequest: - title: "Request Data" - warn: "Data requests are only possible every 3 days." - text: "Once the data is ready to download, an email will be sent to the email address registered to this account." - button: "Request" - _dataSaver: _media: title: "Loading Media" description: "Prevents images/videos from being loaded automatically. Hidden images/videos will be loaded when tapped." _avatar: title: "Avatar image" - description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." + description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." _urlPreview: title: "URL preview thumbnails" description: "URL preview thumbnail images will no longer be loaded." @@ -2800,16 +2668,16 @@ _urlPreviewSetting: title: "URL preview settings" enable: "Enable URL preview" timeout: "Time out when getting preview (ms)" - timeoutDescription: "If it takes longer than this value to get the preview, the preview won't be generated." + timeoutDescription: "If it takes longer than this value to get the preview, the preview won’t be generated." maximumContentLength: "Maximum Content-Length (bytes)" maximumContentLengthDescription: "If Content-Length is higher than this value, the preview won't be generated." - requireContentLength: "Generate the preview only if we can get Content-Length" + requireContentLength: "Generate the preview only if you could get Content-Length" requireContentLengthDescription: "If other server doesn't return Content-Length, the preview won't be generated." userAgent: "User-Agent" userAgentDescription: "Sets the User-Agent to be used when retrieving previews. If left blank, the default User-Agent will be used." - summaryProxy: "Endpoint for proxy to generate previews" - summaryProxyDescription: "Generate previews using Summaly Proxy, instead of Sharkey itself." - summaryProxyDescription2: "The following parameters are sent to the proxy as a query string. If the proxy does not support them, the values are ignored." + summaryProxy: "Proxy endpoints that generate previews" + summaryProxyDescription: "Not Misskey itself, but generate previews using Summaly Proxy." + summaryProxyDescription2: "The following parameters are linked to the proxy as a query string. If the proxy does not support them, the values are ignored." _mediaControls: pip: "Picture in Picture" playbackRate: "Playback Speed" @@ -2819,3 +2687,17 @@ _contextMenu: app: "Application" appWithShift: "Application with shift key" native: "Native" +_embedCodeGen: + title: "Customize embed code" + header: "Show header" + autoload: "Automatically load more (deprecated)" + maxHeight: "Max height" + maxHeightDescription: "Setting it to 0 disables the max height setting. Specify some value to prevent the widget from continuing to expand vertically." + maxHeightWarn: "The max height limit is disabled (0). If this was not intended, set the max height to some value." + previewIsNotActual: "The display differs from the actual embedding because it exceeds the range displayed on the preview screen." + rounded: "Make it rounded" + border: "Add a border to the outer frame" + applyToPreview: "Apply to the preview" + generateCode: "Generate embed code" + codeGenerated: "The code has been generated" + codeGeneratedDescription: "Paste the generated code into your website to embed the content." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index f5d1d11036916e75bd0f1b946755e74d3925034c..d574999e400bcb98dfcba7b409b5c5938264a8bf 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -109,11 +109,14 @@ enterEmoji: "Ingresar emojis" renote: "Renotar" unrenote: "Quitar renota" renoted: "Renotado" +renotedToX: "{name} usuarios han renotado。" cantRenote: "No se puede renotar este post" cantReRenote: "No se puede renotar una renota" quote: "Citar" inChannelRenote: "Renota sólo del canal" inChannelQuote: "Cita sólo del canal" +renoteToChannel: "Renotar a otro canal" +renoteToOtherChannel: "Renotar a otro canal" pinnedNote: "Nota fijada" pinned: "Fijar al perfil" you: "Tú" @@ -152,6 +155,7 @@ editList: "Editar lista" selectChannel: "Seleccionar canal" selectAntenna: "Seleccionar antena" editAntenna: "Editar antena" +createAntenna: "Crear una antena" selectWidget: "Seleccionar widget" editWidgets: "Editar widgets" editWidgetsExit: "Terminar edición" @@ -178,6 +182,10 @@ addAccount: "Agregar Cuenta" reloadAccountsList: "Recargar lista de cuentas" loginFailed: "Error al iniciar sesión." showOnRemote: "Ver en una instancia remota" +continueOnRemote: "Ver en una instancia remota" +chooseServerOnMisskeyHub: "Elegir un servidor en Misskey Hub" +specifyServerHost: "Especifica una instancia directamente" +inputHostName: "Introduzca el dominio" general: "General" wallpaper: "Fondo de pantalla" setWallpaper: "Establecer fondo de pantalla" @@ -406,7 +414,7 @@ antennaKeywordsDescription: "Separar con espacios es una declaración AND, separ notifyAntenna: "Notificar nueva nota" withFileAntenna: "Sólo notas con archivos adjuntados" enableServiceworker: "Activar ServiceWorker" -antennaUsersDescription: "Elegir nombres de usuarios separados por una linea nueva. Utilice \"*@instance.com\" para especificar todos los usuarios de una instancia." +antennaUsersDescription: "Elegir nombres de usuarios separados por una linea nueva" caseSensitive: "Distinguir mayúsculas de minúsculas" withReplies: "Incluir respuestas" connectedTo: "Estas cuentas están conectadas" @@ -494,7 +502,6 @@ uiLanguage: "Idioma de visualización de la interfaz" aboutX: "Acerca de {x}" emojiStyle: "Estilo de emoji" native: "Nativo" -disableDrawer: "No mostrar los menús en cajones" showNoteActionsOnlyHover: "Mostrar acciones de la nota sólo al pasar el cursor" showReactionsCount: "Mostrar el número de reacciones en las notas" noHistory: "No hay datos en el historial" @@ -693,10 +700,7 @@ abuseReported: "Se ha enviado el reporte. Muchas gracias." reporter: "Reportador" reporteeOrigin: "Reportar a" reporterOrigin: "Origen del reporte" -forwardReport: "Transferir un informe a una instancia remota" -forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá como una cuenta anónima del sistema" send: "Enviar" -abuseMarkAsResolved: "Marcar reporte como resuelto" openInNewTab: "Abrir en una Nueva Pestaña" openInSideView: "Abrir en una vista al costado" defaultNavigationBehaviour: "Navegación por defecto" @@ -868,7 +872,7 @@ whatIsNew: "Mostrar cambios" translate: "Traducir" translatedFrom: "Traducido de {x}" accountDeletionInProgress: "La eliminación de la cuenta está en curso" -usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dÃgitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente." +usernameInfo: "Un nombre que identifique su cuenta de otras en este servidor. Puede utilizar el alfabeto (a~z, A~Z), dÃgitos (0~9) o guiones bajos (_). Los nombres de usuario no se pueden cambiar posteriormente." aiChanMode: "Modo Ai" devMode: "Modo de desarrollador" keepCw: "Mantener la advertencia de contenido" @@ -1095,6 +1099,8 @@ preservedUsernames: "Nombre de usuario reservado" preservedUsernamesDescription: "La lista de nombres de usuario para reservar tienen que separarse con saltos de lÃnea.\nEstos estarán indisponibles durante la creación de cuentas, pero pueden ser usados para que los administradores puedan crear esas cuentas manualmente. Las cuentas existentes con esos nombres de usuario no se verán afectadas." createNoteFromTheFile: "Componer una nota desde éste archivo" archive: "Archivo" +archived: "Archivado" +unarchive: "Desarchivar" channelArchiveConfirmTitle: "¿Seguro de archivar {name}?" channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas." thisChannelArchived: "El canal ha sido archivado." @@ -1147,8 +1153,8 @@ currentAnnouncements: "Anuncios actuales" pastAnnouncements: "Anuncios anteriores" youHaveUnreadAnnouncements: "Hay anuncios sin leer" useSecurityKey: "Por favor, sigue las instrucciones de tu dispositivo o navegador para usar tu clave de seguridad o tu clave de paso." -replies: "Respuestas" -renotes: "Renotas" +replies: "Responder" +renotes: "Renotar" loadReplies: "Ver respuestas" loadConversation: "Ver conversación" pinnedList: "Lista fijada" @@ -1300,10 +1306,10 @@ _initialTutorial: _reaction: title: "¿Qué son las reacciones?" description: "Se puede reaccionar a las Notas con diferentes emojis. Las reacciones te permiten expresar matices que no se pueden transmitir con un simple 'me gusta'." - letsTryReacting: "Puedes añadir reacciones pulsando en el botón '{reaction}' de la nota. ¡Intenta reaccionar a esta nota de ejemplo!" + letsTryReacting: "Puedes añadir reacciones pulsando en el botón '+' de la nota. ¡Intenta reaccionar a esta nota de ejemplo!" reactToContinue: "Añade una reacción para continuar." reactNotification: "Recibirás notificaciones en tiempo real cuando alguien reaccione a tu nota." - reactDone: "Puedes deshacer una reacción pulsando en el botón '{undo}'." + reactDone: "Puedes deshacer una reacción pulsando en el botón '-'." _timeline: title: "El concepto de LÃnea de tiempo" description1: "Misskey proporciona múltiples lÃneas de tiempo basadas en su uso (algunas pueden no estar disponibles dependiendo de las polÃticas de la instancia)." @@ -1909,7 +1915,6 @@ _theme: buttonBg: "Fondo de botón" buttonHoverBg: "Fondo de botón (hover)" inputBorder: "Borde de los campos de entrada" - listItemHoverBg: "Fondo de elemento de listas (hover)" driveFolderBg: "Fondo de capeta del drive" wallpaperOverlay: "Transparencia del fondo de pantalla" badge: "Medalla" @@ -2149,7 +2154,6 @@ _poll: remainingHours: "Quedan {h} horas y {m} minutos para que finalice" remainingMinutes: "Quedan {m} minutos y {s} segundos para que finalice" remainingSeconds: "Quedan {s} segundos para que finalice" - multiple: "Opciones múltiples" _visibility: public: "Público" publicDescription: "Visible para todos los usuarios" @@ -2335,6 +2339,7 @@ _notification: followRequestAccepted: "El seguimiento fue aceptado" roleAssigned: "Rol asignado" achievementEarned: "Logro desbloqueado" + login: "Iniciar sesión" app: "Notificaciones desde aplicaciones" _actions: followBack: "Te sigue de vuelta" @@ -2355,7 +2360,7 @@ _deck: newProfile: "Nuevo perfil" deleteProfile: "Eliminar perfil" introduction: "¡Crea la interfaz perfecta para tà organizando las columnas libremente!" - introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras." + introduction2: "Presiona en la + de la derecha de la pantalla para añadir nuevas columnas donde quieras." widgetsIntroduction: "Por favor selecciona \"Editar Widgets\" en el menú columna y agrega un widget." useSimpleUiForNonRootPages: "Mostrar páginas no pertenecientes a la raÃz con la interfaz simple" usedAsMinWidthWhenFlexible: "Se usará el ancho mÃnimo cuando la opción \"Autoajustar ancho\" esté habilitada" @@ -2397,6 +2402,8 @@ _abuseReport: _notificationRecipient: _recipientType: mail: "Correo" + webhook: "Webhook" + keywords: "Palabras Clave" _moderationLogTypes: createRole: "Rol creado" deleteRole: "Rol eliminado" @@ -2500,6 +2507,7 @@ _hemisphere: S: "Hemisferio sur" _reversi: reversi: "Reversi" + rules: "Reglas" won: "{name} ha ganado" total: "Total" _urlPreviewSetting: diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index f615349b89b259f7a6ed43bb05d069b134a452a3..a7060c06fc81cdc72adc0ce12afda651b20b68b5 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -493,7 +493,6 @@ uiLanguage: "Langue d’affichage de l’interface" aboutX: "À propos de {x}" emojiStyle: "Style des émojis" native: "Natif" -disableDrawer: "Les menus ne s'affichent pas dans le tiroir" showNoteActionsOnlyHover: "Afficher les actions de note uniquement au survol" showReactionsCount: "Afficher le nombre de réactions des notes" noHistory: "Pas d'historique" @@ -692,10 +691,7 @@ abuseReported: "Le rapport est envoyé. Merci." reporter: "Signalé par" reporteeOrigin: "Origine du signalement" reporterOrigin: "Signalé par" -forwardReport: "Transférer le signalement à l’instance distante" -forwardReportIsAnonymous: "L'instance distante ne sera pas en mesure de voir vos informations et apparaîtra comme un compte anonyme du système." send: "Envoyer" -abuseMarkAsResolved: "Marquer le signalement comme résolu" openInNewTab: "Ouvrir dans un nouvel onglet" openInSideView: "Ouvrir en vue latérale" defaultNavigationBehaviour: "Navigation par défaut" @@ -1250,7 +1246,7 @@ _announcement: end: "Archiver l'annonce" tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes." readConfirmTitle: "Marquer comme lu ?" - readConfirmText: "Cela marquera le contenu de « {title} » comme lu." + readConfirmText: "Cela marquera le contenu de « {title} » comme lu." shouldNotBeUsedToPresentPermanentInfo: "Puisque cela pourrait nuire considérablement à l'expérience utilisateur pour les nouveaux utilisateurs, il est recommandé d'utiliser les annonces pour afficher des informations temporaires plutôt que des informations persistantes." dialogAnnouncementUxWarn: "Avoir deux ou plus annonces de style dialogue en même temps pourrait nuire considérablement à l'expérience utilisateur. Veuillez les utiliser avec caution." silence: "Ne pas me notifier" @@ -1283,10 +1279,10 @@ _initialTutorial: _reaction: title: "Qu'est-ce que les réactions ?" description: "Vous pouvez ajouter des « réactions » aux notes. Les réactions vous permettent d'exprimer à l'aise des nuances qui ne peuvent pas être exprimées par des mentions j'aime." - letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « {reaction} » de la note. Essayez d'ajouter une réaction à cet exemple de note !" + letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « + » de la note. Essayez d'ajouter une réaction à cet exemple de note !" reactToContinue: "Ajoutez une réaction pour procéder." reactNotification: "Vous recevez des notifications en temps réel lorsque quelqu'un réagit à votre note." - reactDone: "Vous pouvez annuler la réaction en cliquant sur le bouton « {undo} » ." + reactDone: "Vous pouvez annuler la réaction en cliquant sur le bouton « - » ." _timeline: title: "Fonctionnement des fils" description1: "Misskey offre plusieurs fils selon l'usage (certains peuvent être désactivés par le serveur)." @@ -1350,7 +1346,7 @@ _achievements: earnedAt: "Date d'obtention" _types: _notes1: - title: "Je viens tout juste de configurer mon shonk" + title: "Je viens tout juste de configurer mon msky" description: "Publiez votre première note" flavor: "Passez un bon moment avec Misskey !" _notes10: @@ -1705,7 +1701,6 @@ _theme: buttonBg: "Arrière-plan du bouton" buttonHoverBg: "Arrière-plan du bouton (survolé)" inputBorder: "Cadre de la zone de texte" - listItemHoverBg: "Arrière-plan d'item de liste (survolé)" driveFolderBg: "Arrière-plan du dossier de disque" wallpaperOverlay: "Superposition de fond d'écran" badge: "Badge" @@ -2038,6 +2033,7 @@ _notification: followRequestAccepted: "Demande d'abonnement acceptée" roleAssigned: "Rôle reçu" achievementEarned: "Déverrouillage d'accomplissement" + login: "Se connecter" app: "Notifications provenant des apps" _actions: followBack: "Suivre" diff --git a/locales/generateDTS.js b/locales/generateDTS.js index a175247445a819976d084442405158559d01de53..571595de202d008127b178d867c0aed819f9c218 100644 --- a/locales/generateDTS.js +++ b/locales/generateDTS.js @@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import * as yaml from 'js-yaml'; import ts from 'typescript'; +import { merge } from './index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -56,7 +57,10 @@ function createMembers(record) { } export default function generateDTS() { - const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); + const sharkeyLocale = yaml.load(fs.readFileSync(`${__dirname}/../sharkey-locales/en-US.yml`, 'utf-8')); + const misskeyLocale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8')); + const locale = merge(misskeyLocale, sharkeyLocale); + const members = createMembers(locale); const elements = [ ts.factory.createVariableStatement( diff --git a/locales/hu-HU.yml b/locales/hu-HU.yml index 023a91494d027743c7242b61ce5b8a874a5f279d..acc27ed09208a9b4551b34540ad16fa6c8ae1ee4 100644 --- a/locales/hu-HU.yml +++ b/locales/hu-HU.yml @@ -96,6 +96,7 @@ _notification: renote: "Renote" quote: "Idézet" reaction: "Reakciók" + login: "Bejelentkezés" _actions: renote: "Renote" _deck: diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 2f225c1199583e556f4931cbca1cc35b337ebaae..ce3958b167fcaa098c55428b00b8582f2211eff7 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -60,6 +60,7 @@ copyFileId: "Salin Berkas" copyFolderId: "Salin Folder" copyProfileUrl: "Salin Alamat Web Profil" searchUser: "Cari pengguna" +searchThisUsersNotes: "Mencari catatan pengguna" reply: "Balas" loadMore: "Selebihnya" showMore: "Selebihnya" @@ -154,6 +155,7 @@ editList: "Sunting daftar" selectChannel: "Pilih kanal" selectAntenna: "Pilih Antena" editAntenna: "Sunting antena" +createAntenna: "Membuat antena." selectWidget: "Pilih gawit" editWidgets: "Sunting gawit" editWidgetsExit: "Selesai" @@ -502,7 +504,6 @@ uiLanguage: "Bahasa antarmuka pengguna" aboutX: "Tentang {x}" emojiStyle: "Gaya emoji" native: "Native" -disableDrawer: "Jangan gunakan menu bergaya laci" showNoteActionsOnlyHover: "Hanya tampilkan aksi catatan saat ditunjuk" showReactionsCount: "Lihat jumlah reaksi dalam catatan" noHistory: "Tidak ada riwayat" @@ -584,7 +585,7 @@ sort: "Urutkan" ascendingOrder: "Urutkan naik" descendingOrder: "Urutkan menurun" scratchpad: "Scratchpad" -scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScript. Kamu bisa menulis, mengeksuksi, serta mengecek hasil yang berinteraksi dengan Misskey." +scratchpadDescription: "Scratchpad menyediakan lingkungan eksperimen untuk AiScript. Kamu bisa menulis, mengeksuksi, serta mengecek hasil yang berinteraksi dengan Misskey." output: "Keluaran" script: "Script" disablePagesScript: "Nonaktifkan script pada halaman" @@ -701,10 +702,7 @@ abuseReported: "Laporan kamu telah dikirimkan. Terima kasih." reporter: "Pelapor" reporteeOrigin: "Yang dilaporkan" reporterOrigin: "Pelapor" -forwardReport: "Teruskan laporan ke instansi luar" -forwardReportIsAnonymous: "Untuk melindungi privasi akun kamu, akun anonim dari sistem akan digunakan sebagai pelapor pada instansi luar." send: "Kirim" -abuseMarkAsResolved: "Tandai laporan sebagai selesai" openInNewTab: "Buka di tab baru" openInSideView: "Buka di tampilan samping" defaultNavigationBehaviour: "Navigasi bawaan" @@ -1155,7 +1153,7 @@ currentAnnouncements: "Pengumuman Saat Ini" pastAnnouncements: "Pengumuman Terdahulu" youHaveUnreadAnnouncements: "Terdapat pengumuman yang belum dibaca" useSecurityKey: "Mohon ikuti instruksi peramban atau perangkat kamu untuk menggunakan kunci pengaman atau passkey." -replies: "Balasan" +replies: "Balas" renotes: "Renote" loadReplies: "Tampilkan balasan" loadConversation: "Tampilkan percakapan" @@ -1316,10 +1314,10 @@ _initialTutorial: _reaction: title: "Apa itu Reaksi?" description: "Catatan dapat direaksi dengan berbagai emoji. Reaksi memperbolehkan kamu untuk mengekspresikan nuansa yang tidak dapat disampaikan hanya dengan sebuah \"suka\"." - letsTryReacting: "Reaksi dapat ditambahkan dengan mengklik tombol '{reaction}' pada catatan. Coba lakukan mereaksi contoh catatan ini!" + letsTryReacting: "Reaksi dapat ditambahkan dengan mengklik tombol '+' pada catatan. Coba lakukan mereaksi contoh catatan ini!" reactToContinue: "Tambahkan reaksi untuk melanjutkan." reactNotification: "Kamu akan menerima notifikasi real0time ketika seseorang mereaksi catatan kamu." - reactDone: "Kamu dapat mengurungkan reaksi dengan menekan tombol '{undo}'." + reactDone: "Kamu dapat mengurungkan reaksi dengan menekan tombol '-'." _timeline: title: "Konsep Lini Masa" description1: "Misskey menyediakan berbagai lini masa sesuai dengan penggunaan (beberapa mungkin tidak tersedia karena bergantung dengan kebijakan peladen)." @@ -1926,7 +1924,6 @@ _theme: buttonBg: "Latar belakang tombol" buttonHoverBg: "Latar belakang tombol (Mengambang)" inputBorder: "Batas bidang masukan" - listItemHoverBg: "Latar belakang daftar item (Mengambang)" driveFolderBg: "Latar belakang folder drive" wallpaperOverlay: "Lapisan wallpaper" badge: "Lencana" @@ -2353,6 +2350,7 @@ _notification: followRequestAccepted: "Permintaan mengikuti disetujui" roleAssigned: "Peran Diberikan" achievementEarned: "Pencapaian didapatkan" + login: "Masuk" app: "Notifikasi dari aplikasi tertaut" _actions: followBack: "Ikuti Kembali" diff --git a/locales/index.d.ts b/locales/index.d.ts index 177a3c8160c830d3f30a8f124f48723d3d3fddc1..d1cb1f97ea6622c0ca257cca8a6ced9229c378d1 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -18,14 +18,14 @@ export interface Locale extends ILocale { */ "headlineMisskey": string; /** - * よã†ã“ãï¼Sharkeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スã§ã™ã€‚ - * 「ノートã€ã‚’作æˆã—ã¦ã€ã„ã¾èµ·ã“ã£ã¦ã„ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ãªãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡ - * 「リアクションã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ðŸ‘ - * æ–°ã—ã„世界を探検ã—よã†ðŸš€ + * Welcome! Sharkey is an open source, decentralized microblogging service. + * Create "notes" to share your thoughts with everyone around you. 📡 + * With "reactions", you can also quickly express your feelings about everyone's notes. 👠+ * Let's explore a new world! 🚀 */ "introMisskey": string; /** - * {name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Sharkey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ã§ã™ã€‚ + * {name} is one of the services powered by the open source platform <b>Sharkey</b> which is based on Misskey (referred to as a "Misskey instance"). */ "poweredByMisskeyDescription": ParameterizedString<"name">; /** @@ -48,6 +48,20 @@ export interface Locale extends ILocale { * パスワード */ "password": string; + /** + * åˆæœŸè¨å®šé–‹å§‹ç”¨ãƒ‘スワード + */ + "initialPasswordForSetup": string; + /** + * åˆæœŸè¨å®šé–‹å§‹ç”¨ã®ãƒ‘スワードãŒé•ã„ã¾ã™ã€‚ + */ + "initialPasswordIsIncorrect": string; + /** + * Misskeyを自分ã§ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã—ãŸå ´åˆã¯ã€è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã«å…¥åŠ›ã—ãŸãƒ‘スワードを使用ã—ã¦ãã ã•ã„。 + * Misskeyã®ãƒ›ã‚¹ãƒ†ã‚£ãƒ³ã‚°ã‚µãƒ¼ãƒ“スãªã©ã‚’使用ã—ã¦ã„ã‚‹å ´åˆã¯ã€æä¾›ã•ã‚ŒãŸãƒ‘スワードを使用ã—ã¦ãã ã•ã„。 + * パスワードをè¨å®šã—ã¦ã„ãªã„å ´åˆã¯ã€ç©ºæ¬„ã«ã—ãŸã¾ã¾ç¶šè¡Œã—ã¦ãã ã•ã„。 + */ + "initialPasswordForSetupDescription": string; /** * パスワードを忘れ㟠*/ @@ -77,7 +91,7 @@ export interface Locale extends ILocale { */ "enterUsername": string; /** - * {user}ãŒãƒ–ースト + * Boosted by {user} */ "renotedBy": ParameterizedString<"user">; /** @@ -152,10 +166,6 @@ export interface Locale extends ILocale { * ユーザー */ "users": string; - /** - * æ‰¿èª - */ - "approvals": string; /** * ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¿½åŠ */ @@ -201,7 +211,7 @@ export interface Locale extends ILocale { */ "copyLink": string; /** - * ブーストã®ãƒªãƒ³ã‚¯ã‚’コピー + * Copy boost link */ "copyLinkRenote": string; /** @@ -213,7 +223,7 @@ export interface Locale extends ILocale { */ "deleteAndEdit": string; /** - * ã“ã®ãƒŽãƒ¼ãƒˆã‚’削除ã—ã¦ã‚‚ã†ä¸€åº¦ç·¨é›†ã—ã¾ã™ã‹ï¼Ÿã“ã®ãƒŽãƒ¼ãƒˆã¸ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€ãƒ–ーストã€è¿”ä¿¡ã‚‚å…¨ã¦å‰Šé™¤ã•ã‚Œã¾ã™ã€‚ + * Are you sure you want to redraft this note? This means you will lose all reactions, boosts, and replies to it. */ "deleteAndEditConfirm": string; /** @@ -236,10 +246,6 @@ export interface Locale extends ILocale { * ユーザーåをコピー */ "copyUsername": string; - /** - * リモートプãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’é–‹ã - */ - "openRemoteProfile": string; /** * ユーザーIDをコピー */ @@ -449,35 +455,27 @@ export interface Locale extends ILocale { */ "enterEmoji": string; /** - * ブースト + * Boost */ "renote": string; /** - * ブースト解除 + * Remove boost */ "unrenote": string; /** - * ブーストã—ã¾ã—ãŸã€‚ + * Boosted. */ "renoted": string; /** - * {name} ã«ãƒ–ーストã—ã¾ã—ãŸã€‚ + * Boosted to {name} */ "renotedToX": ParameterizedString<"name">; /** - * 引用。 - */ - "quoted": string; - /** - * ブースト解除ã—ã¾ã—ãŸã€‚ - */ - "rmboost": string; - /** - * ã“ã®æŠ•ç¨¿ã¯ãƒ–ーストã§ãã¾ã›ã‚“。 + * This post can't be boosted. */ "cantRenote": string; /** - * ブーストをブーストã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。 + * A boost can't be boosted. */ "cantReRenote": string; /** @@ -485,7 +483,7 @@ export interface Locale extends ILocale { */ "quote": string; /** - * ãƒãƒ£ãƒ³ãƒãƒ«å†…ブースト + * Channel-only Boost */ "inChannelRenote": string; /** @@ -593,11 +591,11 @@ export interface Locale extends ILocale { */ "unmute": string; /** - * ブーストをミュート + * Mute Boosts */ "renoteMute": string; /** - * ブーストã®ãƒŸãƒ¥ãƒ¼ãƒˆã‚’解除 + * Unmute Boosts */ "renoteUnmute": string; /** @@ -608,10 +606,6 @@ export interface Locale extends ILocale { * ブãƒãƒƒã‚¯è§£é™¤ */ "unblock": string; - /** - * ユーザーã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯ã™ã‚‹ - */ - "markAsNSFW": string; /** * å‡çµ */ @@ -628,22 +622,10 @@ export interface Locale extends ILocale { * ブãƒãƒƒã‚¯è§£é™¤ã—ã¾ã™ã‹ï¼Ÿ */ "unblockConfirm": string; - /** - * ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ - */ - "nsfwConfirm": string; - /** - * ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯è§£é™¤ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ - */ - "unNsfwConfirm": string; /** * å‡çµã—ã¾ã™ã‹ï¼Ÿ */ "suspendConfirm": string; - /** - * ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’承èªã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ - */ - "approveConfirm": string; /** * 解å‡ã—ã¾ã™ã‹ï¼Ÿ */ @@ -737,7 +719,7 @@ export interface Locale extends ILocale { */ "flagAsBot": string; /** - * ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒãƒ—ãƒã‚°ãƒ©ãƒ ã«ã‚ˆã£ã¦é‹ç”¨ã•ã‚Œã‚‹å ´åˆã¯ã€ã“ã®ãƒ•ãƒ©ã‚°ã‚’オンã«ã—ã¾ã™ã€‚オンã«ã™ã‚‹ã¨ã€åå¿œã®é€£éŽ–を防ããŸã‚ã®ãƒ•ãƒ©ã‚°ã¨ã—ã¦ä»–ã®é–‹ç™ºè€…ã«å½¹ç«‹ã£ãŸã‚Šã€Sharkeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã®ã«ãªã‚Šã¾ã™ã€‚ + * Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Sharkey's internal systems to treat this account as a bot. */ "flagAsBotDescription": string; /** @@ -748,14 +730,6 @@ export interface Locale extends ILocale { * ã«ã‚ƒã«ã‚ƒã«ã‚ƒï¼Ÿï¼Ÿ */ "flagAsCatDescription": string; - /** - * 猫語ã§è©±ã™ - */ - "flagSpeakAsCat": string; - /** - * 有効ã«ã™ã‚‹ã¨ã€ã‚ãªãŸã®æŠ•ç¨¿ã® 「ãªã€ã‚’「ã«ã‚ƒã€ã«ã—ã¾ã™ã€‚ - */ - "flagSpeakAsCatDescription": string; /** * タイムラインã«ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹ */ @@ -785,11 +759,11 @@ export interface Locale extends ILocale { */ "showOnRemote": string; /** - * リモートã§ç¶šè¡Œ + * Continue on remote instance */ "continueOnRemote": string; /** - * Misskey Hubã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã‚’é¸æŠž + * Choose a instance from Misskey Hub */ "chooseServerOnMisskeyHub": string; /** @@ -905,7 +879,7 @@ export interface Locale extends ILocale { */ "silenceThisInstance": string; /** - * サーãƒãƒ¼ã‚’メディアサイレンス + * Silence media from this instance */ "mediaSilenceThisInstance": string; /** @@ -989,17 +963,25 @@ export interface Locale extends ILocale { */ "silencedInstances": string; /** - * サイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚サイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã™ã¹ã¦ã€Œã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã€ã¨ã—ã¦æ‰±ã‚ã‚Œã€ãƒ•ã‚©ãƒãƒ¼ãŒã™ã¹ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。 + * List the host names of the instances that you want to silence, separated by a new line. All accounts belonging to the listed instances will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances. */ "silencedInstancesDescription": string; /** - * メディアサイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼ + * Media-silenced instances */ "mediaSilencedInstances": string; /** - * メディアサイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚メディアサイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã™ã¹ã¦ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã¨ã—ã¦æ‰±ã‚ã‚Œã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—ãŒä½¿ç”¨ã§ããªã„よã†ã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。 + * List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances. */ "mediaSilencedInstancesDescription": string; + /** + * 連åˆã‚’許å¯ã™ã‚‹ã‚µãƒ¼ãƒãƒ¼ + */ + "federationAllowedHosts": string; + /** + * 連åˆã‚’許å¯ã™ã‚‹ã‚µãƒ¼ãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚ + */ + "federationAllowedHostsDescription": string; /** * ミュートã¨ãƒ–ãƒãƒƒã‚¯ */ @@ -1029,7 +1011,7 @@ export interface Locale extends ILocale { */ "pinLimitExceeded": string; /** - * Sharkeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ã¾ã—ãŸï¼ç®¡ç†è€…アカウントを作æˆã—ã¾ã—ょã†ã€‚ + * Installation of Sharkey has been finished! Please create an admin user. */ "intro": string; /** @@ -1340,10 +1322,6 @@ export interface Locale extends ILocale { * ドライブ */ "drive": string; - /** - * 検索ドライブ - */ - "driveSearchbarPlaceholder": string; /** * ファイルå */ @@ -1365,7 +1343,7 @@ export interface Locale extends ILocale { */ "selectFolders": string; /** - * ファイルãŒé¸æŠžã•ã‚Œã¦ã„ã¾ã›ã‚“ + * No file selected */ "fileNotSelected": string; /** @@ -1396,6 +1374,10 @@ export interface Locale extends ILocale { * ãƒ•ã‚¡ã‚¤ãƒ«ã‚’è¿½åŠ */ "addFile": string; + /** + * ファイルを表示 + */ + "showFile": string; /** * ドライブã¯ç©ºã§ã™ */ @@ -1444,10 +1426,6 @@ export interface Locale extends ILocale { * ãƒãƒŠãƒ¼ */ "banner": string; - /** - * 背景 - */ - "background": string; /** * センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã®è¡¨ç¤º */ @@ -1741,7 +1719,7 @@ export interface Locale extends ILocale { */ "enableServiceworker": string; /** - * ユーザーåを改行ã§åŒºåˆ‡ã£ã¦æŒ‡å®šã—ã¾ã™ + * List one username per line. Use "*@instance.com" to specify all users of an instance */ "antennaUsersDescription": string; /** @@ -1817,7 +1795,7 @@ export interface Locale extends ILocale { */ "about": string; /** - * Sharkeyã«ã¤ã„㦠+ * About Sharkey */ "aboutMisskey": string; /** @@ -1856,6 +1834,10 @@ export interface Locale extends ILocale { * モデレーションノート */ "moderationNote": string; + /** + * モデレーター間ã§ã ã‘共有ã•ã‚Œã‚‹ãƒ¡ãƒ¢ã‚’記入ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "moderationNoteDescription": string; /** * ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãƒŽãƒ¼ãƒˆã‚’è¿½åŠ ã™ã‚‹ */ @@ -1984,14 +1966,6 @@ export interface Locale extends ILocale { * {user}ã®ãƒŽãƒ¼ãƒˆ */ "noteOf": ParameterizedString<"user">; - /** - * ã™ã¹ã¦ã®è¿”ä¿¡ã®å†…容を表示ã™ã‚‹ - */ - "expandAllCws": string; - /** - * ã™ã¹ã¦ã®è¿”ä¿¡ã®å†…å®¹ã‚’éš ã™ - */ - "collapseAllCws": string; /** * 引用付ã */ @@ -2001,7 +1975,7 @@ export interface Locale extends ILocale { */ "quoteQuestion": string; /** - * クリップボードã®ãƒ†ã‚ストãŒé•·ã„ã§ã™ã€‚テã‚ストファイルã¨ã—ã¦æ·»ä»˜ã—ã¾ã™ã‹ï¼Ÿ + * The text in clipboard is long. Would you like to attach it as a text file? */ "attachAsFileQuestion": string; /** @@ -2021,7 +1995,7 @@ export interface Locale extends ILocale { */ "signinRequired": string; /** - * 続行ã™ã‚‹ã«ã¯ã€ãŠä½¿ã„ã®ã‚µãƒ¼ãƒãƒ¼ã«ç§»å‹•ã™ã‚‹ã‹ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«ç™»éŒ²ãƒ»ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ + * To continue, you need to go to your instance to perform this action or sign up / log in to the instance you are trying to interact with. */ "signinOrContinueOnRemote": string; /** @@ -2109,15 +2083,27 @@ export interface Locale extends ILocale { */ "native": string; /** - * メニューをドãƒãƒ¯ãƒ¼ã§è¡¨ç¤ºã—ãªã„ + * メニューã®ã‚¹ã‚¿ã‚¤ãƒ« */ - "disableDrawer": string; + "menuStyle": string; + /** + * スタイル + */ + "style": string; + /** + * ドãƒãƒ¯ãƒ¼ + */ + "drawer": string; + /** + * ãƒãƒƒãƒ—アップ + */ + "popup": string; /** * ノートã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ホãƒãƒ¼æ™‚ã®ã¿è¡¨ç¤ºã™ã‚‹ */ "showNoteActionsOnlyHover": string; /** - * ノートã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³æ•°ã‚’表示ã™ã‚‹ + * Show the number of reactions in notes */ "showReactionsCount": string; /** @@ -2168,10 +2154,6 @@ export interface Locale extends ILocale { * フォントサイズ */ "fontSize": string; - /** - * コーナーã®ä¸¸ã¿ - */ - "cornerRadius": string; /** * ç”»åƒãŒ1æžšã®ã¿ã®ãƒ¡ãƒ‡ã‚£ã‚¢ãƒªã‚¹ãƒˆã®é«˜ã• */ @@ -2188,10 +2170,6 @@ export interface Locale extends ILocale { * ç”»åƒã‚’æ–°ã—ã„タブã§é–‹ã */ "openImageInNewTab": string; - /** - * 代替テã‚ストを入れ忘れãŸã¨ãã«è¦å‘Šã™ã‚‹ - */ - "warnForMissingAltText": string; /** * ダッシュボード */ @@ -2320,14 +2298,6 @@ export interface Locale extends ILocale { * s3ForcePathStyleを有効ã«ã™ã‚‹ã¨ã€ãƒã‚±ãƒƒãƒˆåã‚’URLã®ãƒ›ã‚¹ãƒˆåã§ã¯ãªãパスã®ä¸€éƒ¨ã¨ã—ã¦æŒ‡å®šã™ã‚‹ã“ã¨ã‚’強制ã—ã¾ã™ã€‚セルフホストã•ã‚ŒãŸMinioãªã©ã®ä½¿ç”¨æ™‚ã«æœ‰åŠ¹ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚ */ "s3ForcePathStyleDesc": string; - /** - * DeepLX-JS を使用ã™ã‚‹ (èªè¨¼ã‚ーä¸è¦) - */ - "deeplFreeMode": string; - /** - * DeepLX-JSã®è¨å®šæ–¹æ³•ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ãã ã•ã„。 - */ - "deeplFreeModeDescription": string; /** * サーãƒãƒ¼ãƒã‚° */ @@ -2389,7 +2359,7 @@ export interface Locale extends ILocale { */ "notUseSound": string; /** - * SharkeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹ + * Output sounds only if Sharkey is active. */ "useSoundOnlyWhenActive": string; /** @@ -2453,9 +2423,17 @@ export interface Locale extends ILocale { */ "scratchpad": string; /** - * スクラッãƒãƒ‘ッドã¯ã€AiScriptã®å®Ÿé¨“環境をæä¾›ã—ã¾ã™ã€‚Sharkeyã¨å¯¾è©±ã™ã‚‹ã‚³ãƒ¼ãƒ‰ã®è¨˜è¿°ã€å®Ÿè¡Œã€çµæžœã®ç¢ºèªãŒã§ãã¾ã™ã€‚ + * The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Sharkey in it. */ "scratchpadDescription": string; + /** + * UIインスペクター + */ + "uiInspector": string; + /** + * メモリ上ã«å˜åœ¨ã—ã¦ã„ã‚‹UIコンãƒãƒ¼ãƒãƒ³ãƒˆã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ä¸€è¦§ã‚’見るã“ã¨ãŒã§ãã¾ã™ã€‚UIコンãƒãƒ¼ãƒãƒ³ãƒˆã¯Ui:C:系関数ã«ã‚ˆã‚Šç”Ÿæˆã•ã‚Œã¾ã™ã€‚ + */ + "uiInspectorDescription": string; /** * 出力 */ @@ -2844,26 +2822,6 @@ export interface Locale extends ILocale { * 表示ã™ã‚‹é€šçŸ¥ã®ç¨®åˆ¥ã‚’é¸æŠžã—ã¦ãã ã•ã„。 */ "notificationSettingDesc": string; - /** - * 未èªã®é€šçŸ¥ãŒã‚ã‚‹ã¨ãã«ã‚¿ãƒ–ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’目立ãŸã›ã‚‹ - */ - "enableFaviconNotificationDot": string; - /** - * タブアイコン強調機能ã®å‹•ä½œç¢ºèª - */ - "verifyNotificationDotWorkingButton": string; - /** - * ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ç¾æ™‚点ã§ã¯ã‚¿ãƒ–アイコン強調機能をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 - */ - "notificationDotNotWorking": string; - /** - * タブアイコン強調機能ã¯ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§æ£ã—ã機能ã—ã¦ã„ã¾ã™ã€‚ - */ - "notificationDotWorking": string; - /** - * タブアイコン強調機能ãŒæ©Ÿèƒ½ã—ãªã„å ´åˆã¯ã€ç®¡ç†è€…ã«ãƒ‰ã‚ュメントを確èªã™ã‚‹ã‚ˆã†ã«ä¾é ¼ã—ã¦ãã ã•ã„ {link} - */ - "notificationDotNotWorkingAdvice": ParameterizedString<"link">; /** * ã‚°ãƒãƒ¼ãƒãƒ«è¨å®šã‚’使ㆠ*/ @@ -2913,7 +2871,7 @@ export interface Locale extends ILocale { */ "reportAbuse": string; /** - * ãƒ–ãƒ¼ã‚¹ãƒˆã‚’é€šå ± + * Report boost */ "reportAbuseRenote": string; /** @@ -2940,22 +2898,10 @@ export interface Locale extends ILocale { * é€šå ±å…ƒ */ "reporterOrigin": string; - /** - * リモートサーãƒãƒ¼ã«é€šå ±ã‚’転é€ã™ã‚‹ - */ - "forwardReport": string; - /** - * リモートサーãƒãƒ¼ã‹ã‚‰ã¯ã‚ãªãŸã®æƒ…å ±ã¯è¦‹ã‚Œãšã€åŒ¿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ - */ - "forwardReportIsAnonymous": string; /** * é€ä¿¡ */ "send": string; - /** - * 対応済ã¿ã«ã™ã‚‹ - */ - "abuseMarkAsResolved": string; /** * æ–°ã—ã„タブã§é–‹ã */ @@ -3029,7 +2975,7 @@ export interface Locale extends ILocale { */ "private": string; /** - * Sharkeyã¯æœ‰å¿—ã«ã‚ˆã£ã¦æ§˜ã€…ãªè¨€èªžã«ç¿»è¨³ã•ã‚Œã¦ã„ã¾ã™ã€‚{link}ã§ç¿»è¨³ã«å”力ã§ãã¾ã™ã€‚ + * Misskeyã¯æœ‰å¿—ã«ã‚ˆã£ã¦æ§˜ã€…ãªè¨€èªžã«ç¿»è¨³ã•ã‚Œã¦ã„ã¾ã™ã€‚{link}ã§ç¿»è¨³ã«å”力ã§ãã¾ã™ã€‚ */ "i18nInfo": ParameterizedString<"link">; /** @@ -3049,7 +2995,7 @@ export interface Locale extends ILocale { */ "repliesCount": string; /** - * ブーストã—ãŸæ•° + * Number of boosts sent */ "renotesCount": string; /** @@ -3057,7 +3003,7 @@ export interface Locale extends ILocale { */ "repliedCount": string; /** - * ブーストã•ã‚ŒãŸæ•° + * Number of boosts received */ "renotedCount": string; /** @@ -3120,26 +3066,6 @@ export interface Locale extends ILocale { * 添付画åƒã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’オリジナル画質ã«ã™ã‚‹ */ "loadRawImages": string; - /** - * 返信ã«ã‚µãƒ¼ãƒãƒ¼æƒ…å ±ã‚’è¡¨ç¤ºã™ã‚‹ - */ - "showTickerOnReplies": string; - /** - * 検索MFMã®æ¤œç´¢ã‚¨ãƒ³ã‚¸ãƒ³ - */ - "searchEngine": string; - /** - * カスタム- */ - "searchEngineOther": string; - /** - * カスタム検索エンジンã®URIã¯ã€"https://www.google.com/search?q=\{query}" ã‚„ "https://www.google.com/search?q=%s" ã®ã‚ˆã†ãªå½¢å¼ã§å…¥åŠ›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ - */ - "searchEngineCustomURIDescription": string; - /** - * カスタム検索エンジン URI - */ - "searchEngineCusomURI": string; /** * アニメーション画åƒã‚’å†ç”Ÿã—ãªã„ */ @@ -3208,14 +3134,6 @@ export interface Locale extends ILocale { * オフã«ã™ã‚‹ã¨ã€ã€Œã¿ã¤ã‘ã‚‹ã€ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒè¼‰ã‚‰ãªããªã‚Šã¾ã™ã€‚ */ "makeExplorableDescription": string; - /** - * 公開ノートをインデックスä¸å¯ã«ã™ã‚‹ - */ - "makeIndexable": string; - /** - * ノート検索ãŒã‚ãªãŸã®å…¬é–‹ãƒŽãƒ¼ãƒˆã‚’インデックス化ã—ãªã„よã†ã«ã—ã¾ã™ã€‚ - */ - "makeIndexableDescription": string; /** * タイムラインã®ãƒŽãƒ¼ãƒˆã‚’離ã—ã¦è¡¨ç¤º */ @@ -3241,7 +3159,7 @@ export interface Locale extends ILocale { */ "narrow": string; /** - * è¨å®šã¯ãƒšãƒ¼ã‚¸ãƒªãƒãƒ¼ãƒ‰å¾Œã«åæ˜ ã•ã‚Œã¾ã™ã€‚今ã™ãリãƒãƒ¼ãƒ‰ã—ã¾ã™ã‹ï¼Ÿ + * è¨å®šã¯ãƒšãƒ¼ã‚¸ãƒªãƒãƒ¼ãƒ‰å¾Œã«åæ˜ ã•ã‚Œã¾ã™ã€‚ */ "reloadToApplySetting": string; /** @@ -3273,7 +3191,8 @@ export interface Locale extends ILocale { */ "sendErrorReports": string; /** - * オンã«ã™ã‚‹ã¨ã€å•é¡ŒãŒç™ºç”Ÿã—ãŸã¨ãã«ã‚¨ãƒ©ãƒ¼ã®è©³ç´°æƒ…å ±ãŒSharkeyã«å…±æœ‰ã•ã‚Œã€ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã®å“質å‘上ã«å½¹ç«‹ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã©ãŒå«ã¾ã‚Œã¾ã™ã€‚ + * When turned on, detailed error information will be shared with Sharkey when a problem occurs, helping to improve the quality of Sharkey. + * This will include information such the version of your OS, what browser you're using, your activity in Sharkey, etc. */ "sendErrorReportsDescription": string; /** @@ -3529,7 +3448,7 @@ export interface Locale extends ILocale { */ "noMaintainerInformationWarning": string; /** - * å•ã„åˆã‚ã›å…ˆURLãŒè¨å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。 + * Contact URL is not set. */ "noInquiryUrlWarning": string; /** @@ -3653,7 +3572,7 @@ export interface Locale extends ILocale { */ "learnMore": string; /** - * SharkeyãŒæ›´æ–°ã•ã‚Œã¾ã—ãŸï¼ + * Sharkey has been updated! */ "misskeyUpdated": string; /** @@ -3673,7 +3592,7 @@ export interface Locale extends ILocale { */ "accountDeletionInProgress": string; /** - * サーãƒãƒ¼ä¸Šã§ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’一æ„ã«è˜åˆ¥ã™ã‚‹ãŸã‚ã®åå‰ã€‚アルファベット(a~z, A~Z)ã€æ•°å—(0~9)ã€ãŠã‚ˆã³ã‚¢ãƒ³ãƒ€ãƒ¼ãƒãƒ¼(_)ãŒä½¿ç”¨ã§ãã¾ã™ã€‚ユーザーåã¯å¾Œã‹ã‚‰å¤‰æ›´ã™ã‚‹ã“ã¨ã¯å‡ºæ¥ã¾ã›ã‚“。 + * A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later. */ "usernameInfo": string; /** @@ -3732,10 +3651,6 @@ export interface Locale extends ILocale { * アカウント登録ã«ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’å¿…é ˆã«ã™ã‚‹ */ "emailRequiredForSignup": string; - /** - * アカウント登録を承èªåˆ¶ã«ã™ã‚‹ - */ - "approvalRequiredForSignup": string; /** * æœªèª */ @@ -3793,14 +3708,13 @@ export interface Locale extends ILocale { */ "incorrectPassword": string; /** - * 「{choice}ã€ã«æŠ•ç¥¨ã—ã¾ã™ã‹ï¼Ÿ + * ワンタイムパスワードãŒé–“é•ã£ã¦ã„ã‚‹ã‹ã€æœŸé™åˆ‡ã‚Œã«ãªã£ã¦ã„ã¾ã™ã€‚ */ - "voteConfirm": ParameterizedString<"choice">; + "incorrectTotp": string; /** * 「{choice}ã€ã«æŠ•ç¥¨ã—ã¾ã™ã‹ï¼Ÿ - *  確èªå¾Œã€é¸æŠžè‚¢ã‚’増やã™ã“ã¨ãŒã§ãã¾ã™ã€‚ */ - "voteConfirmMulti": ParameterizedString<"choice">; + "voteConfirm": ParameterizedString<"choice">; /** * éš ã™ */ @@ -3941,10 +3855,6 @@ export interface Locale extends ILocale { * 未対応ã®é€šå ±ãŒã‚ã‚Šã¾ã™ã€‚ */ "thereIsUnresolvedAbuseReportWarning": string; - /** - * 承èªå¾…ã¡ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„ã¾ã™ã€‚ - */ - "pendingUserApprovals": string; /** * 推奨 */ @@ -3977,26 +3887,6 @@ export interface Locale extends ILocale { * アカウント削除 */ "deleteAccount": string; - /** - * 承èªã™ã‚‹ - */ - "approveAccount": string; - /** - * æ‹’å¦ã¨å‰Šé™¤ - */ - "denyAccount": string; - /** - * 承èªæ¸ˆã¿ - */ - "approved": string; - /** - * 承èªã•ã‚Œã¦ã„ãªã„ - */ - "notApproved": string; - /** - * 承èªçŠ¶æ³ - */ - "approvalStatus": string; /** * ドã‚ュメント */ @@ -4009,30 +3899,6 @@ export interface Locale extends ILocale { * 多ãã™ã‚‹ã¨åˆ©ä¾¿æ€§ãŒå‘上ã—ã¾ã™ãŒã€è² è·ã¨ãƒ¡ãƒ¢ãƒªä½¿ç”¨é‡ãŒå¢—ãˆã¾ã™ã€‚ */ "numberOfPageCacheDescription": string; - /** - * スレッド内ã®è¿”ä¿¡æ•° - */ - "numberOfReplies": string; - /** - * ã“ã®æ•°å€¤ã‚’大ããã™ã‚‹ã¨ã€ã‚ˆã‚Šå¤šãã®è¿”ä¿¡ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®å€¤ã‚’大ããã—ã™ãŽã‚‹ã¨ã€UIãŒçª®å±ˆã«ãªã£ã¦èªã¿ã«ãããªã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚ - */ - "numberOfRepliesDescription": string; - /** - * ブーストè¨å®š - */ - "boostSettings": string; - /** - * 公開範囲セレクターを表示 - */ - "showVisibilitySelectorOnBoost": string; - /** - * 無効ã®å ´åˆã€ä»¥ä¸‹ã§è¨å®šã—ãŸãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å…¬é–‹ç¯„囲ãŒä½¿ç”¨ã•ã‚Œã€ã‚»ãƒ¬ã‚¯ã‚¿ãƒ¼ã¯è¡¨ç¤ºã•ã‚Œã¾ã›ã‚“。 - */ - "showVisibilitySelectorOnBoostDescription": string; - /** - * デフォルトã®ãƒ–ースト公開範囲 - */ - "visibilityOnBoost": string; /** * ãƒã‚°ã‚¢ã‚¦ãƒˆã—ã¾ã™ã‹ï¼Ÿ */ @@ -4209,10 +4075,6 @@ export interface Locale extends ILocale { * ã„ã„ãを解除 */ "unlike": string; - /** - * 絵文å—ã®ã‚ˆã†ãªãƒ‡ãƒ•ã‚©ãƒ«ãƒˆ - */ - "defaultLike": string; /** * ã„ã„ãæ•° */ @@ -4230,17 +4092,13 @@ export interface Locale extends ILocale { */ "remindMeLater": string; /** - * Sharkeyã‚’æ°—ã«å…¥ã£ã¦ã„ãŸã ã‘ã¾ã—ãŸã‹ï¼Ÿ + * Have you taken a liking to Sharkey? */ "didYouLikeMisskey": string; /** - * Sharkeyã¯{host}ãŒä½¿ç”¨ã—ã¦ã„ã‚‹ç„¡æ–™ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚ã“ã‚Œã‹ã‚‰ã‚‚開発を続ã‘られるよã†ã«ã€ãœã²å¯„付をãŠé¡˜ã„ã—ã¾ã™ï¼ + * {host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue! */ "pleaseDonate": ParameterizedString<"host">; - /** - * インスタンス管ç†è€…ã¸ã®å¯„付ã«ã‚ˆã£ã¦{host}を直接サãƒãƒ¼ãƒˆã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ - */ - "pleaseDonateInstance": ParameterizedString<"host">; /** * 対応ã™ã‚‹ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã¯{anchor}ã‹ã‚‰åˆ©ç”¨å¯èƒ½ã§ã™ã€‚ */ @@ -4350,45 +4208,13 @@ export interface Locale extends ILocale { */ "thisPostMayBeAnnoyingIgnore": string; /** - * ã‚„ã‚ã‚‹ - */ - "thisPostIsMissingAltTextCancel": string; - /** - * ã“ã®ã¾ã¾æŠ•ç¨¿ - */ - "thisPostIsMissingAltTextIgnore": string; - /** - * 代替テã‚ストãŒãªã„ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ã¾ã™ã€‚ã™ã¹ã¦ã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã«ä»£æ›¿ãƒ†ã‚ストをå«ã‚€ã‚ˆã†ã«ã—ã¦ãã ã•ã„。 - */ - "thisPostIsMissingAltText": string; - /** - * ブーストã®ã‚¹ãƒžãƒ¼ãƒˆçœç•¥ + * Collapse boosts you've already seen */ "collapseRenotes": string; /** - * リアクションやブーストをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã—ã¾ã™ã€‚ + * Collapse boosts that you have boosted or reacted to */ "collapseRenotesDescription": string; - /** - * 返信元ã®ãƒŽãƒ¼ãƒˆã‚’折りãŸãŸã‚€ - */ - "collapseNotesRepliedTo": string; - /** - * ファイルを折りãŸãŸã‚€ - */ - "collapseFiles": string; - /** - * CWを展開ã™ã‚‹ - */ - "uncollapseCW": string; - /** - * é•·ã„投稿を常ã«å±•é–‹ã™ã‚‹ - */ - "expandLongNote": string; - /** - * 会話スレッドを自動ã§èªã¿è¾¼ã‚€ - */ - "autoloadConversation": string; /** * サーãƒãƒ¼å†…部エラー */ @@ -4429,10 +4255,6 @@ export interface Locale extends ILocale { * ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‹›å¾…制ã§ã™ã€‚招待コードをãŠæŒã¡ã®æ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚ */ "invitationRequiredToRegister": string; - /** - * ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‰¿èªåˆ¶ã§ã™ã€‚å‚åŠ ã—ãŸã„ç†ç”±ã‚’記入ã—ã€æ‰¿èªã•ã‚ŒãŸæ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚ - */ - "approvalRequiredToRegister": string; /** * ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§ã¯ãƒ¡ãƒ¼ãƒ«é…ä¿¡ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“ */ @@ -4545,6 +4367,10 @@ export interface Locale extends ILocale { * リモートサーãƒãƒ¼ã®ãƒãƒ£ãƒ¼ãƒˆã‚’ç”Ÿæˆ */ "enableChartsForFederatedInstances": string; + /** + * リモートサーãƒãƒ¼ã®æƒ…å ±ã‚’å–å¾— + */ + "enableStatsForFederatedInstances": string; /** * ノートã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«ã‚¯ãƒªãƒƒãƒ—ã‚’è¿½åŠ */ @@ -4554,7 +4380,7 @@ export interface Locale extends ILocale { */ "reactionsDisplaySize": string; /** - * リアクションã®æœ€å¤§æ¨ªå¹…を制é™ã—ã€ç¸®å°ã—ã¦è¡¨ç¤ºã™ã‚‹ + * Limits the maximum width of reactions and display them in reduced size. */ "limitWidthOfReaction": string; /** @@ -4601,10 +4427,6 @@ export interface Locale extends ILocale { * 常ã«åºƒå‘Šã‚’表示ã™ã‚‹ */ "forceShowAds": string; - /** - * ã«ã‚ƒã‚“ã“フレンド :3 - */ - "oneko": string; /** * ãƒ¡ãƒ¢ã‚’è¿½åŠ */ @@ -4618,7 +4440,7 @@ export interface Locale extends ILocale { */ "reactionsList": string; /** - * ブースト一覧 + * Boosts */ "renotesList": string; /** @@ -4738,15 +4560,15 @@ export interface Locale extends ILocale { */ "specifyUser": string; /** - * 照会ã—ã¾ã™ã‹ï¼Ÿ + * Are you sure that you want to look this up? */ "lookupConfirm": string; /** - * ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒšãƒ¼ã‚¸ã‚’é–‹ãã¾ã™ã‹ï¼Ÿ + * Are you sure you want to open this hashtags page? */ "openTagPageConfirm": string; /** - * ホスト指定 + * Specify a host */ "specifyHost": string; /** @@ -4782,7 +4604,7 @@ export interface Locale extends ILocale { */ "later": string; /** - * Sharkey㸠+ * To Sharkey */ "goToMisskey": string; /** @@ -4801,22 +4623,6 @@ export interface Locale extends ILocale { * サーãƒãƒ¼ã®ãƒžã‚·ãƒ³æƒ…å ±ã‚’å…¬é–‹ã™ã‚‹ */ "enableServerMachineStats": string; - /** - * 実績を有効ã«ã™ã‚‹ - */ - "enableAchievements": string; - /** - * オフã«ã™ã‚‹ã¨å®Ÿç¸¾ã‚·ã‚¹ãƒ†ãƒ ã¯ç„¡åŠ¹ã«ãªã‚Šã¾ã™ã€‚ - */ - "turnOffAchievements": string; - /** - * botã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°è¿½åŠ を許å¯ã™ã‚‹ - */ - "enableBotTrending": string; - /** - * オフã«ã™ã‚‹ã¨ãƒœãƒƒãƒˆãŒãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’入力ã—ãªããªã‚Šã¾ã™ã€‚ - */ - "turnOffBotTrending": string; /** * ユーザーã”ã¨ã®Identicon生æˆã‚’有効ã«ã™ã‚‹ */ @@ -4934,11 +4740,11 @@ export interface Locale extends ILocale { */ "useSecurityKey": string; /** - * 返信 + * Replies */ "replies": string; /** - * ブースト + * Boosts */ "renotes": string; /** @@ -4957,14 +4763,6 @@ export interface Locale extends ILocale { * デãƒã‚¤ã‚¹ã®ç”»é¢ã‚’常ã«ã‚ªãƒ³ã«ã™ã‚‹ */ "keepScreenOn": string; - /** - * クリックã—ã¦ãƒŽãƒ¼ãƒˆã‚’é–‹ã - */ - "clickToOpen": string; - /** - * ボットをタイムラインã«è¡¨ç¤º - */ - "showBots": string; /** * ã“ã®ãƒªãƒ³ã‚¯å…ˆã®æ‰€æœ‰è€…ã§ã‚ã‚‹ã“ã¨ãŒç¢ºèªã•ã‚Œã¾ã—㟠*/ @@ -4990,7 +4788,7 @@ export interface Locale extends ILocale { */ "dateAndTime": string; /** - * ブーストを表示 + * Show boosts */ "showRenotes": string; /** @@ -5046,7 +4844,7 @@ export interface Locale extends ILocale { */ "sourceCode": string; /** - * ソースコードã¯ã¾ã æä¾›ã•ã‚Œã¦ã„ã¾ã›ã‚“。ã“ã®å•é¡Œã®ä¿®æ£ã«ã¤ã„ã¦ç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。 + * The source code is not yet available. Please contact your administrator to fix this problem. */ "sourceCodeIsNotYetProvided": string; /** @@ -5054,7 +4852,7 @@ export interface Locale extends ILocale { */ "repositoryUrl": string; /** - * ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Sharkeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://activitypub.software/TransFem-org/Sharkey/ ã¨è¨˜å…¥ã—ã¾ã™ã€‚ + * If there is a repository where the source code is publicly available, enter its URL. If you are using Sharkey as-is (without any changes to the source code), enter https://activitypub.software/TransFem-org/Sharkey/. */ "repositoryUrlDescription": string; /** @@ -5093,14 +4891,6 @@ export interface Locale extends ILocale { * 利用è¦ç´„・プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼ */ "tosAndPrivacyPolicy": string; - /** - * 寄付ã™ã‚‹ - */ - "donation": string; - /** - * 寄付URL - */ - "donationUrl": string; /** * アイコンデコレーション */ @@ -5178,7 +4968,7 @@ export interface Locale extends ILocale { */ "overwriteContentConfirm": string; /** - * å£ç¯€ã«å¿œã˜ãŸç”»é¢ã®æ¼”出 + * Seasonal screen effects */ "seasonalScreenEffect": string; /** @@ -5294,7 +5084,7 @@ export interface Locale extends ILocale { */ "keepOriginalFilenameDescription": string; /** - * 説明文ã¯ã‚ã‚Šã¾ã›ã‚“ + * No description */ "noDescription": string; /** @@ -5314,7 +5104,7 @@ export interface Locale extends ILocale { */ "confirmWhenRevealingSensitiveMedia": string; /** - * センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã§ã™ã€‚表示ã—ã¾ã™ã‹ï¼Ÿ + * This media might be sensitive. Are you sure you want to reveal it? */ "sensitiveMediaRevealConfirm": string; /** @@ -5325,31 +5115,134 @@ export interface Locale extends ILocale { * 作æˆã—ãŸã‚¢ãƒ³ãƒ†ãƒŠ */ "createdAntennas": string; - "_delivery": { - /** - * é…信状態 - */ - "status": string; - /** - * é…ä¿¡åœæ¢ - */ - "stop": string; - /** - * é…ä¿¡å†é–‹ - */ - "resume": string; - "_type": { - /** - * é…ä¿¡ä¸ - */ - "none": string; - /** - * 手動åœæ¢ä¸ - */ - "manuallySuspended": string; - /** - * サーãƒãƒ¼å‰Šé™¤ã®ãŸã‚åœæ¢ä¸ - */ + /** + * {x}ã‹ã‚‰ + */ + "fromX": ParameterizedString<"x">; + /** + * 埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’ç”Ÿæˆ + */ + "genEmbedCode": string; + /** + * ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆä¸€è¦§ + */ + "noteOfThisUser": string; + /** + * ã“れ以上ã“ã®ã‚¯ãƒªãƒƒãƒ—ã«ãƒŽãƒ¼ãƒˆã‚’è¿½åŠ ã§ãã¾ã›ã‚“。 + */ + "clipNoteLimitExceeded": string; + /** + * パフォーマンス + */ + "performance": string; + /** + * 変更ã‚ã‚Š + */ + "modified": string; + /** + * ç ´æ£„ + */ + "discard": string; + /** + * {n}件ã®å¤‰æ›´ãŒã‚ã‚Šã¾ã™ + */ + "thereAreNChanges": ParameterizedString<"n">; + /** + * パスã‚ーã§ãƒã‚°ã‚¤ãƒ³ + */ + "signinWithPasskey": string; + /** + * 登録ã•ã‚Œã¦ã„ãªã„パスã‚ーã§ã™ã€‚ + */ + "unknownWebAuthnKey": string; + /** + * パスã‚ーã®æ¤œè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ + */ + "passkeyVerificationFailed": string; + /** + * パスã‚ーã®æ¤œè¨¼ã«æˆåŠŸã—ã¾ã—ãŸãŒã€ãƒ‘スワードレスãƒã‚°ã‚¤ãƒ³ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ã€‚ + */ + "passkeyVerificationSucceededButPasswordlessLoginDisabled": string; + /** + * フォãƒãƒ¯ãƒ¼ã¸ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ + */ + "messageToFollower": string; + /** + * 対象 + */ + "target": string; + /** + * CAPTCHAã®ãƒ†ã‚¹ãƒˆã‚’目的ã¨ã—ãŸæ©Ÿèƒ½ã§ã™ã€‚<strong>本番環境ã§ä½¿ç”¨ã—ãªã„ã§ãã ã•ã„。</strong> + */ + "testCaptchaWarning": string; + /** + * ç¦æ¢ãƒ¯ãƒ¼ãƒ‰ï¼ˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã®åå‰ï¼‰ + */ + "prohibitedWordsForNameOfUser": string; + /** + * ã“ã®ãƒªã‚¹ãƒˆã«å«ã¾ã‚Œã‚‹æ–‡å—列ãŒãƒ¦ãƒ¼ã‚¶ãƒ¼ã®åå‰ã«å«ã¾ã‚Œã‚‹å ´åˆã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®åå‰ã®å¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã™ã€‚モデレーター権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã“ã®åˆ¶é™ã®å½±éŸ¿ã‚’å—ã‘ã¾ã›ã‚“。 + */ + "prohibitedWordsForNameOfUserDescription": string; + /** + * 変更ã—よã†ã¨ã—ãŸåå‰ã«ç¦æ¢ã•ã‚ŒãŸæ–‡å—列ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ + */ + "yourNameContainsProhibitedWords": string; + /** + * åå‰ã«ç¦æ¢ã•ã‚Œã¦ã„ã‚‹æ–‡å—列ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ã“ã®åå‰ã‚’使用ã—ãŸã„å ´åˆã¯ã€ã‚µãƒ¼ãƒãƒ¼ç®¡ç†è€…ã«ãŠå•ã„åˆã‚ã›ãã ã•ã„。 + */ + "yourNameContainsProhibitedWordsDescription": string; + "_abuseUserReport": { + /** + * è»¢é€ + */ + "forward": string; + /** + * 匿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦ã€ãƒªãƒ¢ãƒ¼ãƒˆã‚µãƒ¼ãƒãƒ¼ã«é€šå ±ã‚’転é€ã—ã¾ã™ã€‚ + */ + "forwardDescription": string; + /** + * 解決 + */ + "resolve": string; + /** + * æ˜¯èª + */ + "accept": string; + /** + * å¦èª + */ + "reject": string; + /** + * 内容ãŒæ£å½“ã§ã‚ã‚‹é€šå ±ã«å¯¾å¿œã—ãŸå ´åˆã¯ã€Œæ˜¯èªã€ã‚’é¸æŠžã—ã€è‚¯å®šçš„ã«ã‚±ãƒ¼ã‚¹ãŒè§£æ±ºã•ã‚ŒãŸã“ã¨ã‚’マークã—ã¾ã™ã€‚ + * 内容ãŒæ£å½“ã§ãªã„é€šå ±ã®å ´åˆã¯ã€Œå¦èªã€ã‚’é¸æŠžã—ã€å¦å®šçš„ã«ã‚±ãƒ¼ã‚¹ãŒè§£æ±ºã•ã‚ŒãŸã“ã¨ã‚’マークã—ã¾ã™ã€‚ + */ + "resolveTutorial": string; + }; + "_delivery": { + /** + * é…信状態 + */ + "status": string; + /** + * Suspend delivery + */ + "stop": string; + /** + * Resume delivery + */ + "resume": string; + "_type": { + /** + * é…ä¿¡ä¸ + */ + "none": string; + /** + * 手動åœæ¢ä¸ + */ + "manuallySuspended": string; + /** + * サーãƒãƒ¼å‰Šé™¤ã®ãŸã‚åœæ¢ä¸ + */ "goneSuspended": string; /** * サーãƒãƒ¼å¿œç”ãªã—ã®ãŸã‚åœæ¢ä¸ @@ -5460,6 +5353,10 @@ export interface Locale extends ILocale { * オンã«ã™ã‚‹ã¨ã€ã“ã®ãŠçŸ¥ã‚‰ã›ã¯é€šçŸ¥ã•ã‚Œãšã€æ—¢èªã«ã™ã‚‹å¿…è¦ã‚‚ãªããªã‚Šã¾ã™ã€‚ */ "silenceDescription": string; + /** + * New + */ + "new": string; }; "_initialAccountSetting": { /** @@ -5507,7 +5404,7 @@ export interface Locale extends ILocale { */ "haveFun": ParameterizedString<"name">; /** - * ã“ã®ã¾ã¾{name}(Sharkey)ã®ä½¿ã„æ–¹ã«ã¤ã„ã¦ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«é€²ã‚€ã“ã¨ã‚‚ã§ãã¾ã™ãŒã€ã“ã“ã§ä¸æ–ã—ã¦ã™ãã«ä½¿ã„始ã‚ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ + * You can proceed to a tutorial on how to use {name} (Sharkey) or you can exit the setup here and start using it immediately. */ "youCanContinueTutorial": ParameterizedString<"name">; /** @@ -5546,7 +5443,7 @@ export interface Locale extends ILocale { */ "title": string; /** - * ã“ã“ã§ã¯ã€Sharkeyã®åŸºæœ¬çš„ãªä½¿ã„方や機能を確èªã§ãã¾ã™ã€‚ + * Here, you can learn the basics of using Sharkey and its features. */ "description": string; }; @@ -5556,7 +5453,7 @@ export interface Locale extends ILocale { */ "title": string; /** - * Sharkeyã§ã®æŠ•ç¨¿ã¯ã€ŒãƒŽãƒ¼ãƒˆã€ã¨å‘¼ã³ã¾ã™ã€‚ノートã¯ã‚¿ã‚¤ãƒ ラインã«æ™‚系列ã§ä¸¦ã‚“ã§ã„ã¦ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§æ›´æ–°ã•ã‚Œã¦ã„ãã¾ã™ã€‚ + * Posts on Sharkey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time. */ "description": string; /** @@ -5586,7 +5483,7 @@ export interface Locale extends ILocale { */ "description": string; /** - * リアクションã¯ã€ãƒŽãƒ¼ãƒˆã®ã€Œ{reaction}ã€ãƒœã‚¿ãƒ³ã‚’クリックã™ã‚‹ã¨ã¤ã‘られã¾ã™ã€‚試ã—ã«ã“ã®ã‚µãƒ³ãƒ—ルã®ãƒŽãƒ¼ãƒˆã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ã¤ã‘ã¦ã¿ã¦ãã ã•ã„ï¼ + * Reactions can be added by clicking the '{reaction}' button on the note. Try reacting to this sample note! */ "letsTryReacting": ParameterizedString<"reaction">; /** @@ -5598,7 +5495,7 @@ export interface Locale extends ILocale { */ "reactNotification": string; /** - * 「{undo}ã€ãƒœã‚¿ãƒ³ã‚’押ã™ã¨ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’å–り消ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ + * You can undo a reaction by pressing the '{undo}' button. */ "reactDone": ParameterizedString<"undo">; }; @@ -5608,7 +5505,7 @@ export interface Locale extends ILocale { */ "title": string; /** - * Sharkeyã«ã¯ã€ä½¿ã„æ–¹ã«å¿œã˜ã¦è¤‡æ•°ã®ã‚¿ã‚¤ãƒ ラインãŒç”¨æ„ã•ã‚Œã¦ã„ã¾ã™ï¼ˆã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦ã¯ã„ãšã‚Œã‹ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ï¼‰ã€‚ + * Sharkey provides multiple timelines based on usage (some may not be available depending on the server's policies). */ "description1": string; /** @@ -5635,6 +5532,10 @@ export interface Locale extends ILocale { * ãã®ä»–ã«ã‚‚ã€ãƒªã‚¹ãƒˆã‚¿ã‚¤ãƒ ラインやãƒãƒ£ãƒ³ãƒãƒ«ã‚¿ã‚¤ãƒ ラインãªã©ãŒã‚ã‚Šã¾ã™ã€‚詳ã—ãã¯{link}ã‚’ã”覧ãã ã•ã„。 */ "description3": ParameterizedString<"link">; + /** + * You can view notes from connected servers picked by your admins. + */ + "bubble": string; }; "_postNote": { /** @@ -5642,7 +5543,7 @@ export interface Locale extends ILocale { */ "title": string; /** - * Sharkeyã«ãƒŽãƒ¼ãƒˆã‚’投稿ã™ã‚‹éš›ã«ã¯ã€æ§˜ã€…ãªã‚ªãƒ—ションã®è¨å®šãŒå¯èƒ½ã§ã™ã€‚投稿フォームã¯ã“ã®ã‚ˆã†ã«ãªã£ã¦ã„ã¾ã™ã€‚ + * When posting a note on Sharkey, various options are available. The posting form looks like this. */ "description1": string; "_visibility": { @@ -5655,11 +5556,11 @@ export interface Locale extends ILocale { */ "public": string; /** - * ホームタイムラインã®ã¿ã«å…¬é–‹ã€‚フォãƒãƒ¯ãƒ¼ãƒ»ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’見ã«æ¥ãŸäººãƒ»ãƒ–ーストã‹ã‚‰ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚‚見るã“ã¨ãŒã§ãã¾ã™ã€‚ + * Public only on the Home timeline. People visiting your profile, via followers, and through boosts can see it. */ "home": string; /** - * フォãƒãƒ¯ãƒ¼ã«ã®ã¿å…¬é–‹ã€‚本人以外ãŒãƒ–ーストã™ã‚‹ã“ã¨ã¯ã§ããšã€ã¾ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã¯é–²è¦§ã§ãã¾ã›ã‚“。 + * Visible to followers only. Only followers can see it and no one else, and it cannot be boosted by others. */ "followers": string; /** @@ -5738,11 +5639,11 @@ export interface Locale extends ILocale { }; "_done": { /** - * ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã¯çµ‚了ã§ã™ðŸŽ‰ + * The tutorial is complete! 🎉 */ "title": string; /** - * ã“ã“ã§ç´¹ä»‹ã—ãŸæ©Ÿèƒ½ã¯ã»ã‚“ã®ä¸€éƒ¨ã«ã™ãŽã¾ã›ã‚“。Sharkeyã®ä½¿ã„方をより詳ã—ã知るã«ã¯ã€{link}ã‚’ã”覧ãã ã•ã„。 + * The functions introduced here are just a small part. For a more detailed understanding of using Sharkey, please refer to {link}. */ "description": ParameterizedString<"link">; }; @@ -5764,6 +5665,10 @@ export interface Locale extends ILocale { * ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインã§ã¯ã€æŽ¥ç¶šã—ã¦ã„ã‚‹ä»–ã®ã™ã¹ã¦ã®ã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰ã®æŠ•ç¨¿ã‚’見られã¾ã™ã€‚ */ "global": string; + /** + * In the Bubble timeline, you can see notes from connected servers picked by your admins. + */ + "bubble": string; }; "_serverRules": { /** @@ -5817,13 +5722,33 @@ export interface Locale extends ILocale { */ "fanoutTimelineDbFallbackDescription": string; /** - * å•ã„åˆã‚ã›å…ˆURL + * 有効ã«ã™ã‚‹ã¨ã€ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ä½œæˆæ™‚ã®ãƒ‘フォーマンスãŒå¤§å¹…ã«å‘上ã—ã€ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã¸ã®è² è·ã‚’軽減ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚ãŸã ã—ã€Redisã®ãƒ¡ãƒ¢ãƒªä½¿ç”¨é‡ã¯å¢—åŠ ã—ã¾ã™ã€‚ + */ + "reactionsBufferingDescription": string; + /** + * Contact URL */ "inquiryUrl": string; /** - * サーãƒãƒ¼é‹å–¶è€…ã¸ã®ãŠå•ã„åˆã‚ã›ãƒ•ã‚©ãƒ¼ãƒ ã®URLã‚„ã€é‹å–¶è€…ã®é€£çµ¡å…ˆç‰ãŒè¨˜è¼‰ã•ã‚ŒãŸWebページã®URLを指定ã—ã¾ã™ã€‚ + * Specify the URL of a web page that contains a contact form or the instance operators' contact information. */ "inquiryUrlDescription": string; + /** + * 一定期間モデレーターã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティãŒæ¤œå‡ºã•ã‚Œãªã‹ã£ãŸå ´åˆã€ã‚¹ãƒ‘ム防æ¢ã®ãŸã‚ã“ã®è¨å®šã¯è‡ªå‹•ã§ã‚ªãƒ•ã«ãªã‚Šã¾ã™ã€‚ + */ + "thisSettingWillAutomaticallyOffWhenModeratorsInactive": string; + /** + * Logo URL + */ + "sidebarLogoUrl": string; + /** + * Specifies the logo to use instead of the regular icon in high definition, dynamic-width scenarios. + */ + "sidebarLogoDescription": string; + /** + * E.g. In the sidebar, to visitors and in the "About" page. + */ + "sidebarLogoUsageExample": string; }; "_accountMigration": { /** @@ -5857,15 +5782,14 @@ export interface Locale extends ILocale { */ "moveCannotBeUndone": string; /** - * æ–°ã—ã„アカウントã¸ç§»è¡Œã—ã¾ã™ã€‚ - *  ・フォãƒãƒ¯ãƒ¼ãŒæ–°ã—ã„アカウントを自動ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¾ã™ - *  ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã¯å…¨ã¦è§£é™¤ã•ã‚Œã¾ã™ - *  ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã¯ãƒŽãƒ¼ãƒˆã®ä½œæˆãªã©ãŒã§ããªããªã‚Šã¾ã™ + * This will migrate your account to a different one. + *  ・Followers from this account will automatically be migrated to the new account + *  ・This account will unfollow all users it is currently following + *  ・You will be unable to create new notes etc. on this account * - * フォãƒãƒ¯ãƒ¼ã®ç§»è¡Œã¯è‡ªå‹•ã§ã™ãŒã€ãƒ•ã‚©ãƒãƒ¼ã®ç§»è¡Œã¯æ‰‹å‹•ã§è¡Œã†å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚移行å‰ã«ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ãƒ•ã‚©ãƒãƒ¼ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã—ã€ç§»è¡Œå¾Œã™ãã«ç§»è¡Œå…ˆã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¡Œãªã£ã¦ãã ã•ã„。 - * リスト・ミュート・ブãƒãƒƒã‚¯ã«ã¤ã„ã¦ã‚‚åŒæ§˜ã§ã™ã®ã§ã€æ‰‹å‹•ã§ç§»è¡Œã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ + * While migration of followers is automatic, you must manually prepare some steps to migrate the list of users you are following. To do so, carry out a follows export that you will later import on the new account in the settings menu. The same procedure applies to your lists as well as your muted and blocked users. * - * (ã“ã®èª¬æ˜Žã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ï¼ˆSharkey v13.12.0以é™ï¼‰ã®ä»•æ§˜ã§ã™ã€‚Mastodonãªã©ã®ä»–ã®ActivityPubソフトウェアã§ã¯æŒ™å‹•ãŒç•°ãªã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚) + * (This explanation applies to Sharkey v13.12.0 and later. Other ActivityPub software, such as Mastodon, might function differently.) */ "moveAccountDescription": string; /** @@ -5913,7 +5837,7 @@ export interface Locale extends ILocale { */ "description": string; /** - * 良ã„Sharkeyãƒ©ã‚¤ãƒ•ã‚’ï¼ + * Have a good time with Sharkey! */ "flavor": string; }; @@ -6237,7 +6161,7 @@ export interface Locale extends ILocale { */ "description": string; /** - * Sharkeyを使ã£ã¦ãã‚Œã¦ã‚ã‚ŠãŒã¨ã†ï¼ + * Thank you for using Sharkey! */ "flavor": string; }; @@ -6441,11 +6365,11 @@ export interface Locale extends ILocale { */ "title": string; /** - * "I ⤠#Sharkey"を投稿ã—㟠+ * Post "I ⤠#Sharkey" */ "description": string; /** - * Sharkeyを使ã£ã¦ãã ã•ã‚Šã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ï¼ by 開発ãƒãƒ¼ãƒ + * Sharkey's development team greatly appreciates your support! */ "flavor": string; }; @@ -6465,17 +6389,17 @@ export interface Locale extends ILocale { */ "title": string; /** - * クライアントを起動ã—ã¦ã‹ã‚‰30分以上経éŽã—㟠+ * Keep Sharkey opened for at least 30 minutes */ "description": string; }; "_client60min": { /** - * Sharkeyã®è¦‹ã™ãŽ + * No "Miss" in Sharkey */ "title": string; /** - * クライアントを起動ã—ã¦ã‹ã‚‰60分以上経éŽã—㟠+ * Keep Sharkey opened for at least 60 minutes */ "description": string; }; @@ -6695,7 +6619,7 @@ export interface Locale extends ILocale { */ "description": string; /** - * Misskey-Misskey La-Tu-Ma + * Sharkey-Sharkey La-Tu-Ma */ "flavor": string; }; @@ -6711,7 +6635,7 @@ export interface Locale extends ILocale { }; "_tutorialCompleted": { /** - * Sharkeyåˆå¿ƒè€…講座 修了証 + * Sharkey Elementary Course Diploma */ "title": string; /** @@ -6891,10 +6815,6 @@ export interface Locale extends ILocale { * ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインã®é–²è¦§ */ "gtlAvailable": string; - /** - * ãƒãƒ–ルタイムラインã®é–²è¦§ - */ - "btlAvailable": string; /** * ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã®é–²è¦§ */ @@ -6903,10 +6823,6 @@ export interface Locale extends ILocale { * パブリック投稿ã®è¨±å¯ */ "canPublicNote": string; - /** - * ノートã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆãŒå¯èƒ½ - */ - "canImportNotes": string; /** * ノート内ã®æœ€å¤§ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³æ•° */ @@ -6944,7 +6860,7 @@ export interface Locale extends ILocale { */ "alwaysMarkNsfw": string; /** - * アイコンã¨ãƒãƒŠãƒ¼ã®æ›´æ–°ã‚’è¨±å¯ + * Allow users to edit their avatar or banner */ "canUpdateBioMedia": string; /** @@ -7003,6 +6919,34 @@ export interface Locale extends ILocale { * アイコンデコレーションã®æœ€å¤§å–付個数 */ "avatarDecorationLimit": string; + /** + * アンテナã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¨±å¯ + */ + "canImportAntennas": string; + /** + * ブãƒãƒƒã‚¯ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¨±å¯ + */ + "canImportBlocking": string; + /** + * フォãƒãƒ¼ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¨±å¯ + */ + "canImportFollowing": string; + /** + * ミュートã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¨±å¯ + */ + "canImportMuting": string; + /** + * リストã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¨±å¯ + */ + "canImportUserLists": string; + /** + * Can view the bubble timeline + */ + "btlAvailable": string; + /** + * Can import notes + */ + "canImportNotes": string; }; "_condition": { /** @@ -7030,11 +6974,11 @@ export interface Locale extends ILocale { */ "isSuspended": string; /** - * éµã‚¢ã‚«ã‚¦ãƒ³ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ + * Private account */ "isLocked": string; /** - * 「アカウントを見ã¤ã‘ã‚„ã™ãã™ã‚‹ã€ãŒæœ‰åŠ¹ãªãƒ¦ãƒ¼ã‚¶ãƒ¼ + * Account is discoverable */ "isExplorable": string; /** @@ -7135,7 +7079,7 @@ export interface Locale extends ILocale { */ "smtp": string; /** - * ã“ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã§ã¯ç™»éŒ²ã§ãã¾ã›ã‚“ + * This email address is banned */ "banned": string; }; @@ -7167,11 +7111,11 @@ export interface Locale extends ILocale { */ "emailSent": ParameterizedString<"email">; /** - * アカウントãŒä½œæˆã•ã‚Œã€æ‰¿èªå¾…ã¡ã®çŠ¶æ…‹ã§ã™ã€‚ + * Your account has been created and is awaiting approval. */ "approvalPending": string; /** - * インスタンスã«å‚åŠ ã—ãŸã„ç†ç”±ã‚’入力ã—ã¦ãã ã•ã„。 + * Please enter a reason as to why you want to join the instance. */ "reasonInfo": string; }; @@ -7397,11 +7341,11 @@ export interface Locale extends ILocale { }; "_aboutMisskey": { /** - * Sharkeyã¯ã€Misskeyをベースã«ã—ãŸã‚ªãƒ¼ãƒ—ンソースã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚ + * Sharkey is open-source software based on Misskey which has been in development by syuilo since 2014. */ "about": string; /** - * 主ãªã‚³ãƒ³ãƒˆãƒªãƒ“ューター + * コントリビューター */ "contributors": string; /** @@ -7413,29 +7357,21 @@ export interface Locale extends ILocale { */ "source": string; /** - * Misskey オリジナル + * Misskey original */ "original": string; /** - * Sharkey オリジナル - */ - "original_sharkey": string; - /** - * {name}ã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã®Sharkeyを改変ã—ãŸãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’使用ã—ã¦ã„ã¾ã™ã€‚ + * {name} uses a modified version of the original Sharkey. */ "thisIsModifiedVersion": ParameterizedString<"name">; /** - * Sharkeyを翻訳 + * Translate Sharkey */ "translation": string; /** * Misskeyã«å¯„付 */ "donate": string; - /** - * Sharkeyã«å¯„付 - */ - "donate_sharkey": string; /** * ä»–ã«ã‚‚多ãã®æ–¹ãŒæ”¯æ´ã—ã¦ãã‚Œã¦ã„ã¾ã™ã€‚ã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ðŸ¥° */ @@ -7449,7 +7385,15 @@ export interface Locale extends ILocale { */ "projectMembers": string; /** - * テスター + * Sharkey original + */ + "original_sharkey": string; + /** + * Donate to Sharkey + */ + "donate_sharkey": string; + /** + * Testers */ "testers": string; }; @@ -7495,7 +7439,7 @@ export interface Locale extends ILocale { */ "quiet": string; /** - * è¦å‘Šã‚’無効ã«ã™ã‚‹ + * Disable warning */ "disabled": string; }; @@ -7545,7 +7489,7 @@ export interface Locale extends ILocale { */ "nameOnly": string; /** - * ãƒãƒ£ãƒ³ãƒãƒ«å¤–ã¸ã®ãƒ–ーストã¨å¼•ç”¨ãƒ–ーストを許å¯ã™ã‚‹ + * Allow boosts and quote outside the channel */ "allowRenoteToExternal": string; }; @@ -7583,7 +7527,7 @@ export interface Locale extends ILocale { }; "_instanceMute": { /** - * ミュートã—ãŸã‚µãƒ¼ãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¸ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚µãƒ¼ãƒãƒ¼ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨ãƒ–ーストをミュートã—ã¾ã™ã€‚ + * This will mute any notes/boosts from the listed instances, including those of users replying to a user from a muted instance. */ "instanceMuteDescription": string; /** @@ -7845,10 +7789,6 @@ export interface Locale extends ILocale { * 入力ボックスã®ç¸å–ã‚Š */ "inputBorder": string; - /** - * ãƒªã‚¹ãƒˆé …ç›®ã®èƒŒæ™¯ (ホãƒãƒ¼) - */ - "listItemHoverBg": string; /** * ドライブフォルダーã®èƒŒæ™¯ */ @@ -7919,11 +7859,11 @@ export interface Locale extends ILocale { */ "driveFileDurationWarn": string; /** - * é•·ã„音声を使用ã™ã‚‹ã¨Sharkeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ + * Long audio may disrupt using Sharkey. Still continue? */ "driveFileDurationWarnDescription": string; /** - * 音声ãŒèªã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚è¨å®šã‚’変更ã—ã¦ãã ã•ã„ + * The audio couldn't be loaded. Please make sure you selected an audio file. */ "driveFileError": string; }; @@ -8127,7 +8067,7 @@ export interface Locale extends ILocale { */ "backupCodesExhaustedWarning": string; /** - * 詳細ãªã‚¬ã‚¤ãƒ‰ã¯ã“ã¡ã‚‰ + * Click here for a detailed guide */ "moreDetailedGuideHere": string; }; @@ -8506,6 +8446,10 @@ export interface Locale extends ILocale { * アプリケーションã«ã‚¢ã‚¯ã‚»ã‚¹è¨±å¯ã‚’与ãˆã‚‹ã«ã¯ã€ãƒã‚°ã‚¤ãƒ³ãŒå¿…è¦ã§ã™ã€‚ */ "pleaseLogin": string; + /** + * Allowed + */ + "allowed": string; }; "_antennaSources": { /** @@ -8674,14 +8618,14 @@ export interface Locale extends ILocale { * クリッカー */ "clicker": string; - /** - * 検索 - */ - "search": string; /** * 今日誕生日ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ */ "birthdayFollowings": string; + /** + * Search + */ + "search": string; }; "_cw": { /** @@ -8787,7 +8731,7 @@ export interface Locale extends ILocale { */ "remainingSeconds": ParameterizedString<"s">; /** - * 複数ã®é¸æŠžè‚¢ + * Multiple choices */ "multiple": string; }; @@ -8919,33 +8863,45 @@ export interface Locale extends ILocale { */ "changeBanner": string; /** - * æ›´æ–°ãƒãƒŠãƒ¼ + * 内容ã«URLã‚’è¨å®šã™ã‚‹ã¨ã€ãƒªãƒ³ã‚¯å…ˆã®Webサイトã«è‡ªåˆ†ã®ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã¸ã®ãƒªãƒ³ã‚¯ãŒå«ã¾ã‚Œã¦ã„ã‚‹å ´åˆã«æ‰€æœ‰è€…確èªæ¸ˆã¿ã‚¢ã‚¤ã‚³ãƒ³ã‚’表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "verifiedLinkDescription": string; + /** + * 最大{max}ã¤ã¾ã§ãƒ‡ã‚³ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã‚’付ã‘られã¾ã™ã€‚ + */ + "avatarDecorationMax": ParameterizedString<"max">; + /** + * フォãƒãƒ¼ã•ã‚ŒãŸæ™‚ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ + */ + "followedMessage": string; + /** + * フォãƒãƒ¼ã•ã‚ŒãŸæ™‚ã«ç›¸æ‰‹ã«è¡¨ç¤ºã™ã‚‹çŸã„メッセージをè¨å®šã§ãã¾ã™ã€‚ + */ + "followedMessageDescription": string; + /** + * フォãƒãƒ¼ã‚’承èªåˆ¶ã«ã—ã¦ã„ã‚‹å ´åˆã€ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’許å¯ã—ãŸæ™‚ã«è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ + */ + "followedMessageDescriptionForLockedAccount": string; + /** + * Update banner */ "updateBanner": string; /** - * ãƒãƒŠãƒ¼ã‚’削除 + * Remove banner */ "removeBanner": string; /** - * 背景を変更ã™ã‚‹ + * Change background */ "changeBackground": string; /** - * 背景を更新ã™ã‚‹ + * Update background */ "updateBackground": string; /** - * 背景を削除ã™ã‚‹ + * Remove background */ "removeBackground": string; - /** - * 内容ã«URLã‚’è¨å®šã™ã‚‹ã¨ã€ãƒªãƒ³ã‚¯å…ˆã®Webサイトã«è‡ªåˆ†ã®ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã¸ã®ãƒªãƒ³ã‚¯ãŒå«ã¾ã‚Œã¦ã„ã‚‹å ´åˆã«æ‰€æœ‰è€…確èªæ¸ˆã¿ã‚¢ã‚¤ã‚³ãƒ³ã‚’表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "verifiedLinkDescription": string; - /** - * 最大{max}ã¤ã¾ã§ãƒ‡ã‚³ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã‚’付ã‘られã¾ã™ã€‚ - */ - "avatarDecorationMax": ParameterizedString<"max">; }; "_exportOrImport": { /** @@ -9107,7 +9063,7 @@ export interface Locale extends ILocale { */ "global": string; /** - * ãƒãƒƒãƒƒãƒ–ル + * Bubble */ "bubble": string; }; @@ -9352,7 +9308,7 @@ export interface Locale extends ILocale { */ "dynamic": string; /** - * ã“ã®ãƒ–ãƒãƒƒã‚¯ã¯å»ƒæ¢ã•ã‚Œã¦ã„ã¾ã™ã€‚今後ã¯{play}を利用ã—ã¦ãã ã•ã„。 + * This block type has been removed. Please use {play} from now on. */ "dynamicDescription": ParameterizedString<"play">; /** @@ -9407,7 +9363,7 @@ export interface Locale extends ILocale { */ "youGotQuote": ParameterizedString<"name">; /** - * {name}ãŒBoostã—ã¾ã—㟠+ * Boost from {name} */ "youRenoted": ParameterizedString<"name">; /** @@ -9426,10 +9382,6 @@ export interface Locale extends ILocale { * アンケートã®çµæžœãŒå‡ºã¾ã—㟠*/ "pollEnded": string; - /** - * 投稿ãŒç·¨é›†ã•ã‚Œã¾ã—㟠- */ - "edited": string; /** * æ–°ã—ã„投稿 */ @@ -9475,7 +9427,7 @@ export interface Locale extends ILocale { */ "likedBySomeUsers": ParameterizedString<"n">; /** - * {n}人ãŒãƒªãƒŽãƒ¼ãƒˆã—ã¾ã—㟠+ * Boosted by {n} users */ "renotedBySomeUsers": ParameterizedString<"n">; /** @@ -9486,6 +9438,14 @@ export interface Locale extends ILocale { * 通知ã®å±¥æ´ã‚’リセットã™ã‚‹ */ "flushNotification": string; + /** + * {x}ã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆãŒå®Œäº†ã—ã¾ã—㟠+ */ + "exportOfXCompleted": ParameterizedString<"x">; + /** + * ãƒã‚°ã‚¤ãƒ³ãŒã‚ã‚Šã¾ã—㟠+ */ + "login": string; "_types": { /** * ã™ã¹ã¦ @@ -9508,7 +9468,7 @@ export interface Locale extends ILocale { */ "reply": string; /** - * Boost + * Boosts */ "renote": string; /** @@ -9539,12 +9499,24 @@ export interface Locale extends ILocale { * 実績ã®ç²å¾— */ "achievementEarned": string; + /** + * エクスãƒãƒ¼ãƒˆãŒå®Œäº†ã—㟠+ */ + "exportCompleted": string; + /** + * ãƒã‚°ã‚¤ãƒ³ + */ + "login": string; + /** + * 通知ã®ãƒ†ã‚¹ãƒˆ + */ + "test": string; /** * 連æºã‚¢ãƒ—リã‹ã‚‰ã®é€šçŸ¥ */ "app": string; /** - * 編集済㿠+ * Edits */ "edited": string; }; @@ -9558,10 +9530,14 @@ export interface Locale extends ILocale { */ "reply": string; /** - * ブースト + * Boost */ "renote": string; }; + /** + * Note got edited + */ + "edited": string; }; "_deck": { /** @@ -9760,7 +9736,7 @@ export interface Locale extends ILocale { */ "reply": string; /** - * Boostã•ã‚ŒãŸã¨ã + * When boosted */ "renote": string; /** @@ -9778,18 +9754,30 @@ export interface Locale extends ILocale { */ "abuseReport": string; /** - * ユーザーã‹ã‚‰ã®é€šå ±ã‚’処ç†ã—ãŸã¨ã + * When resolved abuse reports */ "abuseReportResolved": string; /** * ユーザーãŒä½œæˆã•ã‚ŒãŸã¨ã */ "userCreated": string; + /** + * モデレーターãŒä¸€å®šæœŸé–“éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã«ãªã£ãŸã¨ã + */ + "inactiveModeratorsWarning": string; + /** + * モデレーターãŒä¸€å®šæœŸé–“éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã ã£ãŸãŸã‚ã€ã‚·ã‚¹ãƒ†ãƒ ã«ã‚ˆã‚Šæ‹›å¾…制ã¸ã¨å¤‰æ›´ã•ã‚ŒãŸã¨ã + */ + "inactiveModeratorsInvitationOnlyChanged": string; }; /** * Webhookを削除ã—ã¾ã™ã‹ï¼Ÿ */ "deleteConfirm": string; + /** + * スイッãƒã®å³ã«ã‚るボタンをクリックã™ã‚‹ã¨ãƒ€ãƒŸãƒ¼ã®ãƒ‡ãƒ¼ã‚¿ã‚’使用ã—ãŸãƒ†ã‚¹ãƒˆç”¨Webhookã‚’é€ä¿¡ã§ãã¾ã™ã€‚ + */ + "testRemarks": string; }; "_abuseReport": { "_notificationRecipient": { @@ -9816,11 +9804,11 @@ export interface Locale extends ILocale { "webhook": string; "_captions": { /** - * モデレーター権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã®ã¿) + * Send an email to the moderators when an abuse report is received. */ "mail": string; /** - * 指定ã—ãŸSystemWebhookã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã¨é€šå ±ã‚’解決ã—ãŸæ™‚ã«ãã‚Œãžã‚Œç™ºä¿¡) + * Send a notification to the SystemWebhook when an abuse report is received or resolved. */ "webhook": string; }; @@ -9864,10 +9852,6 @@ export interface Locale extends ILocale { * ãƒãƒ¼ãƒ«ã®ã‚¢ã‚µã‚¤ãƒ³è§£é™¤ */ "unassignRole": string; - /** - * 承èªæ¸ˆã¿ - */ - "approve": string; /** * å‡çµ */ @@ -9956,6 +9940,14 @@ export interface Locale extends ILocale { * é€šå ±ã‚’è§£æ±º */ "resolveAbuseReport": string; + /** + * é€šå ±ã‚’è»¢é€ + */ + "forwardAbuseReport": string; + /** + * é€šå ±ã®ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãƒŽãƒ¼ãƒˆæ›´æ–° + */ + "updateAbuseReportNote": string; /** * æ‹›å¾…ã‚³ãƒ¼ãƒ‰ã‚’ä½œæˆ */ @@ -10032,362 +10024,64 @@ export interface Locale extends ILocale { * ギャラリーã®æŠ•ç¨¿ã‚’削除 */ "deleteGalleryPost": string; - }; - "_mfm": { - /** - * ã“ã®æ©Ÿèƒ½ã¯ä¸€èˆ¬çš„ã«æ™®åŠã—ã¦ã„ãªã„ãŸã‚ã€ä»–ã®Misskeyフォークをå«ã‚ãŸå¤šãã®Fediverseソフトウェアã§è¡¨ç¤ºã§ããªã„ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚ - */ - "uncommonFeature": string; - /** - * MFM ã¯Misskey, Sharkey, Firefish, Akkomaãªã©ã€å¤šãã®å ´æ‰€ã§ä½¿ç”¨ã§ãるマークアップ言語ã§ã™ã€‚ã“ã“ã§ã¯ã€åˆ©ç”¨ã§ãã‚‹MFM構文ã®ä¸€è¦§ã‚’ã”覧ã„ãŸã ã‘ã¾ã™ã€‚ - */ - "intro": string; - /** - * Sharkeyã§Fediverseã®ä¸–ç•ŒãŒåºƒãŒã‚Šã¾ã™ - */ - "dummy": string; - /** - * メンション - */ - "mention": string; - /** - * アットマーク + ユーザーåã§ã€ç‰¹å®šã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "mentionDescription": string; /** - * ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚° + * Approved */ - "hashtag": string; - /** - * ナンãƒãƒ¼ã‚µã‚¤ãƒ³ + ã‚¿ã‚°ã§ã€ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "hashtagDescription": string; - /** - * URL - */ - "url": string; - /** - * URLを示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "urlDescription": string; + "approve": string; /** - * リンク + * Set remote instance as NSFW */ - "link": string; + "setRemoteInstanceNSFW": string; /** - * æ–‡ç« ã®ç‰¹å®šã®ç¯„囲をã€URLã«ç´ã¥ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + * Set remote instance as NSFW */ - "linkDescription": string; + "unsetRemoteInstanceNSFW": string; /** - * å¤ªå— + * Rejected reports from remote instance */ - "bold": string; + "rejectRemoteInstanceReports": string; /** - * æ–‡å—を太ã表示ã—ã¦å¼·èª¿ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + * Accepted reports from remote instance */ - "boldDescription": string; + "acceptRemoteInstanceReports": string; + }; + "_fileViewer": { /** - * å°æ–‡å— + * ファイルã®è©³ç´° */ - "small": string; + "title": string; /** - * 内容をå°ã•ã・薄ã表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + * ファイルタイプ */ - "smallDescription": string; + "type": string; /** - * ä¸å¤®å¯„ã› + * ファイルサイズ */ - "center": string; + "size": string; /** - * 内容をä¸å¤®å¯„ã›ã§è¡¨ç¤ºã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + * URL */ - "centerDescription": string; + "url": string; /** - * コード(インライン) + * è¿½åŠ æ—¥ */ - "inlineCode": string; + "uploadedAt": string; /** - * プãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’インラインã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚ + * 添付ã•ã‚Œã¦ã„るノート */ - "inlineCodeDescription": string; + "attachedNotes": string; /** - * コード(ブãƒãƒƒã‚¯ï¼‰ + * ã“ã®ãƒšãƒ¼ã‚¸ã¯ã€ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã—ã‹é–²è¦§ã§ãã¾ã›ã‚“。 */ - "blockCode": string; + "thisPageCanBeSeenFromTheAuthor": string; + }; + "_externalResourceInstaller": { /** - * 複数行ã®ãƒ—ãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’ブãƒãƒƒã‚¯ã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚ + * 外部サイトã‹ã‚‰ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ« */ - "blockCodeDescription": string; + "title": string; /** - * æ•°å¼ï¼ˆã‚¤ãƒ³ãƒ©ã‚¤ãƒ³ï¼‰ - */ - "inlineMath": string; - /** - * æ•°å¼ ï¼ˆKaTeXå½¢å¼ï¼‰ã‚’インラインã§è¡¨ç¤ºã—ã¾ã™ã€‚ - */ - "inlineMathDescription": string; - /** - * æ•°å¼ï¼ˆãƒ–ãƒãƒƒã‚¯ï¼‰ - */ - "blockMath": string; - /** - * æ•°å¼ ï¼ˆKaTeXå½¢å¼ï¼‰ã‚’ブãƒãƒƒã‚¯ã§è¡¨ç¤ºã—ã¾ã™ã€‚ - */ - "blockMathDescription": string; - /** - * 引用 - */ - "quote": string; - /** - * 内容ãŒå¼•ç”¨ã§ã‚ã‚‹ã“ã¨ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "quoteDescription": string; - /** - * ã‚«ã‚¹ã‚¿ãƒ çµµæ–‡å— - */ - "emoji": string; - /** - * コãƒãƒ³ã§ã‚«ã‚¹ã‚¿ãƒ 絵文å—åを囲むã¨ã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—を表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "emojiDescription": string; - /** - * 検索 - */ - "search": string; - /** - * 検索ボックスを表示ã§ãã¾ã™ã€‚ - */ - "searchDescription": string; - /** - * å転 - */ - "flip": string; - /** - * 内容を上下ã¾ãŸã¯å·¦å³ã«å転ã•ã›ã¾ã™ã€‚ - */ - "flipDescription": string; - /** - * アニメーション(ã³ã‚ˆã‚“ã³ã‚ˆã‚“) - */ - "jelly": string; - /** - * ゼリーãŒæºã‚Œã‚‹ã‚ˆã†ãªæ„Ÿã˜ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚ - */ - "jellyDescription": string; - /** - * アニメーション(ã˜ã‚ƒãƒ¼ã‚“) - */ - "tada": string; - /** - * 「ã˜ã‚ƒãƒ¼ã‚“ï¼ã€ã¨å¼·èª¿ã™ã‚‹ã‚ˆã†ãªæ„Ÿã˜ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚ - */ - "tadaDescription": string; - /** - * アニメーション(ジャンプ) - */ - "jump": string; - /** - * è·³ãるアニメーションをã•ã›ã¾ã™ã€‚ - */ - "jumpDescription": string; - /** - * アニメーション(ãƒã‚¦ãƒ³ãƒ‰ï¼‰ - */ - "bounce": string; - /** - * è·³ãã¦ç€åœ°ã™ã‚‹ã‚ˆã†ãªã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚ - */ - "bounceDescription": string; - /** - * アニメーション(ã¶ã‚‹ã¶ã‚‹ï¼‰ - */ - "shake": string; - /** - * 震ãˆã‚‹ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚ - */ - "shakeDescription": string; - /** - * アニメーション(ガタガタ) - */ - "twitch": string; - /** - * より激ã—ã震ãˆã‚‹ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚ - */ - "twitchDescription": string; - /** - * アニメーション(回転) - */ - "spin": string; - /** - * 内容を回転ã•ã›ã¾ã™ã€‚ - */ - "spinDescription": string; - /** - * 大 - */ - "x2": string; - /** - * 内容を大ãã表示ã•ã›ã¾ã™ã€‚ - */ - "x2Description": string; - /** - * 特大 - */ - "x3": string; - /** - * 内容をより大ãã表示ã•ã›ã¾ã™ã€‚ - */ - "x3Description": string; - /** - * 超特大 - */ - "x4": string; - /** - * 内容をã•ã‚‰ã«å¤§ãã表示ã•ã›ã¾ã™ã€‚ - */ - "x4Description": string; - /** - * ã¼ã‹ã— - */ - "blur": string; - /** - * 内容をã¼ã‹ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ãƒã‚¤ãƒ³ã‚¿ãƒ¼ã‚’上ã«ä¹—ã›ã‚‹ã¨ã¯ã£ãり見ãˆã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚ - */ - "blurDescription": string; - /** - * フォント - */ - "font": string; - /** - * 内容ã®ãƒ•ã‚©ãƒ³ãƒˆã‚’指定ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "fontDescription": string; - /** - * レインボー - */ - "rainbow": string; - /** - * 内容を虹色ã§è¡¨ç¤ºã•ã›ã¾ã™ã€‚ - */ - "rainbowDescription": string; - /** - * ã‚ラã‚ラ - */ - "sparkle": string; - /** - * ã‚ラã‚ラã¨æ˜Ÿåž‹ã®ãƒ‘ーティクルを表示ã•ã›ã¾ã™ã€‚ - */ - "sparkleDescription": string; - /** - * 角度変更 - */ - "rotate": string; - /** - * 指定ã—ãŸè§’度ã§å›žè»¢ã•ã›ã¾ã™ã€‚ - */ - "rotateDescription": string; - /** - * ä½ç½®å¤‰æ›´ - */ - "position": string; - /** - * ä½ç½®ã‚’ãšã‚‰ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ - */ - "positionDescription": string; - /** - * 切りå–ã‚Š - */ - "crop": string; - /** - * 内容を切り抜ãã¾ã™ã€‚ - */ - "cropDescription": string; - /** - * マウス追従 - */ - "followMouse": string; - /** - * 内容ãŒãƒžã‚¦ã‚¹ã«è¿½å¾“ã—ã¾ã™ã€‚スマホã®å ´åˆã¯ã‚¿ãƒƒãƒ—ã—ãŸå ´æ‰€ã«è¿½å¾“ã—ã¾ã™ã€‚ - */ - "followMouseDescription": string; - /** - * 拡大 - */ - "scale": string; - /** - * 内容を引ã伸ã°ã—ã¦è¡¨ç¤ºã—ã¾ã™ã€‚ - */ - "scaleDescription": string; - /** - * æ–‡å—色 - */ - "foreground": string; - /** - * æ–‡å—色を変更ã—ã¾ã™ã€‚ - */ - "foregroundDescription": string; - /** - * フェード - */ - "fade": string; - /** - * 内容をフェードイン・フェードアウトã•ã›ã¾ã™ã€‚ - */ - "fadeDescription": string; - /** - * 背景色 - */ - "background": string; - /** - * 背景色を変更ã—ã¾ã™ã€‚ - */ - "backgroundDescription": string; - /** - * Plain - */ - "plain": string; - /** - * 内å´ã®æ§‹æ–‡ã‚’å…¨ã¦ç„¡åŠ¹ã«ã—ã¾ã™ã€‚ - */ - "plainDescription": string; - }; - "_fileViewer": { - /** - * ファイルã®è©³ç´° - */ - "title": string; - /** - * ファイルタイプ - */ - "type": string; - /** - * ファイルサイズ - */ - "size": string; - /** - * URL - */ - "url": string; - /** - * è¿½åŠ æ—¥ - */ - "uploadedAt": string; - /** - * 添付ã•ã‚Œã¦ã„るノート - */ - "attachedNotes": string; - /** - * ã“ã®ãƒšãƒ¼ã‚¸ã¯ã€ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップãƒãƒ¼ãƒ‰ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã—ã‹é–²è¦§ã§ãã¾ã›ã‚“。 - */ - "thisPageCanBeSeenFromTheAuthor": string; - }; - "_externalResourceInstaller": { - /** - * 外部サイトã‹ã‚‰ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ« - */ - "title": string; - /** - * é…布元ãŒä¿¡é ¼ã§ãã‚‹ã‹ã‚’確èªã—ãŸä¸Šã§ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã—ã¦ãã ã•ã„。 + * é…布元ãŒä¿¡é ¼ã§ãã‚‹ã‹ã‚’確èªã—ãŸä¸Šã§ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã—ã¦ãã ã•ã„。 */ "checkVendorBeforeInstall": string; "_plugin": { @@ -10517,44 +10211,6 @@ export interface Locale extends ILocale { }; }; }; - "_animatedMFM": { - /** - * MFMアニメーションをå†ç”Ÿ - */ - "play": string; - /** - * MFMアニメーションåœæ¢ - */ - "stop": string; - "_alert": { - /** - * MFMアニメーションã«ã¯ã€é«˜é€Ÿã§ç‚¹æ»…ã—ãŸã‚Šå‹•ã„ãŸã‚Šã™ã‚‹ãƒ†ã‚スト・絵文å—ã‚’å«ã‚€å ´åˆãŒã‚ã‚Šã¾ã™ã€‚ - */ - "text": string; - /** - * å†ç”Ÿã™ã‚‹ - */ - "confirm": string; - }; - }; - "_dataRequest": { - /** - * データリクエスト - */ - "title": string; - /** - * データリクエストã¯3æ—¥ã”ã¨ã«å¯èƒ½ã§ã™ã€‚ - */ - "warn": string; - /** - * データã®ä¿å˜ãŒå®Œäº†ã™ã‚‹ã¨ã€ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç™»éŒ²ã•ã‚Œã¦ã„るメールアドレスã«ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚ - */ - "text": string; - /** - * データリクエスト実行 - */ - "button": string; - }; "_dataSaver": { "_media": { /** @@ -10572,7 +10228,7 @@ export interface Locale extends ILocale { */ "title": string; /** - * アイコン画åƒã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ãŒåœæ¢ã—ã¾ã™ã€‚アニメーション画åƒã¯é€šå¸¸ã®ç”»åƒã‚ˆã‚Šãƒ•ã‚¡ã‚¤ãƒ«ã‚µã‚¤ã‚ºãŒå¤§ãã„ã“ã¨ãŒã‚ã‚‹ã®ã§ã€ãƒ‡ãƒ¼ã‚¿é€šä¿¡é‡ã‚’ã•ã‚‰ã«å‰Šæ¸›ã§ãã¾ã™ã€‚ + * Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic. */ "description": string; }; @@ -10809,7 +10465,7 @@ export interface Locale extends ILocale { */ "timeout": string; /** - * プレビューå–å¾—ã®æ‰€è¦æ™‚é–“ãŒã“ã®å€¤ã‚’超ãˆãŸå ´åˆã€ãƒ—レビューã¯ç”Ÿæˆã•ã‚Œã¾ã›ã‚“。 + * If it takes longer than this value to get the preview, the preview won't be generated. */ "timeoutDescription": string; /** @@ -10821,7 +10477,7 @@ export interface Locale extends ILocale { */ "maximumContentLengthDescription": string; /** - * Content-LengthãŒå–å¾—ã§ããŸå ´åˆã®ã¿ãƒ—ãƒ¬ãƒ“ãƒ¥ãƒ¼ã‚’ç”Ÿæˆ + * Generate the preview only if we can get Content-Length */ "requireContentLength": string; /** @@ -10837,15 +10493,15 @@ export interface Locale extends ILocale { */ "userAgentDescription": string; /** - * プレビューを生æˆã™ã‚‹ãƒ—ãƒã‚ã‚·ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆ + * Endpoint for proxy to generate previews */ "summaryProxy": string; /** - * Misskey本体ã§ã¯ãªãã€ã‚µãƒžãƒªãƒ¼ãƒ—ãƒã‚シを使用ã—ã¦ãƒ—レビューを生æˆã—ã¾ã™ã€‚ + * Generate previews using Summaly Proxy, instead of Sharkey itself. */ "summaryProxyDescription": string; /** - * プãƒã‚ã‚·ã«ã¯ä¸‹è¨˜ãƒ‘ラメータãŒã‚¯ã‚¨ãƒªæ–‡å—列ã¨ã—ã¦é€£æºã•ã‚Œã¾ã™ã€‚プãƒã‚ã‚·å´ãŒã“れらをサãƒãƒ¼ãƒˆã—ãªã„å ´åˆã€è¨å®šå€¤ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚ + * The following parameters are sent to the proxy as a query string. If the proxy does not support them, the values are ignored. */ "summaryProxyDescription2": string; }; @@ -10881,6 +10537,843 @@ export interface Locale extends ILocale { */ "native": string; }; + "_embedCodeGen": { + /** + * 埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’カスタマイズ + */ + "title": string; + /** + * ヘッダーを表示 + */ + "header": string; + /** + * 自動ã§ç¶šãã‚’èªã¿è¾¼ã‚€ï¼ˆéžæŽ¨å¥¨ï¼‰ + */ + "autoload": string; + /** + * 高ã•ã®æœ€å¤§å€¤ + */ + "maxHeight": string; + /** + * 0ã§æœ€å¤§å€¤ã®è¨å®šãŒç„¡åŠ¹ã«ãªã‚Šã¾ã™ã€‚ウィジェットãŒç¸¦ã«ä¼¸ã³ç¶šã‘ã‚‹ã®ã‚’防ããŸã‚ã«ã€ä½•ã‚‰ã‹ã®å€¤ã«æŒ‡å®šã—ã¦ãã ã•ã„。 + */ + "maxHeightDescription": string; + /** + * 高ã•ã®æœ€å¤§å€¤åˆ¶é™ãŒç„¡åŠ¹ï¼ˆ0)ã«ãªã£ã¦ã„ã¾ã™ã€‚ã“ã‚ŒãŒæ„図ã—ãŸå¤‰æ›´ã§ã¯ãªã„å ´åˆã¯ã€é«˜ã•ã®æœ€å¤§å€¤ã‚’何らã‹ã®å€¤ã«è¨å®šã—ã¦ãã ã•ã„。 + */ + "maxHeightWarn": string; + /** + * プレビュー画é¢ã§è¡¨ç¤ºå¯èƒ½ãªç¯„囲を超ãˆãŸãŸã‚ã€å®Ÿéš›ã«åŸ‹ã‚込んã éš›ã¨ã¯è¡¨ç¤ºãŒç•°ãªã‚Šã¾ã™ã€‚ + */ + "previewIsNotActual": string; + /** + * 角丸ã«ã™ã‚‹ + */ + "rounded": string; + /** + * å¤–æž ã«æž ç·šã‚’ã¤ã‘ã‚‹ + */ + "border": string; + /** + * プレビューã«åæ˜ + */ + "applyToPreview": string; + /** + * 埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’ä½œæˆ + */ + "generateCode": string; + /** + * コードãŒç”Ÿæˆã•ã‚Œã¾ã—㟠+ */ + "codeGenerated": string; + /** + * 生æˆã•ã‚ŒãŸã‚³ãƒ¼ãƒ‰ã‚’ウェブサイトã«è²¼ã‚Šä»˜ã‘ã¦ã”利用ãã ã•ã„。 + */ + "codeGeneratedDescription": string; + }; + /** + * Approvals + */ + "approvals": string; + /** + * Open remote profile + */ + "openRemoteProfile": string; + /** + * Link to external site warning exclusion list + */ + "trustedLinkUrlPatterns": string; + /** + * Separate with spaces for an AND condition or with line breaks for an OR condition. Using surrounding keywords with slashes will turn them into a regular expression. If you write only the domain name, it will be a backward match. + */ + "trustedLinkUrlPatternsDescription": string; + /** + * Mutuals + */ + "mutuals": string; + /** + * Private account + */ + "isLocked": string; + /** + * Administrator + */ + "isAdmin": string; + /** + * Bot user + */ + "isBot": string; + /** + * Open + */ + "open": string; + /** + * Destination address + */ + "emailDestination": string; + /** + * Date + */ + "date": string; + /** + * Quoted. + */ + "quoted": string; + /** + * Unboosted. + */ + "rmboost": string; + /** + * Muted + */ + "muted": string; + /** + * Boosts muted + */ + "renoteMuted": string; + /** + * Mark all media from user as NSFW + */ + "markAsNSFW": string; + /** + * Mark as NSFW + */ + "markInstanceAsNSFW": string; + /** + * Are you sure that you want to mark all media from this account as NSFW? + */ + "nsfwConfirm": string; + /** + * Are you sure that you want to unmark all media from this account as NSFW? + */ + "unNsfwConfirm": string; + /** + * Are you sure that you want to approve this account? + */ + "approveConfirm": string; + /** + * Speak as a cat + */ + "flagSpeakAsCat": string; + /** + * Your posts will get nyanified when in cat mode. If this isn't working, then please check that you dont have 'Disable cat speak' on under General/Note Display + */ + "flagSpeakAsCatDescription": string; + /** + * Reject reports from this instance + */ + "rejectReports": string; + /** + * This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s). + */ + "blockedByBase": string; + /** + * This host is silenced implicitly because a base domain is silenced. To un-silence this host, first un-silence the base domain(s). + */ + "silencedByBase": string; + /** + * This host's media is silenced implicitly because a base domain's media is silenced. To un-silence this host, first un-silence the base domain(s). + */ + "mediaSilencedByBase": string; + /** + * Search drive + */ + "driveSearchbarPlaceholder": string; + /** + * Background + */ + "background": string; + /** + * Show content for all replies + */ + "expandAllCws": string; + /** + * Hide content for all replies + */ + "collapseAllCws": string; + /** + * Don't use drawer-style menus + */ + "disableDrawer": string; + /** + * Corner roundness + */ + "cornerRadius": string; + /** + * Warn you when you forget to put alt text + */ + "warnForMissingAltText": string; + /** + * Use DeepLX-JS (No Auth Key) + */ + "deeplFreeMode": string; + /** + * Need Help? Check our documentation to know how to setup DeepLX-JS. + */ + "deeplFreeModeDescription": string; + /** + * Deletion of all files queued + */ + "deleteAllFilesQueued": string; + /** + * This is a system account + */ + "systemAccountTitle": string; + /** + * This account is created and managed automatically by the system, and cannot be logged into. + */ + "systemAccountDescription": string; + /** + * post is hidden by a filter + */ + "postFiltered": string; + /** + * Enable favicon notification dot + */ + "enableFaviconNotificationDot": string; + /** + * Check if the notification dot works on your instance + */ + "verifyNotificationDotWorkingButton": string; + /** + * Unfortunately, this instance does not support the notification dot feature at this time. + */ + "notificationDotNotWorking": string; + /** + * The notification dot is functioning properly on this instance. + */ + "notificationDotWorking": string; + /** + * If the notification dot doesn't work, ask an admin to check our documentation {link} + */ + "notificationDotNotWorkingAdvice": ParameterizedString<"link">; + /** + * Forward report to remote instance + */ + "forwardReport": string; + /** + * Instead of your account, an anonymous system account will be displayed as reporter at the remote instance. + */ + "forwardReportIsAnonymous": string; + /** + * Mark report as resolved + */ + "abuseMarkAsResolved": string; + /** + * Sharkey specific changes are translated in its own {link}. + */ + "i18nInfoSharkey": ParameterizedString<"link">; + /** + * Show instance ticker on replies + */ + "showTickerOnReplies": string; + /** + * Disable cat speak + */ + "disableCatSpeak": string; + /** + * Search Engine For Search MFM + */ + "searchEngine": string; + /** + * Other + */ + "searchEngineOther": string; + /** + * The custom URI must be input in the format like "https://www.google.com/search?q=\{query}" or "https://www.google.com/search?q=%s". + */ + "searchEngineCustomURIDescription": string; + /** + * Custom URI + */ + "searchEngineCusomURI": string; + /** + * Make public notes not indexable + */ + "makeIndexable": string; + /** + * Stop note search from indexing your public notes. + */ + "makeIndexableDescription": string; + /** + * Require approval for new users + */ + "approvalRequiredForSignup": string; + /** + * Confirm your vote for "{choice}"? + *  You can choose more options after confirmation. + */ + "voteConfirmMulti": ParameterizedString<"choice">; + /** + * There are users awaiting approval. + */ + "pendingUserApprovals": string; + /** + * Approve + */ + "approveAccount": string; + /** + * Deny & Delete + */ + "denyAccount": string; + /** + * Approved + */ + "approved": string; + /** + * Not Approved + */ + "notApproved": string; + /** + * Approval Status + */ + "approvalStatus": string; + /** + * Number of replies in a thread + */ + "numberOfReplies": string; + /** + * Increasing this number will display more replies. Setting this too high can cause replies to be cramped and unreadable. + */ + "numberOfRepliesDescription": string; + /** + * Boost Settings + */ + "boostSettings": string; + /** + * Show Visibility Selector + */ + "showVisibilitySelectorOnBoost": string; + /** + * Shows the visiblity selector if enabled when clicking boost, if disabled it will use the default visiblity defined below and the selector will not show up. + */ + "showVisibilitySelectorOnBoostDescription": string; + /** + * Default boost visibility + */ + "visibilityOnBoost": string; + /** + * Default like emoji + */ + "defaultLike": string; + /** + * You can also support {host} directly by donating to your instance administration. + */ + "pleaseDonateInstance": ParameterizedString<"host">; + /** + * Cancel + */ + "thisPostIsMissingAltTextCancel": string; + /** + * Post anyway + */ + "thisPostIsMissingAltTextIgnore": string; + /** + * One of the files attached to this post is missing alt text. Please ensure all the attachments have alt text. + */ + "thisPostIsMissingAltText": string; + /** + * Collapse notes replied to + */ + "collapseNotesRepliedTo": string; + /** + * Collapse files + */ + "collapseFiles": string; + /** + * Uncollapse CWs on notes + */ + "uncollapseCW": string; + /** + * Always expand long notes + */ + "expandLongNote": string; + /** + * Load conversation on replies + */ + "autoloadConversation": string; + /** + * This instance is only accepting users who specify a reason for registration. + */ + "approvalRequiredToRegister": string; + /** + * Cat friend :3 + */ + "oneko": string; + /** + * Enable Achievements + */ + "enableAchievements": string; + /** + * Turning this off will disable the achievement system + */ + "turnOffAchievements": string; + /** + * Populate Hashtags with Bots + */ + "enableBotTrending": string; + /** + * Turning this off will stop Bots from populating Hashtags + */ + "turnOffBotTrending": string; + /** + * Click to open notes + */ + "clickToOpen": string; + /** + * Show bots in timeline + */ + "showBots": string; + /** + * Donate + */ + "donation": string; + /** + * Donation URL + */ + "donationUrl": string; + /** + * Show Below Avatar + */ + "showBelowAvatar": string; + /** + * Break following relationships + */ + "severAllFollowRelations": string; + /** + * Really break all follow relationships? This is irreversible! This will break {followingCount} following and {followersCount} follower relations on {instanceName}! + */ + "severAllFollowRelationsConfirm": ParameterizedString<"followingCount" | "followersCount" | "instanceName">; + /** + * Severing all follow relations with {host} queued. + */ + "severAllFollowRelationsQueued": ParameterizedString<"host">; + /** + * Pending follow requests + */ + "pendingFollowRequests": string; + /** + * Show quotes + */ + "showQuotes": string; + /** + * Show replies + */ + "showReplies": string; + /** + * Show non-public + */ + "showNonPublicNotes": string; + /** + * Allow clicking on pop-up notifications + */ + "allowClickingNotifications": string; + /** + * Pinned + */ + "pinnedOnly": string; + /** + * Blocking you + */ + "blockingYou": string; + /** + * Show warning when opening external URLs + */ + "warnExternalUrl": string; + "_mfm": { + /** + * This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks + */ + "uncommonFeature": string; + /** + * MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax. + */ + "intro": string; + /** + * Sharkey expands the world of the Fediverse + */ + "dummy": string; + /** + * Mention + */ + "mention": string; + /** + * You can specify a user by using an At-Symbol and a username. + */ + "mentionDescription": string; + /** + * Hashtag + */ + "hashtag": string; + /** + * You can specify a hashtag using a number sign and text. + */ + "hashtagDescription": string; + /** + * URL + */ + "url": string; + /** + * URLs can be displayed. + */ + "urlDescription": string; + /** + * Link + */ + "link": string; + /** + * Specific parts of text can be displayed as a URL. + */ + "linkDescription": string; + /** + * Bold + */ + "bold": string; + /** + * Highlights letters by making them thicker. + */ + "boldDescription": string; + /** + * Small + */ + "small": string; + /** + * Displays content small and thin. + */ + "smallDescription": string; + /** + * Center + */ + "center": string; + /** + * Displays content centered. + */ + "centerDescription": string; + /** + * Code (Inline) + */ + "inlineCode": string; + /** + * Displays inline syntax highlighting for (program) code. + */ + "inlineCodeDescription": string; + /** + * Code (Block) + */ + "blockCode": string; + /** + * Displays syntax highlighting for multi-line (program) code in a block. + */ + "blockCodeDescription": string; + /** + * Math (Inline) + */ + "inlineMath": string; + /** + * Display math formulas (KaTeX) in-line + */ + "inlineMathDescription": string; + /** + * Math (Block) + */ + "blockMath": string; + /** + * Display math formulas (KaTeX) in a block + */ + "blockMathDescription": string; + /** + * Quote + */ + "quote": string; + /** + * Displays content as a quote. + */ + "quoteDescription": string; + /** + * Custom Emoji + */ + "emoji": string; + /** + * By surrounding a custom emoji name with colons, custom emoji can be displayed. + */ + "emojiDescription": string; + /** + * Search + */ + "search": string; + /** + * Displays a search box with pre-entered text. + */ + "searchDescription": string; + /** + * Flip + */ + "flip": string; + /** + * Flips content horizontally or vertically. + */ + "flipDescription": string; + /** + * Animation (Jelly) + */ + "jelly": string; + /** + * Gives content a jelly-like animation. + */ + "jellyDescription": string; + /** + * Animation (Tada) + */ + "tada": string; + /** + * Gives content a "Tada!"-like animation. + */ + "tadaDescription": string; + /** + * Animation (Jump) + */ + "jump": string; + /** + * Gives content a jumping animation. + */ + "jumpDescription": string; + /** + * Animation (Bounce) + */ + "bounce": string; + /** + * Gives content a bouncy animation. + */ + "bounceDescription": string; + /** + * Animation (Shake) + */ + "shake": string; + /** + * Gives content a shaking animation. + */ + "shakeDescription": string; + /** + * Animation (Twitch) + */ + "twitch": string; + /** + * Gives content a strongly twitching animation. + */ + "twitchDescription": string; + /** + * Animation (Spin) + */ + "spin": string; + /** + * Gives content a spinning animation. + */ + "spinDescription": string; + /** + * Big + */ + "x2": string; + /** + * Displays content bigger. + */ + "x2Description": string; + /** + * Very big + */ + "x3": string; + /** + * Displays content even bigger. + */ + "x3Description": string; + /** + * Unbelievably big + */ + "x4": string; + /** + * Displays content even bigger than bigger than big. + */ + "x4Description": string; + /** + * Blur + */ + "blur": string; + /** + * Blurs content. It will be displayed clearly when hovered over. + */ + "blurDescription": string; + /** + * Font + */ + "font": string; + /** + * Sets the font to display content in. + */ + "fontDescription": string; + /** + * Rainbow + */ + "rainbow": string; + /** + * Makes the content appear in rainbow colors. + */ + "rainbowDescription": string; + /** + * Sparkle + */ + "sparkle": string; + /** + * Gives content a sparkling particle effect. + */ + "sparkleDescription": string; + /** + * Rotate + */ + "rotate": string; + /** + * Turns content by a specified angle. + */ + "rotateDescription": string; + /** + * Position + */ + "position": string; + /** + * Move content by a specified amount. + */ + "positionDescription": string; + /** + * Crop + */ + "crop": string; + /** + * Crop content. + */ + "cropDescription": string; + /** + * Follow Mouse + */ + "followMouse": string; + /** + * Content will follow the mouse. On mobile it will follow wherever the user taps. + */ + "followMouseDescription": string; + /** + * Scale + */ + "scale": string; + /** + * Scale content by a specified amount. + */ + "scaleDescription": string; + /** + * Foreground color + */ + "foreground": string; + /** + * Change the foreground color of text. + */ + "foregroundDescription": string; + /** + * Fade + */ + "fade": string; + /** + * Fade text in and out. + */ + "fadeDescription": string; + /** + * Background color + */ + "background": string; + /** + * Change the background color of text. + */ + "backgroundDescription": string; + /** + * Plain + */ + "plain": string; + /** + * Deactivates the effects of all MFM contained within this MFM effect. + */ + "plainDescription": string; + }; + "_animatedMFM": { + /** + * Play MFM Animation + */ + "play": string; + /** + * Stop MFM Animation + */ + "stop": string; + "_alert": { + /** + * Animated MFMs could include flashing lights and fast moving text/emojis. + */ + "text": string; + /** + * Animate + */ + "confirm": string; + }; + }; + "_dataRequest": { + /** + * Request Data + */ + "title": string; + /** + * Data requests are only possible every 3 days. + */ + "warn": string; + /** + * Once the data is ready to download, an email will be sent to the email address registered to this account. + */ + "text": string; + /** + * Request + */ + "button": string; + }; + "_externalNavigationWarning": { + /** + * Navigate to an external site + */ + "title": string; + /** + * Leave {host} and go to an external site + */ + "description": ParameterizedString<"host">; + /** + * Trust this domain on this device in the future + */ + "trustThisDomain": string; + }; + /** + * Remote followers may have incomplete or outdated activity + */ + "remoteFollowersWarning": string; } declare const locales: { [lang: string]: Locale; diff --git a/locales/index.js b/locales/index.js index 48ff85f1a5358e99261f8773e57f3e0bb8815976..b08158e55dda9359c2e57e2d662e39a3f0f07650 100644 --- a/locales/index.js +++ b/locales/index.js @@ -5,7 +5,7 @@ import * as fs from 'node:fs'; import * as yaml from 'js-yaml'; -const merge = (...args) => args.reduce((a, c) => ({ +export const merge = (...args) => args.reduce((a, c) => ({ ...a, ...c, ...Object.entries(a) @@ -60,7 +60,10 @@ export function build() { // https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577 // https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785 const metaUrl = import.meta.url; - const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {}); + const sharkeyLocales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`../sharkey-locales/${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {}); + const misskeyLocales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {}); + // merge sharkey and misskey's locales. the second argument (sharkey) overwrites the first argument (misskey). + const locales = merge(misskeyLocales, sharkeyLocales); // 空文å—列ãŒå…¥ã‚‹ã“ã¨ãŒã‚ã‚Šã€ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ãŒå‹•ä½œã—ãªããªã‚‹ã®ã§ãƒ—ãƒãƒ‘ティã”ã¨æ¶ˆã™ const removeEmpty = (obj) => { diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 734b4bd0b19903422bdb004e609573c240f3051f..bcabf1bdb63b4ec0707b9fe2d90cb4ac5a0dd887 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1,6 +1,6 @@ --- _lang_: "Italiano" -headlineMisskey: "Rete collegata tramite note" +headlineMisskey: "Rete collegata tramite Note" introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n\n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n\n👠Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n\n🚀 Esplora un nuovo mondo insieme a noi!" poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source <b>Misskey</b>." monthAndDay: "{day}/{month}" @@ -8,6 +8,9 @@ search: "Cerca" notifications: "Notifiche" username: "Nome utente" password: "Password" +initialPasswordForSetup: "Password iniziale, per avviare le impostazioni" +initialPasswordIsIncorrect: "Password iniziale, sbagliata." +initialPasswordForSetupDescription: "Se hai installato Misskey di persona, usa la password che hai indicato nel file di configurazione.\nSe stai utilizzando un servizio di hosting Misskey, usa la password fornita dal gestore.\nSe non hai una password preimpostata, lascia il campo vuoto e continua." forgotPassword: "Hai dimenticato la password?" fetchingAsApObject: "Recuperando dal Fediverso..." ok: "OK" @@ -54,13 +57,13 @@ addToAntenna: "Aggiungi all'antenna" sendMessage: "Invia messaggio" copyRSS: "Copia RSS" copyUsername: "Copia nome utente" -openRemoteProfile: "Apri profilo remoto" copyUserId: "Copia ID del profilo" copyNoteId: "Copia ID della Nota" copyFileId: "Copia ID del file" copyFolderId: "Copia ID della cartella" copyProfileUrl: "Copia URL del profilo" searchUser: "Cerca profilo" +searchThisUsersNotes: "Cerca le sue Note" reply: "Rispondi" loadMore: "Mostra di più" showMore: "Espandi" @@ -155,6 +158,7 @@ editList: "Modifica Lista" selectChannel: "Seleziona canale" selectAntenna: "Scegli un'antenna" editAntenna: "Modifica Antenna" +createAntenna: "Crea Antenna" selectWidget: "Seleziona il riquadro" editWidgets: "Modifica i riquadri" editWidgetsExit: "Conferma le modifiche" @@ -195,6 +199,7 @@ followConfirm: "Vuoi seguire {name}?" proxyAccount: "Profilo proxy" proxyAccountDescription: "Un profilo proxy funziona come follower per i profili remoti, sotto certe condizioni. Ad esempio, quando un profilo locale ne inserisce uno remoto in una lista (senza seguirlo), se nessun altro segue quel profilo remoto, le attività non possono essere distribuite. Dunque, il profilo proxy le seguirà per tutti." host: "Host" +selectSelf: "Segli me" selectUser: "Seleziona profilo" recipient: "Destinatario" annotation: "Annotazione preventiva" @@ -210,6 +215,7 @@ perDay: "giornaliero" stopActivityDelivery: "Interrompi la distribuzione di attività " blockThisInstance: "Bloccare l'istanza" silenceThisInstance: "Silenziare l'istanza" +mediaSilenceThisInstance: "Silenzia i media dell'istanza" operations: "Operazioni" software: "Software" version: "Versione" @@ -231,6 +237,10 @@ blockedInstances: "Istanze bloccate" blockedInstancesDescription: "Elenca le istanze che vuoi bloccare, una per riga. Esse non potranno più interagire con la tua istanza." silencedInstances: "Istanze silenziate" silencedInstancesDescription: "Elenca i nomi host delle istanze che vuoi silenziare. Tutti i profili nelle istanze silenziate vengono trattati come tali. Possono solo inviare richieste di follow e menzionare soltanto i profili locali che seguono. Le istanze bloccate non sono interessate." +mediaSilencedInstances: "Istanze coi media silenziati" +mediaSilencedInstancesDescription: "Elenca i nomi host delle istanze di cui vuoi silenziare i media, uno per riga. Tutti gli allegati dei profili nelle istanze silenziate per via degli allegati espliciti, verranno impostati come tali, le emoji personalizzate non saranno disponibili. Le istanze bloccate sono escluse." +federationAllowedHosts: "Server a cui consentire la federazione" +federationAllowedHostsDescription: "Indica gli host dei server a cui è consentita la federazione, uno per ogni linea." muteAndBlock: "Silenziare e bloccare" mutedUsers: "Profili silenziati" blockedUsers: "Profili bloccati" @@ -329,6 +339,7 @@ renameFolder: "Rinomina cartella" deleteFolder: "Elimina cartella" folder: "Cartella" addFile: "Allega" +showFile: "Visualizza file" emptyDrive: "Il Drive è vuoto" emptyFolder: "La cartella è vuota" unableToDelete: "Eliminazione impossibile" @@ -443,6 +454,7 @@ totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App d moderator: "Moderatore" moderation: "moderazione" moderationNote: "Promemoria di moderazione" +moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori." addModerationNote: "Aggiungi promemoria di moderazione" moderationLogs: "Cronologia di moderazione" nUsersMentioned: "{n} profili ne parlano" @@ -450,7 +462,7 @@ securityKeyAndPasskey: "Chiave di sicurezza e accesso" securityKey: "Chiave di sicurezza" lastUsed: "Ultima attività " lastUsedAt: "Uso più recente: {t}" -unregister: "Annulla l'iscrizione" +unregister: "Rimuovi autenticazione a due fattori (2FA/MFA)" passwordLessLogin: "Accedi senza password" passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza" resetPassword: "Ripristina la password" @@ -504,7 +516,10 @@ uiLanguage: "Lingua di visualizzazione dell'interfaccia" aboutX: "Informazioni su {x}" emojiStyle: "Stile emoji" native: "Nativo" -disableDrawer: "Non mostrare il menù sul drawer" +menuStyle: "Stile menu" +style: "Stile" +drawer: "Drawer" +popup: "Popup" showNoteActionsOnlyHover: "Mostra le azioni delle Note solo al passaggio del mouse" showReactionsCount: "Visualizza il numero di reazioni su una nota" noHistory: "Nessuna cronologia" @@ -560,7 +575,7 @@ deleteAll: "Cancella cronologia" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita" -newNoteRecived: "Nuove note da leggere" +newNoteRecived: "Nuove Note da leggere" sounds: "Impostazioni suoni" sound: "Suono" listen: "Ascolta" @@ -587,6 +602,8 @@ ascendingOrder: "Aumenta" descendingOrder: "Diminuisce" scratchpad: "ScratchPad" scratchpadDescription: "Lo Scratchpad offre un ambiente per esperimenti di AiScript. È possibile scrivere, eseguire e confermare i risultati dell'interazione del codice con Misskey." +uiInspector: "UI Inspector" +uiInspectorDescription: "Puoi visualizzare un elenco di elementi UI presenti in memoria. I componenti dell'interfaccia utente vengono generati dalle funzioni Ui:C:." output: "Uscita" script: "Script" disablePagesScript: "Disabilita AiScript nelle pagine" @@ -703,10 +720,7 @@ abuseReported: "La segnalazione è stata inviata. Grazie." reporter: "il corrispondente" reporteeOrigin: "Segnalazione a" reporterOrigin: "Segnalazione da" -forwardReport: "Inoltro di un report a un'istanza remota." -forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo." send: "Inviare" -abuseMarkAsResolved: "Risolvi segnalazione" openInNewTab: "Apri in una nuova scheda" openInSideView: "Apri in vista laterale" defaultNavigationBehaviour: "Navigazione preimpostata" @@ -765,8 +779,6 @@ thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe e developer: "Sviluppatore" makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\"" makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"." -makeIndexable: "Non indicizzare le note pubbliche" -makeIndexableDescription: "Le tue note pubbliche non saranno cercabili" showGapBetweenNotesInTimeline: "Mostrare un intervallo tra le note sulla timeline" duplicate: "Duplica" left: "Sinistra" @@ -830,7 +842,7 @@ onlineStatus: "Stato di connessione" hideOnlineStatus: "Modalità invisibile" hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca." online: "Online" -active: "Attività " +active: "Attivo" offline: "Offline" notRecommended: "Sconsigliato" botProtection: "Protezione contro i bot" @@ -910,6 +922,7 @@ followersVisibility: "Visibilità dei profili che ti seguono" continueThread: "Altre conversazioni" deleteAccountConfirm: "Così verrà eliminato il profilo. Vuoi procedere?" incorrectPassword: "La password è errata." +incorrectTotp: "Il codice OTP è sbagliato, oppure scaduto." voteConfirm: "Votare per「{choice}ã€?" hide: "Nascondere" useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile" @@ -1001,7 +1014,6 @@ cannotLoad: "Caricamento impossibile" numberOfProfileView: "Visualizzazioni profilo" like: "Mi piace!" unlike: "Non mi piace" -defaultLike: "Emoji predefinita per \"mi piace\"" numberOfLikes: "Numero di Like" show: "Visualizza" neverShow: "Non mostrare più" @@ -1075,6 +1087,7 @@ retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?" retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." enableChartsForRemoteUser: "Abilita i grafici per i profili remoti" enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate" +enableStatsForFederatedInstances: "Informazioni statistiche sui server federati" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note" reactionsDisplaySize: "Grandezza delle reazioni" limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale" @@ -1110,6 +1123,8 @@ preservedUsernames: "Nomi utente riservati" preservedUsernamesDescription: "Elenca, uno per linea, i nomi utente che non possono essere registrati durante la creazione del profilo. La restrizione non si applica agli amministratori. Inoltre, i profili già registrati sono esenti." createNoteFromTheFile: "Crea Nota da questo file" archive: "Archivio" +archived: "Archiviato" +unarchive: "Annulla archiviazione" channelArchiveConfirmTitle: "Vuoi davvero archiviare {name}?" channelArchiveConfirmDescription: "Un canale archiviato non compare nell'elenco canali, nemmeno nei risultati di ricerca. Non può ricevere nemmeno nuove Note." thisChannelArchived: "Questo canale è stato archiviato." @@ -1120,6 +1135,9 @@ preventAiLearning: "Impedisci l'apprendimento della IA" preventAiLearningDescription: "Aggiungendo il campo \"noai\" alla risposta HTML, si indica ai Robot esterni di non usare testi e allegati per addestrare sistemi di Machine Learning (IA predittiva/generativa). Anche se è impossibile sapere se la richiesta venga onorata o semplicemente ignorata." options: "Opzioni del ruolo" specifyUser: "Profilo specifico" +lookupConfirm: "Vuoi davvero richiedere informazioni?" +openTagPageConfirm: "Vuoi davvero aprire la pagina dell'hashtag?" +specifyHost: "Specifica l'host" failedToPreviewUrl: "Anteprima non disponibile" update: "Aggiorna" rolesThatCanBeUsedThisEmojiAsReaction: "Ruoli che possono usare questa emoji come reazione" @@ -1254,10 +1272,38 @@ inquiry: "Contattaci" tryAgain: "Per favore riprova" confirmWhenRevealingSensitiveMedia: "Richiedi conferma prima di mostrare gli allegati espliciti" sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?" +createdLists: "Liste create" +createdAntennas: "Antenne create" +fromX: "Da {x}" +genEmbedCode: "Ottieni il codice di incorporamento" +noteOfThisUser: "Elenco di Note di questo profilo" +clipNoteLimitExceeded: "Non è possibile aggiungere ulteriori Note a questa Clip." +performance: "Prestazioni" +modified: "Modificato" +discard: "Scarta" +thereAreNChanges: "Ci sono {n} cambiamenti" +signinWithPasskey: "Accedi con passkey" +unknownWebAuthnKey: "Questa è una passkey sconosciuta." +passkeyVerificationFailed: "La verifica della passkey non è riuscita." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato." +messageToFollower: "Messaggio ai follower" +target: "Riferimento" +testCaptchaWarning: "Questa funzione è destinata al test CAPTCHA. <strong>Da non utilizzare in ambiente di produzione.</strong>" +prohibitedWordsForNameOfUser: "Parole proibite (nome utente)" +prohibitedWordsForNameOfUserDescription: "Il sistema rifiuta di rinominare un utente, se il nome contiene qualsiasi parola nell'elenco. Sono esenti i profili con privilegi di moderazione." +yourNameContainsProhibitedWords: "Il nome che hai scelto contiene una o più parole vietate" +yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare questo nome, contatta l''amministrazione." +_abuseUserReport: + forward: "Inoltra" + forwardDescription: "Inoltra il report al server remoto, per mezzo di account di sistema, anonimo." + resolve: "Risolvi" + accept: "Approva" + reject: "Rifiuta" + resolveTutorial: "Se moderi una segnalazione legittima, scegli \"Approva\" per risolvere positivamente.\nSe la segnalazione non è legittima, seleziona \"Rifiuta\" per risolvere negativamente." _delivery: - status: "Stato della distribuzione di attività " - stop: "Sospendi la distribuzione di attività " - resume: "Riprendi la distribuzione di attività " + status: "Stato della consegna" + stop: "Sospensione" + resume: "Riprendi la consegna" _type: none: "Pubblicazione" manuallySuspended: "Sospesa manualmente" @@ -1281,16 +1327,16 @@ _bubbleGame: _announcement: forExistingUsers: "Solo ai profili attuali" forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." - needConfirmationToRead: "Richiede la conferma di lettura" - needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce." + needConfirmationToRead: "Conferma di lettura obbligatoria" + needConfirmationToReadDescription: "I profili riceveranno una finestra di dialogo che richiede di accettare obbligatoriamente per procedere. Tale richiesta è esente da \"conferma tutte\"." end: "Archivia l'annuncio" tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi." readConfirmTitle: "Segnare come già letto?" readConfirmText: "Hai già letto \"{title}Ë?" shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte." dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte." - silence: "Silenziare gli annunci" - silenceDescription: "Se attivi questa opzione, non riceverai notifiche sugli annunci, evitando di contrassegnarle come già lette." + silence: "Annuncio silenzioso" + silenceDescription: "Attivando questa opzione, non invierai la notifica, evitando che debba essere contrassegnata come già letta." _initialAccountSetting: accountCreated: "Il tuo profilo è stato creato!" letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo." @@ -1308,7 +1354,7 @@ _initialAccountSetting: skipAreYouSure: "Vuoi davvero saltare la configurazione iniziale?" laterAreYouSure: "Vuoi davvero rimandare la configurazione iniziale?" _initialTutorial: - launchTutorial: "Guarda il tutorial" + launchTutorial: "Inizia il tutorial" title: "Tutorial" wellDone: "Ottimo lavoro!" skipAreYouSure: "Vuoi davvero interrompere il tutorial?" @@ -1318,17 +1364,17 @@ _initialTutorial: _note: title: "Cosa sono le Note?" description: "Gli status su Misskey sono chiamati \"Note\". Le Note sono elencate in ordine cronologico nelle timeline e vengono aggiornate in tempo reale." - reply: "Puoi rispondere alle Note. Puoi anche rispondere alle risposte e continuare i dialoghi come un conversazioni." - renote: "Puoi ri-condividere le Note, facendole rifluire sulla Timeline. Puoi anche aggiungere testo e citare altri profili." - reaction: "Puoi aggiungere una reazione. Nella pagina successiva spiegheremo i dettagli." - menu: "Puoi svolgere varie attività , come visualizzare i dettagli delle Note o copiare i collegamenti." + reply: "Puoi rispondere alle Note, alle altre risposte e dialogare in conversazioni." + renote: "Puoi ri-condividere le Note, ritorneranno sulla Timeline. Aggiungendo del testo, scriverai una Citazione." + reaction: "Puoi aggiungere una reazione. Nella pagina successiva ti spiego come." + menu: "Per altre attività , ad esempio, vedere i dettagli delle Note o copiare i collegamenti." _reaction: title: "Cosa sono le Reazioni?" - description: "Puoi reagire alle Note. Le sensazioni che non si riescono a trasmettere con i \"Mi piace\" si possono esprimere facilmente inviando una reazione." - letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"{reaction}\" della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" + description: "Reazioni alle Note. Le sensazioni che non si possono descrivere con \"Mi piace\" si esprimono facilmente con le reazioni." + letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"+\" (più) della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" reactToContinue: "Aggiungere la Reazione ti consentirà di procedere col tutorial." reactNotification: "Quando qualcuno reagisce alle tue Note, ricevi una notifica in tempo reale." - reactDone: "Annulla la tua Reazione premendo il bottone \"{undo}\"" + reactDone: "Annulla la tua Reazione premendo il bottone \"ー\" (meno)" _timeline: title: "Come funziona la Timeline" description1: "Misskey fornisce alcune Timeline (sequenze cronologiche di Note). Una di queste potrebbe essere stata disattivata dagli amministratori." @@ -1337,7 +1383,7 @@ _initialTutorial: social: "sia le Note della Timeline Home che quelle della Timeline Locale, insieme!" global: "le Note da pubblicate da tutte le altre istanze federate con la nostra." description2: "Nella parte superiore dello schermo, puoi scegliere una Timeline o l'altra in qualsiasi momento." - description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare il {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, visita {link}." + description3: "Ci sono anche sequenze temporali di elenchi, sequenze temporali di canali, ecc. Per ulteriori dettagli, consultare la {link}.\nPuoi vedere anche Timeline delle liste di profili (se ne hai create), canali, ecc... Per i dettagli, c'è la {link}." _postNote: title: "La Nota e le sue impostazioni" description1: "Quando scrivi una Nota su Misskey, hai a disposizione varie opzioni. Il modulo di invio è simile a questo." @@ -1388,8 +1434,10 @@ _serverSettings: fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare." fanoutTimelineDbFallback: "Elaborazione dati alternativa" fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline." + reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis." inquiryUrl: "URL di contatto" inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo." _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" @@ -1721,6 +1769,11 @@ _role: canSearchNotes: "Ricercare nelle Note" canUseTranslator: "Tradurre le Note" avatarDecorationLimit: "Numero massimo di decorazioni foto profilo installabili" + canImportAntennas: "Può importare Antenne" + canImportBlocking: "Può importare Blocchi" + canImportFollowing: "Può importare Following" + canImportMuting: "Può importare Silenziati" + canImportUserLists: "Può importare liste di Profili" _condition: roleAssignedTo: "Assegnato a ruoli manualmente" isLocal: "Profilo locale" @@ -1783,7 +1836,7 @@ _ad: _forgotPassword: enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo." ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza." - contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n" + contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n" _gallery: my: "Le mie pubblicazioni" liked: "Pubblicazioni che mi piacciono" @@ -1848,7 +1901,6 @@ _serverDisconnectedBehavior: reload: "Ricarica automaticamente" dialog: "Apri avviso in finestra" quiet: "Visualizza avviso in modo discreto" - disabled: "Non visualizzare l'avviso" _channel: create: "Nuovo canale" edit: "Gerisci canale" @@ -1939,7 +1991,6 @@ _theme: buttonBg: "Sfondo del pulsante" buttonHoverBg: "Sfondo del pulsante (sorvolato)" inputBorder: "Inquadra casella di testo" - listItemHoverBg: "Sfondo della voce di elenco (sorvolato)" driveFolderBg: "Sfondo della cartella di disco" wallpaperOverlay: "Sovrapposizione dello sfondo" badge: "Distintivo" @@ -1959,6 +2010,7 @@ _soundSettings: driveFileTypeWarnDescription: "Per favore, scegli un file di tipo audio" driveFileDurationWarn: "La durata dell'audio è troppo lunga" driveFileDurationWarnDescription: "Scegliere un audio lungo potrebbe interferire con l'uso di Misskey. Vuoi continuare lo stesso?" + driveFileError: "Impossibile caricare l'audio. Si prega di modificare le impostazioni" _ago: future: "Futuro" justNow: "Adesso" @@ -2151,7 +2203,7 @@ _widgets: _userList: chooseList: "Seleziona una lista" clicker: "Cliccaggio" - birthdayFollowings: "Chi nacque oggi" + birthdayFollowings: "Compleanni del giorno" _cw: hide: "Nascondere" show: "Continua la lettura..." @@ -2215,6 +2267,9 @@ _profile: changeBanner: "Cambia intestazione" verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo." avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni." + followedMessage: "Messaggio, quando qualcuno ti segue" + followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono." + followedMessageDescriptionForLockedAccount: "Quando approvi una richiesta di follow, verrà visualizzato questo testo." _exportOrImport: allNotes: "Tutte le note" favoritedNotes: "Note preferite" @@ -2307,6 +2362,7 @@ _pages: eyeCatchingImageSet: "Imposta un'immagine attraente" eyeCatchingImageRemove: "Elimina immagine attraente" chooseBlock: "Aggiungi blocco" + enterSectionTitle: "Inserisci il titolo della sezione" selectType: "Seleziona tipo" contentBlocks: "Contenuto" inputBlocks: "Blocchi di input" @@ -2352,6 +2408,8 @@ _notification: renotedBySomeUsers: "{n} Rinota" followedBySomeUsers: "{n} follower" flushNotification: "Azzera le notifiche" + exportOfXCompleted: "Abbiamo completato l'esportazione di {x}" + login: "Autenticazione avvenuta" _types: all: "Tutto" note: "Nuove Note" @@ -2366,6 +2424,9 @@ _notification: followRequestAccepted: "Richiesta di follow accettata" roleAssigned: "Ruolo concesso" achievementEarned: "Risultato raggiunto" + exportCompleted: "Esportazione completata" + login: "Accedi" + test: "Prova la notifica" app: "Notifiche da applicazioni" _actions: followBack: "Segui" @@ -2405,7 +2466,7 @@ _deck: roleTimeline: "Timeline Ruolo" _dialog: charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})" - charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})" + charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})" _disabledTimeline: title: "Timeline disabilitata" description: "Il ruolo in cui sei non ti permette di leggere questa timeline" @@ -2417,6 +2478,7 @@ _webhookSettings: modifyWebhook: "Modifica Webhook" name: "Nome" secret: "Segreto" + trigger: "Trigger" active: "Attivo" _events: follow: "Quando segui un profilo" @@ -2429,7 +2491,11 @@ _webhookSettings: _systemEvents: abuseReport: "Quando arriva una segnalazione" abuseReportResolved: "Quando una segnalazione è risolta" + userCreated: "Quando viene creato un profilo" + inactiveModeratorsWarning: "Quando un profilo moderatore rimane inattivo per un determinato periodo" + inactiveModeratorsInvitationOnlyChanged: "Quando la moderazione è rimasta inattiva per un determinato periodo e il sistema è cambiato in modalità \"solo inviti\"" deleteConfirm: "Vuoi davvero eliminare il Webhook?" + testRemarks: "Clicca il bottone a destra dell'interruttore, per provare l'invio di un webhook con dati fittizi." _abuseReport: _notificationRecipient: createRecipient: "Aggiungi destinatario della segnalazione" @@ -2473,6 +2539,8 @@ _moderationLogTypes: markSensitiveDriveFile: "File nel Drive segnato come esplicito" unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" resolveAbuseReport: "Segnalazione risolta" + forwardAbuseReport: "Segnalazione inoltrata" + updateAbuseReportNote: "Ha aggiornato la segnalazione" createInvitation: "Genera codice di invito" createAd: "Banner creato" deleteAd: "Banner eliminato" @@ -2488,6 +2556,10 @@ _moderationLogTypes: createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni" updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni" deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni" + deleteAccount: "Quando viene eliminato un profilo" + deletePage: "Pagina eliminata" + deleteFlash: "Play eliminato" + deleteGalleryPost: "Eliminazione pubblicazione nella Galleria" _fileViewer: title: "Dettagli del file" type: "Tipo di file" @@ -2619,3 +2691,22 @@ _mediaControls: pip: "Sovraimpressione" playbackRate: "Velocità di riproduzione" loop: "Ripetizione infinita" +_contextMenu: + title: "Menu contestuale" + app: "Applicazione" + appWithShift: "Applicazione Shift+Tasto" + native: "Interfaccia utente del browser" +_embedCodeGen: + title: "Personalizza il codice di incorporamento" + header: "Mostra la testata" + autoload: "Carica automaticamente di più (sconsigliato)" + maxHeight: "Altezza massima" + maxHeightDescription: "Specifica un valore per evitare che continui a crescere verticalmente. Il valore 0 disabilita il limite d'altezza." + maxHeightWarn: "L'altezza massima è disabilitata (0). Se l'effetto è indesiderato, prova a impostare l'altezza massima a un valore specifico." + previewIsNotActual: "Poiché supera l'intervallo che può essere visualizzato in anteprima, la visualizzazione vera e propria sarà diversa quando effettivamente incorporata." + rounded: "Bordo arrotondato" + border: "Aggiungi un bordo al contenitore" + applyToPreview: "Applica all'anteprima" + generateCode: "Crea il codice di incorporamento" + codeGenerated: "Codice generato" + codeGeneratedDescription: "Incolla il codice appena generato sul tuo sito web." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b4d47a449c5b43d8dc89e8f242bb91db5156a7f7..c448d4d50a7357489e205867045a3e1a72055f76 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1,13 +1,16 @@ _lang_: "日本語" headlineMisskey: "ノートã§ã¤ãªãŒã‚‹ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" -introMisskey: "よã†ã“ãï¼Sharkeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スã§ã™ã€‚\n「ノートã€ã‚’作æˆã—ã¦ã€ã„ã¾èµ·ã“ã£ã¦ã„ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ãªãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\n「リアクションã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ðŸ‘\næ–°ã—ã„世界を探検ã—よã†ðŸš€" -poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Sharkey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ã§ã™ã€‚" +introMisskey: "よã†ã“ãï¼Misskeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スã§ã™ã€‚\n「ノートã€ã‚’作æˆã—ã¦ã€ã„ã¾èµ·ã“ã£ã¦ã„ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ãªãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\n「リアクションã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ðŸ‘\næ–°ã—ã„世界を探検ã—よã†ðŸš€" +poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Misskey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ã§ã™ã€‚" monthAndDay: "{month}月 {day}æ—¥" search: "検索" notifications: "通知" username: "ユーザーå" password: "パスワード" +initialPasswordForSetup: "åˆæœŸè¨å®šé–‹å§‹ç”¨ãƒ‘スワード" +initialPasswordIsIncorrect: "åˆæœŸè¨å®šé–‹å§‹ç”¨ã®ãƒ‘スワードãŒé•ã„ã¾ã™ã€‚" +initialPasswordForSetupDescription: "Misskeyを自分ã§ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã—ãŸå ´åˆã¯ã€è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã«å…¥åŠ›ã—ãŸãƒ‘スワードを使用ã—ã¦ãã ã•ã„。\nMisskeyã®ãƒ›ã‚¹ãƒ†ã‚£ãƒ³ã‚°ã‚µãƒ¼ãƒ“スãªã©ã‚’使用ã—ã¦ã„ã‚‹å ´åˆã¯ã€æä¾›ã•ã‚ŒãŸãƒ‘スワードを使用ã—ã¦ãã ã•ã„。\nパスワードをè¨å®šã—ã¦ã„ãªã„å ´åˆã¯ã€ç©ºæ¬„ã«ã—ãŸã¾ã¾ç¶šè¡Œã—ã¦ãã ã•ã„。" forgotPassword: "パスワードを忘れãŸ" fetchingAsApObject: "連åˆã«ç…§ä¼šä¸" ok: "OK" @@ -15,7 +18,7 @@ gotIt: "ã‚ã‹ã£ãŸ" cancel: "ã‚ャンセル" noThankYou: "ã‚„ã‚ã¦ãŠã" enterUsername: "ユーザーåを入力" -renotedBy: "{user}ãŒãƒ–ースト" +renotedBy: "{user}ãŒãƒªãƒŽãƒ¼ãƒˆ" noNotes: "ノートã¯ã‚ã‚Šã¾ã›ã‚“" noNotifications: "通知ã¯ã‚ã‚Šã¾ã›ã‚“" instance: "サーãƒãƒ¼" @@ -34,7 +37,6 @@ signup: "æ–°è¦ç™»éŒ²" uploading: "アップãƒãƒ¼ãƒ‰ä¸" save: "ä¿å˜" users: "ユーザー" -approvals: "承èª" addUser: "ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¿½åŠ " favorite: "ãŠæ°—ã«å…¥ã‚Š" favorites: "ãŠæ°—ã«å…¥ã‚Š" @@ -46,16 +48,15 @@ pin: "ピン留ã‚" unpin: "ピン留ã‚解除" copyContent: "内容をコピー" copyLink: "リンクをコピー" -copyLinkRenote: "ブーストã®ãƒªãƒ³ã‚¯ã‚’コピー" +copyLinkRenote: "リノートã®ãƒªãƒ³ã‚¯ã‚’コピー" delete: "削除" deleteAndEdit: "削除ã—ã¦ç·¨é›†" -deleteAndEditConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’削除ã—ã¦ã‚‚ã†ä¸€åº¦ç·¨é›†ã—ã¾ã™ã‹ï¼Ÿã“ã®ãƒŽãƒ¼ãƒˆã¸ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€ãƒ–ーストã€è¿”ä¿¡ã‚‚å…¨ã¦å‰Šé™¤ã•ã‚Œã¾ã™ã€‚" +deleteAndEditConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’削除ã—ã¦ã‚‚ã†ä¸€åº¦ç·¨é›†ã—ã¾ã™ã‹ï¼Ÿã“ã®ãƒŽãƒ¼ãƒˆã¸ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€ãƒªãƒŽãƒ¼ãƒˆã€è¿”ä¿¡ã‚‚å…¨ã¦å‰Šé™¤ã•ã‚Œã¾ã™ã€‚" addToList: "リストã«è¿½åŠ " addToAntenna: "アンテナã«è¿½åŠ " sendMessage: "メッセージをé€ä¿¡" copyRSS: "RSSをコピー" copyUsername: "ユーザーåをコピー" -openRemoteProfile: "リモートプãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’é–‹ã" copyUserId: "ユーザーIDをコピー" copyNoteId: "ノートIDをコピー" copyFileId: "ファイルIDをコピー" @@ -108,16 +109,14 @@ followRequests: "フォãƒãƒ¼ç”³è«‹" unfollow: "フォãƒãƒ¼è§£é™¤" followRequestPending: "フォãƒãƒ¼è¨±å¯å¾…ã¡" enterEmoji: "絵文å—を入力" -renote: "ブースト" -unrenote: "ブースト解除" -renoted: "ブーストã—ã¾ã—ãŸã€‚" -renotedToX: "{name} ã«ãƒ–ーストã—ã¾ã—ãŸã€‚" -quoted: "引用。" -rmboost: "ブースト解除ã—ã¾ã—ãŸã€‚" -cantRenote: "ã“ã®æŠ•ç¨¿ã¯ãƒ–ーストã§ãã¾ã›ã‚“。" -cantReRenote: "ブーストをブーストã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" +renote: "リノート" +unrenote: "リノート解除" +renoted: "リノートã—ã¾ã—ãŸã€‚" +renotedToX: "{name} ã«ãƒªãƒŽãƒ¼ãƒˆã—ã¾ã—ãŸã€‚" +cantRenote: "ã“ã®æŠ•ç¨¿ã¯ãƒªãƒŽãƒ¼ãƒˆã§ãã¾ã›ã‚“。" +cantReRenote: "リノートをリノートã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" quote: "引用" -inChannelRenote: "ãƒãƒ£ãƒ³ãƒãƒ«å†…ブースト" +inChannelRenote: "ãƒãƒ£ãƒ³ãƒãƒ«å†…リノート" inChannelQuote: "ãƒãƒ£ãƒ³ãƒãƒ«å†…引用" renoteToChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒªãƒŽãƒ¼ãƒˆ" renoteToOtherChannel: "ä»–ã®ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒªãƒŽãƒ¼ãƒˆ" @@ -144,19 +143,15 @@ unmarkAsSensitive: "センシティブを解除ã™ã‚‹" enterFileName: "ファイルåを入力" mute: "ミュート" unmute: "ミュート解除" -renoteMute: "ブーストをミュート" -renoteUnmute: "ブーストã®ãƒŸãƒ¥ãƒ¼ãƒˆã‚’解除" +renoteMute: "リノートをミュート" +renoteUnmute: "リノートã®ãƒŸãƒ¥ãƒ¼ãƒˆã‚’解除" block: "ブãƒãƒƒã‚¯" unblock: "ブãƒãƒƒã‚¯è§£é™¤" -markAsNSFW: "ユーザーã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯ã™ã‚‹" suspend: "å‡çµ" unsuspend: "解å‡" blockConfirm: "ブãƒãƒƒã‚¯ã—ã¾ã™ã‹ï¼Ÿ" unblockConfirm: "ブãƒãƒƒã‚¯è§£é™¤ã—ã¾ã™ã‹ï¼Ÿ" -nsfwConfirm: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" -unNsfwConfirm: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯è§£é™¤ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" suspendConfirm: "å‡çµã—ã¾ã™ã‹ï¼Ÿ" -approveConfirm: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’承èªã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" unsuspendConfirm: "解å‡ã—ã¾ã™ã‹ï¼Ÿ" selectList: "リストをé¸æŠž" editList: "リストを編集" @@ -180,11 +175,9 @@ youCanCleanRemoteFilesCache: "ファイル管ç†ã®ðŸ—‘ï¸ãƒœã‚¿ãƒ³ã§å…¨ã¦ã® cacheRemoteSensitiveFiles: "リモートã®ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãªãƒ•ã‚¡ã‚¤ãƒ«ã‚’ã‚ャッシュã™ã‚‹" cacheRemoteSensitiveFilesDescription: "ã“ã®è¨å®šã‚’無効ã«ã™ã‚‹ã¨ã€ãƒªãƒ¢ãƒ¼ãƒˆã®ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãªãƒ•ã‚¡ã‚¤ãƒ«ã¯ã‚ャッシュã›ãšç›´ãƒªãƒ³ã‚¯ã™ã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚" flagAsBot: "Botã¨ã—ã¦è¨å®š" -flagAsBotDescription: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒãƒ—ãƒã‚°ãƒ©ãƒ ã«ã‚ˆã£ã¦é‹ç”¨ã•ã‚Œã‚‹å ´åˆã¯ã€ã“ã®ãƒ•ãƒ©ã‚°ã‚’オンã«ã—ã¾ã™ã€‚オンã«ã™ã‚‹ã¨ã€åå¿œã®é€£éŽ–を防ããŸã‚ã®ãƒ•ãƒ©ã‚°ã¨ã—ã¦ä»–ã®é–‹ç™ºè€…ã«å½¹ç«‹ã£ãŸã‚Šã€Sharkeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã®ã«ãªã‚Šã¾ã™ã€‚" +flagAsBotDescription: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒãƒ—ãƒã‚°ãƒ©ãƒ ã«ã‚ˆã£ã¦é‹ç”¨ã•ã‚Œã‚‹å ´åˆã¯ã€ã“ã®ãƒ•ãƒ©ã‚°ã‚’オンã«ã—ã¾ã™ã€‚オンã«ã™ã‚‹ã¨ã€åå¿œã®é€£éŽ–を防ããŸã‚ã®ãƒ•ãƒ©ã‚°ã¨ã—ã¦ä»–ã®é–‹ç™ºè€…ã«å½¹ç«‹ã£ãŸã‚Šã€Misskeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã®ã«ãªã‚Šã¾ã™ã€‚" flagAsCat: "ã«ã‚ƒã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼ï¼" flagAsCatDescription: "ã«ã‚ƒã«ã‚ƒã«ã‚ƒï¼Ÿï¼Ÿ" -flagSpeakAsCat: "猫語ã§è©±ã™" -flagSpeakAsCatDescription: "有効ã«ã™ã‚‹ã¨ã€ã‚ãªãŸã®æŠ•ç¨¿ã® 「ãªã€ã‚’「ã«ã‚ƒã€ã«ã—ã¾ã™ã€‚" flagShowTimelineReplies: "タイムラインã«ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹" flagShowTimelineRepliesDescription: "オンã«ã™ã‚‹ã¨ã€ã‚¿ã‚¤ãƒ ラインã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆä»¥å¤–ã«ã‚‚ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ä»–ã®ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã—ã¾ã™ã€‚" autoAcceptFollowed: "フォãƒãƒ¼ä¸ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒªã‚¯ã‚’自動承èª" @@ -246,6 +239,8 @@ silencedInstances: "サイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼" silencedInstancesDescription: "サイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚サイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã™ã¹ã¦ã€Œã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã€ã¨ã—ã¦æ‰±ã‚ã‚Œã€ãƒ•ã‚©ãƒãƒ¼ãŒã™ã¹ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。" mediaSilencedInstances: "メディアサイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼" mediaSilencedInstancesDescription: "メディアサイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚メディアサイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã™ã¹ã¦ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã¨ã—ã¦æ‰±ã‚ã‚Œã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—ãŒä½¿ç”¨ã§ããªã„よã†ã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。" +federationAllowedHosts: "連åˆã‚’許å¯ã™ã‚‹ã‚µãƒ¼ãƒãƒ¼" +federationAllowedHostsDescription: "連åˆã‚’許å¯ã™ã‚‹ã‚µãƒ¼ãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚" muteAndBlock: "ミュートã¨ãƒ–ãƒãƒƒã‚¯" mutedUsers: "ミュートã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" blockedUsers: "ブãƒãƒƒã‚¯ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" @@ -253,7 +248,7 @@ noUsers: "ユーザーã¯ã„ã¾ã›ã‚“" editProfile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’編集" noteDeleteConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’削除ã—ã¾ã™ã‹ï¼Ÿ" pinLimitExceeded: "ã“れ以上ピン留ã‚ã§ãã¾ã›ã‚“" -intro: "Sharkeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ã¾ã—ãŸï¼ç®¡ç†è€…アカウントを作æˆã—ã¾ã—ょã†ã€‚" +intro: "Misskeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ã¾ã—ãŸï¼ç®¡ç†è€…アカウントを作æˆã—ã¾ã—ょã†ã€‚" done: "完了" processing: "処ç†ä¸" preview: "プレビュー" @@ -331,7 +326,6 @@ lightThemes: "明るã„テーマ" darkThemes: "æš—ã„テーマ" syncDeviceDarkMode: "デãƒã‚¤ã‚¹ã®ãƒ€ãƒ¼ã‚¯ãƒ¢ãƒ¼ãƒ‰ã¨åŒæœŸã™ã‚‹" drive: "ドライブ" -driveSearchbarPlaceholder: "検索ドライブ" fileName: "ファイルå" selectFile: "ファイルをé¸æŠž" selectFiles: "ファイルをé¸æŠž" @@ -345,6 +339,7 @@ renameFolder: "フォルダーåを変更" deleteFolder: "フォルダーを削除" folder: "フォルダー" addFile: "ãƒ•ã‚¡ã‚¤ãƒ«ã‚’è¿½åŠ " +showFile: "ファイルを表示" emptyDrive: "ドライブã¯ç©ºã§ã™" emptyFolder: "フォルダーã¯ç©ºã§ã™" unableToDelete: "削除ã§ãã¾ã›ã‚“" @@ -357,7 +352,6 @@ copyUrl: "URLをコピー" rename: "åå‰ã‚’変更" avatar: "アイコン" banner: "ãƒãƒŠãƒ¼" -background: "背景" displayOfSensitiveMedia: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã®è¡¨ç¤º" whenServerDisconnected: "サーãƒãƒ¼ã¨ã®æŽ¥ç¶šãŒå¤±ã‚ã‚ŒãŸã¨ã" disconnectedFromServer: "サーãƒãƒ¼ã‹ã‚‰åˆ‡æ–ã•ã‚Œã¾ã—ãŸ" @@ -450,7 +444,7 @@ exploreFediverse: "Fediverseを探索" popularTags: "人気ã®ã‚¿ã‚°" userList: "リスト" about: "æƒ…å ±" -aboutMisskey: "Sharkeyã«ã¤ã„ã¦" +aboutMisskey: "Misskeyã«ã¤ã„ã¦" administrator: "管ç†è€…" token: "確èªã‚³ãƒ¼ãƒ‰" 2fa: "二è¦ç´ èªè¨¼" @@ -460,6 +454,7 @@ totpDescription: "èªè¨¼ã‚¢ãƒ—リを使ã£ã¦ãƒ¯ãƒ³ã‚¿ã‚¤ãƒ パスワードを moderator: "モデレーター" moderation: "モデレーション" moderationNote: "モデレーションノート" +moderationNoteDescription: "モデレーター間ã§ã ã‘共有ã•ã‚Œã‚‹ãƒ¡ãƒ¢ã‚’記入ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" addModerationNote: "ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãƒŽãƒ¼ãƒˆã‚’è¿½åŠ ã™ã‚‹" moderationLogs: "モデãƒã‚°" nUsersMentioned: "{n}人ãŒæŠ•ç¨¿" @@ -492,8 +487,6 @@ enable: "有効ã«ã™ã‚‹" next: "次" retype: "å†å…¥åŠ›" noteOf: "{user}ã®ãƒŽãƒ¼ãƒˆ" -expandAllCws: "ã™ã¹ã¦ã®è¿”ä¿¡ã®å†…容を表示ã™ã‚‹" -collapseAllCws: "ã™ã¹ã¦ã®è¿”ä¿¡ã®å†…å®¹ã‚’éš ã™" quoteAttached: "引用付ã" quoteQuestion: "引用ã¨ã—ã¦æ·»ä»˜ã—ã¾ã™ã‹ï¼Ÿ" attachAsFileQuestion: "クリップボードã®ãƒ†ã‚ストãŒé•·ã„ã§ã™ã€‚テã‚ストファイルã¨ã—ã¦æ·»ä»˜ã—ã¾ã™ã‹ï¼Ÿ" @@ -523,7 +516,10 @@ uiLanguage: "UIã®è¡¨ç¤ºè¨€èªž" aboutX: "{x}ã«ã¤ã„ã¦" emojiStyle: "絵文å—ã®ã‚¹ã‚¿ã‚¤ãƒ«" native: "ãƒã‚¤ãƒ†ã‚£ãƒ–" -disableDrawer: "メニューをドãƒãƒ¯ãƒ¼ã§è¡¨ç¤ºã—ãªã„" +menuStyle: "メニューã®ã‚¹ã‚¿ã‚¤ãƒ«" +style: "スタイル" +drawer: "ドãƒãƒ¯ãƒ¼" +popup: "ãƒãƒƒãƒ—アップ" showNoteActionsOnlyHover: "ノートã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ホãƒãƒ¼æ™‚ã®ã¿è¡¨ç¤ºã™ã‚‹" showReactionsCount: "ノートã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³æ•°ã‚’表示ã™ã‚‹" noHistory: "å±¥æ´ã¯ã‚ã‚Šã¾ã›ã‚“" @@ -538,12 +534,10 @@ createAccount: "アカウントを作æˆ" existingAccount: "æ—¢å˜ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆ" regenerate: "å†ç”Ÿæˆ" fontSize: "フォントサイズ" -cornerRadius: "コーナーã®ä¸¸ã¿" mediaListWithOneImageAppearance: "ç”»åƒãŒ1æžšã®ã¿ã®ãƒ¡ãƒ‡ã‚£ã‚¢ãƒªã‚¹ãƒˆã®é«˜ã•" limitTo: "{x}を上é™ã«" noFollowRequests: "フォãƒãƒ¼ç”³è«‹ã¯ã‚ã‚Šã¾ã›ã‚“" openImageInNewTab: "ç”»åƒã‚’æ–°ã—ã„タブã§é–‹ã" -warnForMissingAltText: "代替テã‚ストを入れ忘れãŸã¨ãã«è¦å‘Šã™ã‚‹" dashboard: "ダッシュボード" local: "ãƒãƒ¼ã‚«ãƒ«" remote: "リモート" @@ -576,8 +570,6 @@ objectStorageUseProxy: "Proxyを利用ã™ã‚‹" objectStorageUseProxyDesc: "API接続ã«proxyを利用ã—ãªã„å ´åˆã¯ã‚ªãƒ•ã«ã—ã¦ãã ã•ã„" objectStorageSetPublicRead: "アップãƒãƒ¼ãƒ‰æ™‚ã«'public-read'ã‚’è¨å®šã™ã‚‹" s3ForcePathStyleDesc: "s3ForcePathStyleを有効ã«ã™ã‚‹ã¨ã€ãƒã‚±ãƒƒãƒˆåã‚’URLã®ãƒ›ã‚¹ãƒˆåã§ã¯ãªãパスã®ä¸€éƒ¨ã¨ã—ã¦æŒ‡å®šã™ã‚‹ã“ã¨ã‚’強制ã—ã¾ã™ã€‚セルフホストã•ã‚ŒãŸMinioãªã©ã®ä½¿ç”¨æ™‚ã«æœ‰åŠ¹ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" -deeplFreeMode: "DeepLX-JS を使用ã™ã‚‹ (èªè¨¼ã‚ーä¸è¦)" -deeplFreeModeDescription: "DeepLX-JSã®è¨å®šæ–¹æ³•ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ãã ã•ã„。" serverLogs: "サーãƒãƒ¼ãƒã‚°" deleteAll: "å…¨ã¦å‰Šé™¤" showFixedPostForm: "タイムライン上部ã«æŠ•ç¨¿ãƒ•ã‚©ãƒ¼ãƒ を表示ã™ã‚‹" @@ -593,7 +585,7 @@ popout: "ãƒãƒƒãƒ—アウト" volume: "音é‡" masterVolume: "マスター音é‡" notUseSound: "サウンドを出力ã—ãªã„" -useSoundOnlyWhenActive: "SharkeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹" +useSoundOnlyWhenActive: "MisskeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹" details: "詳細" chooseEmoji: "絵文å—ã‚’é¸æŠž" unableToProcess: "æ“作を完了ã§ãã¾ã›ã‚“" @@ -609,7 +601,9 @@ sort: "ソート" ascendingOrder: "æ˜‡é †" descendingOrder: "é™é †" scratchpad: "スクラッãƒãƒ‘ッド" -scratchpadDescription: "スクラッãƒãƒ‘ッドã¯ã€AiScriptã®å®Ÿé¨“環境をæä¾›ã—ã¾ã™ã€‚Sharkeyã¨å¯¾è©±ã™ã‚‹ã‚³ãƒ¼ãƒ‰ã®è¨˜è¿°ã€å®Ÿè¡Œã€çµæžœã®ç¢ºèªãŒã§ãã¾ã™ã€‚" +scratchpadDescription: "スクラッãƒãƒ‘ッドã¯ã€AiScriptã®å®Ÿé¨“環境をæä¾›ã—ã¾ã™ã€‚Misskeyã¨å¯¾è©±ã™ã‚‹ã‚³ãƒ¼ãƒ‰ã®è¨˜è¿°ã€å®Ÿè¡Œã€çµæžœã®ç¢ºèªãŒã§ãã¾ã™ã€‚" +uiInspector: "UIインスペクター" +uiInspectorDescription: "メモリ上ã«å˜åœ¨ã—ã¦ã„ã‚‹UIコンãƒãƒ¼ãƒãƒ³ãƒˆã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ä¸€è¦§ã‚’見るã“ã¨ãŒã§ãã¾ã™ã€‚UIコンãƒãƒ¼ãƒãƒ³ãƒˆã¯Ui:C:系関数ã«ã‚ˆã‚Šç”Ÿæˆã•ã‚Œã¾ã™ã€‚" output: "出力" script: "スクリプト" disablePagesScript: "Pagesã®ã‚¹ã‚¯ãƒªãƒ—トを無効ã«ã™ã‚‹" @@ -707,11 +701,6 @@ channel: "ãƒãƒ£ãƒ³ãƒãƒ«" create: "作æˆ" notificationSetting: "通知è¨å®š" notificationSettingDesc: "表示ã™ã‚‹é€šçŸ¥ã®ç¨®åˆ¥ã‚’é¸æŠžã—ã¦ãã ã•ã„。" -enableFaviconNotificationDot: "未èªã®é€šçŸ¥ãŒã‚ã‚‹ã¨ãã«ã‚¿ãƒ–ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’目立ãŸã›ã‚‹" -verifyNotificationDotWorkingButton: "タブアイコン強調機能ã®å‹•ä½œç¢ºèª" -notificationDotNotWorking: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ç¾æ™‚点ã§ã¯ã‚¿ãƒ–アイコン強調機能をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。" -notificationDotWorking: "タブアイコン強調機能ã¯ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§æ£ã—ã機能ã—ã¦ã„ã¾ã™ã€‚" -notificationDotNotWorkingAdvice: "タブアイコン強調機能ãŒæ©Ÿèƒ½ã—ãªã„å ´åˆã¯ã€ç®¡ç†è€…ã«ãƒ‰ã‚ュメントを確èªã™ã‚‹ã‚ˆã†ã«ä¾é ¼ã—ã¦ãã ã•ã„ {link}" useGlobalSetting: "ã‚°ãƒãƒ¼ãƒãƒ«è¨å®šã‚’使ã†" useGlobalSettingDesc: "オンã«ã™ã‚‹ã¨ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®é€šçŸ¥è¨å®šãŒä½¿ç”¨ã•ã‚Œã¾ã™ã€‚オフã«ã™ã‚‹ã¨ã€å€‹åˆ¥ã«è¨å®šã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚" other: "ãã®ä»–" @@ -724,17 +713,14 @@ behavior: "動作" sample: "サンプル" abuseReports: "é€šå ±" reportAbuse: "é€šå ±" -reportAbuseRenote: "ãƒ–ãƒ¼ã‚¹ãƒˆã‚’é€šå ±" +reportAbuseRenote: "ãƒªãƒŽãƒ¼ãƒˆã‚’é€šå ±" reportAbuseOf: "{name}ã‚’é€šå ±ã™ã‚‹" fillAbuseReportDescription: "é€šå ±ç†ç”±ã®è©³ç´°ã‚’記入ã—ã¦ãã ã•ã„。対象ã®ãƒŽãƒ¼ãƒˆã‚„ページãªã©ãŒã‚ã‚‹å ´åˆã¯ãã®URLも記入ã—ã¦ãã ã•ã„。" abuseReported: "内容ãŒé€ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ã”å ±å‘Šã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã—ãŸã€‚" reporter: "é€šå ±è€…" reporteeOrigin: "é€šå ±å…ˆ" reporterOrigin: "é€šå ±å…ƒ" -forwardReport: "リモートサーãƒãƒ¼ã«é€šå ±ã‚’転é€ã™ã‚‹" -forwardReportIsAnonymous: "リモートサーãƒãƒ¼ã‹ã‚‰ã¯ã‚ãªãŸã®æƒ…å ±ã¯è¦‹ã‚Œãšã€åŒ¿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚" send: "é€ä¿¡" -abuseMarkAsResolved: "対応済ã¿ã«ã™ã‚‹" openInNewTab: "æ–°ã—ã„タブã§é–‹ã" openInSideView: "サイドビューã§é–‹ã" defaultNavigationBehaviour: "デフォルトã®ãƒŠãƒ“ゲーション" @@ -753,14 +739,14 @@ unclip: "クリップ解除" confirmToUnclipAlreadyClippedNote: "ã“ã®ãƒŽãƒ¼ãƒˆã¯ã™ã§ã«ã‚¯ãƒªãƒƒãƒ—「{name}ã€ã«å«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ノートをã“ã®ã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰é™¤å¤–ã—ã¾ã™ã‹ï¼Ÿ" public: "パブリック" private: "éžå…¬é–‹" -i18nInfo: "Sharkeyã¯æœ‰å¿—ã«ã‚ˆã£ã¦æ§˜ã€…ãªè¨€èªžã«ç¿»è¨³ã•ã‚Œã¦ã„ã¾ã™ã€‚{link}ã§ç¿»è¨³ã«å”力ã§ãã¾ã™ã€‚" +i18nInfo: "Misskeyã¯æœ‰å¿—ã«ã‚ˆã£ã¦æ§˜ã€…ãªè¨€èªžã«ç¿»è¨³ã•ã‚Œã¦ã„ã¾ã™ã€‚{link}ã§ç¿»è¨³ã«å”力ã§ãã¾ã™ã€‚" manageAccessTokens: "アクセストークンã®ç®¡ç†" accountInfo: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆæƒ…å ±" notesCount: "ノートã®æ•°" repliesCount: "返信ã—ãŸæ•°" -renotesCount: "ブーストã—ãŸæ•°" +renotesCount: "リノートã—ãŸæ•°" repliedCount: "返信ã•ã‚ŒãŸæ•°" -renotedCount: "ブーストã•ã‚ŒãŸæ•°" +renotedCount: "リノートã•ã‚ŒãŸæ•°" followingCount: "フォãƒãƒ¼æ•°" followersCount: "フォãƒãƒ¯ãƒ¼æ•°" sentReactionsCount: "リアクションã—ãŸæ•°" @@ -776,11 +762,6 @@ noCrawleDescription: "外部ã®æ¤œç´¢ã‚¨ãƒ³ã‚¸ãƒ³ã«ã‚ãªãŸã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒš lockedAccountInfo: "フォãƒãƒ¼ã‚’承èªåˆ¶ã«ã—ã¦ã‚‚ã€ãƒŽãƒ¼ãƒˆã®å…¬é–‹ç¯„囲を「フォãƒãƒ¯ãƒ¼ã€ã«ã—ãªã„é™ã‚Šã€èª°ã§ã‚‚ã‚ãªãŸã®ãƒŽãƒ¼ãƒˆã‚’見るã“ã¨ãŒã§ãã¾ã™ã€‚" alwaysMarkSensitive: "デフォルトã§ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’センシティブè¨å®šã«ã™ã‚‹" loadRawImages: "添付画åƒã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’オリジナル画質ã«ã™ã‚‹" -showTickerOnReplies: "返信ã«ã‚µãƒ¼ãƒãƒ¼æƒ…å ±ã‚’è¡¨ç¤ºã™ã‚‹" -searchEngine: "検索MFMã®æ¤œç´¢ã‚¨ãƒ³ã‚¸ãƒ³" -searchEngineOther: "カスタム" -searchEngineCustomURIDescription: "カスタム検索エンジンã®URIã¯ã€\"https://www.google.com/search?q=\\{query}\" ã‚„ \"https://www.google.com/search?q=%s\" ã®ã‚ˆã†ãªå½¢å¼ã§å…¥åŠ›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" -searchEngineCusomURI: "カスタム検索エンジン URI" disableShowingAnimatedImages: "アニメーション画åƒã‚’å†ç”Ÿã—ãªã„" highlightSensitiveMedia: "メディアãŒã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã§ã‚ã‚‹ã“ã¨ã‚’分ã‹ã‚Šã‚„ã™ã表示" verificationEmailSent: "確èªã®ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚メールã«è¨˜è¼‰ã•ã‚ŒãŸãƒªãƒ³ã‚¯ã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ã€è¨å®šã‚’完了ã—ã¦ãã ã•ã„。" @@ -798,15 +779,13 @@ thisIsExperimentalFeature: "ã“ã‚Œã¯å®Ÿé¨“çš„ãªæ©Ÿèƒ½ã§ã™ã€‚仕様ãŒå¤‰æ›´ developer: "開発者" makeExplorable: "アカウントを見ã¤ã‘ã‚„ã™ãã™ã‚‹" makeExplorableDescription: "オフã«ã™ã‚‹ã¨ã€ã€Œã¿ã¤ã‘ã‚‹ã€ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒè¼‰ã‚‰ãªããªã‚Šã¾ã™ã€‚" -makeIndexable: "公開ノートをインデックスä¸å¯ã«ã™ã‚‹" -makeIndexableDescription: "ノート検索ãŒã‚ãªãŸã®å…¬é–‹ãƒŽãƒ¼ãƒˆã‚’インデックス化ã—ãªã„よã†ã«ã—ã¾ã™ã€‚" showGapBetweenNotesInTimeline: "タイムラインã®ãƒŽãƒ¼ãƒˆã‚’離ã—ã¦è¡¨ç¤º" duplicate: "複製" left: "å·¦" center: "ä¸å¤®" wide: "広ã„" narrow: "ç‹ã„" -reloadToApplySetting: "è¨å®šã¯ãƒšãƒ¼ã‚¸ãƒªãƒãƒ¼ãƒ‰å¾Œã«åæ˜ ã•ã‚Œã¾ã™ã€‚今ã™ãリãƒãƒ¼ãƒ‰ã—ã¾ã™ã‹ï¼Ÿ" +reloadToApplySetting: "è¨å®šã¯ãƒšãƒ¼ã‚¸ãƒªãƒãƒ¼ãƒ‰å¾Œã«åæ˜ ã•ã‚Œã¾ã™ã€‚" needReloadToApply: "åæ˜ ã«ã¯å†èµ·å‹•ãŒå¿…è¦ã§ã™ã€‚" showTitlebar: "タイトルãƒãƒ¼ã‚’表示ã™ã‚‹" clearCache: "ã‚ャッシュをクリア" @@ -814,7 +793,7 @@ onlineUsersCount: "{n}人ãŒã‚ªãƒ³ãƒ©ã‚¤ãƒ³" nUsers: "{n}ユーザー" nNotes: "{n}ノート" sendErrorReports: "エラーリãƒãƒ¼ãƒˆã‚’é€ä¿¡" -sendErrorReportsDescription: "オンã«ã™ã‚‹ã¨ã€å•é¡ŒãŒç™ºç”Ÿã—ãŸã¨ãã«ã‚¨ãƒ©ãƒ¼ã®è©³ç´°æƒ…å ±ãŒSharkeyã«å…±æœ‰ã•ã‚Œã€ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã®å“質å‘上ã«å½¹ç«‹ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã©ãŒå«ã¾ã‚Œã¾ã™ã€‚" +sendErrorReportsDescription: "オンã«ã™ã‚‹ã¨ã€å•é¡ŒãŒç™ºç”Ÿã—ãŸã¨ãã«ã‚¨ãƒ©ãƒ¼ã®è©³ç´°æƒ…å ±ãŒMisskeyã«å…±æœ‰ã•ã‚Œã€ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã®å“質å‘上ã«å½¹ç«‹ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã©ãŒå«ã¾ã‚Œã¾ã™ã€‚" myTheme: "マイテーマ" backgroundColor: "背景" accentColor: "アクセント" @@ -909,7 +888,7 @@ hashtags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" troubleshooting: "トラブルシューティング" useBlurEffect: "UIã«ã¼ã‹ã—効果を使用" learnMore: "詳ã—ã" -misskeyUpdated: "SharkeyãŒæ›´æ–°ã•ã‚Œã¾ã—ãŸï¼" +misskeyUpdated: "MisskeyãŒæ›´æ–°ã•ã‚Œã¾ã—ãŸï¼" whatIsNew: "æ›´æ–°æƒ…å ±ã‚’è¦‹ã‚‹" translate: "翻訳" translatedFrom: "{x}ã‹ã‚‰ç¿»è¨³" @@ -929,7 +908,6 @@ itsOff: "オフã«ãªã£ã¦ã„ã¾ã™" on: "オン" off: "オフ" emailRequiredForSignup: "アカウント登録ã«ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’å¿…é ˆã«ã™ã‚‹" -approvalRequiredForSignup: "アカウント登録を承èªåˆ¶ã«ã™ã‚‹" unread: "未èª" filter: "フィルタ" controlPanel: "コントãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«" @@ -944,8 +922,8 @@ followersVisibility: "フォãƒãƒ¯ãƒ¼ã®å…¬é–‹ç¯„囲" continueThread: "ã•ã‚‰ã«ã‚¹ãƒ¬ãƒƒãƒ‰ã‚’見る" deleteAccountConfirm: "アカウントãŒå‰Šé™¤ã•ã‚Œã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" incorrectPassword: "パスワードãŒé–“é•ã£ã¦ã„ã¾ã™ã€‚" +incorrectTotp: "ワンタイムパスワードãŒé–“é•ã£ã¦ã„ã‚‹ã‹ã€æœŸé™åˆ‡ã‚Œã«ãªã£ã¦ã„ã¾ã™ã€‚" voteConfirm: "「{choice}ã€ã«æŠ•ç¥¨ã—ã¾ã™ã‹ï¼Ÿ" -voteConfirmMulti: "「{choice}ã€ã«æŠ•ç¥¨ã—ã¾ã™ã‹ï¼Ÿ\n 確èªå¾Œã€é¸æŠžè‚¢ã‚’増やã™ã“ã¨ãŒã§ãã¾ã™ã€‚" hide: "éš ã™" useDrawerReactionPickerForMobile: "モãƒã‚¤ãƒ«ãƒ‡ãƒã‚¤ã‚¹ã®ã¨ãドãƒãƒ¯ãƒ¼ã§è¡¨ç¤º" welcomeBackWithName: "ãŠã‹ãˆã‚Šãªã•ã„ã€{name}ã•ã‚“" @@ -981,7 +959,6 @@ recentNHours: "ç›´è¿‘{n}時間" recentNDays: "ç›´è¿‘{n}æ—¥" noEmailServerWarning: "メールサーãƒãƒ¼ã®è¨å®šãŒã•ã‚Œã¦ã„ã¾ã›ã‚“。" thereIsUnresolvedAbuseReportWarning: "未対応ã®é€šå ±ãŒã‚ã‚Šã¾ã™ã€‚" -pendingUserApprovals: "承èªå¾…ã¡ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„ã¾ã™ã€‚" recommended: "推奨" check: "ãƒã‚§ãƒƒã‚¯" driveCapOverrideLabel: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡ä¸Šé™ã‚’変更" @@ -990,20 +967,9 @@ requireAdminForView: "閲覧ã™ã‚‹ã«ã¯ç®¡ç†è€…アカウントã§ãƒã‚°ã‚¤ãƒ³ isSystemAccount: "システムã«ã‚ˆã‚Šè‡ªå‹•ã§ä½œæˆãƒ»ç®¡ç†ã•ã‚Œã¦ã„るアカウントã§ã™ã€‚" typeToConfirm: "ã“ã®æ“作を行ã†ã«ã¯ {x} ã¨å…¥åŠ›ã—ã¦ãã ã•ã„" deleteAccount: "アカウント削除" -approveAccount: "承èªã™ã‚‹" -denyAccount: "æ‹’å¦ã¨å‰Šé™¤" -approved: "承èªæ¸ˆã¿" -notApproved: "承èªã•ã‚Œã¦ã„ãªã„" -approvalStatus: "承èªçŠ¶æ³" document: "ドã‚ュメント" numberOfPageCache: "ページã‚ャッシュ数" numberOfPageCacheDescription: "多ãã™ã‚‹ã¨åˆ©ä¾¿æ€§ãŒå‘上ã—ã¾ã™ãŒã€è² è·ã¨ãƒ¡ãƒ¢ãƒªä½¿ç”¨é‡ãŒå¢—ãˆã¾ã™ã€‚" -numberOfReplies: "スレッド内ã®è¿”ä¿¡æ•°" -numberOfRepliesDescription: "ã“ã®æ•°å€¤ã‚’大ããã™ã‚‹ã¨ã€ã‚ˆã‚Šå¤šãã®è¿”ä¿¡ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®å€¤ã‚’大ããã—ã™ãŽã‚‹ã¨ã€UIãŒçª®å±ˆã«ãªã£ã¦èªã¿ã«ãããªã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" -boostSettings: "ブーストè¨å®š" -showVisibilitySelectorOnBoost: "公開範囲セレクターを表示" -showVisibilitySelectorOnBoostDescription: "無効ã®å ´åˆã€ä»¥ä¸‹ã§è¨å®šã—ãŸãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å…¬é–‹ç¯„囲ãŒä½¿ç”¨ã•ã‚Œã€ã‚»ãƒ¬ã‚¯ã‚¿ãƒ¼ã¯è¡¨ç¤ºã•ã‚Œã¾ã›ã‚“。" -visibilityOnBoost: "デフォルトã®ãƒ–ースト公開範囲" logoutConfirm: "ãƒã‚°ã‚¢ã‚¦ãƒˆã—ã¾ã™ã‹ï¼Ÿ" lastActiveDate: "最終利用日時" statusbar: "ステータスãƒãƒ¼" @@ -1048,14 +1014,12 @@ cannotLoad: "èªã¿è¾¼ã‚ã¾ã›ã‚“" numberOfProfileView: "プãƒãƒ•ã‚£ãƒ¼ãƒ«è¡¨ç¤ºå›žæ•°" like: "ã„ã„ãï¼" unlike: "ã„ã„ãを解除" -defaultLike: "絵文å—ã®ã‚ˆã†ãªãƒ‡ãƒ•ã‚©ãƒ«ãƒˆ" numberOfLikes: "ã„ã„ãæ•°" show: "表示" neverShow: "今後表示ã—ãªã„" remindMeLater: "ã¾ãŸå¾Œã§" -didYouLikeMisskey: "Sharkeyã‚’æ°—ã«å…¥ã£ã¦ã„ãŸã ã‘ã¾ã—ãŸã‹ï¼Ÿ" -pleaseDonate: "Sharkeyã¯{host}ãŒä½¿ç”¨ã—ã¦ã„ã‚‹ç„¡æ–™ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚ã“ã‚Œã‹ã‚‰ã‚‚開発を続ã‘られるよã†ã«ã€ãœã²å¯„付をãŠé¡˜ã„ã—ã¾ã™ï¼" -pleaseDonateInstance: "インスタンス管ç†è€…ã¸ã®å¯„付ã«ã‚ˆã£ã¦{host}を直接サãƒãƒ¼ãƒˆã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" +didYouLikeMisskey: "Misskeyã‚’æ°—ã«å…¥ã£ã¦ã„ãŸã ã‘ã¾ã—ãŸã‹ï¼Ÿ" +pleaseDonate: "Misskeyã¯{host}ãŒä½¿ç”¨ã—ã¦ã„ã‚‹ç„¡æ–™ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚ã“ã‚Œã‹ã‚‰ã‚‚開発を続ã‘られるよã†ã«ã€ãœã²å¯„付をãŠé¡˜ã„ã—ã¾ã™ï¼" correspondingSourceIsAvailable: "対応ã™ã‚‹ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã¯{anchor}ã‹ã‚‰åˆ©ç”¨å¯èƒ½ã§ã™ã€‚" roles: "ãƒãƒ¼ãƒ«" role: "ãƒãƒ¼ãƒ«" @@ -1083,16 +1047,8 @@ thisPostMayBeAnnoying: "ã“ã®æŠ•ç¨¿ã¯è¿·æƒ‘ã«ãªã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ thisPostMayBeAnnoyingHome: "ホームã«æŠ•ç¨¿" thisPostMayBeAnnoyingCancel: "ã‚„ã‚ã‚‹" thisPostMayBeAnnoyingIgnore: "ã“ã®ã¾ã¾æŠ•ç¨¿" -thisPostIsMissingAltTextCancel: "ã‚„ã‚ã‚‹" -thisPostIsMissingAltTextIgnore: "ã“ã®ã¾ã¾æŠ•ç¨¿" -thisPostIsMissingAltText: "代替テã‚ストãŒãªã„ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ã¾ã™ã€‚ã™ã¹ã¦ã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã«ä»£æ›¿ãƒ†ã‚ストをå«ã‚€ã‚ˆã†ã«ã—ã¦ãã ã•ã„。" -collapseRenotes: "ブーストã®ã‚¹ãƒžãƒ¼ãƒˆçœç•¥" -collapseRenotesDescription: "リアクションやブーストをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã—ã¾ã™ã€‚" -collapseNotesRepliedTo: "返信元ã®ãƒŽãƒ¼ãƒˆã‚’折りãŸãŸã‚€" -collapseFiles: "ファイルを折りãŸãŸã‚€" -uncollapseCW: "CWを展開ã™ã‚‹" -expandLongNote: "é•·ã„投稿を常ã«å±•é–‹ã™ã‚‹" -autoloadConversation: "会話スレッドを自動ã§èªã¿è¾¼ã‚€" +collapseRenotes: "リノートã®ã‚¹ãƒžãƒ¼ãƒˆçœç•¥" +collapseRenotesDescription: "リアクションやリノートをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã—ã¾ã™ã€‚" internalServerError: "サーãƒãƒ¼å†…部エラー" internalServerErrorDescription: "サーãƒãƒ¼å†…部ã§äºˆæœŸã—ãªã„エラーãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" copyErrorInfo: "ã‚¨ãƒ©ãƒ¼æƒ…å ±ã‚’ã‚³ãƒ”ãƒ¼" @@ -1103,7 +1059,6 @@ disableFederationConfirm: "連åˆãªã—ã«ã—ã¾ã™ã‹ï¼Ÿ" disableFederationConfirmWarn: "連åˆãªã—ã«ã—ã¦ã‚‚投稿ã¯éžå…¬é–‹ã«ãªã‚Šã¾ã›ã‚“。ã»ã¨ã‚“ã©ã®å ´åˆã€é€£åˆãªã—ã«ã™ã‚‹å¿…è¦ã¯ã‚ã‚Šã¾ã›ã‚“。" disableFederationOk: "連åˆãªã—ã«ã™ã‚‹" invitationRequiredToRegister: "ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‹›å¾…制ã§ã™ã€‚招待コードをãŠæŒã¡ã®æ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚" -approvalRequiredToRegister: "ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‰¿èªåˆ¶ã§ã™ã€‚å‚åŠ ã—ãŸã„ç†ç”±ã‚’記入ã—ã€æ‰¿èªã•ã‚ŒãŸæ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚" emailNotSupported: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§ã¯ãƒ¡ãƒ¼ãƒ«é…ä¿¡ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“" postToTheChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã«æŠ•ç¨¿" cannotBeChangedLater: "後ã‹ã‚‰å¤‰æ›´ã§ãã¾ã›ã‚“。" @@ -1132,6 +1087,7 @@ retryAllQueuesConfirmTitle: "今ã™ãå†è©¦è¡Œã—ã¾ã™ã‹ï¼Ÿ" retryAllQueuesConfirmText: "一時的ã«ã‚µãƒ¼ãƒãƒ¼ã®è² è·ãŒå¢—大ã™ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" enableChartsForRemoteUser: "リモートユーザーã®ãƒãƒ£ãƒ¼ãƒˆã‚’生æˆ" enableChartsForFederatedInstances: "リモートサーãƒãƒ¼ã®ãƒãƒ£ãƒ¼ãƒˆã‚’生æˆ" +enableStatsForFederatedInstances: "リモートサーãƒãƒ¼ã®æƒ…å ±ã‚’å–å¾—" showClipButtonInNoteFooter: "ノートã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«ã‚¯ãƒªãƒƒãƒ—ã‚’è¿½åŠ " reactionsDisplaySize: "リアクションã®è¡¨ç¤ºã‚µã‚¤ã‚º" limitWidthOfReaction: "リアクションã®æœ€å¤§æ¨ªå¹…を制é™ã—ã€ç¸®å°ã—ã¦è¡¨ç¤ºã™ã‚‹" @@ -1146,11 +1102,10 @@ accountMoved: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯æ–°ã—ã„アカウントã«ç§»è¡Œã—ã¾ã— accountMovedShort: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ç§»è¡Œã•ã‚Œã¦ã„ã¾ã™" operationForbidden: "ã“ã®æ“作ã¯ã§ãã¾ã›ã‚“" forceShowAds: "常ã«åºƒå‘Šã‚’表示ã™ã‚‹" -oneko: "ã«ã‚ƒã‚“ã“フレンド :3" addMemo: "ãƒ¡ãƒ¢ã‚’è¿½åŠ " editMemo: "メモを編集" reactionsList: "リアクション一覧" -renotesList: "ブースト一覧" +renotesList: "リノート一覧" notificationDisplay: "通知ã®è¡¨ç¤º" leftTop: "左上" rightTop: "å³ä¸Š" @@ -1191,15 +1146,11 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ãƒãƒ¼ãƒ«ã¯å…¬é–‹ãƒãƒ¼ cancelReactionConfirm: "リアクションをå–り消ã—ã¾ã™ã‹ï¼Ÿ" changeReactionConfirm: "リアクションを変更ã—ã¾ã™ã‹ï¼Ÿ" later: "ã‚ã¨ã§" -goToMisskey: "Sharkeyã¸" +goToMisskey: "Misskeyã¸" additionalEmojiDictionary: "絵文å—ã®è¿½åŠ 辞書" installed: "インストール済ã¿" branding: "ブランディング" enableServerMachineStats: "サーãƒãƒ¼ã®ãƒžã‚·ãƒ³æƒ…å ±ã‚’å…¬é–‹ã™ã‚‹" -enableAchievements: "実績を有効ã«ã™ã‚‹" -turnOffAchievements: "オフã«ã™ã‚‹ã¨å®Ÿç¸¾ã‚·ã‚¹ãƒ†ãƒ ã¯ç„¡åŠ¹ã«ãªã‚Šã¾ã™ã€‚" -enableBotTrending: "botã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°è¿½åŠ を許å¯ã™ã‚‹" -turnOffBotTrending: "オフã«ã™ã‚‹ã¨ãƒœãƒƒãƒˆãŒãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’入力ã—ãªããªã‚Šã¾ã™ã€‚" enableIdenticonGeneration: "ユーザーã”ã¨ã®Identicon生æˆã‚’有効ã«ã™ã‚‹" turnOffToImprovePerformance: "オフã«ã™ã‚‹ã¨ãƒ‘フォーマンスãŒå‘上ã—ã¾ã™ã€‚" createInviteCode: "招待コードを作æˆ" @@ -1230,20 +1181,18 @@ pastAnnouncements: "éŽåŽ»ã®ãŠçŸ¥ã‚‰ã›" youHaveUnreadAnnouncements: "未èªã®ãŠçŸ¥ã‚‰ã›ãŒã‚ã‚Šã¾ã™ã€‚" useSecurityKey: "ブラウザã¾ãŸã¯ãƒ‡ãƒã‚¤ã‚¹ã®æŒ‡ç¤ºã«å¾“ã£ã¦ã€ã‚»ã‚ュリティã‚ーã¾ãŸã¯ãƒ‘スã‚ーを使用ã—ã¦ãã ã•ã„。" replies: "返信" -renotes: "ブースト" +renotes: "リノート" loadReplies: "返信を見る" loadConversation: "会話を見る" pinnedList: "ピン留ã‚ã•ã‚ŒãŸãƒªã‚¹ãƒˆ" keepScreenOn: "デãƒã‚¤ã‚¹ã®ç”»é¢ã‚’常ã«ã‚ªãƒ³ã«ã™ã‚‹" -clickToOpen: "クリックã—ã¦ãƒŽãƒ¼ãƒˆã‚’é–‹ã" -showBots: "ボットをタイムラインã«è¡¨ç¤º" verifiedLink: "ã“ã®ãƒªãƒ³ã‚¯å…ˆã®æ‰€æœ‰è€…ã§ã‚ã‚‹ã“ã¨ãŒç¢ºèªã•ã‚Œã¾ã—ãŸ" notifyNotes: "投稿を通知" unnotifyNotes: "投稿ã®é€šçŸ¥ã‚’解除" authentication: "èªè¨¼" authenticationRequiredToContinue: "続ã‘ã‚‹ã«ã¯èªè¨¼ã‚’è¡Œã£ã¦ãã ã•ã„" dateAndTime: "日時" -showRenotes: "ブーストを表示" +showRenotes: "リノートを表示" edited: "編集済ã¿" notificationRecieveConfig: "通知ã®å—ä¿¡è¨å®š" mutualFollow: "相互フォãƒãƒ¼" @@ -1259,7 +1208,7 @@ externalServices: "外部サービス" sourceCode: "ソースコード" sourceCodeIsNotYetProvided: "ソースコードã¯ã¾ã æä¾›ã•ã‚Œã¦ã„ã¾ã›ã‚“。ã“ã®å•é¡Œã®ä¿®æ£ã«ã¤ã„ã¦ç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。" repositoryUrl: "リãƒã‚¸ãƒˆãƒªURL" -repositoryUrlDescription: "ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Sharkeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://activitypub.software/TransFem-org/Sharkey/ ã¨è¨˜å…¥ã—ã¾ã™ã€‚" +repositoryUrlDescription: "ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Misskeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://github.com/misskey-dev/misskey ã¨è¨˜å…¥ã—ã¾ã™ã€‚" repositoryUrlOrTarballRequired: "リãƒã‚¸ãƒˆãƒªã‚’公開ã—ã¦ã„ãªã„å ´åˆã€ä»£ã‚ã‚Šã«tarballã‚’æä¾›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚詳細ã¯.config/example.ymlã‚’å‚ç…§ã—ã¦ãã ã•ã„。" feedback: "フィードãƒãƒƒã‚¯" feedbackUrl: "フィードãƒãƒƒã‚¯URL" @@ -1269,8 +1218,6 @@ impressumDescription: "ドイツãªã©ã®ä¸€éƒ¨ã®å›½ã¨åœ°åŸŸã§ã¯è¡¨ç¤ºãŒç¾© privacyPolicy: "プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼" privacyPolicyUrl: "プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼URL" tosAndPrivacyPolicy: "利用è¦ç´„・プライãƒã‚·ãƒ¼ãƒãƒªã‚·ãƒ¼" -donation: "寄付ã™ã‚‹" -donationUrl: "寄付URL" avatarDecorations: "アイコンデコレーション" attach: "付ã‘ã‚‹" detach: "外ã™" @@ -1327,6 +1274,33 @@ confirmWhenRevealingSensitiveMedia: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã‚’表示 sensitiveMediaRevealConfirm: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã§ã™ã€‚表示ã—ã¾ã™ã‹ï¼Ÿ" createdLists: "作æˆã—ãŸãƒªã‚¹ãƒˆ" createdAntennas: "作æˆã—ãŸã‚¢ãƒ³ãƒ†ãƒŠ" +fromX: "{x}ã‹ã‚‰" +genEmbedCode: "埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’生æˆ" +noteOfThisUser: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆä¸€è¦§" +clipNoteLimitExceeded: "ã“れ以上ã“ã®ã‚¯ãƒªãƒƒãƒ—ã«ãƒŽãƒ¼ãƒˆã‚’è¿½åŠ ã§ãã¾ã›ã‚“。" +performance: "パフォーマンス" +modified: "変更ã‚ã‚Š" +discard: "ç ´æ£„" +thereAreNChanges: "{n}件ã®å¤‰æ›´ãŒã‚ã‚Šã¾ã™" +signinWithPasskey: "パスã‚ーã§ãƒã‚°ã‚¤ãƒ³" +unknownWebAuthnKey: "登録ã•ã‚Œã¦ã„ãªã„パスã‚ーã§ã™ã€‚" +passkeyVerificationFailed: "パスã‚ーã®æ¤œè¨¼ã«å¤±æ•—ã—ã¾ã—ãŸã€‚" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスã‚ーã®æ¤œè¨¼ã«æˆåŠŸã—ã¾ã—ãŸãŒã€ãƒ‘スワードレスãƒã‚°ã‚¤ãƒ³ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ã€‚" +messageToFollower: "フォãƒãƒ¯ãƒ¼ã¸ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸" +target: "対象" +testCaptchaWarning: "CAPTCHAã®ãƒ†ã‚¹ãƒˆã‚’目的ã¨ã—ãŸæ©Ÿèƒ½ã§ã™ã€‚<strong>本番環境ã§ä½¿ç”¨ã—ãªã„ã§ãã ã•ã„。</strong>" +prohibitedWordsForNameOfUser: "ç¦æ¢ãƒ¯ãƒ¼ãƒ‰ï¼ˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã®åå‰ï¼‰" +prohibitedWordsForNameOfUserDescription: "ã“ã®ãƒªã‚¹ãƒˆã«å«ã¾ã‚Œã‚‹æ–‡å—列ãŒãƒ¦ãƒ¼ã‚¶ãƒ¼ã®åå‰ã«å«ã¾ã‚Œã‚‹å ´åˆã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®åå‰ã®å¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã™ã€‚モデレーター権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã“ã®åˆ¶é™ã®å½±éŸ¿ã‚’å—ã‘ã¾ã›ã‚“。" +yourNameContainsProhibitedWords: "変更ã—よã†ã¨ã—ãŸåå‰ã«ç¦æ¢ã•ã‚ŒãŸæ–‡å—列ãŒå«ã¾ã‚Œã¦ã„ã¾ã™" +yourNameContainsProhibitedWordsDescription: "åå‰ã«ç¦æ¢ã•ã‚Œã¦ã„ã‚‹æ–‡å—列ãŒå«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ã“ã®åå‰ã‚’使用ã—ãŸã„å ´åˆã¯ã€ã‚µãƒ¼ãƒãƒ¼ç®¡ç†è€…ã«ãŠå•ã„åˆã‚ã›ãã ã•ã„。" + +_abuseUserReport: + forward: "転é€" + forwardDescription: "匿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦ã€ãƒªãƒ¢ãƒ¼ãƒˆã‚µãƒ¼ãƒãƒ¼ã«é€šå ±ã‚’転é€ã—ã¾ã™ã€‚" + resolve: "解決" + accept: "是èª" + reject: "å¦èª" + resolveTutorial: "内容ãŒæ£å½“ã§ã‚ã‚‹é€šå ±ã«å¯¾å¿œã—ãŸå ´åˆã¯ã€Œæ˜¯èªã€ã‚’é¸æŠžã—ã€è‚¯å®šçš„ã«ã‚±ãƒ¼ã‚¹ãŒè§£æ±ºã•ã‚ŒãŸã“ã¨ã‚’マークã—ã¾ã™ã€‚\n内容ãŒæ£å½“ã§ãªã„é€šå ±ã®å ´åˆã¯ã€Œå¦èªã€ã‚’é¸æŠžã—ã€å¦å®šçš„ã«ã‚±ãƒ¼ã‚¹ãŒè§£æ±ºã•ã‚ŒãŸã“ã¨ã‚’マークã—ã¾ã™ã€‚" _delivery: status: "é…信状態" @@ -1380,7 +1354,7 @@ _initialAccountSetting: pushNotificationDescription: "プッシュ通知を有効ã«ã™ã‚‹ã¨{name}ã®é€šçŸ¥ã‚’ãŠä½¿ã„ã®ãƒ‡ãƒã‚¤ã‚¹ã§å—ã‘å–ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" initialAccountSettingCompleted: "åˆæœŸè¨å®šãŒå®Œäº†ã—ã¾ã—ãŸï¼" haveFun: "{name}ã‚’ãŠæ¥½ã—ã¿ãã ã•ã„ï¼" - youCanContinueTutorial: "ã“ã®ã¾ã¾{name}(Sharkey)ã®ä½¿ã„æ–¹ã«ã¤ã„ã¦ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«é€²ã‚€ã“ã¨ã‚‚ã§ãã¾ã™ãŒã€ã“ã“ã§ä¸æ–ã—ã¦ã™ãã«ä½¿ã„始ã‚ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" + youCanContinueTutorial: "ã“ã®ã¾ã¾{name}(Misskey)ã®ä½¿ã„æ–¹ã«ã¤ã„ã¦ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«é€²ã‚€ã“ã¨ã‚‚ã§ãã¾ã™ãŒã€ã“ã“ã§ä¸æ–ã—ã¦ã™ãã«ä½¿ã„始ã‚ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" startTutorial: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã‚’開始" skipAreYouSure: "åˆæœŸè¨å®šã‚’スã‚ップã—ã¾ã™ã‹ï¼Ÿ" laterAreYouSure: "åˆæœŸè¨å®šã‚’ã‚ã¨ã§ã‚„ã‚Šç›´ã—ã¾ã™ã‹ï¼Ÿ" @@ -1392,10 +1366,10 @@ _initialTutorial: skipAreYouSure: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã‚’終了ã—ã¾ã™ã‹ï¼Ÿ" _landing: title: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã¸ã‚ˆã†ã“ã" - description: "ã“ã“ã§ã¯ã€Sharkeyã®åŸºæœ¬çš„ãªä½¿ã„方や機能を確èªã§ãã¾ã™ã€‚" + description: "ã“ã“ã§ã¯ã€Misskeyã®åŸºæœ¬çš„ãªä½¿ã„方や機能を確èªã§ãã¾ã™ã€‚" _note: title: "ノートã£ã¦ä½•ï¼Ÿ" - description: "Sharkeyã§ã®æŠ•ç¨¿ã¯ã€ŒãƒŽãƒ¼ãƒˆã€ã¨å‘¼ã³ã¾ã™ã€‚ノートã¯ã‚¿ã‚¤ãƒ ラインã«æ™‚系列ã§ä¸¦ã‚“ã§ã„ã¦ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§æ›´æ–°ã•ã‚Œã¦ã„ãã¾ã™ã€‚" + description: "Misskeyã§ã®æŠ•ç¨¿ã¯ã€ŒãƒŽãƒ¼ãƒˆã€ã¨å‘¼ã³ã¾ã™ã€‚ノートã¯ã‚¿ã‚¤ãƒ ラインã«æ™‚系列ã§ä¸¦ã‚“ã§ã„ã¦ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§æ›´æ–°ã•ã‚Œã¦ã„ãã¾ã™ã€‚" reply: "返信ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚返信ã«å¯¾ã—ã¦ã®è¿”ä¿¡ã‚‚å¯èƒ½ã§ã€ã‚¹ãƒ¬ãƒƒãƒ‰ã®ã‚ˆã†ã«ä¼šè©±ã‚’続ã‘ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" renote: "ãã®ãƒŽãƒ¼ãƒˆã‚’自分ã®ã‚¿ã‚¤ãƒ ラインã«æµã—ã¦å…±æœ‰ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚テã‚ã‚¹ãƒˆã‚’è¿½åŠ ã—ã¦å¼•ç”¨ã™ã‚‹ã“ã¨ã‚‚å¯èƒ½ã§ã™ã€‚" reaction: "リアクションをã¤ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚詳ã—ãã¯æ¬¡ã®ãƒšãƒ¼ã‚¸ã§è§£èª¬ã—ã¾ã™ã€‚" @@ -1403,13 +1377,13 @@ _initialTutorial: _reaction: title: "リアクションã£ã¦ä½•ï¼Ÿ" description: "ノートã«ã¯ã€Œãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€ã‚’ã¤ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚「ã„ã„ãã€ã§ã¯ä¼ã‚らãªã„ニュアンスもã€ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ç°¡å˜ãƒ»æ°—軽ã«è¡¨ç¾ã§ãã¾ã™ã€‚" - letsTryReacting: "リアクションã¯ã€ãƒŽãƒ¼ãƒˆã®ã€Œ{reaction}ã€ãƒœã‚¿ãƒ³ã‚’クリックã™ã‚‹ã¨ã¤ã‘られã¾ã™ã€‚試ã—ã«ã“ã®ã‚µãƒ³ãƒ—ルã®ãƒŽãƒ¼ãƒˆã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ã¤ã‘ã¦ã¿ã¦ãã ã•ã„ï¼" + letsTryReacting: "リアクションã¯ã€ãƒŽãƒ¼ãƒˆã®ã€Œï¼‹ã€ãƒœã‚¿ãƒ³ã‚’クリックã™ã‚‹ã¨ã¤ã‘られã¾ã™ã€‚試ã—ã«ã“ã®ã‚µãƒ³ãƒ—ルã®ãƒŽãƒ¼ãƒˆã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ã¤ã‘ã¦ã¿ã¦ãã ã•ã„ï¼" reactToContinue: "リアクションをã¤ã‘ã‚‹ã¨å…ˆã«é€²ã‚るよã†ã«ãªã‚Šã¾ã™ã€‚" reactNotification: "ã‚ãªãŸã®ãƒŽãƒ¼ãƒˆãŒèª°ã‹ã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã•ã‚Œã‚‹ã¨ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§é€šçŸ¥ã‚’å—ã‘å–ã‚Šã¾ã™ã€‚" - reactDone: "「{undo}ã€ãƒœã‚¿ãƒ³ã‚’押ã™ã¨ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’å–り消ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" + reactDone: "「ーã€ãƒœã‚¿ãƒ³ã‚’押ã™ã¨ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’å–り消ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" _timeline: title: "タイムラインã®ã—ãã¿" - description1: "Sharkeyã«ã¯ã€ä½¿ã„æ–¹ã«å¿œã˜ã¦è¤‡æ•°ã®ã‚¿ã‚¤ãƒ ラインãŒç”¨æ„ã•ã‚Œã¦ã„ã¾ã™ï¼ˆã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦ã¯ã„ãšã‚Œã‹ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ï¼‰ã€‚" + description1: "Misskeyã«ã¯ã€ä½¿ã„æ–¹ã«å¿œã˜ã¦è¤‡æ•°ã®ã‚¿ã‚¤ãƒ ラインãŒç”¨æ„ã•ã‚Œã¦ã„ã¾ã™ï¼ˆã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦ã¯ã„ãšã‚Œã‹ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ï¼‰ã€‚" home: "ã‚ãªãŸãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るアカウントã®æŠ•ç¨¿ã‚’見られã¾ã™ã€‚" local: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«ã„るユーザー全員ã®æŠ•ç¨¿ã‚’見られã¾ã™ã€‚" social: "ホームタイムラインã¨ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã®æŠ•ç¨¿ãŒä¸¡æ–¹è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚" @@ -1418,12 +1392,12 @@ _initialTutorial: description3: "ãã®ä»–ã«ã‚‚ã€ãƒªã‚¹ãƒˆã‚¿ã‚¤ãƒ ラインやãƒãƒ£ãƒ³ãƒãƒ«ã‚¿ã‚¤ãƒ ラインãªã©ãŒã‚ã‚Šã¾ã™ã€‚詳ã—ãã¯{link}ã‚’ã”覧ãã ã•ã„。" _postNote: title: "ノートã®æŠ•ç¨¿è¨å®š" - description1: "Sharkeyã«ãƒŽãƒ¼ãƒˆã‚’投稿ã™ã‚‹éš›ã«ã¯ã€æ§˜ã€…ãªã‚ªãƒ—ションã®è¨å®šãŒå¯èƒ½ã§ã™ã€‚投稿フォームã¯ã“ã®ã‚ˆã†ã«ãªã£ã¦ã„ã¾ã™ã€‚" + description1: "Misskeyã«ãƒŽãƒ¼ãƒˆã‚’投稿ã™ã‚‹éš›ã«ã¯ã€æ§˜ã€…ãªã‚ªãƒ—ションã®è¨å®šãŒå¯èƒ½ã§ã™ã€‚投稿フォームã¯ã“ã®ã‚ˆã†ã«ãªã£ã¦ã„ã¾ã™ã€‚" _visibility: description: "ノートを表示ã§ãる相手を制é™ã§ãã¾ã™ã€‚" public: "ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«å…¬é–‹ã€‚" - home: "ホームタイムラインã®ã¿ã«å…¬é–‹ã€‚フォãƒãƒ¯ãƒ¼ãƒ»ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’見ã«æ¥ãŸäººãƒ»ãƒ–ーストã‹ã‚‰ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚‚見るã“ã¨ãŒã§ãã¾ã™ã€‚" - followers: "フォãƒãƒ¯ãƒ¼ã«ã®ã¿å…¬é–‹ã€‚本人以外ãŒãƒ–ーストã™ã‚‹ã“ã¨ã¯ã§ããšã€ã¾ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã¯é–²è¦§ã§ãã¾ã›ã‚“。" + home: "ホームタイムラインã®ã¿ã«å…¬é–‹ã€‚フォãƒãƒ¯ãƒ¼ãƒ»ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’見ã«æ¥ãŸäººãƒ»ãƒªãƒŽãƒ¼ãƒˆã‹ã‚‰ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚‚見るã“ã¨ãŒã§ãã¾ã™ã€‚" + followers: "フォãƒãƒ¯ãƒ¼ã«ã®ã¿å…¬é–‹ã€‚本人以外ãŒãƒªãƒŽãƒ¼ãƒˆã™ã‚‹ã“ã¨ã¯ã§ããšã€ã¾ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã¯é–²è¦§ã§ãã¾ã›ã‚“。" direct: "指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã®ã¿å…¬é–‹ã•ã‚Œã€ã¾ãŸç›¸æ‰‹ã«é€šçŸ¥ãŒå…¥ã‚Šã¾ã™ã€‚ダイレクトメッセージã®ã‹ã‚ã‚Šã«ãŠä½¿ã„ã„ãŸã ã‘ã¾ã™ã€‚" doNotSendConfidencialOnDirect1: "æ©Ÿå¯†æƒ…å ±ã¯é€ä¿¡ã™ã‚‹éš›ã¯æ³¨æ„ã—ã¦ãã ã•ã„。" doNotSendConfidencialOnDirect2: "é€ä¿¡å…ˆã®ã‚µãƒ¼ãƒãƒ¼ã®ç®¡ç†è€…ã¯æŠ•ç¨¿å†…容を見るã“ã¨ãŒå¯èƒ½ãªã®ã§ã€ä¿¡é ¼ã§ããªã„サーãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ã‚’é€ä¿¡ã™ã‚‹å ´åˆã¯ã€æ©Ÿå¯†æƒ…å ±ã®æ‰±ã„ã«æ³¨æ„ãŒå¿…è¦ã§ã™ã€‚" @@ -1446,7 +1420,7 @@ _initialTutorial: doItToContinue: "ç”»åƒã‚’センシティブã«è¨å®šã™ã‚‹ã¨å…ˆã«é€²ã‚るよã†ã«ãªã‚Šã¾ã™ã€‚" _done: title: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã¯çµ‚了ã§ã™ðŸŽ‰" - description: "ã“ã“ã§ç´¹ä»‹ã—ãŸæ©Ÿèƒ½ã¯ã»ã‚“ã®ä¸€éƒ¨ã«ã™ãŽã¾ã›ã‚“。Sharkeyã®ä½¿ã„方をより詳ã—ã知るã«ã¯ã€{link}ã‚’ã”覧ãã ã•ã„。" + description: "ã“ã“ã§ç´¹ä»‹ã—ãŸæ©Ÿèƒ½ã¯ã»ã‚“ã®ä¸€éƒ¨ã«ã™ãŽã¾ã›ã‚“。Misskeyã®ä½¿ã„方をより詳ã—ã知るã«ã¯ã€{link}ã‚’ã”覧ãã ã•ã„。" _timelineDescription: home: "ホームタイムラインã§ã¯ã€ã‚ãªãŸãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るアカウントã®æŠ•ç¨¿ã‚’見られã¾ã™ã€‚" @@ -1469,8 +1443,10 @@ _serverSettings: fanoutTimelineDescription: "有効ã«ã™ã‚‹ã¨ã€å„種タイムラインをå–å¾—ã™ã‚‹éš›ã®ãƒ‘フォーマンスãŒå¤§å¹…ã«å‘上ã—ã€ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã¸ã®è² è·ã‚’軽減ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚ãŸã ã—ã€Redisã®ãƒ¡ãƒ¢ãƒªä½¿ç”¨é‡ã¯å¢—åŠ ã—ã¾ã™ã€‚サーãƒãƒ¼ã®ãƒ¡ãƒ¢ãƒªå®¹é‡ãŒå°‘ãªã„å ´åˆã€ã¾ãŸã¯å‹•ä½œãŒä¸å®‰å®šãªå ´åˆã¯ç„¡åŠ¹ã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" fanoutTimelineDbFallback: "データベースã¸ã®ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯" fanoutTimelineDbFallbackDescription: "有効ã«ã™ã‚‹ã¨ã€ã‚¿ã‚¤ãƒ ラインãŒã‚ャッシュã•ã‚Œã¦ã„ãªã„å ´åˆã«DBã¸è¿½åŠ ã§å•ã„åˆã‚ã›ã‚’è¡Œã†ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’è¡Œã„ã¾ã™ã€‚無効ã«ã™ã‚‹ã¨ã€ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’è¡Œã‚ãªã„ã“ã¨ã§ã•ã‚‰ã«ã‚µãƒ¼ãƒãƒ¼ã®è² è·ã‚’軽減ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ãŒã€ã‚¿ã‚¤ãƒ ラインãŒå–å¾—ã§ãる範囲ã«åˆ¶é™ãŒç”Ÿã˜ã¾ã™ã€‚" + reactionsBufferingDescription: "有効ã«ã™ã‚‹ã¨ã€ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ä½œæˆæ™‚ã®ãƒ‘フォーマンスãŒå¤§å¹…ã«å‘上ã—ã€ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ã¸ã®è² è·ã‚’軽減ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚ãŸã ã—ã€Redisã®ãƒ¡ãƒ¢ãƒªä½¿ç”¨é‡ã¯å¢—åŠ ã—ã¾ã™ã€‚" inquiryUrl: "å•ã„åˆã‚ã›å…ˆURL" inquiryUrlDescription: "サーãƒãƒ¼é‹å–¶è€…ã¸ã®ãŠå•ã„åˆã‚ã›ãƒ•ã‚©ãƒ¼ãƒ ã®URLã‚„ã€é‹å–¶è€…ã®é€£çµ¡å…ˆç‰ãŒè¨˜è¼‰ã•ã‚ŒãŸWebページã®URLを指定ã—ã¾ã™ã€‚" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティãŒæ¤œå‡ºã•ã‚Œãªã‹ã£ãŸå ´åˆã€ã‚¹ãƒ‘ム防æ¢ã®ãŸã‚ã“ã®è¨å®šã¯è‡ªå‹•ã§ã‚ªãƒ•ã«ãªã‚Šã¾ã™ã€‚" _accountMigration: moveFrom: "別ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç§»è¡Œ" @@ -1480,7 +1456,7 @@ _accountMigration: moveTo: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’æ–°ã—ã„アカウントã¸ç§»è¡Œ" moveToLabel: "移行先ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆ:" moveCannotBeUndone: "アカウントを移行ã™ã‚‹ã¨ã€å–り消ã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。" - moveAccountDescription: "æ–°ã—ã„アカウントã¸ç§»è¡Œã—ã¾ã™ã€‚\n ・フォãƒãƒ¯ãƒ¼ãŒæ–°ã—ã„アカウントを自動ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¾ã™\n ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã¯å…¨ã¦è§£é™¤ã•ã‚Œã¾ã™\n ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã¯ãƒŽãƒ¼ãƒˆã®ä½œæˆãªã©ãŒã§ããªããªã‚Šã¾ã™\n\nフォãƒãƒ¯ãƒ¼ã®ç§»è¡Œã¯è‡ªå‹•ã§ã™ãŒã€ãƒ•ã‚©ãƒãƒ¼ã®ç§»è¡Œã¯æ‰‹å‹•ã§è¡Œã†å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚移行å‰ã«ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ãƒ•ã‚©ãƒãƒ¼ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã—ã€ç§»è¡Œå¾Œã™ãã«ç§»è¡Œå…ˆã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¡Œãªã£ã¦ãã ã•ã„。\nリスト・ミュート・ブãƒãƒƒã‚¯ã«ã¤ã„ã¦ã‚‚åŒæ§˜ã§ã™ã®ã§ã€æ‰‹å‹•ã§ç§»è¡Œã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\n\n(ã“ã®èª¬æ˜Žã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ï¼ˆSharkey v13.12.0以é™ï¼‰ã®ä»•æ§˜ã§ã™ã€‚Mastodonãªã©ã®ä»–ã®ActivityPubソフトウェアã§ã¯æŒ™å‹•ãŒç•°ãªã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚)" + moveAccountDescription: "æ–°ã—ã„アカウントã¸ç§»è¡Œã—ã¾ã™ã€‚\n ・フォãƒãƒ¯ãƒ¼ãŒæ–°ã—ã„アカウントを自動ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¾ã™\n ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã¯å…¨ã¦è§£é™¤ã•ã‚Œã¾ã™\n ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã¯ãƒŽãƒ¼ãƒˆã®ä½œæˆãªã©ãŒã§ããªããªã‚Šã¾ã™\n\nフォãƒãƒ¯ãƒ¼ã®ç§»è¡Œã¯è‡ªå‹•ã§ã™ãŒã€ãƒ•ã‚©ãƒãƒ¼ã®ç§»è¡Œã¯æ‰‹å‹•ã§è¡Œã†å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚移行å‰ã«ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ãƒ•ã‚©ãƒãƒ¼ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã—ã€ç§»è¡Œå¾Œã™ãã«ç§»è¡Œå…ˆã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¡Œãªã£ã¦ãã ã•ã„。\nリスト・ミュート・ブãƒãƒƒã‚¯ã«ã¤ã„ã¦ã‚‚åŒæ§˜ã§ã™ã®ã§ã€æ‰‹å‹•ã§ç§»è¡Œã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\n\n(ã“ã®èª¬æ˜Žã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ï¼ˆMisskey v13.12.0以é™ï¼‰ã®ä»•æ§˜ã§ã™ã€‚Mastodonãªã©ã®ä»–ã®ActivityPubソフトウェアã§ã¯æŒ™å‹•ãŒç•°ãªã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚)" moveAccountHowTo: "アカウントã®ç§»è¡Œã«ã¯ã€ã¾ãšã¯ç§»è¡Œå…ˆã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«å¯¾ã—エイリアスを作æˆã—ã¾ã™ã€‚\nエイリアス作æˆå¾Œã€ç§»è¡Œå…ˆã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’次ã®ã‚ˆã†ã«å…¥åŠ›ã—ã¦ãã ã•ã„: @username@server.example.com" startMigration: "移行ã™ã‚‹" migrationConfirm: "本当ã«ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’ {account} ã«ç§»è¡Œã—ã¾ã™ã‹ï¼Ÿä¸€åº¦ç§»è¡Œã™ã‚‹ã¨å–り消ã›ãšã€äºŒåº¦ã¨ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’å…ƒã®çŠ¶æ…‹ã§ä½¿ç”¨ã§ããªããªã‚Šã¾ã™ã€‚" @@ -1492,9 +1468,9 @@ _achievements: earnedAt: "ç²å¾—日時" _types: _notes1: - title: "just setting up my shonk" + title: "just setting up my msky" description: "åˆã‚ã¦ãƒŽãƒ¼ãƒˆã‚’投稿ã—ãŸ" - flavor: "良ã„Sharkeyライフをï¼" + flavor: "良ã„Misskeyライフをï¼" _notes10: title: "ã„ãã¤ã‹ã®ãƒŽãƒ¼ãƒˆ" description: "ノートを10回投稿ã—ãŸ" @@ -1590,7 +1566,7 @@ _achievements: _login1000: title: "ノートマスターⅢ" description: "通算ãƒã‚°ã‚¤ãƒ³æ—¥æ•°ãŒ1,000æ—¥" - flavor: "Sharkeyを使ã£ã¦ãã‚Œã¦ã‚ã‚ŠãŒã¨ã†ï¼" + flavor: "Misskeyを使ã£ã¦ãã‚Œã¦ã‚ã‚ŠãŒã¨ã†ï¼" _noteClipped1: title: "クリップã›ãšã«ã¯ã„られãªã„ãª" description: "åˆã‚ã¦ãƒŽãƒ¼ãƒˆã‚’クリップã—ãŸ" @@ -1650,9 +1626,9 @@ _achievements: title: "実績好ã" description: "実績一覧を3分以上眺ã‚続ã‘ãŸ" _iLoveMisskey: - title: "I Love Sharkey" - description: "\"I ⤠#Sharkey\"を投稿ã—ãŸ" - flavor: "Sharkeyを使ã£ã¦ãã ã•ã‚Šã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ï¼ by 開発ãƒãƒ¼ãƒ " + title: "I Love Misskey" + description: "\"I ⤠#Misskey\"を投稿ã—ãŸ" + flavor: "Misskeyを使ã£ã¦ãã ã•ã‚Šã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ï¼ by 開発ãƒãƒ¼ãƒ " _foundTreasure: title: "å®æŽ¢ã—" description: "éš ã•ã‚ŒãŸãŠå®ã‚’発見ã—ãŸ" @@ -1660,7 +1636,7 @@ _achievements: title: "ã²ã¨ã‚„ã™ã¿" description: "クライアントを起動ã—ã¦ã‹ã‚‰30分以上経éŽã—ãŸ" _client60min: - title: "Sharkeyã®è¦‹ã™ãŽ" + title: "Misskeyã®è¦‹ã™ãŽ" description: "クライアントを起動ã—ã¦ã‹ã‚‰60分以上経éŽã—ãŸ" _noteDeletedWithin1min: title: "ã„ã¾ã®ãªã—" @@ -1731,7 +1707,7 @@ _achievements: title: "テストéŽå‰°" description: "通知ã®ãƒ†ã‚¹ãƒˆã‚’ã”ãçŸæ™‚é–“ã®ã†ã¡ã«é€£ç¶šã—ã¦è¡Œã£ãŸ" _tutorialCompleted: - title: "Sharkeyåˆå¿ƒè€…講座 修了証" + title: "Misskeyåˆå¿ƒè€…講座 修了証" description: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã‚’完了ã—ãŸ" _bubbleGameExplodingHead: title: "🤯" @@ -1779,10 +1755,8 @@ _role: high: "高" _options: gtlAvailable: "ã‚°ãƒãƒ¼ãƒãƒ«ã‚¿ã‚¤ãƒ ラインã®é–²è¦§" - btlAvailable: "ãƒãƒ–ルタイムラインã®é–²è¦§" ltlAvailable: "ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã®é–²è¦§" canPublicNote: "パブリック投稿ã®è¨±å¯" - canImportNotes: "ノートã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆãŒå¯èƒ½" mentionMax: "ノート内ã®æœ€å¤§ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³æ•°" canInvite: "サーãƒãƒ¼æ‹›å¾…コードã®ç™ºè¡Œ" inviteLimit: "招待コードã®ä½œæˆå¯èƒ½æ•°" @@ -1807,6 +1781,11 @@ _role: canSearchNotes: "ノート検索ã®åˆ©ç”¨" canUseTranslator: "翻訳機能ã®åˆ©ç”¨" avatarDecorationLimit: "アイコンデコレーションã®æœ€å¤§å–付個数" + canImportAntennas: "アンテナã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’許å¯" + canImportBlocking: "ブãƒãƒƒã‚¯ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’許å¯" + canImportFollowing: "フォãƒãƒ¼ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’許å¯" + canImportMuting: "ミュートã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’許å¯" + canImportUserLists: "リストã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’許å¯" _condition: roleAssignedTo: "マニュアルãƒãƒ¼ãƒ«ã«ã‚¢ã‚µã‚¤ãƒ³æ¸ˆã¿" isLocal: "ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼" @@ -1854,8 +1833,6 @@ _signup: almostThere: "ã»ã¨ã‚“ã©å®Œäº†ã§ã™" emailAddressInfo: "ã‚ãªãŸãŒä½¿ã£ã¦ã„るメールアドレスを入力ã—ã¦ãã ã•ã„。メールアドレスãŒå…¬é–‹ã•ã‚Œã‚‹ã“ã¨ã¯ã‚ã‚Šã¾ã›ã‚“。" emailSent: "入力ã•ã‚ŒãŸãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹({email})å®›ã«ç¢ºèªã®ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã—ãŸã€‚メールã«è¨˜è¼‰ã•ã‚ŒãŸãƒªãƒ³ã‚¯ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã¨ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ä½œæˆãŒå®Œäº†ã—ã¾ã™ã€‚メールã«è¨˜è¼‰ã•ã‚Œã¦ã„るリンクã®æœ‰åŠ¹æœŸé™ã¯30分ã§ã™ã€‚" - approvalPending: "アカウントãŒä½œæˆã•ã‚Œã€æ‰¿èªå¾…ã¡ã®çŠ¶æ…‹ã§ã™ã€‚" - reasonInfo: "インスタンスã«å‚åŠ ã—ãŸã„ç†ç”±ã‚’入力ã—ã¦ãã ã•ã„。" _accountDelete: accountDelete: "アカウントã®å‰Šé™¤" @@ -1926,20 +1903,17 @@ _registry: createKey: "ã‚ーを作æˆ" _aboutMisskey: - about: "Sharkeyã¯ã€Misskeyをベースã«ã—ãŸã‚ªãƒ¼ãƒ—ンソースã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚" - contributors: "主ãªã‚³ãƒ³ãƒˆãƒªãƒ“ューター" + about: "Misskeyã¯syuiloã«ã‚ˆã£ã¦2014å¹´ã‹ã‚‰é–‹ç™ºã•ã‚Œã¦ã„ã‚‹ã€ã‚ªãƒ¼ãƒ—ンソースã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚" + contributors: "コントリビューター" allContributors: "å…¨ã¦ã®ã‚³ãƒ³ãƒˆãƒªãƒ“ューター" source: "ソースコード" - original: "Misskey オリジナル" - original_sharkey: "Sharkey オリジナル" - thisIsModifiedVersion: "{name}ã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã®Sharkeyを改変ã—ãŸãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’使用ã—ã¦ã„ã¾ã™ã€‚" - translation: "Sharkeyを翻訳" + original: "オリジナル" + thisIsModifiedVersion: "{name}ã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã®Misskeyを改変ã—ãŸãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’使用ã—ã¦ã„ã¾ã™ã€‚" + translation: "Misskeyを翻訳" donate: "Misskeyã«å¯„付" - donate_sharkey: "Sharkeyã«å¯„付" morePatrons: "ä»–ã«ã‚‚多ãã®æ–¹ãŒæ”¯æ´ã—ã¦ãã‚Œã¦ã„ã¾ã™ã€‚ã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ðŸ¥°" patrons: "支æ´è€…" projectMembers: "プãƒã‚¸ã‚§ã‚¯ãƒˆãƒ¡ãƒ³ãƒãƒ¼" - testers: "テスター" _displayOfSensitiveMedia: respect: "センシティブè¨å®šã•ã‚ŒãŸãƒ¡ãƒ‡ã‚£ã‚¢ã‚’éš ã™" @@ -1955,7 +1929,6 @@ _serverDisconnectedBehavior: reload: "自動ã§ãƒªãƒãƒ¼ãƒ‰" dialog: "ダイアãƒã‚°ã§è¦å‘Š" quiet: "控ãˆã‚ã«è¦å‘Š" - disabled: "è¦å‘Šã‚’無効ã«ã™ã‚‹" _channel: create: "ãƒãƒ£ãƒ³ãƒãƒ«ã‚’作æˆ" @@ -1969,7 +1942,7 @@ _channel: notesCount: "{n}投稿ãŒã‚ã‚Šã¾ã™" nameAndDescription: "åå‰ã¨èª¬æ˜Ž" nameOnly: "åå‰ã®ã¿" - allowRenoteToExternal: "ãƒãƒ£ãƒ³ãƒãƒ«å¤–ã¸ã®ãƒ–ーストã¨å¼•ç”¨ãƒ–ーストを許å¯ã™ã‚‹" + allowRenoteToExternal: "ãƒãƒ£ãƒ³ãƒãƒ«å¤–ã¸ã®ãƒªãƒŽãƒ¼ãƒˆã¨å¼•ç”¨ãƒªãƒŽãƒ¼ãƒˆã‚’許å¯ã™ã‚‹" _menuDisplay: sideFull: "横" @@ -1983,7 +1956,7 @@ _wordMute: muteWordsDescription2: "ã‚ーワードをスラッシュã§å›²ã‚€ã¨æ£è¦è¡¨ç¾ã«ãªã‚Šã¾ã™ã€‚" _instanceMute: - instanceMuteDescription: "ミュートã—ãŸã‚µãƒ¼ãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¸ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚µãƒ¼ãƒãƒ¼ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨ãƒ–ーストをミュートã—ã¾ã™ã€‚" + instanceMuteDescription: "ミュートã—ãŸã‚µãƒ¼ãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¸ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚µãƒ¼ãƒãƒ¼ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨Renoteをミュートã—ã¾ã™ã€‚" instanceMuteDescription2: "改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™" title: "è¨å®šã—ãŸã‚µãƒ¼ãƒãƒ¼ã®ãƒŽãƒ¼ãƒˆã‚’éš ã—ã¾ã™ã€‚" heading: "ミュートã™ã‚‹ã‚µãƒ¼ãƒãƒ¼" @@ -2037,7 +2010,7 @@ _theme: hashtag: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" mention: "メンション" mentionMe: "ã‚ãªãŸå®›ã¦ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³" - renote: "Boost" + renote: "Renote" modalBg: "モーダルã®èƒŒæ™¯" divider: "分割線" scrollbarHandle: "スクãƒãƒ¼ãƒ«ãƒãƒ¼ã®å–ã£æ‰‹" @@ -2052,7 +2025,6 @@ _theme: buttonBg: "ボタンã®èƒŒæ™¯" buttonHoverBg: "ボタンã®èƒŒæ™¯ (ホãƒãƒ¼)" inputBorder: "入力ボックスã®ç¸å–ã‚Š" - listItemHoverBg: "ãƒªã‚¹ãƒˆé …ç›®ã®èƒŒæ™¯ (ホãƒãƒ¼)" driveFolderBg: "ドライブフォルダーã®èƒŒæ™¯" wallpaperOverlay: "å£ç´™ã®ã‚ªãƒ¼ãƒãƒ¼ãƒ¬ã‚¤" badge: "ãƒãƒƒã‚¸" @@ -2073,7 +2045,7 @@ _soundSettings: driveFileTypeWarn: "ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“" driveFileTypeWarnDescription: "音声ファイルをé¸æŠžã—ã¦ãã ã•ã„" driveFileDurationWarn: "音声ãŒé•·ã™ãŽã¾ã™" - driveFileDurationWarnDescription: "é•·ã„音声を使用ã™ã‚‹ã¨Sharkeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ" + driveFileDurationWarnDescription: "é•·ã„音声を使用ã™ã‚‹ã¨Misskeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ" driveFileError: "音声ãŒèªã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚è¨å®šã‚’変更ã—ã¦ãã ã•ã„" _ago: @@ -2276,7 +2248,6 @@ _widgets: _userList: chooseList: "リストをé¸æŠž" clicker: "クリッカー" - search: "検索" birthdayFollowings: "今日誕生日ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼" _cw: @@ -2307,7 +2278,6 @@ _poll: remainingHours: "終了ã¾ã§ã‚ã¨{h}時間{m}分" remainingMinutes: "終了ã¾ã§ã‚ã¨{m}分{s}秒" remainingSeconds: "終了ã¾ã§ã‚ã¨{s}秒" - multiple: "複数ã®é¸æŠžè‚¢" _visibility: public: "パブリック" @@ -2345,13 +2315,11 @@ _profile: metadataContent: "内容" changeAvatar: "アイコン画åƒã‚’変更" changeBanner: "ãƒãƒŠãƒ¼ç”»åƒã‚’変更" - updateBanner: "æ›´æ–°ãƒãƒŠãƒ¼" - removeBanner: "ãƒãƒŠãƒ¼ã‚’削除" - changeBackground: "背景を変更ã™ã‚‹" - updateBackground: "背景を更新ã™ã‚‹" - removeBackground: "背景を削除ã™ã‚‹" verifiedLinkDescription: "内容ã«URLã‚’è¨å®šã™ã‚‹ã¨ã€ãƒªãƒ³ã‚¯å…ˆã®Webサイトã«è‡ªåˆ†ã®ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã¸ã®ãƒªãƒ³ã‚¯ãŒå«ã¾ã‚Œã¦ã„ã‚‹å ´åˆã«æ‰€æœ‰è€…確èªæ¸ˆã¿ã‚¢ã‚¤ã‚³ãƒ³ã‚’表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" avatarDecorationMax: "最大{max}ã¤ã¾ã§ãƒ‡ã‚³ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã‚’付ã‘られã¾ã™ã€‚" + followedMessage: "フォãƒãƒ¼ã•ã‚ŒãŸæ™‚ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸" + followedMessageDescription: "フォãƒãƒ¼ã•ã‚ŒãŸæ™‚ã«ç›¸æ‰‹ã«è¡¨ç¤ºã™ã‚‹çŸã„メッセージをè¨å®šã§ãã¾ã™ã€‚" + followedMessageDescriptionForLockedAccount: "フォãƒãƒ¼ã‚’承èªåˆ¶ã«ã—ã¦ã„ã‚‹å ´åˆã€ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’許å¯ã—ãŸæ™‚ã«è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚" _exportOrImport: allNotes: "å…¨ã¦ã®ãƒŽãƒ¼ãƒˆ" @@ -2398,7 +2366,6 @@ _timelines: local: "ãƒãƒ¼ã‚«ãƒ«" social: "ソーシャル" global: "ã‚°ãƒãƒ¼ãƒãƒ«" - bubble: "ãƒãƒƒãƒƒãƒ–ル" _play: new: "Playã®ä½œæˆ" @@ -2481,12 +2448,11 @@ _notification: youGotMention: "{name}ã‹ã‚‰ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³" youGotReply: "{name}ã‹ã‚‰ã®ãƒªãƒ—ライ" youGotQuote: "{name}ã«ã‚ˆã‚‹å¼•ç”¨" - youRenoted: "{name}ãŒBoostã—ã¾ã—ãŸ" + youRenoted: "{name}ãŒRenoteã—ã¾ã—ãŸ" youWereFollowed: "フォãƒãƒ¼ã•ã‚Œã¾ã—ãŸ" youReceivedFollowRequest: "フォãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒæ¥ã¾ã—ãŸ" yourFollowRequestAccepted: "フォãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒæ‰¿èªã•ã‚Œã¾ã—ãŸ" pollEnded: "アンケートã®çµæžœãŒå‡ºã¾ã—ãŸ" - edited: "投稿ãŒç·¨é›†ã•ã‚Œã¾ã—ãŸ" newNote: "æ–°ã—ã„投稿" unreadAntennaNote: "アンテナ {name}" roleAssigned: "ãƒãƒ¼ãƒ«ãŒä»˜ä¸Žã•ã‚Œã¾ã—ãŸ" @@ -2501,6 +2467,8 @@ _notification: renotedBySomeUsers: "{n}人ãŒãƒªãƒŽãƒ¼ãƒˆã—ã¾ã—ãŸ" followedBySomeUsers: "{n}人ã«ãƒ•ã‚©ãƒãƒ¼ã•ã‚Œã¾ã—ãŸ" flushNotification: "通知ã®å±¥æ´ã‚’リセットã™ã‚‹" + exportOfXCompleted: "{x}ã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆãŒå®Œäº†ã—ã¾ã—ãŸ" + login: "ãƒã‚°ã‚¤ãƒ³ãŒã‚ã‚Šã¾ã—ãŸ" _types: all: "ã™ã¹ã¦" @@ -2508,7 +2476,7 @@ _notification: follow: "フォãƒãƒ¼" mention: "メンション" reply: "リプライ" - renote: "Boost" + renote: "Renote" quote: "引用" reaction: "リアクション" pollEnded: "アンケートãŒçµ‚了" @@ -2516,13 +2484,15 @@ _notification: followRequestAccepted: "フォãƒãƒ¼ãŒå—ç†ã•ã‚ŒãŸ" roleAssigned: "ãƒãƒ¼ãƒ«ãŒä»˜ä¸Žã•ã‚ŒãŸ" achievementEarned: "実績ã®ç²å¾—" + exportCompleted: "エクスãƒãƒ¼ãƒˆãŒå®Œäº†ã—ãŸ" + login: "ãƒã‚°ã‚¤ãƒ³" + test: "通知ã®ãƒ†ã‚¹ãƒˆ" app: "連æºã‚¢ãƒ—リã‹ã‚‰ã®é€šçŸ¥" - edited: "編集済ã¿" _actions: followBack: "フォãƒãƒ¼ãƒãƒƒã‚¯" reply: "返信" - renote: "ブースト" + renote: "Renote" _deck: alwaysShowMainColumn: "常ã«ãƒ¡ã‚¤ãƒ³ã‚«ãƒ©ãƒ を表示" @@ -2582,14 +2552,17 @@ _webhookSettings: followed: "フォãƒãƒ¼ã•ã‚ŒãŸã¨ã" note: "ノートを投稿ã—ãŸã¨ã" reply: "返信ã•ã‚ŒãŸã¨ã" - renote: "Boostã•ã‚ŒãŸã¨ã" + renote: "Renoteã•ã‚ŒãŸã¨ã" reaction: "リアクションãŒã‚ã£ãŸã¨ã" mention: "メンションã•ã‚ŒãŸã¨ã" _systemEvents: abuseReport: "ユーザーã‹ã‚‰é€šå ±ãŒã‚ã£ãŸã¨ã" abuseReportResolved: "ユーザーã‹ã‚‰ã®é€šå ±ã‚’処ç†ã—ãŸã¨ã" userCreated: "ユーザーãŒä½œæˆã•ã‚ŒãŸã¨ã" + inactiveModeratorsWarning: "モデレーターãŒä¸€å®šæœŸé–“éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã«ãªã£ãŸã¨ã" + inactiveModeratorsInvitationOnlyChanged: "モデレーターãŒä¸€å®šæœŸé–“éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã ã£ãŸãŸã‚ã€ã‚·ã‚¹ãƒ†ãƒ ã«ã‚ˆã‚Šæ‹›å¾…制ã¸ã¨å¤‰æ›´ã•ã‚ŒãŸã¨ã" deleteConfirm: "Webhookを削除ã—ã¾ã™ã‹ï¼Ÿ" + testRemarks: "スイッãƒã®å³ã«ã‚るボタンをクリックã™ã‚‹ã¨ãƒ€ãƒŸãƒ¼ã®ãƒ‡ãƒ¼ã‚¿ã‚’使用ã—ãŸãƒ†ã‚¹ãƒˆç”¨Webhookã‚’é€ä¿¡ã§ãã¾ã™ã€‚" _abuseReport: _notificationRecipient: @@ -2613,7 +2586,6 @@ _moderationLogTypes: updateRole: "ãƒãƒ¼ãƒ«ã‚’æ›´æ–°" assignRole: "ãƒãƒ¼ãƒ«ã¸ã‚¢ã‚µã‚¤ãƒ³" unassignRole: "ãƒãƒ¼ãƒ«ã®ã‚¢ã‚µã‚¤ãƒ³è§£é™¤" - approve: "承èªæ¸ˆã¿" suspend: "å‡çµ" unsuspend: "å‡çµè§£é™¤" addCustomEmoji: "カスタム絵文å—è¿½åŠ " @@ -2636,6 +2608,8 @@ _moderationLogTypes: markSensitiveDriveFile: "ファイルをセンシティブ付与" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" resolveAbuseReport: "é€šå ±ã‚’è§£æ±º" + forwardAbuseReport: "é€šå ±ã‚’è»¢é€" + updateAbuseReportNote: "é€šå ±ã®ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãƒŽãƒ¼ãƒˆæ›´æ–°" createInvitation: "招待コードを作æˆ" createAd: "広告を作æˆ" deleteAd: "広告を削除" @@ -2656,87 +2630,6 @@ _moderationLogTypes: deleteFlash: "Playを削除" deleteGalleryPost: "ギャラリーã®æŠ•ç¨¿ã‚’削除" -_mfm: - uncommonFeature: "ã“ã®æ©Ÿèƒ½ã¯ä¸€èˆ¬çš„ã«æ™®åŠã—ã¦ã„ãªã„ãŸã‚ã€ä»–ã®Misskeyフォークをå«ã‚ãŸå¤šãã®Fediverseソフトウェアã§è¡¨ç¤ºã§ããªã„ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" - intro: "MFM ã¯Misskey, Sharkey, Firefish, Akkomaãªã©ã€å¤šãã®å ´æ‰€ã§ä½¿ç”¨ã§ãるマークアップ言語ã§ã™ã€‚ã“ã“ã§ã¯ã€åˆ©ç”¨ã§ãã‚‹MFM構文ã®ä¸€è¦§ã‚’ã”覧ã„ãŸã ã‘ã¾ã™ã€‚" - dummy: "Sharkeyã§Fediverseã®ä¸–ç•ŒãŒåºƒãŒã‚Šã¾ã™" - mention: "メンション" - mentionDescription: "アットマーク + ユーザーåã§ã€ç‰¹å®šã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" - hashtag: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" - hashtagDescription: "ナンãƒãƒ¼ã‚µã‚¤ãƒ³ + ã‚¿ã‚°ã§ã€ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" - url: "URL" - urlDescription: "URLを示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" - link: "リンク" - linkDescription: "æ–‡ç« ã®ç‰¹å®šã®ç¯„囲をã€URLã«ç´ã¥ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" - bold: "太å—" - boldDescription: "æ–‡å—を太ã表示ã—ã¦å¼·èª¿ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" - small: "å°æ–‡å—" - smallDescription: "内容をå°ã•ã・薄ã表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" - center: "ä¸å¤®å¯„ã›" - centerDescription: "内容をä¸å¤®å¯„ã›ã§è¡¨ç¤ºã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" - inlineCode: "コード(インライン)" - inlineCodeDescription: "プãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’インラインã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚" - blockCode: "コード(ブãƒãƒƒã‚¯ï¼‰" - blockCodeDescription: "複数行ã®ãƒ—ãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’ブãƒãƒƒã‚¯ã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚" - inlineMath: "æ•°å¼ï¼ˆã‚¤ãƒ³ãƒ©ã‚¤ãƒ³ï¼‰" - inlineMathDescription: "æ•°å¼ ï¼ˆKaTeXå½¢å¼ï¼‰ã‚’インラインã§è¡¨ç¤ºã—ã¾ã™ã€‚" - blockMath: "æ•°å¼ï¼ˆãƒ–ãƒãƒƒã‚¯ï¼‰" - blockMathDescription: "æ•°å¼ ï¼ˆKaTeXå½¢å¼ï¼‰ã‚’ブãƒãƒƒã‚¯ã§è¡¨ç¤ºã—ã¾ã™ã€‚" - quote: "引用" - quoteDescription: "内容ãŒå¼•ç”¨ã§ã‚ã‚‹ã“ã¨ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" - emoji: "カスタム絵文å—" - emojiDescription: "コãƒãƒ³ã§ã‚«ã‚¹ã‚¿ãƒ 絵文å—åを囲むã¨ã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—を表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" - search: "検索" - searchDescription: "検索ボックスを表示ã§ãã¾ã™ã€‚" - flip: "å転" - flipDescription: "内容を上下ã¾ãŸã¯å·¦å³ã«å転ã•ã›ã¾ã™ã€‚" - jelly: "アニメーション(ã³ã‚ˆã‚“ã³ã‚ˆã‚“)" - jellyDescription: "ゼリーãŒæºã‚Œã‚‹ã‚ˆã†ãªæ„Ÿã˜ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" - tada: "アニメーション(ã˜ã‚ƒãƒ¼ã‚“)" - tadaDescription: "「ã˜ã‚ƒãƒ¼ã‚“ï¼ã€ã¨å¼·èª¿ã™ã‚‹ã‚ˆã†ãªæ„Ÿã˜ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" - jump: "アニメーション(ジャンプ)" - jumpDescription: "è·³ãるアニメーションをã•ã›ã¾ã™ã€‚" - bounce: "アニメーション(ãƒã‚¦ãƒ³ãƒ‰ï¼‰" - bounceDescription: "è·³ãã¦ç€åœ°ã™ã‚‹ã‚ˆã†ãªã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" - shake: "アニメーション(ã¶ã‚‹ã¶ã‚‹ï¼‰" - shakeDescription: "震ãˆã‚‹ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" - twitch: "アニメーション(ガタガタ)" - twitchDescription: "より激ã—ã震ãˆã‚‹ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" - spin: "アニメーション(回転)" - spinDescription: "内容を回転ã•ã›ã¾ã™ã€‚" - x2: "大" - x2Description: "内容を大ãã表示ã•ã›ã¾ã™ã€‚" - x3: "特大" - x3Description: "内容をより大ãã表示ã•ã›ã¾ã™ã€‚" - x4: "超特大" - x4Description: "内容をã•ã‚‰ã«å¤§ãã表示ã•ã›ã¾ã™ã€‚" - blur: "ã¼ã‹ã—" - blurDescription: "内容をã¼ã‹ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ãƒã‚¤ãƒ³ã‚¿ãƒ¼ã‚’上ã«ä¹—ã›ã‚‹ã¨ã¯ã£ãり見ãˆã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚" - font: "フォント" - fontDescription: "内容ã®ãƒ•ã‚©ãƒ³ãƒˆã‚’指定ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" - rainbow: "レインボー" - rainbowDescription: "内容を虹色ã§è¡¨ç¤ºã•ã›ã¾ã™ã€‚" - sparkle: "ã‚ラã‚ラ" - sparkleDescription: "ã‚ラã‚ラã¨æ˜Ÿåž‹ã®ãƒ‘ーティクルを表示ã•ã›ã¾ã™ã€‚" - rotate: "角度変更" - rotateDescription: "指定ã—ãŸè§’度ã§å›žè»¢ã•ã›ã¾ã™ã€‚" - position: "ä½ç½®å¤‰æ›´" - positionDescription: "ä½ç½®ã‚’ãšã‚‰ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" - crop: "切りå–ã‚Š" - cropDescription: "内容を切り抜ãã¾ã™ã€‚" - followMouse: "マウス追従" - followMouseDescription: "内容ãŒãƒžã‚¦ã‚¹ã«è¿½å¾“ã—ã¾ã™ã€‚スマホã®å ´åˆã¯ã‚¿ãƒƒãƒ—ã—ãŸå ´æ‰€ã«è¿½å¾“ã—ã¾ã™ã€‚" - scale: "拡大" - scaleDescription: "内容を引ã伸ã°ã—ã¦è¡¨ç¤ºã—ã¾ã™ã€‚" - foreground: "æ–‡å—色" - foregroundDescription: "æ–‡å—色を変更ã—ã¾ã™ã€‚" - fade: 'フェード' - fadeDescription: '内容をフェードイン・フェードアウトã•ã›ã¾ã™ã€‚' - background: "背景色" - backgroundDescription: "背景色を変更ã—ã¾ã™ã€‚" - plain: "Plain" - plainDescription: "内å´ã®æ§‹æ–‡ã‚’å…¨ã¦ç„¡åŠ¹ã«ã—ã¾ã™ã€‚" - _fileViewer: title: "ファイルã®è©³ç´°" type: "ファイルタイプ" @@ -2788,19 +2681,6 @@ _externalResourceInstaller: title: "テーマã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã«å¤±æ•—ã—ã¾ã—ãŸ" description: "テーマã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ä¸ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。エラーã®è©³ç´°ã¯Javascriptコンソールをã”覧ãã ã•ã„。" -_animatedMFM: - play: "MFMアニメーションをå†ç”Ÿ" - stop: "MFMアニメーションåœæ¢" - _alert: - text: "MFMアニメーションã«ã¯ã€é«˜é€Ÿã§ç‚¹æ»…ã—ãŸã‚Šå‹•ã„ãŸã‚Šã™ã‚‹ãƒ†ã‚スト・絵文å—ã‚’å«ã‚€å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" - confirm: "å†ç”Ÿã™ã‚‹" - -_dataRequest: - title: "データリクエスト" - warn: "データリクエストã¯3æ—¥ã”ã¨ã«å¯èƒ½ã§ã™ã€‚" - text: "データã®ä¿å˜ãŒå®Œäº†ã™ã‚‹ã¨ã€ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç™»éŒ²ã•ã‚Œã¦ã„るメールアドレスã«ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚" - button: "データリクエスト実行" - _dataSaver: _media: title: "メディアã®èªã¿è¾¼ã¿ã‚’無効化" @@ -2894,3 +2774,18 @@ _contextMenu: app: "アプリケーション" appWithShift: "Shiftã‚ーã§ã‚¢ãƒ—リケーション" native: "ブラウザã®UI" + +_embedCodeGen: + title: "埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’カスタマイズ" + header: "ヘッダーを表示" + autoload: "自動ã§ç¶šãã‚’èªã¿è¾¼ã‚€ï¼ˆéžæŽ¨å¥¨ï¼‰" + maxHeight: "高ã•ã®æœ€å¤§å€¤" + maxHeightDescription: "0ã§æœ€å¤§å€¤ã®è¨å®šãŒç„¡åŠ¹ã«ãªã‚Šã¾ã™ã€‚ウィジェットãŒç¸¦ã«ä¼¸ã³ç¶šã‘ã‚‹ã®ã‚’防ããŸã‚ã«ã€ä½•ã‚‰ã‹ã®å€¤ã«æŒ‡å®šã—ã¦ãã ã•ã„。" + maxHeightWarn: "高ã•ã®æœ€å¤§å€¤åˆ¶é™ãŒç„¡åŠ¹ï¼ˆ0)ã«ãªã£ã¦ã„ã¾ã™ã€‚ã“ã‚ŒãŒæ„図ã—ãŸå¤‰æ›´ã§ã¯ãªã„å ´åˆã¯ã€é«˜ã•ã®æœ€å¤§å€¤ã‚’何らã‹ã®å€¤ã«è¨å®šã—ã¦ãã ã•ã„。" + previewIsNotActual: "プレビュー画é¢ã§è¡¨ç¤ºå¯èƒ½ãªç¯„囲を超ãˆãŸãŸã‚ã€å®Ÿéš›ã«åŸ‹ã‚込んã éš›ã¨ã¯è¡¨ç¤ºãŒç•°ãªã‚Šã¾ã™ã€‚" + rounded: "角丸ã«ã™ã‚‹" + border: "å¤–æž ã«æž ç·šã‚’ã¤ã‘ã‚‹" + applyToPreview: "プレビューã«åæ˜ " + generateCode: "埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’作æˆ" + codeGenerated: "コードãŒç”Ÿæˆã•ã‚Œã¾ã—ãŸ" + codeGeneratedDescription: "生æˆã•ã‚ŒãŸã‚³ãƒ¼ãƒ‰ã‚’ウェブサイトã«è²¼ã‚Šä»˜ã‘ã¦ã”利用ãã ã•ã„。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 448355eb4e4668529ba8742fc7f70020cac74cad..0a8b3828f2de8d5ba80c21ca29493324caaa7700 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1,8 +1,8 @@ --- _lang_: "日本語 (関西å¼)" headlineMisskey: "ノートã§ã¤ãªãŒã‚‹ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" -introMisskey: "よã†ãŠè¶Šã—ï¼Sharkeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スやãん。\n「ノートã€ã‚’作ã£ã¦ã€ã„ã¾èµ·ã“ã£ã¨ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ã‚“ãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\n「ツッコミã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã—ãŸã‚Šã‚‚ã§ãã‚‹ã§âœŒ\nã»ãªã€æ–°ã—ã„世界を探検ã—よã‹ðŸš€" -poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Sharkey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ãªã‚“ã‚„ã§ã€‚" +introMisskey: "よã†ãŠè¶Šã—ï¼Misskeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スやãん。\n「ノートã€ã‚’作ã£ã¦ã€ã„ã¾èµ·ã“ã£ã¨ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ã‚“ãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\n「ツッコミã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã—ãŸã‚Šã‚‚ã§ãã‚‹ã§âœŒ\nã»ãªã€æ–°ã—ã„世界を探検ã—よã‹ðŸš€" +poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Misskey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ãªã‚“ã‚„ã§ã€‚" monthAndDay: "{month}月 {day}æ—¥" search: "探ã™" notifications: "通知" @@ -15,7 +15,7 @@ gotIt: "ã»ã„" cancel: "ã‚„ã‚ã¨ã" noThankYou: "ã‚„ã‚ã¨ã" enterUsername: "ユーザーåを入れã¦ã‚„" -renotedBy: "{user}ãŒãƒ–ーストã—ãŸã§" +renotedBy: "{user}ãŒãƒªãƒŽãƒ¼ãƒˆã—ãŸã§" noNotes: "ノートã¯ã‚らã¸ã‚“" noNotifications: "通知ã¯ã‚らã¸ã‚“" instance: "サーãƒãƒ¼" @@ -45,10 +45,10 @@ pin: "ピン留ã‚ã—ã¨ã" unpin: "ã‚„ã£ã±ãƒ”ン留ã‚ã›ã‚“" copyContent: "内容をコピー" copyLink: "リンクをコピー" -copyLinkRenote: "ブーストã®ãƒªãƒ³ã‚¯ã‚’コピーã™ã‚‹ã§ï¼Ÿ" +copyLinkRenote: "リノートã®ãƒªãƒ³ã‚¯ã‚’コピーã™ã‚‹ã§ï¼Ÿ" delete: "ã»ã‹ã™" deleteAndEdit: "ã»ã‹ã—ã¦ç›´ã™" -deleteAndEditConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’ã»ã‹ã—ã¦ã‚‚ã£ã‹ã„ç›´ã™ï¼Ÿã“ã®ãƒŽãƒ¼ãƒˆã¸ã®ãƒ„ッコミã€ãƒ–ーストã€è¿”信も全部消ãˆã‚‹ã‚“ã‚„ã‘ã©ãã‚Œã§ã‚‚ãˆãˆã‚“?" +deleteAndEditConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’ã»ã‹ã—ã¦ã‚‚ã£ã‹ã„ç›´ã™ï¼Ÿã“ã®ãƒŽãƒ¼ãƒˆã¸ã®ãƒ„ッコミã€ãƒªãƒŽãƒ¼ãƒˆã€è¿”信も全部消ãˆã‚‹ã‚“ã‚„ã‘ã©ãã‚Œã§ã‚‚ãˆãˆã‚“?" addToList: "リストã«å…¥ã‚ŒãŸã‚‹" addToAntenna: "アンテナã«å…¥ã‚Œã‚‹" sendMessage: "メッセージをé€ã‚‹" @@ -113,7 +113,7 @@ renotedToX: "{name}ã«ãƒªãƒŽãƒ¼ãƒˆã—ãŸã§" cantRenote: "ã“ã®æŠ•ç¨¿ã¯ãƒªãƒŽãƒ¼ãƒˆã§ãã¸ã‚“ã£ã½ã„。" cantReRenote: "リノート自体ã¯ãƒªãƒŽãƒ¼ãƒˆã§ãã¸ã‚“ã§ã€‚" quote: "引用" -inChannelRenote: "ãƒãƒ£ãƒ³ãƒãƒ«ã®ä¸ã§ãƒ–ースト" +inChannelRenote: "ãƒãƒ£ãƒ³ãƒãƒ«ã®ä¸ã§ãƒªãƒŽãƒ¼ãƒˆ" inChannelQuote: "ãƒãƒ£ãƒ³ãƒãƒ«å†…引用" renoteToChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒªãƒŽãƒ¼ãƒˆ" renoteToOtherChannel: "ä»–ã®ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒªãƒŽãƒ¼ãƒˆ" @@ -140,8 +140,8 @@ unmarkAsSensitive: "ãã“ã¾ã§ã‚¢ã‚«ãƒ³ã“ã¨ãªã„ã‚„ã‚" enterFileName: "ファイルåを入れã¦ã‚„" mute: "ミュート" unmute: "ミュートやã‚ãŸã‚‹" -renoteMute: "ブーストã¯è¦‹ã„ã²ã‚“" -renoteUnmute: "ブーストもやã£ã±è¦‹ã‚‹ã‚" +renoteMute: "リノートã¯è¦‹ã„ã²ã‚“" +renoteUnmute: "リノートもやã£ã±è¦‹ã‚‹ã‚" block: "ブãƒãƒƒã‚¯" unblock: "ブãƒãƒƒã‚¯ã‚„ã‚ãŸã‚‹" suspend: "å‡çµ" @@ -172,11 +172,9 @@ youCanCleanRemoteFilesCache: "ファイル管ç†ã«ã‚る🗑ï¸ãƒœã‚¿ãƒ³ã§ã‚ cacheRemoteSensitiveFiles: "リモートã®ãã‚ã©ã„ファイルをã‚ャッシュã«çªã£è¾¼ã‚€" cacheRemoteSensitiveFilesDescription: "ã“ã®è¨å®šã‚’切るã¨ã€ãƒªãƒ¢ãƒ¼ãƒˆã®ãã‚ã©ã„ファイルã¯ã‚ャッシュã›ãšç›´ã§ãƒªãƒ³ã‚¯ã™ã‚‹ã‚ˆã†ã«ãªã‚‹ã§ã€‚" flagAsBot: "Botã«ã™ã‚‹ã§" -flagAsBotDescription: "ã‚‚ã—ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’プãƒã‚°ãƒ©ãƒ 使ã†ã¦é‹ç”¨ã™ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ã“ã®ãƒ•ãƒ©ã‚°ã‚’オンã«ã—ã¦ã‚„。オンã«ã™ã‚Œã°ã€åå¿œãŒãƒãƒ¼ãƒƒã¦é€£éŽ–ã›ã‚“よã†ã«é–‹ç™ºè€…ãŒä½¿ã†ãŸã‚Šã€Sharkeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã‚“ã«ãªã‚‹ã‹ã‚‰ãªã€‚" +flagAsBotDescription: "ã‚‚ã—ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’プãƒã‚°ãƒ©ãƒ 使ã†ã¦é‹ç”¨ã™ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ã“ã®ãƒ•ãƒ©ã‚°ã‚’オンã«ã—ã¦ã‚„。オンã«ã™ã‚Œã°ã€åå¿œãŒãƒãƒ¼ãƒƒã¦é€£éŽ–ã›ã‚“よã†ã«é–‹ç™ºè€…ãŒä½¿ã†ãŸã‚Šã€Misskeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã‚“ã«ãªã‚‹ã‹ã‚‰ãªã€‚" flagAsCat: "猫や。ã‹ã‚ãˆãˆãªã€‚" flagAsCatDescription: "ãƒã‚³ã«ãªã‚ŠãŸã„ã‚“ãªã‚‰ã“ã‚Œã¤ã‘ã¨ã。" -flagSpeakAsCat: "猫語ã§è©±ã™ã§" -flagSpeakAsCatDescription: "有効ã«ã™ã‚‹ã¨ã€ã‚ãªãŸã®æŠ•ç¨¿ã® 「ãªã€ã‚’「ã«ã‚ƒã€ã«ã™ã‚‹ã§ãƒ¼ã€‚" flagShowTimelineReplies: "タイムラインã«ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹ã§" flagShowTimelineRepliesDescription: "オンã«ã—ãŸã‚‰ã€ã‚¿ã‚¤ãƒ ラインã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆã®ä»–ã«ã‚‚ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ä»–ã®ãƒŽãƒ¼ãƒˆã¸ã®è¿”信を表示ã™ã‚‹ã§ã€‚" autoAcceptFollowed: "フォãƒãƒ¼ã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’å‹æ‰‹ã«è¨±å¯ã—ã¨ã" @@ -245,7 +243,7 @@ noUsers: "ユーザーã¯ãŠã‚‰ã‚“" editProfile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’ã„ã˜ã‚‹" noteDeleteConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’ã»ã‹ã—ã¦ãˆãˆã‹ï¼Ÿ" pinLimitExceeded: "ã“れ以上ピン留ã‚ã§ãã²ã‚“" -intro: "Sharkeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ãŸã§ï¼ç®¡ç†è€…アカウントを作ã£ã¦ã‚„。" +intro: "Misskeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ãŸã§ï¼ç®¡ç†è€…アカウントを作ã£ã¦ã‚„。" done: "ã§ã‘ãŸ" processing: "処ç†ã—ã¨ã‚‹" preview: "プレビュー" @@ -440,7 +438,7 @@ exploreFediverse: "Fediverseを探ã£ã¦ã¿ã‚‹" popularTags: "人気ã®ã‚¿ã‚°" userList: "リスト" about: "æƒ…å ±" -aboutMisskey: "Sharkeyã£ã¦ãªã‚“や?" +aboutMisskey: "Misskeyã£ã¦ãªã‚“や?" administrator: "管ç†è€…" token: "確èªã‚³ãƒ¼ãƒ‰" 2fa: "二è¦ç´ èªè¨¼" @@ -511,7 +509,6 @@ uiLanguage: "UIã®è¡¨ç¤ºè¨€èªž" aboutX: "{x}ã«ã¤ã„ã¦" emojiStyle: "絵文å—ã®ã‚¹ã‚¿ã‚¤ãƒ«" native: "ãƒã‚¤ãƒ†ã‚£ãƒ–" -disableDrawer: "メニューをドãƒãƒ¯ãƒ¼ã§è¡¨ç¤ºã›ãˆã¸ã‚“" showNoteActionsOnlyHover: "ノートã®æ“作部をホãƒãƒ¼æ™‚ã®ã¿è¡¨ç¤ºã™ã‚‹ã§" showReactionsCount: "ノートã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³æ•°ã‚’表示ã™ã‚‹" noHistory: "å±¥æ´ã¯ãªã„ã‚。" @@ -577,7 +574,7 @@ popout: "ãƒãƒƒãƒ—アウト" volume: "ã‚„ã‹ã¾ã—ã•" masterVolume: "全体ã®ã‚„ã‹ã¾ã—ã•" notUseSound: "音出ã•ã¸ã‚“" -useSoundOnlyWhenActive: "SharkeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã¨ãã ã‘音出ã™" +useSoundOnlyWhenActive: "MisskeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã¨ãã ã‘音出ã™" details: "ã‚‚ã£ã¨" chooseEmoji: "絵文å—ã‚’é¸ã¶" unableToProcess: "ãªã‚“ã‹å¥¥ã®æ–¹ã§è©°ã¾ã£ã¦ã‚‚ã†ãŸ" @@ -593,7 +590,7 @@ sort: "仕分ã‘ã‚‹" ascendingOrder: "å°ã•ã„é †" descendingOrder: "大ãã„é †" scratchpad: "スクラッãƒãƒ‘ッド" -scratchpadDescription: "スクラッãƒãƒ‘ッドã§ã¯AiScriptを色々試ã™ã“ã¨ãŒã§ãるんや。Sharkeyã«å¯¾ã—ã¦è‰²ã€…ã§ãるコードを書ã„ã¦å‹•ã‹ã—ã¦ã¿ãŸã‚Šã€çµæžœã‚’見ãŸã‚Šã§ãã‚‹ã§ã€‚" +scratchpadDescription: "スクラッãƒãƒ‘ッドã§ã¯AiScriptを色々試ã™ã“ã¨ãŒã§ãるんや。Misskeyã«å¯¾ã—ã¦è‰²ã€…ã§ãるコードを書ã„ã¦å‹•ã‹ã—ã¦ã¿ãŸã‚Šã€çµæžœã‚’見ãŸã‚Šã§ãã‚‹ã§ã€‚" output: "出力" script: "スクリプト" disablePagesScript: "Pagesã®ã‚¹ã‚¯ãƒªãƒ—トを無効ã«ã—ã¦ã‚„" @@ -703,17 +700,14 @@ behavior: "動作" sample: "サンプル" abuseReports: "é€šå ±" reportAbuse: "é€šå ±" -reportAbuseRenote: "ブースト苦情ã ã™ã§ï¼Ÿ" +reportAbuseRenote: "リノート苦情ã ã™ã§ï¼Ÿ" reportAbuseOf: "{name}ã‚’é€šå ±ã™ã‚‹" fillAbuseReportDescription: "ç´°ã‹ã„é€šå ±ç†ç”±ã‚’書ã„ã¦ãªãƒ¼ã€‚対象ノートãŒã‚る時ã¯ãã®URLも書ã„ã¨ã„ã¦ãªãƒ¼ã€‚" abuseReported: "無事内容ãŒé€ä¿¡ã•ã‚ŒãŸã¿ãŸã„ã‚„ã§ã€‚ãŠãŠãã«ã€œã€‚" reporter: "é€šå ±è€…" reporteeOrigin: "é€šå ±å…ˆ" reporterOrigin: "é€šå ±å…ƒ" -forwardReport: "リモートサーãƒãƒ¼ã«é€šå ±ã‚’転é€ã™ã‚‹ã§" -forwardReportIsAnonymous: "リモートサーãƒãƒ¼ã‹ã‚‰ã¯ã‚ã‚“ãŸã®æƒ…å ±ã¯è¦‹ãˆã‚“ãªã£ã¦ã€åŒ¿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦è¡¨ç¤ºã•ã‚Œã‚‹ã§ã€‚" send: "é€ä¿¡" -abuseMarkAsResolved: "対応ã—ãŸã§" openInNewTab: "æ–°ã—ã„タブã§é–‹ã" openInSideView: "サイドビューã§é–‹ã" defaultNavigationBehaviour: "デフォルトã®ãƒŠãƒ“ゲーション" @@ -732,14 +726,14 @@ unclip: "クリップやã‚ã¨ã" confirmToUnclipAlreadyClippedNote: "ã“ã®ãƒŽãƒ¼ãƒˆã¯ã‚‚ã†ã€Œ{name}ã€ã«å«ã¾ã‚Œã¨ã‚‹ã§ã€‚ノートã€ã“ã®ã‚¯ãƒªãƒƒãƒ—ã‹ã‚‰å¤–ãã‹ï¼Ÿ" public: "パブリック" private: "éžå…¬é–‹" -i18nInfo: "Sharkeyã¯æœ‰å¿—ãŒã„ã‚ã‚“ãªè¨€èªžã«è¨³ã—ã¨ã‚‹ã§ã€‚{link}ã§ç¿»è¨³ã«å”力ã—ãŸã£ã¦ã‚„ー。" +i18nInfo: "Misskeyã¯æœ‰å¿—ãŒã„ã‚ã‚“ãªè¨€èªžã«è¨³ã—ã¨ã‚‹ã§ã€‚{link}ã§ç¿»è¨³ã«å”力ã—ãŸã£ã¦ã‚„ー。" manageAccessTokens: "アクセストークンã®ç®¡ç†" accountInfo: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆæƒ…å ±" notesCount: "ノートã®æ•°ã‚„ã§" repliesCount: "返信ã—ãŸæ•°ã‚„ã§" -renotesCount: "ブーストã—ãŸæ•°ã‚„ã§" +renotesCount: "リノートã—ãŸæ•°ã‚„ã§" repliedCount: "返信ã•ã‚ŒãŸæ•°ã‚„ã§" -renotedCount: "ブーストã•ã‚ŒãŸæ•°ã‚„ã§" +renotedCount: "リノートã•ã‚ŒãŸæ•°ã‚„ã§" followingCount: "フォãƒãƒ¼æ•°ã‚„ã§" followersCount: "フォãƒãƒ¯ãƒ¼æ•°ã‚„ã§" sentReactionsCount: "ツッコんã æ•°" @@ -786,7 +780,7 @@ onlineUsersCount: "{n}人ãŒèµ·ãã¨ã‚‹ã§" nUsers: "{n}ユーザー" nNotes: "{n}ノート" sendErrorReports: "エラーリãƒãƒ¼ãƒˆã‚’é€ã‚‹" -sendErrorReportsDescription: "オンã«ã—ãŸã‚‰ã€ãªã‚“ã‹å¤‰ãªã“ã¨ãŒèµ·ããŸã¨ãã€è©³ã—ã„ã®ãŒå…¨éƒ¨Sharkeyã«é€ã‚‰ã‚Œã¦ã€ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚’ã‚‚ã£ã¨è‰¯ã†ã™ã‚‹ã§ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã‚“ã‹ãŒå«ã¾ã‚Œã‚‹ãªã€‚" +sendErrorReportsDescription: "オンã«ã—ãŸã‚‰ã€ãªã‚“ã‹å¤‰ãªã“ã¨ãŒèµ·ããŸã¨ãã€è©³ã—ã„ã®ãŒå…¨éƒ¨Misskeyã«é€ã‚‰ã‚Œã¦ã€ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚’ã‚‚ã£ã¨è‰¯ã†ã™ã‚‹ã§ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã‚“ã‹ãŒå«ã¾ã‚Œã‚‹ãªã€‚" myTheme: "マイテーマ" backgroundColor: "背景" accentColor: "アクセント" @@ -881,7 +875,7 @@ hashtags: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" troubleshooting: "トラブルシューティング" useBlurEffect: "UIã«ã¼ã‹ã—効果を使ã†ã§" learnMore: "詳ã—ã" -misskeyUpdated: "SharkeyãŒæ›´æ–°ã•ã‚ŒãŸã§ï¼\nモデレーターã®äººã‚‰ã«æ„Ÿè¬ã›ãªã‚ã‹ã‚“ã§" +misskeyUpdated: "MisskeyãŒæ›´æ–°ã•ã‚ŒãŸã§ï¼\nモデレーターã®äººã‚‰ã«æ„Ÿè¬ã›ãªã‚ã‹ã‚“ã§" whatIsNew: "æ›´æ–°æƒ…å ±ã‚’è¦‹ã‚‹ã§" translate: "翻訳" translatedFrom: "{x}ã‹ã‚‰ç¿»è¨³ã™ã‚‹ã§" @@ -1010,8 +1004,8 @@ numberOfLikes: "ã„ã„ãæ•°" show: "表示" neverShow: "今後表示ã—ãªã„" remindMeLater: "ã¾ãŸå¾Œã§" -didYouLikeMisskey: "Sharkeyæ°—ã«å…¥ã£ã¦ãã‚ŒãŸï¼Ÿ" -pleaseDonate: "Sharkeyã¯{host}ãŒä½¿ã†ã¨ã‚‹ç„¡æ–™ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚„ã§ã€‚ã“ã‚Œã‹ã‚‰ã‚‚開発を続ã‘れるよã†ã«ã€å¯„付ã—ãŸã£ã¦ãªï½žã€‚" +didYouLikeMisskey: "Misskeyæ°—ã«å…¥ã£ã¦ãã‚ŒãŸï¼Ÿ" +pleaseDonate: "Misskeyã¯{host}ãŒä½¿ã†ã¨ã‚‹ç„¡æ–™ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚„ã§ã€‚ã“ã‚Œã‹ã‚‰ã‚‚開発を続ã‘れるよã†ã«ã€å¯„付ã—ãŸã£ã¦ãªï½žã€‚" correspondingSourceIsAvailable: "{anchor}" roles: "ãƒãƒ¼ãƒ«" role: "ãƒãƒ¼ãƒ«" @@ -1039,8 +1033,8 @@ thisPostMayBeAnnoying: "ã“ã®æŠ•ç¨¿ã¯è¿·æƒ‘ã‹ã‚‚ã—らんã§ã€‚" thisPostMayBeAnnoyingHome: "ホームã«æŠ•ç¨¿" thisPostMayBeAnnoyingCancel: "ã‚„ã‚ã¨ã" thisPostMayBeAnnoyingIgnore: "ã“ã®ã¾ã¾æŠ•ç¨¿" -collapseRenotes: "見ãŸã“ã¨ã‚るブーストã¯é£›ã°ã—ã¦è¡¨ç¤ºã™ã‚‹ã§" -collapseRenotesDescription: "リアクションやブーストをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã™ã‚‹ã§ã€‚" +collapseRenotes: "見ãŸã“ã¨ã‚るリノートã¯é£›ã°ã—ã¦è¡¨ç¤ºã™ã‚‹ã§" +collapseRenotesDescription: "リアクションやリノートをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã™ã‚‹ã§ã€‚" internalServerError: "サーãƒãƒ¼å†…部エラー" internalServerErrorDescription: "サーãƒãƒ¼ã§ãªã‚“ã‹å¤‰ãªã“ã¨èµ·ã“ã£ã¨ã‚‹ã‚。" copyErrorInfo: "ã‚¨ãƒ©ãƒ¼æƒ…å ±ã‚’ã‚³ãƒ”ã‚‹ã§" @@ -1096,7 +1090,7 @@ forceShowAds: "ã„ã£ã¤ã‚‚åºƒå‘Šã‚’æ˜ ã™" addMemo: "メモを足ã™" editMemo: "メモをã„らã†" reactionsList: "ツッコミ一覧" -renotesList: "ブースト一覧" +renotesList: "リノート一覧" notificationDisplay: "通知見ã›ã‚‹" leftTop: "左上" rightTop: "å³ä¸Š" @@ -1137,7 +1131,7 @@ rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "ãƒãƒ¼ãƒ«ã¯å…¬é–‹ãƒãƒ¼ cancelReactionConfirm: "ツッコむんをやã£ã±ã‚„ã‚ã‚‹ã‹ï¼Ÿ" changeReactionConfirm: "ツッコミを別ã®ã«å¤‰ãˆã‚‹ã‹ï¼Ÿ" later: "ã‚ã¨ã§" -goToMisskey: "Sharkeyã¸" +goToMisskey: "Misskeyã¸" additionalEmojiDictionary: "絵文å—ã®è¿½åŠ 辞書" installed: "インストールã—ã¨ã‚‹" branding: "ブランディング" @@ -1172,7 +1166,7 @@ pastAnnouncements: "éŽåŽ»ã®ãŠçŸ¥ã‚‰ã›ã‚„ã§" youHaveUnreadAnnouncements: "ã‚ã‚“ãŸã¾ã ã“ã®ãŠçŸ¥ã‚‰ã›èªã‚“ã©ã‚‰ã‚“ã‚„ã‚。" useSecurityKey: "ブラウザã¾ãŸã¯ãƒ‡ãƒã‚¤ã‚¹ã®è¨€ã†é€šã‚Šã«ã€ã‚»ã‚ュリティã‚ーã¾ãŸã¯ãƒ‘スã‚ーを使ã£ã¦ã‚„。" replies: "返事" -renotes: "ブースト" +renotes: "リノート" loadReplies: "返信を見るã§" loadConversation: "会話を見るã§" pinnedList: "ピン留ã‚ã—ã¯ã£ãŸãƒªã‚¹ãƒˆ" @@ -1183,7 +1177,7 @@ unnotifyNotes: "投稿ã®é€šçŸ¥ã‚„ã‚ã‚‹" authentication: "èªè¨¼" authenticationRequiredToContinue: "続ã‘ã‚‹ã‚“ãªã‚‰èªè¨¼ã—ã¦ã‚„。" dateAndTime: "日時" -showRenotes: "ブースト出ã™" +showRenotes: "リノート出ã™" edited: "ã„ã˜ã£ãŸã‚„ã¤" notificationRecieveConfig: "通知もらã†ã‹ã®è¨å®š" mutualFollow: "ãŠäº’ã„フォãƒãƒ¼ã—ã¦ã‚“ã§" @@ -1314,7 +1308,7 @@ _initialAccountSetting: pushNotificationDescription: "プッシュ通知を有効ã«ã™ã‚‹ã¨{name}ã®é€šçŸ¥ã‚’ã‚ã‚“ãŸã®ãƒ‡ãƒã‚¤ã‚¹ã§å—ã‘å–れるã§ã€‚" initialAccountSettingCompleted: "åˆæœŸè¨å®šçµ‚ã‚ã‚Šã‚„ï¼" haveFun: "{name}ã€æ¥½ã—ã‚“ã§ãªï½ž" - youCanContinueTutorial: "ã“ã‚“ã¾ã¾{name}(Sharkey)ã®ä½¿ã„æ–¹ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«ã‚‚è¡Œã‘ã‚‹ã‘ã©ã€ã“ã“ã§ã‚„ã‚ã¦ã™ãã«ä½¿ã„始ã‚ã¦ã‚‚ãˆãˆã§ã€‚" + youCanContinueTutorial: "ã“ã‚“ã¾ã¾{name}(Misskey)ã®ä½¿ã„æ–¹ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«ã‚‚è¡Œã‘ã‚‹ã‘ã©ã€ã“ã“ã§ã‚„ã‚ã¦ã™ãã«ä½¿ã„始ã‚ã¦ã‚‚ãˆãˆã§ã€‚" startTutorial: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã¯ã˜ã‚ã‚‹" skipAreYouSure: "åˆæœŸè¨å®šé£›ã°ã™ã‹ï¼Ÿ" laterAreYouSure: "åˆæœŸè¨å®šã‚ã¨ã§ã‚„ã‚Šç›´ã™ã‚“?" @@ -1325,10 +1319,10 @@ _initialTutorial: skipAreYouSure: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã‚„ã‚ã‚‹ã‹ï¼Ÿ" _landing: title: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«ã‚ˆã†æ¥ãŸãª" - description: "ã“ã“ã§ã¯ã€Sharkeyã®ã‚«ãƒ³ã‚¿ãƒ³ãªä½¿ã„æ–¹ã¨ã‹æ©Ÿèƒ½ã‚’確ã‹ã‚れんã§ã€‚" + description: "ã“ã“ã§ã¯ã€Misskeyã®ã‚«ãƒ³ã‚¿ãƒ³ãªä½¿ã„æ–¹ã¨ã‹æ©Ÿèƒ½ã‚’確ã‹ã‚れんã§ã€‚" _note: title: "ノートã£ã¦ãªã‚“や?" - description: "Sharkeyã§ã®æŠ•ç¨¿ã¯ã€ŒãƒŽãƒ¼ãƒˆã€ã£ã¦å‘¼ã°ã‚Œã¦ã‚“ã§ã€‚ノートã¯é †ã€…ã«ã‚¿ã‚¤ãƒ ラインã«è¼‰ã£ã¦ã¦ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§æ–°ã—ããªã£ã¦ã£ã¦ã‚“ã§ã€‚" + description: "Misskeyã§ã®æŠ•ç¨¿ã¯ã€ŒãƒŽãƒ¼ãƒˆã€ã£ã¦å‘¼ã°ã‚Œã¦ã‚“ã§ã€‚ノートã¯é †ã€…ã«ã‚¿ã‚¤ãƒ ラインã«è¼‰ã£ã¦ã¦ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§æ–°ã—ããªã£ã¦ã£ã¦ã‚“ã§ã€‚" reply: "返信もã§ãã‚‹ã§ã€‚返信ã®è¿”ä¿¡ã‚‚ã§ãã‚‹ã‹ã‚‰ã€ã‚¹ãƒ¬ãƒƒãƒ‰ã£ã½ã会話をãã®ã¾ã¾ç¶šã‘れもã™ã‚‹ã§ã€‚" renote: "ãã®ãƒŽãƒ¼ãƒˆã‚’自分ã®ã‚¿ã‚¤ãƒ ラインã«æµã—ã¦å…±æœ‰ã§ãã‚‹ã§ã€‚テã‚スト入れã¦å¼•ç”¨ã—ã¦ã‚‚ãˆãˆãªã€‚" reaction: "ツッコミをã¤ã‘ã‚‹ã“ã¨ã‚‚ã§ãã‚‹ã§ã€‚ç´°ã‹ã„ã“ã¨ã¯æ¬¡ã®ãƒšãƒ¼ã‚¸ã‚„。" @@ -1336,13 +1330,13 @@ _initialTutorial: _reaction: title: "ツッコミã£ã¦ãªã‚“や?" description: "ノートã«ã¯ã€Œãƒ„ッコミã€ã§ãã‚“ãん。「ã„ã„ãã€ã¨ã‹ä½•è¨€ã£ã¨ã‚‹ã‹ã‚ã‹ã‚‰ã‚“ã—ã€ç°¡å˜ã«è¡¨ç¾ã§ãã‚‹ã®ã¯ãˆãˆã“ã¨ã‚„ん?" - letsTryReacting: "ノートã®ã€Œ{reaction}ã€ãƒœã‚¿ãƒ³ã§ãƒ„ッコã‚ã‚‹ã‚。試ã—ã«ä¸‹ã®ãƒŽãƒ¼ãƒˆã«ãƒ„ッコんã§ã¿ã€‚" + letsTryReacting: "ノートã®ã€Œï¼‹ã€ãƒœã‚¿ãƒ³ã§ãƒ„ッコã‚ã‚‹ã‚。試ã—ã«ä¸‹ã®ãƒŽãƒ¼ãƒˆã«ãƒ„ッコんã§ã¿ã€‚" reactToContinue: "ツッコんã ら進ã‚るよã†ã«ãªã‚‹ã§ã€‚" reactNotification: "ã‚ã‚“ãŸã®ãƒŽãƒ¼ãƒˆãŒèª°ã‹ã«ãƒ„ッコã¾ã‚ŒãŸã‚‰ã€ã™ã通知ã™ã‚‹ã§ã€‚" - reactDone: "「{undo}ã€ãƒœã‚¿ãƒ³ã§ãƒ„ッコミやã‚れるã§ã€‚" + reactDone: "「ーã€ãƒœã‚¿ãƒ³ã§ãƒ„ッコミやã‚れるã§ã€‚" _timeline: title: "タイムラインã®ã—ãã¿" - description1: "Sharkeyã«ã¯ã€ã„ã‚ã„ã‚タイムラインãŒã‚ã‚“ã§ï¼ˆãŸã ã€ã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦ã¯ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã‚‹ã¨ã“ã‚ã‚‚ã‚ã‚‹ãªï¼‰ã€‚" + description1: "Misskeyã«ã¯ã€ã„ã‚ã„ã‚タイムラインãŒã‚ã‚“ã§ï¼ˆãŸã ã€ã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦ã¯ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã‚‹ã¨ã“ã‚ã‚‚ã‚ã‚‹ãªï¼‰ã€‚" home: "ã‚ã‚“ãŸãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®æŠ•ç¨¿ãŒè¦‹ã‚Œã‚“ãん。" local: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã®ä¸ã«ãŠã‚‹å…¨å“¡ã®æŠ•ç¨¿ãŒè¦‹ã‚Œã‚‹ã§ã€‚" social: "ホームタイムラインã®æŠ•ç¨¿ã‚‚ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã®ã‚‚一緒ã«è¦‹ã‚Œã‚‹ã§ã€‚" @@ -1351,12 +1345,12 @@ _initialTutorial: description3: "ãã®ä»–ã«ã‚‚ã€ãƒªã‚¹ãƒˆã‚¿ã‚¤ãƒ ラインã¨ã‹ãƒãƒ£ãƒ³ãƒãƒ«ã‚¿ã‚¤ãƒ ラインã¨ã‹ãŒã‚ã‚“ãん。詳ã—ã„ã®ã¯{link}を見ã¨ã。" _postNote: title: "ノートã®æŠ•ç¨¿è¨å®š" - description1: "Sharkeyã«ãƒŽãƒ¼ãƒˆã‚’投稿ã™ã‚‹ã¨ãã€ã„ã‚ã‚“ãªã‚ªãƒ—ションãŒä»˜ã‘れるã§ã€‚投稿画é¢ã¯ã“ã‚“ãªæ„Ÿã˜ã‚„。" + description1: "Misskeyã«ãƒŽãƒ¼ãƒˆã‚’投稿ã™ã‚‹ã¨ãã€ã„ã‚ã‚“ãªã‚ªãƒ—ションãŒä»˜ã‘れるã§ã€‚投稿画é¢ã¯ã“ã‚“ãªæ„Ÿã˜ã‚„。" _visibility: description: "ノートを見れる相手を制é™ã§ãã‚‹ã‚。" public: "ã¿ã‚“ãªã«è¦‹ã›ã‚‹ã§ã€‚" - home: "ホームタイムラインã«ã ã‘見ã›ã‚‹ã§ã€‚フォãƒãƒ¯ãƒ¼ã¨ã‹ã€ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’見ã«æ¥ãŸäººã€ãƒ–ーストã‹ã‚‰ã‚‚見れるã‹ã‚‰ã€å®Ÿè³ªã¯å…¨å“¡è¦‹ã‚Œã‚‹ã‘ã©ãªã€‚ã‚ã‚“ã¾ã—広ãŒã‚Šã«ãã„ã£ã¦ã“ã¨ã‚„。" - followers: "フォãƒãƒ¯ãƒ¼ã«ã ã‘見ã›ã‚‹ã§ã€‚自分以外ã¯ãƒ–ーストã§ãã¸ã‚“ã—ã€ãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã¯çµ¶å¯¾ã«è¦‹ã‚Œã¸ã‚“。" + home: "ホームタイムラインã«ã ã‘見ã›ã‚‹ã§ã€‚フォãƒãƒ¯ãƒ¼ã¨ã‹ã€ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’見ã«æ¥ãŸäººã€ãƒªãƒŽãƒ¼ãƒˆã‹ã‚‰ã‚‚見れるã‹ã‚‰ã€å®Ÿè³ªã¯å…¨å“¡è¦‹ã‚Œã‚‹ã‘ã©ãªã€‚ã‚ã‚“ã¾ã—広ãŒã‚Šã«ãã„ã£ã¦ã“ã¨ã‚„。" + followers: "フォãƒãƒ¯ãƒ¼ã«ã ã‘見ã›ã‚‹ã§ã€‚自分以外ã¯ãƒªãƒŽãƒ¼ãƒˆã§ãã¸ã‚“ã—ã€ãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã¯çµ¶å¯¾ã«è¦‹ã‚Œã¸ã‚“。" direct: "指定ã—ãŸäººã«ã ã‘公開ã•ã‚Œã¦ã€ã¤ã„ã§ã«é€šçŸ¥ã‚‚é€ã‚‹ã§ã€‚ダイレクトメールã®ä»£ã‚ã‚Šã¨ã—ã¦ä½¿ã£ã¦ãªã€‚" doNotSendConfidencialOnDirect1: "æ©Ÿå¯†æƒ…å ±ã‚’é€ã‚‹ã¨ãã¯å分注æ„ã›ãˆã‚ˆã€‚" doNotSendConfidencialOnDirect2: "é€ä¿¡å…ˆã®ã‚µãƒ¼ãƒãƒ¼ã®ç®¡ç†è€…ã¯æŠ•ç¨¿å†…容ãŒè¦‹ã‚Œã‚‹ã‹ã‚‰ã€ä¿¡ç”¨ã§ãã¸ã‚“サーãƒãƒ¼ã®ã²ã¨ã«ãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆæŠ•ç¨¿ã™ã‚‹ã¨ãã«ã¯ã€ã‚ã£ã¡ã‚ƒç”¨å¿ƒã—ã¨ãã‚“ã‚„ã§ã€‚" @@ -1422,7 +1416,7 @@ _achievements: _notes1: title: "ã¾ã„ã©ï¼" description: "åˆã‚ã¦ãƒŽãƒ¼ãƒˆæŠ•ç¨¿ã—ãŸã£ãŸ" - flavor: "Sharkeyを楽ã—ã‚“ã§ãªï½ž" + flavor: "Misskeyを楽ã—ã‚“ã§ãªï½ž" _notes10: title: "ノートã®å¤©ä¿å±±" description: "ノートを10回投稿ã—ãŸ" @@ -1518,7 +1512,7 @@ _achievements: _login1000: title: "ノートマイスターⅢ" description: "通算1,000æ—¥ãƒã‚°ã‚¤ãƒ³ã—ãŸ" - flavor: "Sharkeyよã†ã•ã‚“使ã¦ã‚‚ã‚ã¦ãŠãŠãã«ãªï¼" + flavor: "Misskeyよã†ã•ã‚“使ã¦ã‚‚ã‚ã¦ãŠãŠãã«ãªï¼" _noteClipped1: title: "アカンã©ã‚Œã‚‚クリップã—ãŸã„ã‚" description: "åˆã‚ã¦ãƒŽãƒ¼ãƒˆã‚’クリップã—ãŸ" @@ -1578,9 +1572,9 @@ _achievements: title: "実績好ã" description: "実績一覧を3分以上眺ã‚続ã‘ãŸ" _iLoveMisskey: - title: "Sharkey好ãã‚„ãã‚“" - description: "\"I ⤠#Sharkey\"を投稿ã—ãŸ" - flavor: "Sharkeyを使ã£ã¦ãã‚Œã¦ãŠãŠãã«ãªï½žã€€by 開発ãƒãƒ¼ãƒ " + title: "Misskey好ãã‚„ãã‚“" + description: "\"I ⤠#Misskey\"を投稿ã—ãŸ" + flavor: "Misskeyを使ã£ã¦ãã‚Œã¦ãŠãŠãã«ãªï½žã€€by 開発ãƒãƒ¼ãƒ " _foundTreasure: title: "ãªã‚“ã§ã‚‚鑑定団" description: "éš ã•ã‚ŒãŸãŠå®ã‚’発見ã—ãŸ" @@ -1588,7 +1582,7 @@ _achievements: title: "ãã‚“ã" description: "クライアントを起動ã—ã¦ã‹ã‚‰30分以上経éŽã—ãŸ" _client60min: - title: "Sharkeyã®è¦‹éŽãŽã‚„ï¼" + title: "Misskeyã®è¦‹éŽãŽã‚„ï¼" description: "クライアント付ã‘ã¦ã‹ã‚‰ï¼‘時間経ã£ã¦ã‚‚ã†ãŸã§ã€‚" _noteDeletedWithin1min: title: "*ãŠãŠã£ã¨ï¼Š" @@ -1659,7 +1653,7 @@ _achievements: title: "心é…性" description: "通知ã®ãƒ†ã‚¹ãƒˆã—ã™ãŽã‚„ã£ã¦" _tutorialCompleted: - title: "Sharkeyã²ã‚ˆã£ã“講座 修了証" + title: "Misskeyã²ã‚ˆã£ã“講座 修了証" description: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«å…¨éƒ¨ã‚„ã£ãŸ" _bubbleGameExplodingHead: title: "🤯" @@ -1836,14 +1830,14 @@ _registry: domain: "ドメイン" createKey: "ã‚ーを作る" _aboutMisskey: - about: "Sharkeyã¯ã€Misskeyをベースã«ã—ãŸã‚ªãƒ¼ãƒ—ンソースãªã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚„。" + about: "Misskeyã¯syuiloãŒ2014å¹´ã‹ã‚‰ãšã£ã¨ä½œã£ã¦ã¯ã‚‹ã€ã‚ªãƒ¼ãƒ—ンソースãªã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚„。" contributors: "主ãªè²¢çŒ®è€…" allContributors: "å…¨ã¦ã®è²¢çŒ®è€…" source: "ソースコード" original: "オリジナル" - thisIsModifiedVersion: "{name}ã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã®Sharkeyã‚’ã„ã˜ã£ãŸãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’ã¤ã“ã†ã¦ã‚‹ã§ã€‚" - translation: "Sharkeyを翻訳" - donate: "Sharkeyã«å¯„付" + thisIsModifiedVersion: "{name}ã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã®Misskeyã‚’ã„ã˜ã£ãŸãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’ã¤ã“ã†ã¦ã‚‹ã§ã€‚" + translation: "Misskeyを翻訳" + donate: "Misskeyã«å¯„付" morePatrons: "ä»–ã«ã‚‚ãŽã‚‡ã†ã•ã‚“ã®äººã‹ã‚‰ã‚µãƒãƒ¼ãƒˆã—ã¦ã‚‚ã‚ã¦ã‚“ãん。ã»ã‚“ã¾ãŠãŠãã«ðŸ¥°" patrons: "支æ´è€…" projectMembers: "" @@ -1871,7 +1865,7 @@ _channel: notesCount: "{n}ã“投稿ãŒã‚ã‚‹ã§" nameAndDescription: "åå‰ã¨èª¬æ˜Ž" nameOnly: "åå‰ã ã‘" - allowRenoteToExternal: "ãƒãƒ£ãƒ³ãƒãƒ«ã®å¤–ã«ãƒ–ーストã§ãるよã†ã«ã™ã‚‹" + allowRenoteToExternal: "ãƒãƒ£ãƒ³ãƒãƒ«ã®å¤–ã«ãƒªãƒŽãƒ¼ãƒˆã§ãるよã†ã«ã™ã‚‹" _menuDisplay: sideFull: "横" sideIcon: "横(アイコン)" @@ -1949,7 +1943,6 @@ _theme: buttonBg: "ボタンã®èƒŒæ™¯" buttonHoverBg: "ボタンã®èƒŒæ™¯ (ホãƒãƒ¼)" inputBorder: "入力ボックスã®ç¸å–ã‚Š" - listItemHoverBg: "ãƒªã‚¹ãƒˆé …ç›®ã®èƒŒæ™¯ (ホãƒãƒ¼)" driveFolderBg: "ドライブフォルダーã®èƒŒæ™¯" wallpaperOverlay: "å£ç´™ã®ã‚ªãƒ¼ãƒãƒ¼ãƒ¬ã‚¤" badge: "ãƒãƒƒã‚¸" @@ -1968,7 +1961,7 @@ _soundSettings: driveFileTypeWarn: "ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯å¯¾å¿œã—ã¨ã‚‰ã¸ã‚“" driveFileTypeWarnDescription: "音声ファイルをé¸ã³ã‚„" driveFileDurationWarn: "音ãŒé•·ã™ãŽã‚‹ã‚" - driveFileDurationWarnDescription: "é•·ã„音使ã†ãŸã‚‰Sharkey使ã†ã®ã«è‰¯ã†ãªã„ã‹ã‚‚ã—ã‚Œã¸ã‚“ã§ã€‚ãã‚Œã§ã‚‚ãˆãˆã‹ï¼Ÿ" + driveFileDurationWarnDescription: "é•·ã„音使ã†ãŸã‚‰Misskey使ã†ã®ã«è‰¯ã†ãªã„ã‹ã‚‚ã—ã‚Œã¸ã‚“ã§ã€‚ãã‚Œã§ã‚‚ãˆãˆã‹ï¼Ÿ" driveFileError: "音声ãŒèªã¿è¾¼ã‚ã¸ã‚“ã‹ã£ãŸã§ã€‚è¨å®šã‚’変更ã›ãˆã‚„" _ago: future: "未æ¥" @@ -2344,7 +2337,7 @@ _notification: youGotMention: "{name}ã‹ã‚‰ã®ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³" youGotReply: "{name}ã‹ã‚‰ã®ãƒªãƒ—ライ" youGotQuote: "{name}ã«ã‚ˆã‚‹å¼•ç”¨" - youRenoted: "{name}ãŒãƒ–ーストã—ãŸã¿ãŸã„ã‚„ã§" + youRenoted: "{name}ãŒãƒªãƒŽãƒ¼ãƒˆã—ãŸã¿ãŸã„ã‚„ã§" youWereFollowed: "フォãƒãƒ¼ã•ã‚ŒãŸã§" youReceivedFollowRequest: "フォãƒãƒ¼è¨±å¯ã—ã¦ã»ã—ã„ã¿ãŸã„ã‚„ãª" yourFollowRequestAccepted: "フォãƒãƒ¼ã•ã›ã¦ã‚‚ã‚ãŸã§" @@ -2360,7 +2353,7 @@ _notification: notificationWillBeDisplayedLikeThis: "通知ã¯ã“ã®ã‚ˆã†ã«è¡¨ç¤ºã•ã‚Œã‚‹ã§" reactedBySomeUsers: "{n}人ãŒãƒ„ッコんã ã§" likedBySomeUsers: "{n}人ãŒã„ã„ãã—ãŸã§" - renotedBySomeUsers: "{n}人ãŒãƒ–ーストã—ãŸã§" + renotedBySomeUsers: "{n}人ãŒãƒªãƒŽãƒ¼ãƒˆã—ãŸã§" followedBySomeUsers: "{n}人ã«ãƒ•ã‚©ãƒãƒ¼ã•ã‚ŒãŸã§" flushNotification: "通知ã®å±¥æ´ã‚’リセットã™ã‚‹" _types: @@ -2377,6 +2370,7 @@ _notification: followRequestAccepted: "フォãƒãƒ¼ãŒå—ç†ã•ã‚ŒãŸã§" roleAssigned: "ãƒãƒ¼ãƒ«ãŒä»˜ä¸Žã•ã‚ŒãŸ" achievementEarned: "実績ã®ç²å¾—" + login: "ãƒã‚°ã‚¤ãƒ³" app: "連æºã‚¢ãƒ—リã‹ã‚‰ã®é€šçŸ¥ã‚„" _actions: followBack: "フォãƒãƒ¼ãƒãƒƒã‚¯" @@ -2435,7 +2429,7 @@ _webhookSettings: followed: "フォãƒãƒ¼ã‚‚らã£ãŸã¨ã~ï¼" note: "ノートを投稿ã—ãŸã¨ã~ï¼" reply: "返信ãŒã‚ã‚‹ã¨ã~ï¼" - renote: "ブーストã•ã‚Œã‚‹ã¨ã~ï¼" + renote: "リノートã•ã‚Œã‚‹ã¨ã~ï¼" reaction: "ツッコã¾ã‚ŒãŸã¨ã~ï¼" mention: "メンションãŒã‚ã‚‹ã¨ã~ï¼" _systemEvents: diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml index b3ad46f2b16a9b964c56420a50d93f290940f358..222599572a61287000c0b9531106f2f6c82e9b63 100644 --- a/locales/kn-IN.yml +++ b/locales/kn-IN.yml @@ -77,6 +77,8 @@ _profile: username: "ಬಳಕೆಹೆಸರà³" _notification: youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರà³" + _types: + login: "ಪà³à²°à²µà³‡à²¶" _actions: reply: "ಉತà³à²¤à²°à²¿à²¸à³" _deck: diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 9323ed2a26ae54927368c6e10d3477006f35e4b4..6c667b48da4f5d676dad01f7d58704f96c12ec15 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -476,7 +476,6 @@ uiLanguage: "UI 표시 언어" aboutX: "{x}ì— ëŒ€í•´ì„œ" emojiStyle: "ì´ëª¨ì§€ 모양" native: "기본" -disableDrawer: "드로어 메뉴 쓰지 않기" showNoteActionsOnlyHover: "마우스 ì˜¬ë§€ì„ ë•Œë§Œ 노트 ì•¡ì…˜ 버턴 ë³´ì´ê¸°" noHistory: "기ë¡ì´ ì—†ì‹ë‹ˆë‹¤" signinHistory: "ë¡œê·¸ì¸ ê¸°ë¡" @@ -583,6 +582,9 @@ describeFile: "캡션 옇기" enterFileDescription: "캡션 서기" author: "ë§¨ë˜ ì‚¬ëžŒ" manage: "간리" +large: "커게" +medium: "엔갆게" +small: "쪼맪게" emailServer: "ì „ìžìš°íŽœ 서버" email: "ì „ìžìš°íŽœ" emailAddress: "ì „ìžìš°íŽœ 주소" @@ -599,7 +601,6 @@ reportAbuseOf: "{name}님얼 ì‹ ê³ í•˜ê¸°" reporter: "ì‹ ê³ í•œ 사람" reporteeOrigin: "ì‹ ê³ ë´ ì‚¬ëžŒ" reporterOrigin: "ì‹ ê³ í•œ ê³³" -forwardReport: "ì›¬ê² ì„œë²„ì— ì‹ ê³ ë³´ë‚´ê¸°" waitingFor: "{x}(ì–¼)럴 ì§€ë‹¬ë¦¬ê³ ìž‡ì‹ë‹ˆë‹¤" random: "무작ì´" system: "시스템" @@ -613,12 +614,14 @@ followersCount: "팔로워 수" noteFavoritesCount: "질겨찾기한 노트 수" clips: "í´ë¦½ 맨걸기" clearCache: "ìºì‹œ 비우기" +nUsers: "{n} 사용ìž" typingUsers: "{users} ë‹˜ì´ ì„œê³ ìž‡ì–´ì˜ˆ" unlikeConfirm: "좋네예럴 무룹니꺼?" info: "ì •ë³´" selectAccount: "ê³„ì • 개리기" user: "사용ìž" administration: "간리" +middle: "엔갆게" translatedFrom: "{x}ì„œ 번옉" on: "í‚´" off: "껌" @@ -633,6 +636,7 @@ oneMonth: "í•œ 달" file: "파ì¼" typeToConfirm: "게ì†í• ë¼ë¨¼ {x}럴 ëˆ„ì§ˆë¼ ì£¼ì´ì†Œ" pleaseSelect: "개리 주ì´ì†Œ" +remoteOnly: "웬ê²ë§Œ" tools: "ë„구" like: "좋네예!" unlike: "좋네예 무루기" @@ -643,7 +647,10 @@ role: "ì˜‰í• " noRole: "ì˜‰í• ì´ á„‹á…¥á‡ì‹ë‹ˆë‹¤" thisPostMayBeAnnoyingCancel: "ì•„ì´ì˜ˆ" likeOnly: "좋네예마" +hiddenTags: "수ᇚ훈 해시태그" myClips: "ë‚´ í´ë¦½" +preservedUsernames: "예약 ì‚¬ìš©ìž ì´ëŸ¼" +specifyUser: "ì‚¬ìš©ìž ì§€ì •" icon: "아바타" replies: "답하기" renotes: "리노트" @@ -709,6 +716,16 @@ _achievements: description: "0분 0ì´ˆì— ë…¸íŠ¸ë¥¼ 섰어예" _tutorialCompleted: description: "길ë¼ìž¡ì´ëŸ´ 껕냇ì‹ë‹ˆë‹¤" +_role: + displayOrder: "보기 순서" + _priority: + middle: "엔갆게" + _options: + canHideAds: "ê°•ê³ á„‰á…®á‡ší›„ê¸°" + _condition: + isRemote: "ì›¬ê² ì‚¬ìš©ìž" + isCat: "ê°±ì´ ì‚¬ìš©ìž" + isBot: "ìžë™ 사용ìž" _gallery: my: "ë‚´ 걸" liked: "좋네예한 걸" @@ -794,6 +811,7 @@ _notification: mention: "멘션" quote: "따오기" reaction: "반엉" + login: "로그ì¸" _actions: reply: "답하기" _deck: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 64bae5a9d7b27db0bbc6bb579b575dfb03960fbb..414202adab50fb0e2ae339e7115d8b902b7e7644 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -8,6 +8,9 @@ search: "검색" notifications: "알림" username: "ìœ ì €ëª…" password: "비밀번호" +initialPasswordForSetup: "초기 ì„¤ì •ìš© 비밀번호" +initialPasswordIsIncorrect: "초기 ì„¤ì •ìš© 비밀번호가 올바르지 않습니다." +initialPasswordForSetupDescription: "Misskey를 ì§ì ‘ 설치하는 경우, ì„¤ì • 파ì¼ì— ìž…ë ¥í•´ë‘” 비밀번호를 사용하세요.\nMisskey 설치를 ë„와주는 호스팅 서비스 ë“±ì„ ì‚¬ìš©í•˜ëŠ” 경우, 서비스 ì œê³µìžë¡œë¶€í„° ë°›ì€ ë¹„ë°€ë²ˆí˜¸ë¥¼ 사용하세요.\n비밀번호를 ë”°ë¡œ ì„¤ì •í•˜ì§€ ì•Šì€ ê²½ìš°, ì•„ë¬´ê²ƒë„ ìž…ë ¥í•˜ì§€ ì•Šì•„ë„ ë©ë‹ˆë‹¤." forgotPassword: "비밀번호 ìž¬ì„¤ì •" fetchingAsApObject: "ì—°í•©ì—ì„œ 찾아보는 중" ok: "확ì¸" @@ -52,14 +55,15 @@ deleteAndEditConfirm: "ì´ ë…¸íŠ¸ë¥¼ ì‚ì œí•œ ë’¤ 다시 íŽ¸ì§‘í•˜ì‹œê² ìŠµë‹ˆ addToList: "ë¦¬ìŠ¤íŠ¸ì— ì¶”ê°€" addToAntenna: "ì•ˆí…Œë‚˜ì— ì¶”ê°€" sendMessage: "메시지 보내기" -copyRSS: "RSS 주소 복사" +copyRSS: "RSS 복사" copyUsername: "ìœ ì €ëª… 복사" copyUserId: "ìœ ì € ID 복사" copyNoteId: "노트 ID 복사" copyFileId: "íŒŒì¼ ID 복사" copyFolderId: "í´ë” ID 복사" copyProfileUrl: "프로필 URL 복사" -searchUser: "ìœ ì € 검색" +searchUser: "ì‚¬ìš©ìž ê²€ìƒ‰" +searchThisUsersNotes: "사용ìžì˜ 노트 검색" reply: "답글" loadMore: "ë” ë³´ê¸°" showMore: "ë” ë³´ê¸°" @@ -154,6 +158,7 @@ editList: "리스트 편집" selectChannel: "ì±„ë„ ì„ íƒ" selectAntenna: "안테나 ì„ íƒ" editAntenna: "안테나 편집" +createAntenna: "안테나 만들기" selectWidget: "ìœ„ì ¯ ì„ íƒ" editWidgets: "ìœ„ì ¯ 편집" editWidgetsExit: "편집 종료" @@ -194,6 +199,7 @@ followConfirm: "{name}ë‹˜ì„ íŒ”ë¡œìš° í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" proxyAccount: "프ë¡ì‹œ ê³„ì •" proxyAccountDescription: "프ë¡ì‹œ ê³„ì •ì€ íŠ¹ì • ì¡°ê±´ 하ì—ì„œ ìœ ì €ì˜ ë¦¬ëª¨íŠ¸ 팔로우를 대행하는 ê³„ì •ìž…ë‹ˆë‹¤. 예를 들면, ìœ ì €ê°€ 리모트 ìœ ì €ë¥¼ ë¦¬ìŠ¤íŠ¸ì— ë„£ì—ˆì„ ë•Œ, ë¦¬ìŠ¤íŠ¸ì— ë“¤ì–´ê°„ ìœ ì €ë¥¼ ì•„ë¬´ë„ íŒ”ë¡œìš°í•œ ì ì´ ì—†ë‹¤ë©´ 액티비티가 서버로 배달ë˜ì§€ 않기 때문ì—, ëŒ€ì‹ í”„ë¡ì‹œ ê³„ì •ì´ í•´ë‹¹ ìœ ì €ë¥¼ 팔로우하ë„ë¡ í•©ë‹ˆë‹¤." host: "호스트" +selectSelf: "본ì¸ì„ ì„ íƒ" selectUser: "ìœ ì € ì„ íƒ" recipient: "ìˆ˜ì‹ ì¸" annotation: "ë‚´ìš©ì— ëŒ€í•œ 주ì„" @@ -209,6 +215,7 @@ perDay: "1ì¼ë§ˆë‹¤" stopActivityDelivery: "액티비티 보내지 않기" blockThisInstance: "ì´ ì„œë²„ë¥¼ 차단" silenceThisInstance: "서버를 사ì¼ëŸ°ìŠ¤" +mediaSilenceThisInstance: "ì„œë²„ì˜ ë¯¸ë””ì–´ë¥¼ 사ì¼ëŸ°ìŠ¤" operations: "ìž‘ì—…" software: "소프트웨어" version: "ë²„ì „" @@ -230,6 +237,10 @@ blockedInstances: "ì°¨ë‹¨ëœ ì„œë²„" blockedInstancesDescription: "ì°¨ë‹¨í•˜ë ¤ëŠ” ì„œë²„ì˜ í˜¸ìŠ¤íŠ¸ ì´ë¦„ì„ ì¤„ë°”ê¿ˆìœ¼ë¡œ 구분하여 ì„¤ì •í•©ë‹ˆë‹¤. ì°¨ë‹¨ëœ ì¸ìŠ¤í„´ìŠ¤ëŠ” ì´ ì¸ìŠ¤í„´ìŠ¤ì™€ í†µì‹ í• ìˆ˜ 없게 ë©ë‹ˆë‹¤." silencedInstances: "사ì¼ëŸ°ìŠ¤í•œ 서버" silencedInstancesDescription: "사ì¼ëŸ°ìŠ¤í•˜ë ¤ëŠ” ì„œë²„ì˜ í˜¸ìŠ¤íŠ¸ëª…ì„ í•œ ì¤„ì— í•˜ë‚˜ì”© ìž…ë ¥í•©ë‹ˆë‹¤. 사ì¼ëŸ°ìŠ¤ëœ ì„œë²„ì— ì†Œì†ëœ ìœ ì €ëŠ” ëª¨ë‘ '사ì¼ëŸ°ìŠ¤'ëœ ìƒíƒœë¡œ 취급ë˜ë©°, ì´ ì„œë²„ë¡œë¶€í„°ì˜ íŒ”ë¡œìš°ê°€ 프로필 ì„¤ì •ê³¼ 무관하게 승ì¸ì œë¡œ 변경ë˜ê³ , 팔로워가 ì•„ë‹Œ 로컬 ìœ ì €ì—게는 ë©˜ì…˜í• ìˆ˜ 없게 ë©ë‹ˆë‹¤. ì •ì§€ëœ ì„œë²„ì—는 ì ìš©ë˜ì§€ 않습니다." +mediaSilencedInstances: "미디어를 사ì¼ëŸ°ìŠ¤í•œ 서버" +mediaSilencedInstancesDescription: "미디어를 사ì¼ëŸ°ìŠ¤ í•˜ë ¤ëŠ” ì„œë²„ì˜ í˜¸ìŠ¤íŠ¸ë¥¼ í•œ ì¤„ì— í•˜ë‚˜ì”© ìž…ë ¥í•©ë‹ˆë‹¤. 미디어가 사ì¼ëŸ°ìŠ¤ëœ ì„œë²„ì˜ ìœ ì €ê°€ 업로드한 파ì¼ì€ ëª¨ë‘ ë¯¼ê°í•œ 미디어로 처리ë˜ë©°, 커스텀 ì´ëª¨ì§€ë¥¼ ì‚¬ìš©í• ìˆ˜ 없게 ë©ë‹ˆë‹¤. ë˜í•œ, 차단한 ì¸ìŠ¤í„´ìŠ¤ì—는 ì ìš©ë˜ì§€ 않습니다." +federationAllowedHosts: "ì—°í•©ì„ í—ˆê°€í•˜ëŠ” 서버" +federationAllowedHostsDescription: "ì—°í•©ì„ í—ˆê°€í•˜ëŠ” ì„œë²„ì˜ í˜¸ìŠ¤íŠ¸ë¥¼ 엔터로 구분해서 ì„¤ì •í•©ë‹ˆë‹¤." muteAndBlock: "뮤트 ë° ì°¨ë‹¨" mutedUsers: "뮤트한 ìœ ì €" blockedUsers: "차단한 ìœ ì €" @@ -328,6 +339,7 @@ renameFolder: "í´ë” ì´ë¦„ 바꾸기" deleteFolder: "í´ë” ì‚ì œ" folder: "í´ë”" addFile: "íŒŒì¼ ì¶”ê°€" +showFile: "íŒŒì¼ í‘œì‹œí•˜ê¸°" emptyDrive: "ë“œë¼ì´ë¸Œê°€ 비어 있습니다" emptyFolder: "í´ë”ê°€ 비어 있습니다" unableToDelete: "ì‚ì œí• ìˆ˜ 없습니다" @@ -373,7 +385,7 @@ registration: "등ë¡" enableRegistration: "ì‹ ê·œ 회ì›ê°€ìž…ì„ í™œì„±í™”" invite: "초대" driveCapacityPerLocalAccount: "로컬 ìœ ì € í•œ 명당 ë“œë¼ì´ë¸Œ 용량" -driveCapacityPerRemoteAccount: "리모트 ìœ ì € í•œ 명당 ë“œë¼ì´ë¸Œ 용량" +driveCapacityPerRemoteAccount: "ì›ê²© 사용ìžë³„ ë“œë¼ì´ë¸Œ 용량" inMb: "메가바ì´íŠ¸ 단위" bannerUrl: "배너 ì´ë¯¸ì§€ URL" backgroundImageUrl: "ë°°ê²½ ì´ë¯¸ì§€ URL" @@ -442,6 +454,7 @@ totpDescription: "ì¸ì¦ ì•±ì„ ì‚¬ìš©í•˜ì—¬ ì¼íšŒì„± 비밀번호 ìž…ë ¥" moderator: "모ë”ë ˆì´í„°" moderation: "ì¡°ì •" moderationNote: "ì¡°ì • 기ë¡" +moderationNoteDescription: "모ë”ë ˆì´í„° ì—í• ì„ ê°€ì§„ ìœ ì €ë§Œ ë³´ì´ëŠ” 메모를 ì ì„ ìˆ˜ 있습니다." addModerationNote: "ì¡°ì • ê¸°ë¡ ì¶”ê°€í•˜ê¸°" moderationLogs: "모ë”ë ˆì´ì…˜ 로그" nUsersMentioned: "{n}ëª…ì´ ì–¸ê¸‰í•¨" @@ -503,7 +516,10 @@ uiLanguage: "UI 표시 언어" aboutX: "{x}ì— ëŒ€í•˜ì—¬" emojiStyle: "ì´ëª¨ì§€ 스타ì¼" native: "기본" -disableDrawer: "드로어 메뉴를 사용하지 않기" +menuStyle: "메뉴 스타ì¼" +style: "스타ì¼" +drawer: "ì„œëž" +popup: "íŒì—…" showNoteActionsOnlyHover: "마우스가 올ë¼ê°„ ë•Œì—만 노트 ë™ìž‘ ë²„íŠ¼ì„ í‘œì‹œí•˜ê¸°" showReactionsCount: "ë…¸íŠ¸ì˜ ë°˜ì‘ ìˆ˜ë¥¼ 표시하기" noHistory: "기ë¡ì´ 없습니다" @@ -586,6 +602,8 @@ ascendingOrder: "오름차순" descendingOrder: "내림차순" scratchpad: "스í¬ëž˜ì¹˜ 패드" scratchpadDescription: "스í¬ëž˜ì¹˜ 패드는 AiScript ì˜ í…ŒìŠ¤íŠ¸ í™˜ê²½ì„ ì œê³µí•©ë‹ˆë‹¤. Misskey 와 ìƒí˜¸ 작용하는 코드를 작성, 실행 ë° ê²°ê³¼ë¥¼ 확ì¸í• 수 있습니다." +uiInspector: "UI ì¸ìŠ¤íŽ™í„°" +uiInspectorDescription: "ë©”ëª¨ë¦¬ì— ìžˆëŠ” UI ì»´í¬ë„ŒíŠ¸ì˜ ì¸ìŠ¤í„´íŠ¸ 목ë¡ì„ ë³¼ 수 있습니다. UI ì»´í¬ë„ŒíŠ¸ëŠ” Ui:C: 계열 함수로 만들어집니다." output: "ì¶œë ¥" script: "스í¬ë¦½íŠ¸" disablePagesScript: "Pages ì—ì„œ AiScript 를 사용하지 ì•ŠìŒ" @@ -702,10 +720,7 @@ abuseReported: "ì‹ ê³ ë¥¼ 보냈습니다. ì‹ ê³ í•´ 주셔서 ê°ì‚¬í•©ë‹ˆë‹¤." reporter: "ì‹ ê³ ìž" reporteeOrigin: "í”¼ì‹ ê³ ìž" reporterOrigin: "ì‹ ê³ ìž" -forwardReport: "리모트 서버ì—ë„ ì‹ ê³ ë‚´ìš© 보내기" -forwardReportIsAnonymous: "리모트 서버ì—서는 ë‚˜ì˜ ì •ë³´ë¥¼ ë³¼ 수 없으며, ìµëª…ì˜ ì‹œìŠ¤í…œ ê³„ì •ìœ¼ë¡œ 표시ë©ë‹ˆë‹¤." send: "ì „ì†¡" -abuseMarkAsResolved: "í•´ê²°ë¨ìœ¼ë¡œ 표시" openInNewTab: "새 íƒì—ì„œ 열기" openInSideView: "사ì´ë“œë·°ë¡œ 열기" defaultNavigationBehaviour: "기본 íƒìƒ‰ ë™ìž‘" @@ -907,6 +922,7 @@ followersVisibility: "íŒ”ë¡œì›Œì˜ ê³µê°œ 범위" continueThread: "글타래 ë” ë³´ê¸°" deleteAccountConfirm: "ê³„ì •ì´ ì‚ì œë˜ê³ ë˜ëŒë¦´ 수 없게 ë©ë‹ˆë‹¤. 계ì†í•˜ì‹œê² 습니까? " incorrectPassword: "비밀번호가 올바르지 않습니다." +incorrectTotp: "OTP 번호가 í‹€ë ¸ê±°ë‚˜ ìœ íš¨ê¸°ê°„ì´ ë§Œë£Œë˜ì–´ ìžˆì„ ìˆ˜ 있습니다." voteConfirm: "\"{choice}\"ì— íˆ¬í‘œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" hide: "숨기기" useDrawerReactionPickerForMobile: "모바ì¼ì—ì„œ 드로어 메뉴로 표시" @@ -1071,6 +1087,7 @@ retryAllQueuesConfirmTitle: "지금 다시 ì‹œë„í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" retryAllQueuesConfirmText: "ì¼ì‹œì 으로 ì„œë²„ì˜ ë¶€í•˜ê°€ ì¦ê°€í• 수 있습니다." enableChartsForRemoteUser: "리모트 ìœ ì €ì˜ ì°¨íŠ¸ë¥¼ ìƒì„±" enableChartsForFederatedInstances: "리모트 ì„œë²„ì˜ ì°¨íŠ¸ë¥¼ ìƒì„±" +enableStatsForFederatedInstances: "리모트 서버 ì •ë³´ 받아오기" showClipButtonInNoteFooter: "노트 ë™ìž‘ì— í´ë¦½ì„ 추가" reactionsDisplaySize: "리액션 표시 í¬ê¸°" limitWidthOfReaction: "ë¦¬ì•¡ì…˜ì˜ ìµœëŒ€ íì„ ì œí•œí•˜ê³ ìž‘ê²Œ 표시하기" @@ -1106,6 +1123,8 @@ preservedUsernames: "예약한 ì‚¬ìš©ìž ì´ë¦„" preservedUsernamesDescription: "ì˜ˆì•½í• ì‚¬ìš©ìžëª…ì„ í•œ ì¤„ì— í•˜ë‚˜ì”© ìž…ë ¥í•©ë‹ˆë‹¤. 여기ì—ì„œ ì§€ì •í•œ 사용ìžëª…으로는 ê³„ì •ì„ ìƒì„±í• 수 없게 ë©ë‹ˆë‹¤. 단, ê´€ë¦¬ìž ê¶Œí•œìœ¼ë¡œ ê³„ì •ì„ ìƒì„±í• ë•Œì—는 해당ë˜ì§€ 않으며, ì´ë¯¸ 존재하는 ê³„ì •ë„ ì˜í–¥ì„ 받지 않습니다." createNoteFromTheFile: "ì´ íŒŒì¼ë¡œ 노트를 작성" archive: "ì•„ì¹´ì´ë¸Œ" +archived: "ì•„ì¹´ì´ë¸Œ ë¨" +unarchive: "ë³´ê´€ 취소" channelArchiveConfirmTitle: "{name} 채ë„ì„ ë³´ì¡´í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" channelArchiveConfirmDescription: "ë³´ì¡´í•œ 채ë„ì€ ì±„ë„ ëª©ë¡ê³¼ 검색 ê²°ê³¼ì— í‘œì‹œë˜ì§€ 않으며 새로운 ë…¸íŠ¸ë„ ìž‘ì„±í• ìˆ˜ 없습니다." thisChannelArchived: "ì´ ì±„ë„ì€ ë³´ì¡´ë˜ì—ˆìŠµë‹ˆë‹¤." @@ -1116,6 +1135,9 @@ preventAiLearning: "기계학습(ìƒì„±í˜• AI)ìœ¼ë¡œì˜ ì‚¬ìš©ì„ ê±°ë¶€" preventAiLearningDescription: "ì™¸ë¶€ì˜ ë¬¸ìž¥ ìƒì„± AI나 ì´ë¯¸ì§€ ìƒì„± AIì— ëŒ€í•´ ì œì¶œí•œ 노트나 ì´ë¯¸ì§€ ë“±ì˜ ì½˜í…ì¸ ë¥¼ í•™ìŠµì˜ ëŒ€ìƒìœ¼ë¡œ 사용하지 ì•Šë„ë¡ ìš”êµ¬í•©ë‹ˆë‹¤. 다만, ì´ ìš”êµ¬ì‚¬í•ì„ 지킬 ì˜ë¬´ëŠ” 없기 ë•Œë¬¸ì— í•™ìŠµì„ ì™„ì „ížˆ 방지하는 ê²ƒì€ ì•„ë‹™ë‹ˆë‹¤." options: "옵션" specifyUser: "ì‚¬ìš©ìž ì§€ì •" +lookupConfirm: "조회 í• ê¹Œìš”?" +openTagPageConfirm: "í•´ì‹œíƒœê·¸ì˜ íŽ˜ì´ì§€ë¥¼ 열까요?" +specifyHost: "호스트 ì§€ì •" failedToPreviewUrl: "미리 ë³¼ 수 ì—†ìŒ" update: "ì—…ë°ì´íŠ¸" rolesThatCanBeUsedThisEmojiAsReaction: "ì´ ì´ëª¨ì§€ë¥¼ 리액션으로 ì‚¬ìš©í• ìˆ˜ 있는 ì—í• " @@ -1249,6 +1271,35 @@ alwaysConfirmFollow: "íŒ”ë¡œìš°ì¼ ë•Œ í•ìƒ 확ì¸í•˜ê¸°" inquiry: "문ì˜í•˜ê¸°" tryAgain: "다시 ì‹œë„í•´ 주세요." confirmWhenRevealingSensitiveMedia: "민ê°í•œ 미디어를 ì—´ ë•Œ ë‘ ë²ˆ 확ì¸" +sensitiveMediaRevealConfirm: "민ê°í•œ 미디어입니다. í‘œì‹œí• ê¹Œìš”?" +createdLists: "ë§Œë“ ë¦¬ìŠ¤íŠ¸" +createdAntennas: "ë§Œë“ ì•ˆí…Œë‚˜" +fromX: "{x}부터" +genEmbedCode: "ìž„ë² ë””ë“œ 코드 만들기" +noteOfThisUser: "ì´ ìœ ì €ì˜ ë…¸íŠ¸ 목ë¡" +clipNoteLimitExceeded: "ë” ì´ìƒ ì´ í´ë¦½ì— 노트를 추가 í• ìˆ˜ 없습니다." +performance: "í¼í¬ë¨¼ìŠ¤" +modified: "변경 있ìŒ" +discard: "파기" +thereAreNChanges: "{n}ê±´ ë³€ê²½ì´ ìžˆìŠµë‹ˆë‹¤." +signinWithPasskey: "패스키로 로그ì¸" +unknownWebAuthnKey: "등ë¡ë˜ì§€ ì•Šì€ íŒ¨ìŠ¤í‚¤ìž…ë‹ˆë‹¤." +passkeyVerificationFailed: "패스키 ê²€ì¦ì„ 실패했습니다." +passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 ê²€ì¦í–ˆìœ¼ë‚˜, 비밀번호 ì—†ì´ ë¡œê·¸ì¸í•˜ê¸°ê°€ êº¼ì ¸ 있습니다." +messageToFollower: "íŒ”ë¡œì›Œì— ë³´ë‚¼ 메시지" +target: "대ìƒ" +testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>ì‹¤ì œ 환경ì—서는 사용하지 마세요.</strong>" +prohibitedWordsForNameOfUser: "금지 단어 (ì‚¬ìš©ìž ì´ë¦„)" +prohibitedWordsForNameOfUserDescription: "ì´ ëª©ë¡ì— í¬í•¨ë˜ëŠ” 키워드가 ì‚¬ìš©ìž ì´ë¦„ì— ìžˆëŠ” 경우, ì¼ë°˜ 사용ìžëŠ” ì´ë¦„ì„ ë°”ê¿€ 수 없습니다. 모ë”ë ˆì´í„° ê¶Œí•œì„ ê°€ì§„ 사용ìžëŠ” ì œí•œ 대ìƒì—ì„œ ì œì™¸ë©ë‹ˆë‹¤." +yourNameContainsProhibitedWords: "ë°”ê¾¸ë ¤ëŠ” ì´ë¦„ì— ê¸ˆì§€ëœ í‚¤ì›Œë“œê°€ í¬í•¨ë˜ì–´ 있습니다." +yourNameContainsProhibitedWordsDescription: "ì´ë¦„ì— ê¸ˆì§€ëœ í‚¤ì›Œë“œê°€ 있습니다. ì´ë¦„ì„ ì‚¬ìš©í•´ì•¼ 하는 경우, 서버 관리ìžì— 문ì˜í•˜ì„¸ìš”." +_abuseUserReport: + forward: "ì „ë‹¬" + forwardDescription: "ìµëª… 시스템 ê³„ì •ì„ ì‚¬ìš©í•˜ì—¬ 리모트 ì„œë²„ì— ì‹ ê³ ë‚´ìš©ì„ ì „ë‹¬í• ìˆ˜ 있습니다." + resolve: "í•´ê²°ë¨" + accept: "ì¸ìš©" + reject: "기ê°" + resolveTutorial: "ì ì ˆí•œ ì‹ ê³ ë‚´ìš©ì— ëŒ€ì‘í•œ 경우, \"ì¸ìš©\"ì„ ì„ íƒí•˜ì—¬ \"í•´ê²°ë¨\"으로 기ë¡í•©ë‹ˆë‹¤.\nì ì ˆí•˜ì§€ ì•Šì€ ì‹ ê³ ë¥¼ ë°›ì€ ê²½ìš°, \"기ê°\"ì„ ì„ íƒí•˜ì—¬ \"기ê°\"으로 기ë¡í•©ë‹ˆë‹¤." _delivery: status: "ì „ì†¡ ìƒíƒœ" stop: "ì •ì§€ë¨" @@ -1320,10 +1371,10 @@ _initialTutorial: _reaction: title: "'리액션'ì´ ë¬´ì—‡ì¸ê°€ìš”?" description: "ë…¸íŠ¸ì— '리액션'ì„ ë³´ë‚¼ 수 있습니다. '좋아요'만으로는 충분히 ì „í•´ì§€ì§€ 않는 ê°ì •ì„, ì´ëª¨ì§€ì— 실어서 ê°€ë³ê²Œ 보낼 수 있습니다." - letsTryReacting: "ë¦¬ì•¡ì…˜ì€ ë…¸íŠ¸ì˜ '{reaction}' ë²„íŠ¼ì„ í´ë¦í•˜ì—¬ ë¶™ì¼ ìˆ˜ 있습니다. 지금 표시ë˜ëŠ” 샘플 ë…¸íŠ¸ì— ë¦¬ì•¡ì…˜ì„ ë‹¬ì•„ 보세요!" + letsTryReacting: "ë¦¬ì•¡ì…˜ì€ ë…¸íŠ¸ì˜ '+' ë²„íŠ¼ì„ í´ë¦í•˜ì—¬ ë¶™ì¼ ìˆ˜ 있습니다. 지금 표시ë˜ëŠ” 샘플 ë…¸íŠ¸ì— ë¦¬ì•¡ì…˜ì„ ë‹¬ì•„ 보세요!" reactToContinue: "다ìŒìœ¼ë¡œ ì§„í–‰í•˜ë ¤ë©´ ë¦¬ì•¡ì…˜ì„ ë³´ë‚´ì„¸ìš”." reactNotification: "누군가가 ë‚˜ì˜ ë…¸íŠ¸ì— ë¦¬ì•¡ì…˜ì„ ë³´ë‚´ë©´ 실시간으로 ì•Œë¦¼ì„ ë°›ê²Œ ë©ë‹ˆë‹¤." - reactDone: "'{undo}' ë²„íŠ¼ì„ ëˆŒëŸ¬ì„œ ë¦¬ì•¡ì…˜ì„ ì·¨ì†Œí• ìˆ˜ 있습니다." + reactDone: "'-' ë²„íŠ¼ì„ ëˆŒëŸ¬ì„œ ë¦¬ì•¡ì…˜ì„ ì·¨ì†Œí• ìˆ˜ 있습니다." _timeline: title: "타임ë¼ì¸ì— 대하여" description1: "Misskeyì—는 ì¢…ë¥˜ì— ë”°ë¼ ì—¬ëŸ¬ ê°€ì§€ì˜ íƒ€ìž„ë¼ì¸ìœ¼ë¡œ 구성ë˜ì–´ 있습니다.(ì„œë²„ì— ë”°ë¼ì„œëŠ” ì¼ë¶€ 타임ë¼ì¸ì„ ì‚¬ìš©í• ìˆ˜ 없는 경우가 있습니다)" @@ -1383,8 +1434,10 @@ _serverSettings: fanoutTimelineDescription: "활성화하면 ê°ì¢… 타임ë¼ì¸ì„ ê°€ì ¸ì˜¬ ë•Œì˜ ì„±ëŠ¥ì„ ëŒ€í í–¥ìƒí•˜ë©°, ë°ì´í„°ë² ì´ìŠ¤ì˜ 부하를 ì¤„ì¼ ìˆ˜ 있습니다. 단, Redisì˜ ë©”ëª¨ë¦¬ ì‚¬ìš©ëŸ‰ì´ ì¦ê°€í•©ë‹ˆë‹¤. ì„œë²„ì˜ ë©”ëª¨ë¦¬ ìš©ëŸ‰ì´ ìž‘ê±°ë‚˜, 서비스가 ë¶ˆì•ˆì •í•´ì§€ëŠ” 경우 ë¹„í™œì„±í™”í• ìˆ˜ 있습니다." fanoutTimelineDbFallback: "ë°ì´í„°ë² ì´ìŠ¤ë¥¼ 예비로 사용하기" fanoutTimelineDbFallbackDescription: "활성화하면 타임ë¼ì¸ì˜ ìºì‹œë˜ì–´ 있지 ì•Šì€ ë¶€ë¶„ì— ëŒ€í•´ DBì— ì§ˆì˜í•˜ì—¬ ì •ë³´ë¥¼ ê°€ì ¸ì˜µë‹ˆë‹¤. 비활성화하면 ì´ë¥¼ 실행하지 ì•ŠìŒìœ¼ë¡œì¨ ì„œë²„ì˜ ë¶€í•˜ë¥¼ ì¤„ì¼ ìˆ˜ 있지만, 타임ë¼ì¸ì—ì„œ ê°€ì ¸ì˜¬ 수 있는 게시물 범위가 í•œì •ë©ë‹ˆë‹¤." + reactionsBufferingDescription: "활성화 í•œ 경우, 리액션 작성 í¼í¬ë¨¼ìŠ¤ê°€ 대í í–¥ìƒë˜ì–´ DBì˜ ë¶€í•˜ë¥¼ ì¤„ì¼ ìˆ˜ 있으나, Redisì˜ ë©”ëª¨ë¦¬ ì‚¬ìš©ëŸ‰ì´ ë§Žì•„ì§‘ë‹ˆë‹¤." inquiryUrl: "문ì˜ì²˜ URL" inquiryUrlDescription: "서버 ìš´ì˜ìžì—게 보내는 ë¬¸ì˜ ì–‘ì‹ì˜ URLì´ë‚˜ ìš´ì˜ìžì˜ ì—°ë½ì²˜ ë“±ì´ ì 힌 웹 페ì´ì§€ì˜ URLì„ ì„¤ì •í•©ë‹ˆë‹¤." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "ì¼ì • 기간ë™ì•ˆ 모ë”ë ˆì´í„°ì˜ 활ë™ì´ ê°ì§€ë˜ì§€ 않는 경우, 스팸 방지를 위해 ì´ ì„¤ì •ì€ ìžë™ìœ¼ë¡œ 꺼집니다." _accountMigration: moveFrom: "다른 ê³„ì •ì—ì„œ ì´ ê³„ì •ìœ¼ë¡œ ì´ì‚¬" moveFromSub: "다른 ê³„ì •ì— ëŒ€í•œ 별ì¹ì„ ìƒì„±" @@ -1716,10 +1769,15 @@ _role: canSearchNotes: "노트 검색 ì´ìš© 가능 여부" canUseTranslator: "ë²ˆì— ê¸°ëŠ¥ì˜ ì‚¬ìš©" avatarDecorationLimit: "아바타 장ì‹ì˜ 최대 붙임 개수" + canImportAntennas: "안테나 ê°€ì ¸ì˜¤ê¸° 허용" + canImportBlocking: "차단 ëª©ë¡ ê°€ì ¸ì˜¤ê¸° 허용" + canImportFollowing: "팔로우 ê°€ì ¸ì˜¤ê¸° 허용" + canImportMuting: "뮤트 ëª©ë¡ ê°€ì ¸ì˜¤ê¸° 허용" + canImportUserLists: "리스트 ëª©ë¡ ê°€ì ¸ì˜¤ê¸° 허용" _condition: roleAssignedTo: "ìˆ˜ë™ ì—í• ì— ì´ë¯¸ í• ë‹¹ë¨" isLocal: "로컬 사용ìž" - isRemote: "리모트 사용ìž" + isRemote: "ì›ê²© 사용ìž" isCat: "ê³ ì–‘ì´ ì‚¬ìš©ìž" isBot: "ë´‡ 사용ìž" isSuspended: "ì •ì§€ëœ ì‚¬ìš©ìž" @@ -1933,7 +1991,6 @@ _theme: buttonBg: "버튼 ë°°ê²½" buttonHoverBg: "버튼 ë°°ê²½ (호버)" inputBorder: "ìž…ë ¥ í•„ë“œ í…Œë‘리" - listItemHoverBg: "리스트 í•ëª© ë°°ê²½ (호버)" driveFolderBg: "ë“œë¼ì´ë¸Œ í´ë” ë°°ê²½" wallpaperOverlay: "배경화면 ì˜¤ë²„ë ˆì´" badge: "배지" @@ -1949,10 +2006,11 @@ _sfx: _soundSettings: driveFile: "ë“œë¼ì´ë¸Œì— 있는 오디오를 사용" driveFileWarn: "ë“œë¼ì´ë¸Œì— 있는 파ì¼ì„ ì„ íƒí•˜ì„¸ìš”." - driveFileTypeWarn: "ì´ íŒŒì¼ì€ 지ì›ë˜ì§€ 않습니다." + driveFileTypeWarn: "ì´ íŒŒì´" driveFileTypeWarnDescription: "오디오 파ì¼ì„ ì„ íƒí•˜ì„¸ìš”." driveFileDurationWarn: "오디오가 너무 ê¹ë‹ˆë‹¤" driveFileDurationWarnDescription: "긴 오디오로 ì„¤ì •í• ê²½ìš° 미스키 ì‚¬ìš©ì— ì§€ìž¥ì´ ê°ˆ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. ê·¸ëž˜ë„ ê´œì°®ìŠµë‹ˆê¹Œ?" + driveFileError: "오디오를 불러올 수 없습니다. ì„¤ì •ì„ ë°”ê¿”ì£¼ì„¸ìš”." _ago: future: "미래" justNow: "방금 ì „" @@ -2209,6 +2267,9 @@ _profile: changeBanner: "배너 ì´ë¯¸ì§€ 변경" verifiedLinkDescription: "ë‚´ìš©ì— ìžì‹ ì˜ í”„ë¡œí•„ë¡œ 향하는 ë§í¬ê°€ í¬í•¨ëœ 페ì´ì§€ì˜ URLì„ ì‚½ìž…í•˜ë©´ ì†Œìœ ìž ì¸ì¦ 마í¬ê°€ 표시ë©ë‹ˆë‹¤." avatarDecorationMax: "최대 {max}개까지 장ì‹ì„ í• ìˆ˜ 있습니다." + followedMessage: "팔로우 ë°›ì•˜ì„ ë•Œ 메시지" + followedMessageDescription: "팔로우 ë°›ì•˜ì„ ë•Œ ìƒëŒ€ë°©ì—게 보여줄 단문 메시지를 ì„¤ì •í• ìˆ˜ 있습니다." + followedMessageDescriptionForLockedAccount: "팔로우를 승ì¸ì œë¡œ í•œ 경우, 팔로우 ìš”ì²ì„ 수ë½í–ˆì„ ë•Œ ë³´ì—¬ì¤ë‹ˆë‹¤." _exportOrImport: allNotes: "ëª¨ë“ ë…¸íŠ¸" favoritedNotes: "ì¦ê²¨ì°¾ê¸°í•œ 노트" @@ -2301,6 +2362,7 @@ _pages: eyeCatchingImageSet: "ì•„ì´ìºì¹˜ ì´ë¯¸ì§€ë¥¼ ì„¤ì •" eyeCatchingImageRemove: "ì•„ì´ìºì¹˜ ì´ë¯¸ì§€ë¥¼ ì‚ì œ" chooseBlock: "ë¸”ë¡ ì¶”ê°€" + enterSectionTitle: "섹션 타ì´í‹€ì„ ìž…ë ¥í•˜ê¸°" selectType: "종류 ì„ íƒ" contentBlocks: "콘í…ì¸ " inputBlocks: "ìž…ë ¥" @@ -2346,6 +2408,8 @@ _notification: renotedBySomeUsers: "{n}ëª…ì´ ë¦¬ë…¸íŠ¸í–ˆìŠµë‹ˆë‹¤" followedBySomeUsers: "{n}명ì—게 팔로우ë¨" flushNotification: "알림 ì´ë ¥ì„ 초기화" + exportOfXCompleted: "{x} ì¶”ì¶œì— ì„±ê³µí–ˆìŠµë‹ˆë‹¤." + login: "ë¡œê·¸ì¸ ì•Œë¦¼ì´ ìžˆìŠµë‹ˆë‹¤" _types: all: "ì „ë¶€" note: "사용ìžì˜ 새 글" @@ -2360,6 +2424,9 @@ _notification: followRequestAccepted: "팔로우 ìš”ì²ì´ 승ì¸ë˜ì—ˆì„ ë•Œ" roleAssigned: "ì—í• ì´ ë¶€ì—¬ ë¨" achievementEarned: "ë„ì „ ê³¼ì œ íšë“" + exportCompleted: "ì¶”ì¶œì„ ì„±ê³µí•¨" + login: "로그ì¸" + test: "알림 테스트" app: "ì—°ë™ëœ ì•±ì„ í†µí•œ 알림" _actions: followBack: "팔로우" @@ -2411,6 +2478,7 @@ _webhookSettings: modifyWebhook: "Webhook ìˆ˜ì •" name: "ì´ë¦„" secret: "ì‹œí¬ë¦¿" + trigger: "트리거" active: "활성화" _events: follow: "누군가를 íŒ”ë¡œìš°í–ˆì„ ë•Œ" @@ -2421,15 +2489,18 @@ _webhookSettings: reaction: "누군가 ë‚´ ë…¸íŠ¸ì— ë¦¬ì•¡ì…˜í–ˆì„ ë•Œ" mention: "누군가 나를 ë©˜ì…˜í–ˆì„ ë•Œ" _systemEvents: - abuseReport: "ìœ ì €ë¡œë¶€í„° ì‹ ê³ ë¥¼ ë°›ì•˜ì„ ë•Œ" + abuseReport: "ìœ ì €ë¡" abuseReportResolved: "ë°›ì€ ì‹ ê³ ë¥¼ ì²˜ë¦¬í–ˆì„ ë•Œ" userCreated: "ìœ ì €ê°€ ìƒì„±ë˜ì—ˆì„ ë•Œ" + inactiveModeratorsWarning: "모ë”ë ˆì´í„°ê°€ ì¼ì • 기간ë™ì•ˆ 활ë™í•˜ì§€ ì•Šì€ ê²½ìš°" + inactiveModeratorsInvitationOnlyChanged: "모ë”ë ˆì´í„°ê°€ ì¼ì • 기간 활ë™í•˜ì§€ ì•Šì•„ ì‹œìŠ¤í…œì— ì˜í•´ ì´ˆëŒ€ì œë¡œ ë°”ë€ ê²½ìš°" deleteConfirm: "Webhookì„ ì‚ì œí• ê¹Œìš”?" + testRemarks: "스위치 ì˜¤ë¥¸ìª½ì— ìžˆëŠ” ë²„íŠ¼ì„ í´ë¦í•˜ì—¬ ë”미 ë°ì´í„°ë¥¼ 사용한 테스트용 웹 í›…ì„ ë³´ë‚¼ 수 있습니다." _abuseReport: _notificationRecipient: createRecipient: "ì‹ ê³ ìˆ˜ì‹ ìž ì¶”ê°€" modifyRecipient: "ì‹ ê³ ìˆ˜ì‹ ìž íŽ¸ì§‘" - recipientType: "알림 ìˆ˜ì‹ ìœ í˜•" + recipientType: "알림 종류" _recipientType: mail: "ì´ë©”ì¼" webhook: "Webhook" @@ -2437,7 +2508,7 @@ _abuseReport: mail: "모ë”ë ˆì´í„° ê¶Œí•œì„ ê°€ì§„ 사용ìžì˜ ì´ë©”ì¼ ì£¼ì†Œì— ì•Œë¦¼ì„ ë³´ëƒ…ë‹ˆë‹¤ (ì‹ ê³ ë¥¼ ë°›ì€ ë•Œì—만)" webhook: "ì§€ì •í•œ SystemWebhookì— ì•Œë¦¼ì„ ë³´ëƒ…ë‹ˆë‹¤ (ì‹ ê³ ë¥¼ ë°›ì€ ë•Œì™€ í•´ê²°í–ˆì„ ë•Œì— ì†¡ì‹ )" keywords: "키워드" - notifiedUser: "ì‹ ê³ ì•Œë¦¼ì„ ë³´ë‚¼ ìœ ì €" + notifiedUser: "알릴 사용ìž" notifiedWebhook: "ì‚¬ìš©í• Webhook" deleteConfirm: "ìˆ˜ì‹ ìžë¥¼ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" _moderationLogTypes: @@ -2468,6 +2539,8 @@ _moderationLogTypes: markSensitiveDriveFile: "파ì¼ì— 열람주ì˜ë¥¼ ì„¤ì •" unmarkSensitiveDriveFile: "파ì¼ì— 열람주ì˜ë¥¼ í•´ì œ" resolveAbuseReport: "ì‹ ê³ ì²˜ë¦¬" + forwardAbuseReport: "ì‹ ê³ ì „ë‹¬" + updateAbuseReportNote: "ì‹ ê³ ì¡°ì • 노트 ê°±ì‹ " createInvitation: "초대 코드 ìƒì„±" createAd: "ê´‘ê³ ìƒì„±" deleteAd: "ê´‘ê³ ì‚ì œ" @@ -2483,6 +2556,10 @@ _moderationLogTypes: createAbuseReportNotificationRecipient: "ì‹ ê³ ì•Œë¦¼ ìˆ˜ì‹ ìž ìƒì„±" updateAbuseReportNotificationRecipient: "ì‹ ê³ ì•Œë¦¼ ìˆ˜ì‹ ìž íŽ¸ì§‘" deleteAbuseReportNotificationRecipient: "ì‹ ê³ ì•Œë¦¼ ìˆ˜ì‹ ìž ì‚ì œ" + deleteAccount: "ê³„ì •ì„ ì‚ì œ" + deletePage: "페ì´ì§€ë¥¼ ì‚ì œ" + deleteFlash: "Play를 ì‚ì œ" + deleteGalleryPost: "갤러리 í¬ìŠ¤íŠ¸ë¥¼ ì‚ì œ" _fileViewer: title: "íŒŒì¼ ìƒì„¸" type: "íŒŒì¼ ìœ í˜•" @@ -2603,7 +2680,7 @@ _urlPreviewSetting: timeoutDescription: "미리보기를 ë¡œë”©í•˜ëŠ”ë° ê±¸ë¦¬ëŠ” ì‹œê°„ì´ ì •í•œ 시간보다 오래 걸리는 경우, 미리보기를 ìƒì„±í•˜ì§€ 않습니다." maximumContentLength: "Content-Lengthì˜ ìµœëŒ€ì¹˜ (byte)" maximumContentLengthDescription: "Content-Lengthê°€ ì´ ê°’ì„ ë„˜ì–´ì„œë©´ 미리보기를 ìƒì„±í•˜ì§€ 않습니다." - requireContentLength: "Content-Length를 ì–»ì—ˆì„ ë•Œë§Œ 미리보기 만들기" + requireContentLength: "Content-Length를 받아온 경우ì—만 " requireContentLengthDescription: "ìƒëŒ€ 서버가 Content-Length를 ë˜ëŒë ¤ì£¼ì§€ 않는다면 미리보기를 만들지 않습니다." userAgent: "User-Agent" userAgentDescription: "미리보기를 ì–»ì„ ë•Œ 사용한 User-Agent를 ì„¤ì •í•©ë‹ˆë‹¤. 비어 있다면 ê¸°ë³¸ê°’ì˜ User-Agent를 사용합니다." @@ -2614,3 +2691,22 @@ _mediaControls: pip: "화면 ì† í™”ë©´" playbackRate: "ìž¬ìƒ ì†ë„" loop: "반복 재ìƒ" +_contextMenu: + title: "컨í…스트 메뉴" + app: "ì• í”Œë¦¬ì¼€ì´ì…˜" + appWithShift: "Shift 키로 ì• í”Œë¦¬ì¼€ì´ì…˜" + native: "브ë¼ìš°ì €ì˜ UI" +_embedCodeGen: + title: "ìž„ë² ë””ë“œ 코드를 커스터마ì´ì¦ˆ" + header: "í•´ë”를 표시" + autoload: "ìžë™ìœ¼ë¡œ ë‹¤ìŒ ì½”ë“œë¥¼ 실행 (비권장)" + maxHeight: "최대 높ì´" + maxHeightDescription: "최대 ê°’ì„ ë¬´ì‹œí•˜ë ¤ë©´ 0ì„ ìž…ë ¥í•˜ì„¸ìš”. ìœ„ì ¯ì´ ìƒí•˜ë¡œ 길어지는 ê²ƒì„ ë°©ì§€í•˜ë ¤ë©´, ìž„ì˜ì˜ ê°’ì„ ìž…ë ¥í•´ 주세요." + maxHeightWarn: "ë†’ì´ ìµœëŒ€ ê°’ì´ ì„¤ì •ë˜ì–´ì ¸ 있지 않습니다(0). ì˜ë„ì 으로 ì„¤ì • 하지 않았다면 ìž„ì˜ì˜ ê°’ì„ ì„¤ì •í•´ì£¼ì„¸ìš”." + previewIsNotActual: "미리보기로 í‘œì‹œí• ìˆ˜ 있는 í¬ê¸°ë³´ë‹¤ í½ë‹ˆë‹¤. ì‹¤ì œë¡œ ë„£ì€ ì½”ë“œì˜ í‘œì‹œê°€ 다른 경우가 있습니다." + rounded: "ì™¸ê³½ì„ ì„ ë‘¥ê¸€ê²Œ 하기" + border: "ì™¸ê³½ì„ ì— í…Œë‘리를 씌우기" + applyToPreview: "ë¯¸ë¦¬ë³´ê¸°ì— ë°˜ì˜" + generateCode: "ìž„ë² ë””ë“œ 코드를 만들기" + codeGenerated: "코드를 만들었습니다." + codeGeneratedDescription: "만들어진 코드를 웹 사ì´íŠ¸ì— 붙여서 사용하세요." diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 1bead5635dfa1500ac0dadc1063a6d25fc59c5a2..b100d0300f269bcafbc5b0d4ada334b8245cc73f 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -456,6 +456,7 @@ _notification: renote: "Renote" quote: "àºà»‰àº²àº‡àºàºµàº‡" reaction: "Reaction" + login: "ເຂົ້າ​ສູ່​ລະ​ບົບ" _actions: reply: "ຕàºàºšâ€‹àºàº±àºš" renote: "Renote" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 686d532c4c30ee24ae129dced23ce126fb1e23f9..dde3035357dd686e5d3ad6999f1132b8788168ba 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -251,7 +251,7 @@ upload: "Uploaden" keepOriginalUploading: "Origineel beeld behouden." keepOriginalUploadingDescription: "Bewaar de originele versie bij het uploaden van afbeeldingen. Indien uitgeschakeld, wordt bij het uploaden een alternatieve versie voor webpublicatie genereert." fromDrive: "Van schijf" -fromUrl: "Van URL" +fromUrl: "Van URL" uploadFromUrl: "Uploaden vanaf een URL" uploadFromUrlDescription: "URL van het bestand dat je wil uploaden" uploadFromUrlRequested: "Uploadverzoek" @@ -427,7 +427,7 @@ windowMaximize: "Maximaliseren" windowRestore: "Herstellen" loggedInAsBot: "Momenteel als bot ingelogd" icon: "Avatar" -replies: "Antwoorden" +replies: "Antwoord" renotes: "Herdelen" _delivery: stop: "Opgeschort" @@ -486,6 +486,7 @@ _notification: renote: "Herdelen" quote: "Quote" reaction: "Reacties" + login: "Inloggen" _actions: reply: "Antwoord" renote: "Herdelen" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index cd00ecf9abe6eaa494d8fd7bb4d188acf6321fda..c5f61db7450694108098ad5ac8021613e3e68aed 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -701,6 +701,7 @@ _notification: renote: "Renotes" quote: "Sitater" reaction: "Reaksjoner" + login: "Logg inn" _actions: reply: "Svar" renote: "Renote" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index b20eabf7f22e7d346ce159405cebff6d32d81320..d7afd577607e0843a7994041a74dcecbac2c09d6 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -81,7 +81,7 @@ exportRequested: "ZażądaÅ‚eÅ› eksportu. Może to zająć trochÄ™ czasu. Po zak importRequested: "Zażądano importu. Może to zająć chwilÄ™." lists: "Listy" noLists: "Nie masz żadnych list" -note: "Wpis" +note: "Utwórz wpis" notes: "Wpisy" following: "Obserwowani" followers: "ObserwujÄ…cy" @@ -492,7 +492,6 @@ uiLanguage: "JÄ™zyk wyÅ›wietlania UI" aboutX: "O {x}" emojiStyle: "Styl emoji" native: "Natywny" -disableDrawer: "Nie używaj menu w stylu szuflady" showNoteActionsOnlyHover: "Pokazuj akcje notatek tylko po najechaniu myszkÄ…" showReactionsCount: "WyÅ›wietl liczbÄ™ reakcji na notatkÄ™" noHistory: "Brak historii" @@ -690,10 +689,7 @@ abuseReported: "Twoje zgÅ‚oszenie zostaÅ‚o wysÅ‚ane. DziÄ™kujemy." reporter: "ZgÅ‚aszajÄ…cy" reporteeOrigin: "Pochodzenie zgÅ‚oszonego" reporterOrigin: "Pochodzenie zgÅ‚aszajÄ…cego" -forwardReport: "Przekaż zgÅ‚oszenie do innej instancji" -forwardReportIsAnonymous: "Zamiast twojego konta, anonimowe konto systemowe bÄ™dzie wyÅ›wietlone jako zgÅ‚aszajÄ…cy na instancji zdalnej." send: "WyÅ›lij" -abuseMarkAsResolved: "Oznacz zgÅ‚oszenie jako rozwiÄ…zane" openInNewTab: "Otwórz w nowej karcie" openInSideView: "Otwórz w bocznym widoku" defaultNavigationBehaviour: "DomyÅ›lne zachowanie nawigacji" @@ -865,7 +861,7 @@ whatIsNew: "Pokaż zmiany" translate: "PrzetÅ‚umacz" translatedFrom: "PrzetÅ‚umaczone z {x}" accountDeletionInProgress: "Trwa usuwanie konta" -usernameInfo: "Nazwa, która identyfikuje Twoje konto spoÅ›ród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreÅ›lników (_). Nazwy użytkownika nie mogÄ… być później zmieniane." +usernameInfo: "Nazwa, która identyfikuje Twoje konto spoÅ›ród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreÅ›lników (_). Nazwy użytkownika nie mogÄ… być później zmieniane." aiChanMode: "Tryb Ai" devMode: "Tryb programisty" keepCw: "Zostaw ostrzeżenia o zawartoÅ›ci" @@ -1016,8 +1012,8 @@ emailNotSupported: "WysyÅ‚anie wiadomoÅ›ci E-mail nie jest obsÅ‚ugiwane na tym s postToTheChannel: "Publikuj na kanale" youFollowing: "Åšledzeni" icon: "Awatar" -replies: "Odpowiedzi" -renotes: "UdostÄ™pnieÅ„" +replies: "Odpowiedz" +renotes: "UdostÄ™pnij" sourceCode: "Kod źródÅ‚owy" flip: "Odwróć" lastNDays: "W ciÄ…gu ostatnich {n} dni" @@ -1209,7 +1205,6 @@ _theme: buttonBg: "TÅ‚o przycisku" buttonHoverBg: "TÅ‚o przycisku (po najechaniu)" inputBorder: "Obramowanie pola wejÅ›cia" - listItemHoverBg: "TÅ‚o elementu listy (po najechaniu)" driveFolderBg: "TÅ‚o folderu na dysku" wallpaperOverlay: "NakÅ‚adka tapety" badge: "Odznaka" @@ -1510,6 +1505,7 @@ _notification: reaction: "Reakcja" receiveFollowRequest: "Otrzymano proÅ›bÄ™ o możliwość obserwacji" followRequestAccepted: "PrzyjÄ™to proÅ›bÄ™ o możliwość obserwacji" + login: "Zaloguj siÄ™" app: "Powiadomienia z aplikacji" _actions: followBack: "zaobserwowaÅ‚ ciÄ™ z powrotem" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 71ba7c4371a9bbb9c724188a6f29ab6168b63809..9039fd21414bd1c7144a14edb7c6a556d947c537 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -82,7 +82,7 @@ exportRequested: "A sua solicitação de exportação foi enviada. Isso pode lev importRequested: "A sua solicitação de importação foi enviada. Isso pode levar algum tempo." lists: "Listas" noLists: "Não possui nenhuma lista" -note: "Post" +note: "Publicar" notes: "Posts" following: "Seguindo" followers: "Seguidores" @@ -296,7 +296,7 @@ explore: "Explorar" messageRead: "Lida" noMoreHistory: "Não existe histórico anterior" startMessaging: "Iniciar conversação" -nUsersRead: "{n} Pessoas leem" +nUsersRead: "{n} pessoas leram" agreeTo: "Eu concordo com {0}" agree: "Concordar" agreeBelow: "Eu concordo com o seguinte" @@ -312,7 +312,7 @@ birthday: "Aniversário" yearsOld: "{age} anos" registeredDate: "Data de registro" location: "Localização" -theme: "tema" +theme: "Tema" themeForLightMode: "Temas usados ​​no modo de luz" themeForDarkMode: "Temas usados ​​no modo escuro" light: "Claro" @@ -509,7 +509,6 @@ uiLanguage: "Idioma de exibição da interface " aboutX: "Sobre {x}" emojiStyle: "Estilo de emojis" native: "Nativo" -disableDrawer: "Não mostrar o menu em formato de gaveta" showNoteActionsOnlyHover: "Exibir as ações da nota somente ao passar o cursor sobre ela" showReactionsCount: "Ver o número de reações nas notas" noHistory: "Ainda não há histórico" @@ -708,10 +707,7 @@ abuseReported: "Denúncia enviada. Obrigado por sua ajuda." reporter: "Denunciante" reporteeOrigin: "Origem da denúncia" reporterOrigin: "Origem do denunciante" -forwardReport: "Encaminhar a denúncia para o servidor remoto" -forwardReportIsAnonymous: "No servidor remoto, suas informações não serão visÃveis, e você será apresentado como uma conta do sistema anônima." send: "Enviar" -abuseMarkAsResolved: "Marcar denúncia como resolvida" openInNewTab: "Abrir em nova aba" openInSideView: "Abrir em visão lateral" defaultNavigationBehaviour: "Navegação padrão" @@ -1062,7 +1058,7 @@ resetPasswordConfirm: "Deseja realmente mudar a sua senha?" sensitiveWords: "Palavras sensÃveis" sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"InÃcio\" automaticamente. Você pode listar várias delas separando-as por linha." sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)" -prohibitedWords: "Palavras proibÃdas" +prohibitedWords: "Palavras proibidas" prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha." prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)" hiddenTags: "Hashtags escondidas" @@ -1420,7 +1416,7 @@ _achievements: _types: _notes1: title: "Configurando o meu misskey" - description: "Post uma nota pela primeira vez" + description: "Poste uma nota pela primeira vez" flavor: "Divirta-se com o Misskey!" _notes10: title: "Algumas notas" @@ -1948,7 +1944,6 @@ _theme: buttonBg: "Plano de fundo de botão" buttonHoverBg: "Plano de fundo de botão (Selecionado)" inputBorder: "Borda de campo digitável" - listItemHoverBg: "Plano de fundo do item de uma lista (Selecionado)" driveFolderBg: "Plano de fundo da pasta no Drive" wallpaperOverlay: "Sobreposição do papel de parede." badge: "Emblema" @@ -2377,6 +2372,7 @@ _notification: followRequestAccepted: "Aceitou pedidos de seguidor" roleAssigned: "Cargo dado" achievementEarned: "Conquista desbloqueada" + login: "Iniciar sessão" app: "Notificações de aplicativos conectados" _actions: followBack: "te seguiu de volta" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 95c1e1650827a7bc6b0e76024c94a703b123206c..3cc09aa5c2d67568f59442b4e745ac3ef0826686 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -453,7 +453,6 @@ or: "Sau" language: "Limbă" uiLanguage: "Limba interfeÈ›ei" aboutX: "Despre {x}" -disableDrawer: "Nu folosi meniuri în stil sertar" noHistory: "Nu există istoric" signinHistory: "Istoric autentificări" doing: "Se procesează..." @@ -626,10 +625,7 @@ abuseReported: "Raportul tău a fost trimis. MulÈ›umim." reporter: "Raportorul" reporteeOrigin: "Originea raportatului" reporterOrigin: "Originea raportorului" -forwardReport: "RedirecÈ›ionează raportul către instanÈ›a externă" -forwardReportIsAnonymous: "ÃŽn locul contului tău, va fi afiÈ™at un cont anonim, de sistem, ca raportor către instanÈ›a externă." send: "Trimite" -abuseMarkAsResolved: "Marchează raportul ca rezolvat" openInNewTab: "Deschide în tab nou" openInSideView: "Deschide în vedere laterală" defaultNavigationBehaviour: "Comportament de navigare implicit" @@ -649,7 +645,7 @@ searchByGoogle: "Caută" file: "FiÈ™iere" show: "Arată" icon: "Avatar" -replies: "Răspunsuri" +replies: "Răspunde" renotes: "Re-notează" _delivery: stop: "Suspendat" @@ -715,6 +711,7 @@ _notification: renote: "Re-notează" quote: "Citează" reaction: "ReacÈ›ie" + login: "Autentifică-te" _actions: reply: "Răspunde" renote: "Re-notează" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 88f59155d656da1e3f2fb7e24a9aa8ebac14e79a..70178ec2fd63a1cc5c0cc26257aff77959cdfcb3 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -2,7 +2,7 @@ _lang_: "РуÑÑкий" headlineMisskey: "Сеть, ÑÐ¿Ð»ÐµÑ‚Ñ‘Ð½Ð½Ð°Ñ Ð¸Ð· заметок" introMisskey: "Добро пожаловать! Misskey — Ñто децентрализованный ÑÐµÑ€Ð²Ð¸Ñ Ð¼Ð¸ÐºÑ€Ð¾Ð±Ð»Ð¾Ð³Ð¾Ð² Ñ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ñ‹Ð¼ иÑходным кодом.\nПишите «заметки» — делитеÑÑŒ Ñо вÑеми проиÑходÑщим вокруг или раÑÑказывайте о Ñебе 📡\nСтавьте «реакции» — выражайте Ñвои чувÑтва и Ñмоции от заметок других ðŸ‘\nОткройте Ð´Ð»Ñ ÑÐµÐ±Ñ Ð½Ð¾Ð²Ñ‹Ð¹ мир 🚀" -poweredByMisskeyDescription: "{name} – ÑÐµÑ€Ð²Ð¸Ñ Ð½Ð° платформе Ñ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ñ‹Ð¼ иÑходным кодом <b>Misskey</b>, называемый инÑтанÑом Misskey." +poweredByMisskeyDescription: "{name} – ÑÐµÑ€Ð²Ð¸Ñ Ð½Ð° платформе Ñ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ñ‹Ð¼ иÑходным кодом <b>Misskey</b>, называемый ÑкземплÑром Misskey." monthAndDay: "{day}.{month}" search: "ПоиÑк" notifications: "УведомлениÑ" @@ -10,15 +10,15 @@ username: "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ" password: "Пароль" forgotPassword: "Забыли пароль?" fetchingAsApObject: "Приём Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… Ñайтов" -ok: "Окей" +ok: "Подтвердить" gotIt: "ЯÑно!" cancel: "Отмена" noThankYou: "Ðет, ÑпаÑибо" enterUsername: "Введите Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ" -renotedBy: "{user} делитÑÑ" +renotedBy: "{user} репоÑтнул(а)" noNotes: "Ðет ни одной заметки" noNotifications: "Ðет уведомлений" -instance: "ИнÑтанÑ" +instance: "ÐкземплÑÑ€" settings: "ÐаÑтройки" notificationSettings: "ÐаÑтройки уведомлений" basicSettings: "ОÑновные наÑтройки" @@ -45,22 +45,24 @@ pin: "Закрепить в профиле" unpin: "Открепить от профилÑ" copyContent: "Скопировать Ñодержимое" copyLink: "Скопировать ÑÑылку" +copyLinkRenote: "Скопировать ÑÑылку на репоÑÑ‚" delete: "Удалить" deleteAndEdit: "Удалить и отредактировать" -deleteAndEditConfirm: "Удалить Ñту заметку и Ñоздать отредактированную? Ð’Ñе реакции, ÑÑылки и ответы на ÑущеÑтвующую будут будут потерÑны." +deleteAndEditConfirm: "Удалить Ñтот поÑÑ‚ и отредактировать заново? Ð’Ñе реакции, репоÑÑ‚Ñ‹ и ответы на него также будут удалены." addToList: "Добавить в ÑпиÑок" addToAntenna: "Добавить к антенне" sendMessage: "Отправить Ñообщение" copyRSS: "Скопировать RSS" copyUsername: "Скопировать Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ" -copyUserId: "Скопировать идентификатор пользователÑ" -copyNoteId: "Скопировать идентификатор заметки" +copyUserId: "Скопировать ID пользователÑ" +copyNoteId: "Скопировать ID поÑта" copyFileId: "Скопировать ID файла" copyFolderId: "Скопировать ID папки" -copyProfileUrl: "Скопировать URL Ð¿Ñ€Ð¾Ñ„Ð¸Ð»Ñ " +copyProfileUrl: "Скопировать ÑÑылку на профиль" searchUser: "ПоиÑк людей" +searchThisUsersNotes: "ИÑкать по заметкам пользователÑ" reply: "Ответ" -loadMore: "Показать еще" +loadMore: "Загрузить ещё" showMore: "Показать ещё" showLess: "Закрыть" youGotNewFollower: "Ðовый подпиÑчик" @@ -107,11 +109,14 @@ enterEmoji: "Введите Ñмодзи" renote: "РепоÑÑ‚" unrenote: "Отмена репоÑта" renoted: "РепоÑÑ‚ Ñовершён." +renotedToX: "РепоÑтнуть в {name}." cantRenote: "Ðто Ð½ÐµÐ»ÑŒÐ·Ñ Ñ€ÐµÐ¿Ð¾Ñтить." cantReRenote: "Ðевозможно репоÑтить репоÑÑ‚." quote: "Цитата" inChannelRenote: "Ð’ канале" inChannelQuote: "Заметки в канале" +renoteToChannel: "РепоÑтнуть в канал" +renoteToOtherChannel: "РепоÑтнуть в другой канал" pinnedNote: "Ð—Ð°ÐºÑ€ÐµÐ¿Ð»Ñ‘Ð½Ð½Ð°Ñ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ°" pinned: "Закрепить в профиле" you: "Ð’Ñ‹" @@ -150,6 +155,7 @@ editList: "Редактировать ÑпиÑок" selectChannel: "Выберите канал" selectAntenna: "Выберите антенну" editAntenna: "Редактировать антенну" +createAntenna: "Создать антенну" selectWidget: "Выберите виджет" editWidgets: "Редактировать виджеты" editWidgetsExit: "Готово" @@ -157,11 +163,12 @@ customEmojis: "СобÑтвенные Ñмодзи" emoji: "Ðмодзи" emojis: "Ðмодзи" emojiName: "Ðазвание Ñмодзи" -emojiUrl: "URL Ñмодзи" +emojiUrl: "СÑылка на Ñмодзи" addEmoji: "Добавить Ñмодзи" settingGuide: "Рекомендуемые наÑтройки" cacheRemoteFiles: "Кешировать внешние файлы" cacheRemoteFilesDescription: "Когда Ñта наÑтройка отключена, файлы Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… Ñайтов будут загружатьÑÑ Ð¿Ñ€Ñмо оттуда. Ðто ÑÑкономит меÑто на Ñервере, но увеличит трафик, так как не будут ÑоздаватьÑÑ ÑÑкизы." +youCanCleanRemoteFilesCache: "Ð’Ñ‹ можете очиÑтить кÑш, нажав на кнопку ðŸ—‘ï¸ Ð² меню ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñ„Ð°Ð¹Ð»Ð°Ð¼Ð¸." cacheRemoteSensitiveFiles: "КÑшировать внешние файлы «не Ð´Ð»Ñ Ð²Ñех»" cacheRemoteSensitiveFilesDescription: "ЕÑли отключено, файлы «не Ð´Ð»Ñ Ð²Ñех» загружаютÑÑ Ð½ÐµÐ¿Ð¾ÑредÑтвенно Ñ ÑƒÐ´Ð°Ð»Ñ‘Ð½Ð½Ñ‹Ñ… Ñерверов, не кÑшируÑÑÑŒ." flagAsBot: "Ðккаунт бота" @@ -175,6 +182,10 @@ addAccount: "Добавить учётную запиÑÑŒ" reloadAccountsList: "Обновить ÑпиÑок учётных запиÑей" loginFailed: "ÐÐµÑƒÐ´Ð°Ñ‡Ð½Ð°Ñ Ð¿Ð¾Ð¿Ñ‹Ñ‚ÐºÐ° входа" showOnRemote: "Перейти к оригиналу на Ñайт" +continueOnRemote: "Продолжить на удалённом Ñервере" +chooseServerOnMisskeyHub: "Выбрать Ñервер Ñ Misskey Hub" +specifyServerHost: "Укажите Ñервер напрÑмую" +inputHostName: "Введите домен" general: "Общее" wallpaper: "Обои" setWallpaper: "УÑтановить обои" @@ -185,6 +196,7 @@ followConfirm: "ПодпиÑатьÑÑ Ð½Ð° {name}?" proxyAccount: "Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ прокÑи" proxyAccountDescription: "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ прокÑи предназначена Ñлужить подпиÑчиком на пользователей Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… Ñайтов. Ðапример, еÑли пользователь добавит кого-то Ñ Ð´Ñ€ÑƒÐ³Ð¾Ð³Ð¾ Ñайта а ÑпиÑок, деÑтельноÑÑ‚ÑŒ того не отобразитÑÑ, пока никто Ñ Ñтого же Ñайта не подпиÑан на него. Чтобы Ñто Ñтало возможным, на него подпиÑываетÑÑ Ð¿Ñ€Ð¾ÐºÑи." host: "ХоÑÑ‚" +selectSelf: "Выбрать ÑебÑ" selectUser: "Выберите пользователÑ" recipient: "Кому" annotation: "ОпиÑание" @@ -199,6 +211,7 @@ perHour: "По чаÑам" perDay: "По днÑм" stopActivityDelivery: "ОÑтановить отправку обновлений активноÑти" blockThisInstance: "Блокировать Ñтот инÑтанÑ" +silenceThisInstance: "Заглушить Ñтот инÑтанÑ" operations: "Операции" software: "Программы" version: "ВерÑиÑ" @@ -218,6 +231,7 @@ clearCachedFiles: "ОчиÑтить кÑш" clearCachedFilesConfirm: "Удалить вÑе закÑшированные файлы Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… Ñайтов?" blockedInstances: "Заблокированные инÑтанÑÑ‹" blockedInstancesDescription: "Введите ÑпиÑок инÑтанÑов, которые хотите заблокировать. Они больше не Ñмогут обмениватьÑÑ Ñ Ð²Ð°ÑˆÐ¸Ð¼ инÑтанÑом." +silencedInstances: "Заглушённые инÑтанÑÑ‹" muteAndBlock: "Скрытие и блокировка" mutedUsers: "Скрытые пользователи" blockedUsers: "Заблокированные пользователи" @@ -236,7 +250,7 @@ noJobs: "Ðет заданий" federating: "ФедерируетÑÑ" blocked: "Заблокировано" suspended: "Заморожено" -all: "Ð’ÑÑ‘" +all: "Ð’Ñе" subscribing: "ПодпиÑка" publishing: "ПубликациÑ" notResponding: "Ðет ответа" @@ -268,7 +282,7 @@ messaging: "СообщениÑ" upload: "Загрузить" keepOriginalUploading: "Сохранить иÑходное изображение" keepOriginalUploadingDescription: "СохранÑет иÑходную верÑию при загрузке изображений. ЕÑли выключить, то при загрузке браузер генерирует изображение Ð´Ð»Ñ Ð¿ÑƒÐ±Ð»Ð¸ÐºÐ°Ñ†Ð¸Ð¸." -fromDrive: "С «диÑка»" +fromDrive: "С ДиÑка" fromUrl: "По ÑÑылке" uploadFromUrl: "Загрузить по ÑÑылке" uploadFromUrlDescription: "СÑылка на файл, который хотите загрузить" @@ -308,6 +322,7 @@ selectFile: "Выберите файл" selectFiles: "Выберите файлы" selectFolder: "Выберите папку" selectFolders: "Выберите папки" +fileNotSelected: "Файл не выбран" renameFile: "Переименовать файл" folderName: "Ð˜Ð¼Ñ Ð¿Ð°Ð¿ÐºÐ¸" createFolder: "Создать папку" @@ -359,8 +374,8 @@ disablingTimelinesInfo: "У админиÑтраторов и модератор registration: "РегиÑтрациÑ" enableRegistration: "Разрешить региÑтрацию" invite: "ПриглаÑить" -driveCapacityPerLocalAccount: "Объём диÑка на одного локального пользователÑ" -driveCapacityPerRemoteAccount: "Объём диÑка на одного Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ñ Ð´Ñ€ÑƒÐ³Ð¾Ð³Ð¾ Ñайта" +driveCapacityPerLocalAccount: "Объём ДиÑка на одного локального пользователÑ" +driveCapacityPerRemoteAccount: "Объём ДиÑка на одного Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ñ Ð´Ñ€ÑƒÐ³Ð¾Ð³Ð¾ ÑкземплÑра" inMb: "Ð’ мегабайтах" bannerUrl: "СÑылка на изображение в шапке" backgroundImageUrl: "СÑылка на фоновое изображение" @@ -379,6 +394,7 @@ mcaptcha: "mCaptcha" enableMcaptcha: "Включить mCaptcha" mcaptchaSiteKey: "Ключ Ñайта" mcaptchaSecretKey: "Секретный ключ" +mcaptchaInstanceUrl: "СÑылка на Ñервер mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Включить reCAPTCHA" recaptchaSiteKey: "Ключ Ñайта" @@ -393,7 +409,8 @@ manageAntennas: "ÐаÑтройки антенн" name: "Ðазвание" antennaSource: "ИÑточник антенны" antennaKeywords: "Ключевые Ñлова" -antennaExcludeKeywords: "ИÑключениÑ" +antennaExcludeKeywords: "Чёрный ÑпиÑок Ñлов" +antennaExcludeBots: "ИÑключать ботов" antennaKeywordsDescription: "Пишите Ñлова через пробел в одной Ñтроке, чтобы ловить их поÑвление вмеÑте; на отдельных Ñтроках раÑполагайте Ñлова, или группы Ñлов, чтобы ловить любые из них." notifyAntenna: "УведомлÑÑ‚ÑŒ о новых заметках" withFileAntenna: "Только заметки Ñ Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñми" @@ -426,6 +443,7 @@ totp: "Приложение-аутентификатор" totpDescription: "ОпиÑание приложениÑ-аутентификатора" moderator: "Модератор" moderation: "МодерациÑ" +moderationLogs: "Журнал модерации" nUsersMentioned: "УпомÑнуло пользователей: {n}" securityKeyAndPasskey: "Ключ безопаÑноÑти и Ð¿Ð°Ñ€Ð¾Ð»ÑŒÐ½Ð°Ñ Ñ„Ñ€Ð°Ð·Ð°" securityKey: "Ключ безопаÑноÑти" @@ -458,10 +476,12 @@ retype: "Введите ещё раз" noteOf: "Что пишет {user}" quoteAttached: "Цитата" quoteQuestion: "Хотите добавить цитату?" +attachAsFileQuestion: "ТекÑта в буфере обмена Ñлишком много. Прикрепить как текÑтовый файл?" noMessagesYet: "Пока ни одного ÑообщениÑ" newMessageExists: "Ðовое Ñообщение" onlyOneFileCanBeAttached: "К Ñообщению можно прикрепить только один файл" signinRequired: "ПожалуйÑта, войдите" +signinOrContinueOnRemote: "Чтобы продолжить, вам необходимо войти в аккаунт на Ñвоём Ñервере или зарегиÑтрироватьÑÑ / войти в аккаунт на Ñтом." invitations: "ПриглашениÑ" invitationCode: "Код приглашениÑ" checking: "Проверка" @@ -471,7 +491,7 @@ usernameInvalidFormat: "Можно иÑпользовать только лат tooShort: "Слишком короткий" tooLong: "Слишком длинный" weakPassword: "Слабый пароль" -normalPassword: "Годный пароль" +normalPassword: "Хороший пароль" strongPassword: "Ðадёжный пароль" passwordMatched: "Совпали" passwordNotMatched: "Ðе Ñовпадают" @@ -483,8 +503,8 @@ uiLanguage: "Язык интерфейÑа" aboutX: "ОпиÑание {x}" emojiStyle: "Стиль Ñмодзи" native: "СиÑтемные" -disableDrawer: "Ðе иÑпользовать выдвижные меню" showNoteActionsOnlyHover: "Показывать кнопки у заметок только при наведении" +showReactionsCount: "Видеть количеÑтво реакций на заметках" noHistory: "ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð¿Ð¾ÐºÐ° пуÑта" signinHistory: "Журнал поÑещений" enableAdvancedMfm: "Включить раÑширенный MFM" @@ -547,7 +567,7 @@ popout: "Развернуть" volume: "ГромкоÑÑ‚ÑŒ" masterVolume: "ОÑÐ½Ð¾Ð²Ð½Ð°Ñ Ñ€ÐµÐ³ÑƒÐ»Ð¸Ñ€Ð¾Ð²ÐºÐ° громкоÑти" notUseSound: "Выключить звук" -useSoundOnlyWhenActive: "ИÑпользовать звук, когда Misskey активен." +useSoundOnlyWhenActive: "ВоÑпроизводить звук только когда Misskey активен." details: "Подробнее" chooseEmoji: "Выберите Ñмодзи" unableToProcess: "Ðе удаётÑÑ Ð·Ð°Ð²ÐµÑ€ÑˆÐ¸Ñ‚ÑŒ операцию" @@ -601,7 +621,7 @@ poll: "ОпроÑ" useCw: "Скрывать Ñодержимое под предупреждением" enablePlayer: "Включить проигрыватель" disablePlayer: "Выключить проигрыватель" -expandTweet: "Развернуть твит" +expandTweet: "Развернуть заметку" themeEditor: "Редактор темы оформлениÑ" description: "ОпиÑание" describeFile: "Добавить подпиÑÑŒ" @@ -613,7 +633,7 @@ plugins: "РаÑширениÑ" preferencesBackups: "Ð ÐµÐ·ÐµÑ€Ð²Ð½Ð°Ñ ÐºÐ¾Ð¿Ð¸Ñ" deck: "Пульт" undeck: "Покинуть пульт" -useBlurEffectForModal: "Размывка под формой поверх вÑего" +useBlurEffectForModal: "Размытие за формой ввода заметки" useFullReactionPicker: "Полнофункциональный выбор реакций" width: "Ширина" height: "Ð’Ñ‹Ñота" @@ -644,7 +664,7 @@ smtpSecure: "ИÑпользовать SSL/TLS Ð´Ð»Ñ SMTP-Ñоединений" smtpSecureInfo: "Выключите при иÑпользовании STARTTLS." testEmail: "Проверка доÑтавки Ñлектронной почты" wordMute: "Скрытие Ñлов" -hardWordMute: "" +hardWordMute: "Строгое Ñкрытие Ñлов" regexpError: "Ошибка в регулÑрном выражении" regexpErrorDescription: "Ð’ ÑпиÑке {tab} Ñкрытых Ñлов, в Ñтроке {line} обнаружена ÑинтакÑичеÑÐºÐ°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ°:" instanceMute: "Глушение инÑтанÑов" @@ -680,10 +700,7 @@ abuseReported: "Жалоба отправлена. Большое ÑпаÑибо reporter: "Сообщивший" reporteeOrigin: "О ком Ñообщено" reporterOrigin: "Кто Ñообщил" -forwardReport: "Отправить жалобу на инÑÑ‚Ð°Ð½Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð°." -forwardReportIsAnonymous: "Жалоба на удалённый инÑÑ‚Ð°Ð½Ñ Ð±ÑƒÐ´ÐµÑ‚ отправлена анонимно. ВмеÑто ваших данных у Ð¿Ð¾Ð»ÑƒÑ‡Ð°Ñ‚ÐµÐ»Ñ Ð±ÑƒÐ´ÐµÑ‚ отображена ÑиÑÑ‚ÐµÐ¼Ð½Ð°Ñ ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ." send: "Отправить" -abuseMarkAsResolved: "Отметить жалобу как решённую" openInNewTab: "Открыть в новой вкладке" openInSideView: "Открывать в боковой колонке" defaultNavigationBehaviour: "Поведение навигации по умолчанию" @@ -726,6 +743,7 @@ lockedAccountInfo: "Даже еÑли вы вручную подтверждае alwaysMarkSensitive: "Отмечать файлы как «Ñодержимое не Ð´Ð»Ñ Ð²Ñех» по умолчанию" loadRawImages: "Сразу показывать Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð² полном размере" disableShowingAnimatedImages: "Ðе проигрывать анимацию" +highlightSensitiveMedia: "ВыделÑÑ‚ÑŒ Ñодержимое не Ð´Ð»Ñ Ð²Ñех" verificationEmailSent: "Вам отправлено пиÑьмо Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ. Пройдите, пожалуйÑта, по ÑÑылке из пиÑьма, чтобы завершить проверку." notSet: "Ðе наÑтроено" emailVerified: "ÐÐ´Ñ€ÐµÑ Ñлектронной почты подтверждён." @@ -743,7 +761,7 @@ makeExplorable: "Опубликовать профиль в «Обзоре»." makeExplorableDescription: "ЕÑли выключить, ваш профиль не будет показан в разделе «Обзор»." showGapBetweenNotesInTimeline: "Показывать разделитель между заметками в ленте" duplicate: "Дубликат" -left: "Влево" +left: "Слева" center: "По центру" wide: "ТолÑтый" narrow: "Тонкий" @@ -822,7 +840,7 @@ noMaintainerInformationWarning: "Ðе заполнены ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾Ð± noBotProtectionWarning: "Ботозащита не наÑтроена" configure: "ÐаÑтроить" postToGallery: "Опубликовать в галерею" -postToHashtag: "ÐапиÑать заметку Ñ Ñтим Ñ…Ñштегом" +postToHashtag: "ÐапиÑать заметку Ñ Ñтим хештегом" gallery: "ГалереÑ" recentPosts: "Ðедавние публикации" popularPosts: "ПопулÑрные публикации" @@ -839,13 +857,13 @@ emailNotConfiguredWarning: "Ðе указан Ð°Ð´Ñ€ÐµÑ Ñлектронной ratio: "Соотношение" previewNoteText: "Предварительный проÑмотр" customCss: "Индивидуальный CSS" -customCssWarn: "ИÑпользуйте Ñту наÑтройку только еÑли знаете, что делаете. Ошибки здеÑÑŒ чреваты тем, что Ñайт переÑтанет нормально работать у ваÑ." +customCssWarn: "ИÑпользуйте Ñту наÑтройку только еÑли знаете, что делаете. Ошибки здеÑÑŒ чреваты тем, что у Ð²Ð°Ñ Ð¿ÐµÑ€ÐµÑтанет нормально работать Ñайт." global: "Ð’ÑеобщаÑ" squareAvatars: "Квадратные аватарки" sent: "Отправить" received: "Получено" searchResult: "Результаты поиÑка" -hashtags: "Ð¥Ñштег" +hashtags: "Хештеги" troubleshooting: "Разрешение проблем" useBlurEffect: "Размытие в интерфейÑе" learnMore: "Подробнее" @@ -857,7 +875,7 @@ accountDeletionInProgress: "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½Ñет usernameInfo: "ИмÑ, которое отличает вашу учетную запиÑÑŒ от других на Ñтом Ñервере. Ð’Ñ‹ можете иÑпользовать алфавит (a~z, A~Z), цифры (0~9) или Ñимволы Ð¿Ð¾Ð´Ñ‡ÐµÑ€ÐºÐ¸Ð²Ð°Ð½Ð¸Ñ (_). Имена пользователей не могут быть изменены позже." aiChanMode: "Режим Ðй" devMode: "Режим разработчика" -keepCw: "СохранÑйте ÐŸÑ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¾ Ñодержимом" +keepCw: "СохранÑйте Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¾ Ñодержимом" pubSub: "Учётные запиÑи Pub/Sub" lastCommunication: "ПоÑледнее Ñообщение" resolved: "Решено" @@ -878,6 +896,8 @@ makeReactionsPublicDescription: "СпиÑок Ñделанных вами реа classic: "КлаÑÑика" muteThread: "Скрыть цепочку" unmuteThread: "Отменить Ñокрытие цепочки" +followingVisibility: "ВидимоÑÑ‚ÑŒ подпиÑок" +followersVisibility: "ВидимоÑÑ‚ÑŒ подпиÑчиков" continueThread: "Показать Ñледующие ответы" deleteAccountConfirm: "Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ будет безвозвратно удалена. Подтверждаете?" incorrectPassword: "Пароль неверен." @@ -987,6 +1007,7 @@ assign: "Ðазначить" unassign: "Отменить назначение" color: "Цвет" manageCustomEmojis: "УправлÑÑ‚ÑŒ пользовательÑкими Ñмодзи" +manageAvatarDecorations: "Управление украшениÑми аватара" youCannotCreateAnymore: "Ð’Ñ‹ доÑтигли лимита ÑозданиÑ." cannotPerformTemporary: "Временно недоÑтупен" cannotPerformTemporaryDescription: "Ðто дейÑтвие временно невозможно выполнить из-за Ð¿Ñ€ÐµÐ²Ñ‹ÑˆÐµÐ½Ð¸Ñ Ð»Ð¸Ð¼Ð¸Ñ‚Ð° выполнениÑ." @@ -1003,7 +1024,8 @@ thisPostMayBeAnnoying: "Ðто Ñообщение может быть непри thisPostMayBeAnnoyingHome: "Ðтот поÑÑ‚ может быть отправлен на главную" thisPostMayBeAnnoyingCancel: "Ðтот поÑÑ‚ не может быть отменен." thisPostMayBeAnnoyingIgnore: "Ðтот поÑÑ‚ может быть проигнорирован " -collapseRenotes: "Свернуть репоÑÑ‚Ñ‹" +collapseRenotes: "Сворачивать увиденные репоÑÑ‚Ñ‹" +collapseRenotesDescription: "Сворачивать поÑÑ‚Ñ‹ Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ð¼Ð¸ вы взаимодейÑтвовали." internalServerError: "ВнутреннÑÑ Ð¾ÑˆÐ¸Ð±ÐºÐ° Ñервера" internalServerErrorDescription: "Внутри Ñервера произошла Ð½ÐµÐ¿Ñ€ÐµÐ´Ð²Ð¸Ð´ÐµÐ½Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ°." copyErrorInfo: "Скопировать код ошибки" @@ -1027,7 +1049,10 @@ resetPasswordConfirm: "СброÑить пароль?" sensitiveWords: "ЧувÑтвительные Ñлова" sensitiveWordsDescription: "УÑтановите общедоÑтупный диапазон заметки, Ñодержащей заданное Ñлово, на домашний. Можно Ñделать неÑколько наÑтроек, разделив их переноÑами Ñтрок." sensitiveWordsDescription2: "Разделение пробелом Ñоздаёт Ñпецификацию AND, а разделение коÑой чертой Ñоздаёт регулÑрное выражение." +prohibitedWords: "Запрещённые Ñлова" +prohibitedWordsDescription: "Включает вывод ошибки при попытке опубликовать поÑÑ‚, Ñодержащий указанное Ñлово/набор Ñлов.\nМножеÑтво Ñлов может быть указано, разделÑемые новой Ñтрокой." prohibitedWordsDescription2: "Разделение пробелом Ñоздаёт Ñпецификацию AND, а разделение коÑой чертой Ñоздаёт регулÑрное выражение." +hiddenTags: "Скрытые хештеги" notesSearchNotAvailable: "ПоиÑк заметок недоÑтупен" license: "ЛицензиÑ" unfavoriteConfirm: "Удалить избранное?" @@ -1038,9 +1063,14 @@ retryAllQueuesConfirmTitle: "Хотите попробовать ещё раз?" retryAllQueuesConfirmText: "Ðагрузка на Ñервер может увеличитьÑÑ" enableChartsForRemoteUser: "Создание диаграмм Ð´Ð»Ñ ÑƒÐ´Ð°Ð»Ñ‘Ð½Ð½Ñ‹Ñ… пользователей" enableChartsForFederatedInstances: "Создание диаграмм Ð´Ð»Ñ ÑƒÐ´Ð°Ð»Ñ‘Ð½Ð½Ñ‹Ñ… Ñерверов" +showClipButtonInNoteFooter: "Показать кнопку Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð² подборку в меню дейÑтвий Ñ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ¾Ð¹" +reactionsDisplaySize: "Размер реакций" +limitWidthOfReaction: "Ограничить макÑимальную ширину реакций и отображать их в уменьшенном размере." noteIdOrUrl: "ID или ÑÑылка на заметку" video: "Видео" videos: "Видео" +audio: "Звук" +audioFiles: "Звуковые файлы" dataSaver: "ÐÐºÐ¾Ð½Ð¾Ð¼Ð¸Ñ Ñ‚Ñ€Ð°Ñ„Ð¸ÐºÐ°" accountMigration: "ÐŸÐµÑ€ÐµÐ½Ð¾Ñ ÑƒÑ‡Ñ‘Ñ‚Ð½Ð¾Ð¹ запиÑи" accountMoved: "Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ перенеÑена" @@ -1052,12 +1082,13 @@ editMemo: "Изменить памÑтку" reactionsList: "СпиÑок реакций" renotesList: "РепоÑÑ‚Ñ‹" notificationDisplay: "Отображение уведомлений" -leftTop: "Влево вверх" -rightTop: "Вправо вверх" -leftBottom: "Влево вниз" -rightBottom: "Вправо вниз" -vertical: "ВертикальнаÑ" -horizontal: "Сбоку" +leftTop: "Слева вверху" +rightTop: "Справа Ñверху" +leftBottom: "Слева внизу" +rightBottom: "Справа внизу" +stackAxis: "Положение уведомлений" +vertical: "Вертикально" +horizontal: "Горизонтально" position: "ПозициÑ" serverRules: "Правила Ñервера" pleaseConfirmBelowBeforeSignup: "Ð”Ð»Ñ Ñ€ÐµÐ³Ð¸Ñтрации на данном Ñервере, необходимо ÑоглаÑитÑÑ Ñ Ð½Ð¸Ð¶ÐµÑледующими положениÑми." @@ -1069,57 +1100,114 @@ createNoteFromTheFile: "Создать заметку из Ñтого файла archive: "Ðрхив" channelArchiveConfirmTitle: "ПеремеÑтить {name} в архив?" channelArchiveConfirmDescription: "Ðрхивированные каналы переÑтанут отображатьÑÑ Ð² ÑпиÑке каналов или результатах поиÑка. Ð’ них также Ð½ÐµÐ»ÑŒÐ·Ñ Ð±ÑƒÐ´ÐµÑ‚ добавлÑÑ‚ÑŒ новые запиÑи." +thisChannelArchived: "Ðтот канал находитÑÑ Ð² архиве." displayOfNote: "Отображение заметок" initialAccountSetting: "ÐаÑтройка профилÑ" youFollowing: "ПодпиÑки" preventAiLearning: "ОтказатьÑÑ Ð¾Ñ‚ иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð² машинном обучении (Генеративный ИИ)" +preventAiLearningDescription: "ЗапроÑить краулеров не иÑпользовать опубликованный текÑÑ‚ или Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¸ Ñ‚.д. Ð´Ð»Ñ Ð¼Ð°ÑˆÐ¸Ð½Ð½Ð¾Ð³Ð¾ Ð¾Ð±ÑƒÑ‡ÐµÐ½Ð¸Ñ (Прогнозирующий / Генеративный ИИ) датаÑетов. Ðто доÑтигаетÑÑ Ð¿ÑƒÑ‚Ñ‘Ð¼ Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ \"noai\" HTTP-заголовка в ответ на ÑоответÑтвующий контент. Полного Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ñ‡ÐµÑ€ÐµÐ· Ñтот заголовок не избежать, так как он может быть проÑто проигнорирован." options: "ÐаÑтройки ролей" specifyUser: "Указанный пользователь" +openTagPageConfirm: "Открыть Ñтраницу Ñтого хештега?" +specifyHost: "Указать Ñайт" failedToPreviewUrl: "Предварительный проÑмотр недоÑтупен" update: "Обновить" rolesThatCanBeUsedThisEmojiAsReaction: "Роли тех, кому можно иÑпользовать Ñти Ñмодзи как реакцию" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "ЕÑли здеÑÑŒ ничего не указать, в качеÑтве реакции Ñту Ñмодзи Ñможет иÑпользовать каждый." +rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Ðти роли должны быть общедоÑтупными." +cancelReactionConfirm: "Ð’Ñ‹ дейÑтвительно хотите удалить Ñвою реакцию?" later: "Позже" goToMisskey: "К Misskey" additionalEmojiDictionary: "Дополнительные Ñловари Ñмодзи" installed: "УÑтановлено" branding: "Бренд" +enableServerMachineStats: "Опубликовать характериÑтики Ñервера" enableIdenticonGeneration: "Включить генерацию иконки пользователÑ" turnOffToImprovePerformance: "Отключение Ñтого параметра может повыÑить производительноÑÑ‚ÑŒ." +createInviteCode: "Создать код приглашениÑ" +createCount: "КоличеÑтво приглашений" expirationDate: "Дата иÑтечениÑ" -unused: "ÐеиÑпользуемый" +noExpirationDate: "БеÑÑрочно" +unused: "ÐеиÑпользованное" +used: "ИÑпользован" expired: "Срок дейÑÑ‚Ð²Ð¸Ñ Ð¿Ñ€Ð¸Ð³Ð»Ð°ÑˆÐµÐ½Ð¸Ñ Ð¸Ñтёк" doYouAgree: "СоглаÑны?" icon: "Ðватар" replies: "Ответы" renotes: "РепоÑÑ‚" loadReplies: "Показать ответы" +pinnedList: "Закреплённый ÑпиÑок" +keepScreenOn: "Держать Ñкран включённым" +showRenotes: "Показывать репоÑÑ‚Ñ‹" +mutualFollow: "Взаимные подпиÑки" +followingOrFollower: "ПодпиÑки или подпиÑчики" +fileAttachedOnly: "Только заметки Ñ Ñ„Ð°Ð¹Ð»Ð°Ð¼Ð¸" +showRepliesToOthersInTimeline: "Показывать ответы в ленте" +showRepliesToOthersInTimelineAll: "Показывать в ленте ответы пользователей, на которых вы подпиÑаны" +hideRepliesToOthersInTimelineAll: "Скрывать в ленте ответы пользователей, на которых вы подпиÑаны" sourceCode: "ИÑходный код" +sourceCodeIsNotYetProvided: "ИÑходный код пока не доÑтупен. СвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором, чтобы иÑправить Ñту проблему." +repositoryUrl: "СÑылка на репозиторий" +repositoryUrlDescription: "ЕÑли вы иÑпользуете Misskey как еÑÑ‚ÑŒ (без изменений в иÑходном коде), введите https://github.com/misskey-dev/misskey" +privacyPolicy: "Политика КонфиденциальноÑти" +privacyPolicyUrl: "СÑылка на Политику КонфиденциальноÑти" +attach: "Прикрепить" +angle: "Угол" flip: "Переворот" +disableStreamingTimeline: "Отключить обновление ленты в режиме реального времени" +useGroupedNotifications: "Отображать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ñгруппировано" +doReaction: "Добавить реакцию" code: "Код" +remainingN: "ОÑтаётÑÑ: {n}" +seasonalScreenEffect: "Ðффект времени года на Ñкране" +decorate: "УкраÑить" +addMfmFunction: "Добавить MFM" lastNDays: "ПоÑледние {n} Ñут" +hemisphere: "МеÑто проживаниÑ" +enableHorizontalSwipe: "Смахните в Ñторону, чтобы Ñменить вкладки" surrender: "Ðтот поÑÑ‚ не может быть отменен." +useNativeUIForVideoAudioPlayer: "ИÑпользовать Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð±Ñ€Ð°ÑƒÐ·ÐµÑ€Ð° при проигрывании видео и звука" +keepOriginalFilename: "СохранÑÑ‚ÑŒ иÑходное Ð¸Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð°" +keepOriginalFilenameDescription: "ЕÑли вы выключите данную наÑтройку, имена файлов будут автоматичеÑки заменены Ñлучайной Ñтрокой при загрузке." +alwaysConfirmFollow: "Ð’Ñегда подтверждать подпиÑку" +inquiry: "СвÑзатьÑÑ" _delivery: stop: "Заморожено" _type: none: "ПубликациÑ" +_announcement: + tooManyActiveAnnouncementDescription: "Большое количеÑтво оповещений может ухудшить пользовательÑкий опыт. РаÑÑмотрите архивирование неактуальных оповещений. " _initialAccountSetting: accountCreated: "Ðккаунт уÑпешно Ñоздан!" letsStartAccountSetup: "Давайте наÑтроим вашу учётную запиÑÑŒ." profileSetting: "ÐаÑтройки профилÑ" privacySetting: "ÐаÑтройки конфиденциальноÑти" initialAccountSettingCompleted: "ÐŸÐµÑ€Ð²Ð¾Ð½Ð°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ Ð½Ð°Ñтройка уÑпешно завершена!" + startTutorial: "Пройти Обучение" skipAreYouSure: "ПропуÑтить наÑтройку?" _initialTutorial: + launchTutorial: "Пройти обучение" _note: description: "ПоÑÑ‚Ñ‹ в Misskey называютÑÑ 'Заметками.' Заметки отÑортированы в хронологичеÑком порÑдке в ленте и обновлÑÑŽÑ‚ÑÑ Ð² режиме реального времени." + _reaction: + reactToContinue: "Добавьте реакцию, чтобы продолжить." + _postNote: + _visibility: + public: "Ð¢Ð²Ð¾Ñ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ° будет видна вÑем." + doNotSendConfidencialOnDirect2: "ÐдминиÑтратор целевого Ñервера может видеть что вы отправлÑете. Будьте оÑторожны Ñ ÐºÐ¾Ð½Ñ„Ð¸Ð´ÐµÐ½Ñ†Ð¸Ð°Ð»ÑŒÐ½Ð¾Ð¹ информацией, когда отправлÑете личные заметки пользователÑм Ñ Ð½ÐµÐ½Ð°Ð´Ñ‘Ð¶Ð½Ñ‹Ñ… Ñерверов." _timelineDescription: home: "Ð’ перÑональной ленте раÑполагаютÑÑ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ¸ тех, на которых вы подпиÑаны." - local: "МеÑÑ‚Ð½Ð°Ñ Ð»ÐµÐ½Ñ‚Ð° показывает заметки вÑех пользователей Ñтого Ñайта." + local: "МеÑÑ‚Ð½Ð°Ñ Ð»ÐµÐ½Ñ‚Ð° показывает заметки вÑех пользователей Ñтого ÑкземплÑра." social: "Ð’ Ñоциальной ленте ÑобираетÑÑ Ð²ÑÑ‘, что еÑÑ‚ÑŒ в перÑональной и меÑтной лентах." - global: "Ð’ глобальную ленту попадает вообще вÑÑ‘ Ñо ÑвÑзанных инÑтанÑов." + global: "Ð’ глобальную ленту попадает вообще вÑÑ‘ Ñо ÑвÑзанных ÑкземплÑров." _serverSettings: iconUrl: "ÐÐ´Ñ€ÐµÑ Ð½Ð° иконку роли" +_accountMigration: + moveFrom: "ПеренеÑти другую учётную запиÑÑŒ Ñюда" + moveTo: "ПеренеÑти учётную запиÑÑŒ на другой Ñервер" + moveAccountDescription: "Ðто дейÑтвие перенеÑÑ‘Ñ‚ ваш аккаунт на другой Ñервер.\n ・ПодпиÑчики Ñ Ñтого аккаунта автоматичеÑки подпишутÑÑ Ð½Ð° новый\n ・Ðтот аккаунт отпишетÑÑ Ð¾Ñ‚ вÑех пользователей, на которых подпиÑан ÑейчаÑ\n ・Вы не Ñможете Ñоздавать новые заметки и Ñ‚.д. на Ñтом аккаунте\n\nТогда как Ð¿ÐµÑ€ÐµÐ½Ð¾Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñчиков проиÑходит автоматичеÑки, вы должны будете подготовитьÑÑ, Ñделав некоторые шаги, чтобы перенеÑти ÑпиÑок пользователей, на которых вы подпиÑаны. Чтобы Ñделать Ñто, ÑкÑпортируйте ÑпиÑок подпиÑчиков в файл, который затем импортируете на новом аккаунте в меню наÑтроек. То же Ñамое необходимо будет Ñделать Ñо ÑпиÑками, также как и Ñо Ñкрытыми и заблокированными пользователÑми.\n\n(Ðто объÑÑнение применÑетÑÑ Ðº Misskey v13.12.0 и выше. Другое ActivityPub программное обеÑпечение, такое, как Mastodon, может работать по-другому." + startMigration: "ПеренеÑти" + movedAndCannotBeUndone: "Ðккаунт был перемещён. Ðто дейÑтвие необратимо." _achievements: earnedAt: "Разблокировано в" _types: @@ -1395,6 +1483,7 @@ _role: canPublicNote: "Может публиковать общедоÑтупные заметки" canInvite: "Может Ñоздавать приглаÑительные коды" canManageCustomEmojis: "УправлÑÑ‚ÑŒ пользовательÑкими Ñмодзи" + canManageAvatarDecorations: "Управление украшениÑми аватара" driveCapacity: "ДоÑтупное проÑтранÑтво на «диÑке»" alwaysMarkNsfw: "Ð’Ñегда отмечать файлы как «не Ð´Ð»Ñ Ð²Ñех»" pinMax: "ДоÑтупное количеÑтво закреплённых заметок" @@ -1505,6 +1594,11 @@ _aboutMisskey: donate: "Пожертвование на Misskey" morePatrons: "Большое ÑпаÑибо и многим другим, кто принÑл учаÑтие в Ñтом проекте! 🥰" patrons: "ÐœÐ°Ñ‚ÐµÑ€Ð¸Ð°Ð»ÑŒÐ½Ð°Ñ Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶ÐºÐ°" + projectMembers: "УчаÑтники проекта" +_displayOfSensitiveMedia: + respect: "Скрывать Ñодержимое не Ð´Ð»Ñ Ð²Ñех" + ignore: "Показывать Ñодержимое не Ð´Ð»Ñ Ð²Ñех" + force: "Скрывать вÑÑ‘ Ñодержимое" _instanceTicker: none: "Ðе показывать" remote: "Только Ð´Ð»Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… Ñайтов" @@ -1533,7 +1627,7 @@ _wordMute: muteWordsDescription: "Пишите Ñлова через пробел в одной Ñтроке, чтобы фильтровать их поÑвление вмеÑте; а еÑли хотите фильтровать любое из них, пишите в отдельных Ñтроках." muteWordsDescription2: "ЗдеÑÑŒ можно иÑпользовать регулÑрные Ð²Ñ‹Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ â€” проÑто заключите их между Ð´Ð²ÑƒÐ¼Ñ Ð´Ñ€Ð¾Ð±Ð½Ñ‹Ð¼Ð¸ чертами (/)." _instanceMute: - instanceMuteDescription: "Заметки и репоÑÑ‚Ñ‹ Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ñ… здеÑÑŒ инÑтанÑов, а также ответы пользователÑм оттуда же не будут отображатьÑÑ." + instanceMuteDescription: "Любые активноÑти, затрагивающие инÑтанÑÑ‹ из данного ÑпиÑка, будут Ñкрыты." instanceMuteDescription2: "Пишите каждый инÑÑ‚Ð°Ð½Ñ Ð½Ð° отдельной Ñтроке" title: "Скрывает заметки Ñ Ð·Ð°Ð´Ð°Ð½Ð½Ñ‹Ñ… инÑтанÑов." heading: "СпиÑок Ñкрытых инÑтанÑов" @@ -1582,7 +1676,7 @@ _theme: navActive: "ТекÑÑ‚ на боковой панели (активирован)" navIndicator: "Индикатор на боковой панели" link: "СÑылка" - hashtag: "Ð¥Ñштег" + hashtag: "Хештег" mention: "Упоминание" mentionMe: "Ð£Ð¿Ð¾Ð¼Ð¸Ð½Ð°Ð½Ð¸Ñ Ð²Ð°Ñ" renote: "РепоÑÑ‚" @@ -1600,7 +1694,6 @@ _theme: buttonBg: "Фон кнопки" buttonHoverBg: "ТекÑÑ‚ кнопки" inputBorder: "Рамка Ð¿Ð¾Ð»Ñ Ð²Ð²Ð¾Ð´Ð°" - listItemHoverBg: "Фон пункта ÑпиÑка (под указателем)" driveFolderBg: "Фон папки «ДиÑка»" wallpaperOverlay: "Слой обоев" badge: "Значок" @@ -1612,6 +1705,10 @@ _sfx: note: "Заметки" noteMy: "СобÑтвенные заметки" notification: "УведомлениÑ" + reaction: "При выборе реакции" +_soundSettings: + driveFile: "ИÑпользовать аудиофайл Ñ Ð”Ð¸Ñка." + driveFileWarn: "Выбрать аудиофайл Ñ Ð”Ð¸Ñка." _ago: future: "Из будущего" justNow: "Только что" @@ -1690,6 +1787,7 @@ _permissions: "write:gallery": "Редактирование галереи" "read:gallery-likes": "ПроÑмотр ÑпиÑка понравившегоÑÑ Ð² галерее" "write:gallery-likes": "Изменение ÑпиÑка понравившегоÑÑ Ð² галерее" + "write:admin:reset-password": "СброÑить пароль пользователю" _auth: shareAccessTitle: "Ð Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹" shareAccess: "Дать доÑтуп Ð´Ð»Ñ Â«{name}» к вашей учётной запиÑи?" @@ -1743,6 +1841,7 @@ _widgets: _userList: chooseList: "Выберите ÑпиÑок" clicker: "Счётчик щелчков" + birthdayFollowings: "Пользователи, у которых ÑÐµÐ³Ð¾Ð´Ð½Ñ Ð´ÐµÐ½ÑŒ рождениÑ" _cw: hide: "СпрÑтать" show: "Показать" @@ -1796,7 +1895,7 @@ _profile: name: "ИмÑ" username: "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ" description: "О Ñебе" - youCanIncludeHashtags: "Можете иÑпользовать здеÑÑŒ Ñ…Ñштеги" + youCanIncludeHashtags: "Можете иÑпользовать здеÑÑŒ хештеги." metadata: "Дополнительные ÑведениÑ" metadataEdit: "Редактировать дополнительные ÑведениÑ" metadataDescription: "Можно добавить до четырёх дополнительных граф в профиль." @@ -1804,6 +1903,8 @@ _profile: metadataContent: "Содержимое" changeAvatar: "ПоменÑÑ‚ÑŒ аватар" changeBanner: "ПоменÑÑ‚ÑŒ изображение в шапке" + verifiedLinkDescription: "Ð£ÐºÐ°Ð·Ñ‹Ð²Ð°Ñ Ð·Ð´ÐµÑÑŒ URL, Ñодержащий ÑÑылку на профиль, иконка Ð²Ð»Ð°Ð´ÐµÐ½Ð¸Ñ Ñ€ÐµÑурÑом может быть отображена Ñ€Ñдом Ñ Ð¿Ð¾Ð»ÐµÐ¼" + avatarDecorationMax: "Ð’Ñ‹ можете добавить до {max} украшений." _exportOrImport: allNotes: "Ð’Ñе заметки\n" favoritedNotes: "Избранное" @@ -1926,6 +2027,9 @@ _notification: unreadAntennaNote: "Ðнтенна {name}" emptyPushNotificationMessage: "Обновлены push-уведомлениÑ" achievementEarned: "Получено доÑтижение" + checkNotificationBehavior: "Проверить внешний вид уведомлениÑ" + sendTestNotification: "Отправить теÑтовое уведомление" + flushNotification: "ОчиÑтить уведомлениÑ" _types: all: "Ð’Ñе" follow: "ПодпиÑки" @@ -1938,6 +2042,7 @@ _notification: receiveFollowRequest: "Получен Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° подпиÑку" followRequestAccepted: "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° подпиÑку одобрен" achievementEarned: "Получение доÑтижений" + login: "Войти" app: "Ð£Ð²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¸Ð· приложений" _actions: followBack: "отвечает взаимной подпиÑкой" @@ -1977,19 +2082,57 @@ _dialog: _disabledTimeline: title: "Лента отключена" description: "Ваша Ñ‚ÐµÐºÑƒÑ‰Ð°Ñ Ñ€Ð¾Ð»ÑŒ не позволÑет пользоватьÑÑ Ñтой лентой." +_drivecleaner: + orderBySizeDesc: "Размеры файлов по убыванию" + orderByCreatedAtAsc: "По увеличению даты" _webhookSettings: createWebhook: "Создать вебхук" + modifyWebhook: "Изменить Вебхук" name: "Ðазвание" + secret: "Секрет" + trigger: "УÑловие ÑрабатываниÑ" active: "Вкл." + _events: + follow: "Когда подпиÑалиÑÑŒ на пользователÑ" + followed: "Когда на Ð²Ð°Ñ Ð¿Ð¾Ð´Ð¿Ð¸ÑалиÑÑŒ" + note: "Когда Ñоздали заметку" + reply: "Когда получили ответ на заметку" + renote: "Когда Ð²Ð°Ñ Ñ€ÐµÐ¿Ð¾Ñтнули" + reaction: "Когда получили реакцию" + mention: "Когда Ð²Ð°Ñ ÑƒÐ¿Ð¾Ð¼Ð¸Ð½Ð°ÑŽÑ‚" + _systemEvents: + abuseReport: "Когда приходит жалоба" + abuseReportResolved: "Когда разрешаетÑÑ Ð¶Ð°Ð»Ð¾Ð±Ð°" + userCreated: "Когда Ñоздан пользователь" + deleteConfirm: "Ð’Ñ‹ уверены, что хотите удалить Ñтот Вебхук?" _abuseReport: _notificationRecipient: _recipientType: mail: "ÐÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð°" + webhook: "Вебхук" + _captions: + webhook: "Отправить уведомление СиÑтемному Вебхуку при получении или разрешении жалоб." + notifiedWebhook: "ИÑпользуемый Вебхук" _moderationLogTypes: suspend: "Заморозить" addCustomEmoji: "Добавлено Ñмодзи" updateCustomEmoji: "Изменено Ñмодзи" deleteCustomEmoji: "Удалено Ñмодзи" + deleteDriveFile: "Файл удалён" resetPassword: "Ð¡Ð±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ:" + createInvitation: "Создать код приглашениÑ" + createSystemWebhook: "Создать СиÑтемный Вебхук" + updateSystemWebhook: "Обновить СиÑтемый Вебхук" + deleteSystemWebhook: "Удалить СиÑтемный Вебхук" +_fileViewer: + url: "СÑылка" + attachedNotes: "Закреплённые заметки" +_dataSaver: + _code: + title: "ПодÑветка кода" +_hemisphere: + N: "Северное полушарие" + S: "Южное полушарие" + caption: "ИÑпользуетÑÑ Ð´Ð»Ñ Ð½ÐµÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ñ… наÑтроек клиента Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ñезона." _reversi: total: "Ð’Ñего" diff --git a/locales/si-LK.yml b/locales/si-LK.yml index e130d68ed8501d67b26a587d399d6e811411c702..c43f3d860d87559378ff2682e861e4a47cef569f 100644 --- a/locales/si-LK.yml +++ b/locales/si-LK.yml @@ -17,3 +17,6 @@ _sfx: note: "නà·à¶§à·Š" _profile: username: "පරිà·à·“ලක නà·à¶¸à¶º" +_notification: + _types: + login: "පිවිසෙන්න" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 409d682af7a4587b16fe0148b861a717f62798b7..60ce45a6b926d0782ac744acf974f3363597555f 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -454,7 +454,6 @@ uiLanguage: "Jazyk použÃvateľského prostredia" aboutX: "O {x}" emojiStyle: "Å týl emoji" native: "NatÃvne" -disableDrawer: "NepoužÃvaÅ¥ Å¡uflÃkové menu" showNoteActionsOnlyHover: "Ovládacie prvky poznámky sa zobrazujú len po nabehnutà myÅ¡i" noHistory: "Žiadna história" signinHistory: "História prihlásenÃ" @@ -632,10 +631,7 @@ abuseReported: "VaÅ¡e nahlásenie je odoslané. Veľmi pekne Äakujeme." reporter: "Nahlásil" reporteeOrigin: "Pôvod nahláseného" reporterOrigin: "Pôvod nahlasovaÄa" -forwardReport: "PreposlaÅ¥ nahlásenie na server" -forwardReportIsAnonymous: "Namiesto vášho úÄtu bude zobrazený anonymný systémový úÄet na vzdialenom serveri ako autor nahlásenia." send: "PoslaÅ¥" -abuseMarkAsResolved: "OznaÄiÅ¥ nahlásenia ako vyrieÅ¡ené" openInNewTab: "OtvoriÅ¥ v novom tabe" openInSideView: "OtvoriÅ¥ v boÄnom paneli" defaultNavigationBehaviour: "Predvolené správanie navigácie" @@ -917,7 +913,7 @@ color: "Farba" horizontal: "Strana" youFollowing: "Sledované" icon: "Avatar" -replies: "Odpovede" +replies: "OdpovedaÅ¥" renotes: "PreposlaÅ¥" sourceCode: "Zdrojový kód" flip: "PreklopiÅ¥" @@ -1112,7 +1108,6 @@ _theme: buttonBg: "Pozadie tlaÄidla" buttonHoverBg: "Pozadie tlaÄidla (pod kurzorom)" inputBorder: "Okraj vstupného poľa" - listItemHoverBg: "Pozadie položky zoznamu (pod kurzorom)" driveFolderBg: "Pozadie prieÄinu disku" wallpaperOverlay: "Vrstvenie pozadia" badge: "Odznak" @@ -1410,6 +1405,7 @@ _notification: pollEnded: "Hlasovanie skonÄilo" receiveFollowRequest: "DoruÄené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie" + login: "PrihlásiÅ¥ sa" app: "Oznámenia z prepojených aplikáciÃ" _actions: followBack: "SledovaÅ¥ späť\n" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index c98782450f163d0b8ad9d2d88f382dcdb84bc91c..5a0de660e8963b78ca4f6ce9f95a425d7578a708 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -486,7 +486,7 @@ pleaseDonate: "Misskey är en gratis programvara som används pÃ¥ {host}. Donera resetPasswordConfirm: "Ã…terställ verkligen ditt lösenord?" dataSaver: "Databesparing" icon: "Profilbild" -replies: "Svar" +replies: "Svara" renotes: "Omnotera" _delivery: stop: "Suspenderad" @@ -562,6 +562,7 @@ _notification: renote: "Omnotera" quote: "Citat" reaction: "Reaktioner" + login: "Logga in" _actions: reply: "Svara" renote: "Omnotera" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 508ca38949a456a881773c138bca7edddc769047..c70d448e2b8b3e8706b18c314a4da0776801eb55 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -509,7 +509,6 @@ uiLanguage: "ภาษาà¸à¸´à¸™à¹€à¸—à¸à¸£à¹Œà¹€à¸Ÿà¸‹à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡ aboutX: "เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸š {x}" emojiStyle: "สไตล์ขà¸à¸‡à¹€à¸à¹‚มจิ" native: "ภาษาà¹à¸¡à¹ˆ" -disableDrawer: "ไม่à¹à¸ªà¸”งเมนูในรูปà¹à¸šà¸šà¸¥à¸´à¹‰à¸™à¸Šà¸±à¸" showNoteActionsOnlyHover: "à¹à¸ªà¸”งà¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¹‚น้ตเมื่à¸à¹‚ฮเวà¸à¸£à¹Œ(วางเมาส์เหนืà¸)เท่านั้น" showReactionsCount: "à¹à¸ªà¸”งจำนวนรีà¹à¸à¸à¸Šà¸±à¹ˆà¸™à¹ƒà¸™à¹‚น้ต" noHistory: "ไม่มีประวัติ" @@ -708,10 +707,7 @@ abuseReported: "เราได้ส่งรายงานขà¸à¸‡à¸„ุณ reporter: "ผู้รายงาน" reporteeOrigin: "ปลายทางรายงาน" reporterOrigin: "à¹à¸«à¸¥à¹ˆà¸‡à¸œà¸¹à¹‰à¸£à¸²à¸¢à¸‡à¸²à¸™" -forwardReport: "ส่งต่à¸à¸£à¸²à¸¢à¸‡à¸²à¸™à¹„ปยังเซิร์ฟเวà¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" -forwardReportIsAnonymous: "ข้à¸à¸¡à¸¹à¸¥à¸‚à¸à¸‡à¸„ุณจะไม่ปราà¸à¸à¸šà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹à¸¥à¸°à¸›à¸£à¸²à¸à¸à¹€à¸›à¹‡à¸™à¸šà¸±à¸à¸Šà¸µà¸£à¸°à¸šà¸šà¸—ี่ไม่ระบุชื่à¸" send: "ส่ง" -abuseMarkAsResolved: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸£à¸²à¸¢à¸‡à¸²à¸™à¸§à¹ˆà¸²à¹à¸à¹‰à¹„ขà¹à¸¥à¹‰à¸§" openInNewTab: "เปิดในà¹à¸—็บใหม่" openInSideView: "เปิดในมุมมà¸à¸‡à¸”้านข้าง" defaultNavigationBehaviour: "พฤติà¸à¸£à¸£à¸¡à¸à¸²à¸£à¸™à¸³à¸—างที่เป็นค่าเริ่มต้น" @@ -1334,10 +1330,10 @@ _initialTutorial: _reaction: title: "รีà¹à¸à¸„ชั่นคืà¸à¸à¸°à¹„ร?" description: "โน้ตสามารถ“รีà¹à¸à¸„ชั่นâ€à¸”้วยเà¸à¹‚มจิต่างๆ ซึ่งทำให้สามารถà¹à¸ªà¸”งความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¹€à¸¥à¹‡à¸à¹† น้à¸à¸¢à¹† ที่à¸à¸²à¸ˆà¹„ม่สามารถสื่à¸à¸à¸à¸à¸¡à¸²à¹„ด้ด้วยà¸à¸²à¸£à¹à¸„่à¸à¸²à¸£à¸à¸” “ถูà¸à¹ƒà¸ˆâ€" - letsTryReacting: "คุณสามารถเพิ่มรีà¹à¸à¸„ชั่นได้ด้วยà¸à¸²à¸£à¸„ลิà¸à¸›à¸¸à¹ˆà¸¡ “{reaction}†บนโน้ต ลà¸à¸‡à¸£à¸µà¹à¸à¸„ชั่นโน้ตตัวà¸à¸¢à¹ˆà¸²à¸‡à¸™à¸µà¹‰à¸”ูสิ!" + letsTryReacting: "คุณสามารถเพิ่มรีà¹à¸à¸„ชั่นได้ด้วยà¸à¸²à¸£à¸„ลิà¸à¸›à¸¸à¹ˆà¸¡ “+†บนโน้ต ลà¸à¸‡à¸£à¸µà¹à¸à¸„ชั่นโน้ตตัวà¸à¸¢à¹ˆà¸²à¸‡à¸™à¸µà¹‰à¸”ูสิ!" reactToContinue: "เพิ่มรีà¹à¸à¸„ชั่นเพื่à¸à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸" reactNotification: "คุณจะได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¹€à¸£à¸µà¸¢à¸¥à¹„ทม์เมื่à¸à¸¡à¸µà¸„นตà¸à¸šà¸£à¸µà¹à¸à¸„ชั่นโน้ตขà¸à¸‡à¸„ุณ" - reactDone: "คุณสามารถยà¸à¹€à¸¥à¸´à¸à¸£à¸µà¹à¸à¸„ชั่นได้โดยà¸à¸²à¸£à¸à¸”ปุ่ม “{undo}â€" + reactDone: "คุณสามารถยà¸à¹€à¸¥à¸´à¸à¸£à¸µà¹à¸à¸„ชั่นได้โดยà¸à¸²à¸£à¸à¸”ปุ่ม “-â€" _timeline: title: "à¹à¸™à¸§à¸„ิดเรื่à¸à¸‡à¸‚à¸à¸‡à¹„ทม์ไลน์" description1: "Misskey มีหลายไทม์ไลน์ขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¸§à¸´à¸˜à¸µà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸‚à¸à¸‡à¸„ุณ (บางไทม์ไลน์à¸à¸²à¸ˆà¹„ม่สามารถใช้ได้ขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¸™à¹‚ยบายขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ)" @@ -1418,7 +1414,7 @@ _achievements: earnedAt: "ได้รับเมื่à¸" _types: _notes1: - title: "just setting up my shonk" + title: "just setting up my msky" description: "โพสต์โน้ตเป็นครั้งà¹à¸£à¸" flavor: "ขà¸à¹ƒà¸«à¹‰à¸¡à¸µà¸Šà¹ˆà¸§à¸‡à¹€à¸§à¸¥à¸²à¸—ี่ดีà¸à¸±à¸š Misskey นะคะ!" _notes10: @@ -1947,7 +1943,6 @@ _theme: buttonBg: "ปุ่มพื้นหลัง" buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวà¸à¸£à¹Œ)" inputBorder: "เส้นขà¸à¸šà¸‚à¸à¸‡à¸Šà¹ˆà¸à¸‡à¸›à¹‰à¸à¸™à¸‚้à¸à¸¡à¸¹à¸¥" - listItemHoverBg: "รายà¸à¸²à¸£à¹„à¸à¹€à¸—มพื้นหลัง (โฮเวà¸à¸£à¹Œ)" driveFolderBg: "พื้นหลังโฟลเดà¸à¸£à¹Œà¹„ดรฟ์" wallpaperOverlay: "วà¸à¸¥à¸¥à¹Œà¹€à¸›à¹€à¸›à¸à¸£à¹Œà¸‹à¹‰à¸à¸™à¸—ับ" badge: "ตรา" @@ -2375,6 +2370,7 @@ _notification: followRequestAccepted: "à¸à¸™à¸¸à¸¡à¸±à¸•à¸´à¹ƒà¸«à¹‰à¸•à¸´à¸”ตามà¹à¸¥à¹‰à¸§" roleAssigned: "ให้บทบาท" achievementEarned: "ปลดล็à¸à¸à¸„วามสำเร็จà¹à¸¥à¹‰à¸§" + login: "เข้าสู่ระบบ" app: "à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸ˆà¸²à¸à¹à¸à¸›à¸—ี่มีลิงà¸à¹Œ" _actions: followBack: "ติดตามà¸à¸¥à¸±à¸šà¸”้วย" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 266fb3161c606ee4de0a7d6daef14815419d45b0..fe2f158ff687bd876d6e0e165edfd4c4b2023f81 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -359,7 +359,7 @@ smtpUser: "Kullanıcı Adı" smtpPass: "Åžifre" notificationSetting: "Bildirim ayarları" instanceTicker: "Notların sunucu bilgileri" -noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et." +noCrawleDescription: "Arama motorlarından profilinde, notlarında, sayfalarında vb. dolaşılmamasını ve dizine eklememesini talep et." clearCache: "Ön belleÄŸi temizle" onlineUsersCount: "{n} kullanıcı çevrim içi" user: "Kullanıcı" @@ -446,6 +446,7 @@ _notification: reaction: "Tepkiler" receiveFollowRequest: "Takip isteÄŸi alındı" followRequestAccepted: "Takip isteÄŸi kabul edildi" + login: "GiriÅŸ Yap " _actions: reply: "yanıt" renote: "vazgeçme" diff --git a/locales/ug-CN.yml b/locales/ug-CN.yml index e48f64511cf51651e6aa54f789986ef6f6dcf198..fef26040a5b4debe46f3a64e4bec61590e9f0623 100644 --- a/locales/ug-CN.yml +++ b/locales/ug-CN.yml @@ -17,3 +17,6 @@ _2fa: renewTOTPCancel: "ئۇنى توختىتىÚ" _widgets: profile: "profile" +_notification: + _types: + login: "كىرىش" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 36d741d30e4a92e97fb1c25ae2992baf7a6545a3..f2262cd71f097fed8a416669387d8e44696b27fe 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -452,7 +452,6 @@ language: "Мова" uiLanguage: "Мова інтерфейÑу" aboutX: "Про {x}" native: "міÑцевий" -disableDrawer: "Ðе викориÑтовувати виÑувні меню" noHistory: "ІÑÑ‚Ð¾Ñ€Ñ–Ñ Ð¿Ð¾Ñ€Ð¾Ð¶Ð½Ñ" signinHistory: "ІÑÑ‚Ð¾Ñ€Ñ–Ñ Ð²Ñ…Ð¾Ð´Ñ–Ð²" enableAdvancedMfm: "Увімкнути розширений MFM" @@ -631,10 +630,7 @@ abuseReported: "ДÑкуємо, вашу Ñкаргу було відправл reporter: "Репортер" reporteeOrigin: "Про кого повідомлено" reporterOrigin: "Хто повідомив" -forwardReport: "ПереÑлати звіт на віддалений інÑтанÑ" -forwardReportIsAnonymous: "ЗаміÑÑ‚ÑŒ вашого облікового запиÑу анонімний ÑиÑтемний обліковий Ð·Ð°Ð¿Ð¸Ñ Ð±ÑƒÐ´Ðµ відображатиÑÑ Ñк доповідач на віддаленому інÑтанÑÑ–" send: "Відправити" -abuseMarkAsResolved: "Позначити Ñкаргу Ñк вирішену" openInNewTab: "Відкрити в новій вкладці" openInSideView: "Відкрити збоку" defaultNavigationBehaviour: "Поведінка навігації за замовчуваннÑм" @@ -1306,7 +1302,6 @@ _theme: buttonBg: "Фон кнопки" buttonHoverBg: "Фон кнопки (при наведенні)" inputBorder: "Край Ð¿Ð¾Ð»Ñ Ð²Ð²Ð¾Ð´Ñƒ" - listItemHoverBg: "Фон елементу в ÑпиÑку (при наведенні)" driveFolderBg: "Фон папки на диÑку" wallpaperOverlay: "ÐÐ°ÐºÐ»Ð°Ð´Ð°Ð½Ð½Ñ ÑˆÐ¿Ð°Ð»ÐµÑ€" badge: "Значок" @@ -1588,6 +1583,7 @@ _notification: reaction: "Реакції" receiveFollowRequest: "Запити на підпиÑку" followRequestAccepted: "ПрийнÑÑ‚Ñ– підпиÑки" + login: "Увійти" app: "Ð¡Ð¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ додатків" _actions: reply: "ВідповіÑти" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 2023771104bfc30eebb6afeeb99b31069dd8abe1..37a550008ab955a7ac1bf26ddc988e6eeb3a0b2b 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -443,7 +443,7 @@ text: "Matn" enable: "Yoqish" next: "Keyingisi" retype: "Qayta kiriting" -noteOf: "{user} tomonidan joylandi\n" +noteOf: "{user} tomonidan joylandi\n" quoteAttached: "Iqtibos" quoteQuestion: "Iqtibos sifatida qo'shilsinmi?" noMessagesYet: "Bu yerda xabarlar yo'q" @@ -471,7 +471,6 @@ uiLanguage: "Interfeys tili" aboutX: "{x} haqida" emojiStyle: "Emoji ko'rinishi" native: "Mahalliy" -disableDrawer: "Slayd menyusidan foydalanmang" showNoteActionsOnlyHover: "Eslatma amallarini faqat sichqonchani olib borganda ko‘rsatish" noHistory: "Tarix yo'q" signinHistory: "kirish tarixi" @@ -630,10 +629,7 @@ abuseReported: "Shikoyatingiz yetkazildi. Ma'lumot uchun rahmat." reporter: "Shikoyat qiluvchi" reporteeOrigin: "Xabarning kelib chiqishi" reporterOrigin: "Xabarchining joylashuvi" -forwardReport: "Xabarni masofadagi serverga yuborish" -forwardReportIsAnonymous: "Sizning yuborayotgan xabaringiz o'z akkountingiz emas balki anonim tarzda qoladi" send: "Yuborish" -abuseMarkAsResolved: "Yuborilgan xabarni hal qilingan deb belgilash" openInNewTab: "Yangi tab da ochish" openInSideView: "Yon panelda ochish" defaultNavigationBehaviour: "Standart navigatsiya harakati" @@ -843,7 +839,7 @@ rolesAssignedToMe: "Mening rollarim" resetPasswordConfirm: "Qayta parol o'rnatmoqchimisiz?" sensitiveWords: "Ta'sirchan so'zlar" icon: "Avatar" -replies: "Javoblar" +replies: "Javob berish" renotes: "Qayta qayd etish" flip: "Teskari" _delivery: @@ -1058,6 +1054,7 @@ _notification: quote: "Iqtibos keltirish" reaction: "Reaktsiyalar" receiveFollowRequest: "Qabul qilingan kuzatuv so'rovlari" + login: "Kirish" _actions: reply: "Javob berish" renote: "Qayta qayd qilish" diff --git a/locales/version.d.ts b/locales/version.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..54ceec744371f00ddaa5a89cc91d2dc68ce5d8f1 --- /dev/null +++ b/locales/version.d.ts @@ -0,0 +1 @@ +export const localesVersion: string; diff --git a/locales/version.js b/locales/version.js new file mode 100644 index 0000000000000000000000000000000000000000..e84414b74d85a06e717657fedae272cab7bed77e --- /dev/null +++ b/locales/version.js @@ -0,0 +1,14 @@ +import { createHash } from 'crypto'; +import locales from './index.js'; + +// MD5 is acceptable because we don't need cryptographic security. +const hash = createHash('md5'); + +// Derive the version hash from locale content exclusively. +// This avoids the problem of "stuck" translations after modifying locale files. +const localesText = JSON.stringify(locales); +hash.update(localesText, 'utf8'); + +// We can't use regular base64 since this becomes part of a filename. +// Base64URL avoids special characters that would cause an issue. +export const localesVersion = hash.digest().toString('base64url'); diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 87b4403c27aa33e29b8c0d0bf08bf8c4c0fc9b03..235497d844289569a7c7b0fe0d09e9ce03322760 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -486,7 +486,6 @@ uiLanguage: "Ngôn ngữ giao diện" aboutX: "Giá»›i thiệu {x}" emojiStyle: "Kiểu cách Emoji" native: "Bản xứ" -disableDrawer: "Không dùng menu thanh bên" showNoteActionsOnlyHover: "Chỉ hiển thị các hà nh Ä‘á»™ng ghi chú khi di chuá»™t" noHistory: "Không có dữ liệu" signinHistory: "Lịch sỠđăng nháºp" @@ -676,10 +675,7 @@ abuseReported: "Báo cáo đã được gá»i. Cảm Æ¡n bạn nhiá»u." reporter: "NgÆ°á»i báo cáo" reporteeOrigin: "Bị báo cáo" reporterOrigin: "Máy chủ ngÆ°á»i báo cáo" -forwardReport: "Chuyển tiếp báo cáo cho máy chủ từ xa" -forwardReportIsAnonymous: "Thay vì tà i khoản của bạn, má»™t tà i khoản hệ thống ẩn danh sẽ được hiển thị dÆ°á»›i dạng ngÆ°á»i báo cáo ở máy chủ từ xa." send: "Gá»i" -abuseMarkAsResolved: "Äánh dấu đã xá» lý" openInNewTab: "Mở trong tab má»›i" openInSideView: "Mở trong thanh bên" defaultNavigationBehaviour: "Thao tác Ä‘iá»u hÆ°á»›ng mặc định" @@ -1163,7 +1159,7 @@ _achievements: earnedAt: "Ngà y thu nháºn" _types: _notes1: - title: "just setting up my shonk" + title: "just setting up my msky" description: "Lần đầu tiên đăng bà i" flavor: "Chúc bạn trên Miskey vui vẻ nha!!" _notes10: @@ -1288,7 +1284,7 @@ _achievements: _iLoveMisskey: title: "Tôi Yêu Misskey" description: "Äăng lá»i nói \"I ⤠#Misskey\"" - flavor: "Xin chân thà nh cảm Æ¡n bạn đã sá» dụng Misskey!! by Äá»™i ngÅ© phát triển" + flavor: "Xin chân thà nh cảm Æ¡n bạn đã sá» dụng Misskey!! by Äá»™i ngÅ© phát triển" _foundTreasure: title: "Tìm kiếm kho báu" description: "Tìm thấy được những kho báu cất giấu" @@ -1329,7 +1325,7 @@ _achievements: description: "Bấm chá»— nà y" _justPlainLucky: title: "Chỉ là má»™t cuá»™c máy mắn" - description: "Má»—i 10 giây thu nháºn được vá»›i tá»· lệ 0.005%" + description: "Má»—i 10 giây thu nháºn được vá»›i tá»· lệ 0.005%" _setNameToSyuilo: title: "Ngưỡng má»™ vá»›i vị thần" description: "Äạt tên là syuilo" @@ -1483,7 +1479,7 @@ _wordMute: muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription2: "Bao quanh các từ khóa bằng dấu gạch chéo để sá» dụng cụm từ thông dụng." _instanceMute: - instanceMuteDescription: "Thao tác nà y sẽ ẩn má»i tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lá»i từ máy chủ bị ẩn." + instanceMuteDescription: "Thao tác nà y sẽ ẩn má»i tút/lượt đăng lại từ các máy chủ được liệt kê, bao gồm cả những tút dạng trả lá»i từ máy chủ bị ẩn." instanceMuteDescription2: "Tách bằng cách xuống dòng" title: "Ẩn tút từ những máy chủ đã liệt kê." heading: "Danh sách những máy chủ bị ẩn" @@ -1550,7 +1546,6 @@ _theme: buttonBg: "Ná»n nút" buttonHoverBg: "Ná»n nút (Chạm)" inputBorder: "ÄÆ°á»ng viá»n khung soạn thảo" - listItemHoverBg: "Ná»n mục liệt kê (Chạm)" driveFolderBg: "Ná»n thÆ° mục á»” Ä‘Ä©a" wallpaperOverlay: "Lá»›p phủ hình ná»n" badge: "Huy hiệu" @@ -1879,6 +1874,7 @@ _notification: receiveFollowRequest: "Yêu cầu theo dõi" followRequestAccepted: "Yêu cầu theo dõi được chấp nháºn" achievementEarned: "Hoà n thà nh Achievement" + login: "Äăng nháºp" app: "Từ app liên kết" _actions: followBack: "đã theo dõi lại bạn" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 1bc1df7930d9910c4395d7ee05ef1a6d87a319dc..b81018cc1f10fc60277cde6aa82bc60708f83875 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -8,6 +8,9 @@ search: "æœç´¢" notifications: "通知" username: "用户å" password: "密ç " +initialPasswordForSetup: "åˆå§‹åŒ–密ç " +initialPasswordIsIncorrect: "åˆå§‹åŒ–密ç ä¸æ£ç¡®" +initialPasswordForSetupDescription: "如果是自己安装的 Misskey,请输入é…置文件里设好的密ç 。\n如果使用的是 Misskey 的托管æœåŠ¡ç‰ï¼Œè¯·è¾“å…¥æœåŠ¡å•†æ供的密ç 。\n如果没有设置密ç ,请留空并继ç»ã€‚" forgotPassword: "忘记密ç " fetchingAsApObject: "在è”邦宇宙查询ä¸..." ok: "OK" @@ -90,7 +93,7 @@ followsYou: "æ£åœ¨å…³æ³¨ä½ " createList: "创建列表" manageLists: "管ç†åˆ—表" error: "错误" -somethingHappened: "出现了一些问题ï¼" +somethingHappened: "出错了" retry: "é‡è¯•" pageLoadError: "页é¢åŠ 载失败。" pageLoadErrorDescription: "这通常是由于网络或æµè§ˆå™¨ç¼“å˜çš„åŽŸå› ã€‚è¯·æ¸…é™¤ç¼“å˜æˆ–ç‰å¾…片刻åŽé‡è¯•ã€‚" @@ -167,7 +170,7 @@ emojiUrl: "emoji 地å€" addEmoji: "æ·»åŠ è¡¨æƒ…ç¬¦å·" settingGuide: "推èé…ç½®" cacheRemoteFiles: "缓å˜è¿œç¨‹æ–‡ä»¶" -cacheRemoteFilesDescription: "å¯ç”¨æ¤è®¾å®šæ—¶ï¼Œå°†åœ¨æ¤æœåŠ¡å™¨ä¸Šç¼“å˜è¿œç¨‹æ–‡ä»¶ã€‚虽然å¯ä»¥åŠ 快图片显示的速度,但是相对的会消耗大é‡çš„æœåŠ¡å™¨å˜å‚¨ç©ºé—´ã€‚用户角色内的网盘容é‡å†³å®šäº†è¿™ä¸ªè¿œç¨‹ç”¨æˆ·èƒ½åœ¨æœåŠ¡å™¨ä¸Šä¿ç•™ä¿ç•™å¤šå°‘缓å˜ã€‚当超出了这个é™åˆ¶æ—¶ï¼Œæ—§çš„文件将从缓å˜ä¸è¢«åˆ 除,æˆä¸ºé“¾æŽ¥ã€‚当ç¦ç”¨æ¤è®¾å®šæ—¶ï¼Œåˆ™æ˜¯ä»Žä¸€å¼€å§‹å°±å°†è¿œç¨‹æ–‡ä»¶ä¿ç•™ä¸ºé“¾æŽ¥ã€‚æ¤æ—¶æŽ¨èå°† default.yml çš„ proxyRemoteFiles 设置为 true 以优化缩略图生æˆåŠä¿æŠ¤ç”¨æˆ·éšç§ã€‚" +cacheRemoteFilesDescription: "å¯ç”¨æ¤è®¾å®šæ—¶ï¼Œå°†åœ¨æ¤æœåŠ¡å™¨ä¸Šç¼“å˜è¿œç¨‹æ–‡ä»¶ã€‚虽然å¯ä»¥åŠ 快图片显示的速度,但是相对的会消耗大é‡çš„æœåŠ¡å™¨å˜å‚¨ç©ºé—´ã€‚用户角色内的网盘容é‡å†³å®šäº†è¿™ä¸ªè¿œç¨‹ç”¨æˆ·èƒ½åœ¨æœåŠ¡å™¨ä¸Šä¿ç•™å¤šå°‘缓å˜ã€‚当超出了这个é™åˆ¶æ—¶ï¼Œæ—§çš„文件将从缓å˜ä¸è¢«åˆ 除,æˆä¸ºé“¾æŽ¥ã€‚当ç¦ç”¨æ¤è®¾å®šæ—¶ï¼Œåˆ™æ˜¯ä»Žä¸€å¼€å§‹å°±å°†è¿œç¨‹æ–‡ä»¶ä¿ç•™ä¸ºé“¾æŽ¥ã€‚æ¤æ—¶æŽ¨èå°† default.yml çš„ proxyRemoteFiles 设置为 true 以优化缩略图生æˆåŠä¿æŠ¤ç”¨æˆ·éšç§ã€‚" youCanCleanRemoteFilesCache: "å¯ä»¥ä½¿ç”¨æ–‡ä»¶ç®¡ç†çš„🗑ï¸æŒ‰é’®æ¥åˆ 除所有的缓å˜ã€‚" cacheRemoteSensitiveFiles: "缓å˜è¿œç¨‹æ•æ„Ÿåª’体文件" cacheRemoteSensitiveFilesDescription: "如果ç¦ç”¨è¿™é¡¹è®¾å®šï¼Œè¿œç¨‹æœåŠ¡å™¨çš„æ•æ„Ÿåª’体将ä¸ä¼šè¢«ç¼“å˜ï¼Œè€Œæ˜¯ç›´æŽ¥é“¾æŽ¥ã€‚" @@ -236,6 +239,8 @@ silencedInstances: "被é™éŸ³çš„æœåŠ¡å™¨" silencedInstancesDescription: "设置è¦é™éŸ³çš„æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œåˆ†éš”。被é™éŸ³çš„æœåŠ¡å™¨å†…所有的账户将默认处于「é™éŸ³ã€çŠ¶æ€ï¼Œä»…能å‘é€å…³æ³¨è¯·æ±‚,并且在未关注状æ€ä¸‹æ— 法æåŠæœ¬åœ°è´¦æˆ·ã€‚被阻æ¢çš„实例ä¸å—å½±å“。" mediaSilencedInstances: "å·²éšè—媒体文件的æœåŠ¡å™¨" mediaSilencedInstancesDescription: "设置è¦éšè—媒体文件的æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œåˆ†éš”。被设置为éšè—媒体文件æœåŠ¡å™¨å†…所有账å·çš„文件å‡æŒ‰ç…§ã€Œæ•æ„Ÿå†…容ã€å¤„ç†ï¼Œä¸”å°†æ— æ³•ä½¿ç”¨è‡ªå®šä¹‰è¡¨æƒ…ç¬¦å·ã€‚被阻æ¢çš„实例ä¸å—å½±å“。" +federationAllowedHosts: "å…许è”åˆçš„æœåŠ¡å™¨" +federationAllowedHostsDescription: "设定å…许è”åˆçš„æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œåˆ†éš”。" muteAndBlock: "é™éŸ³/拉黑" mutedUsers: "å·²é™éŸ³ç”¨æˆ·" blockedUsers: "已拉黑的用户" @@ -334,6 +339,7 @@ renameFolder: "é‡å‘½å文件夹" deleteFolder: "åˆ é™¤æ–‡ä»¶å¤¹" folder: "文件夹" addFile: "æ·»åŠ æ–‡ä»¶" +showFile: "显示文件" emptyDrive: "网盘ä¸æ— 文件" emptyFolder: "æ¤æ–‡ä»¶å¤¹ä¸æ— 文件" unableToDelete: "æ— æ³•åˆ é™¤" @@ -448,6 +454,7 @@ totpDescription: "使用验è¯å™¨è¾“入一次性密ç " moderator: "监察员" moderation: "管ç†" moderationNote: "管ç†ç¬”è®°" +moderationNoteDescription: "å¯ä»¥ç”¨æ¥è®°å½•ä»…在管ç†å‘˜ä¹‹é—´å…±äº«çš„笔记。" addModerationNote: "æ·»åŠ ç®¡ç†ç¬”è®°" moderationLogs: "管ç†æ—¥å¿—" nUsersMentioned: "{n} 被æ到" @@ -509,7 +516,10 @@ uiLanguage: "显示è¯è¨€" aboutX: "关于 {x}" emojiStyle: "表情符å·çš„æ ·å¼" native: "原生" -disableDrawer: "ä¸æ˜¾ç¤ºæŠ½å±‰èœå•" +menuStyle: "èœå•æ ·å¼" +style: "æ ·å¼" +drawer: "抽屉" +popup: "弹窗" showNoteActionsOnlyHover: "仅在悬åœæ—¶æ˜¾ç¤ºå¸–åæ“作" showReactionsCount: "显示帖å的回应数" noHistory: "没有历å²è®°å½•" @@ -592,6 +602,8 @@ ascendingOrder: "å‡åº" descendingOrder: "é™åº" scratchpad: "AiScript 控制å°" scratchpadDescription: "AiScript 控制å°ä¸º AiScript æ供了实验环境。您å¯ä»¥ç¼–写代ç 与 Misskey 交互,è¿è¡Œå¹¶æŸ¥çœ‹ç»“果。" +uiInspector: "UI 检查器" +uiInspectorDescription: "查看所有内å˜ä¸ç”± UI 组件生æˆå‡ºçš„实例。UI 组件由 UI:C 系列函数所生æˆã€‚" output: "输出" script: "脚本" disablePagesScript: "ç¦ç”¨é¡µé¢è„šæœ¬" @@ -708,10 +720,7 @@ abuseReported: "内容已å‘é€ã€‚感谢您æ交信æ¯ã€‚" reporter: "举报者" reporteeOrigin: "举报æ¥æº" reporterOrigin: "举报者æ¥æº" -forwardReport: "将该举报信æ¯è½¬å‘给远程æœåŠ¡å™¨" -forwardReportIsAnonymous: "在远程实例上显示的报告者是匿å的系统账å·ï¼Œè€Œä¸æ˜¯æ‚¨çš„è´¦å·ã€‚" send: "å‘é€" -abuseMarkAsResolved: "处ç†å®Œæ¯•" openInNewTab: "åœ¨æ–°æ ‡ç¾é¡µä¸æ‰“å¼€" openInSideView: "在侧边æ ä¸æ‰“å¼€" defaultNavigationBehaviour: "默认导航" @@ -913,6 +922,7 @@ followersVisibility: "关注者的公开范围" continueThread: "查看更多帖å" deleteAccountConfirm: "å°†è¦åˆ 除账户。是å¦ç¡®è®¤ï¼Ÿ" incorrectPassword: "密ç 错误" +incorrectTotp: "一次性密ç ä¸æ£ç¡®æˆ–已过期" voteConfirm: "确定投给 “{choice}†?" hide: "éšè—" useDrawerReactionPickerForMobile: "在移动设备上使用抽屉显示" @@ -1077,6 +1087,7 @@ retryAllQueuesConfirmTitle: "è¦å†å°è¯•ä¸€æ¬¡å—?" retryAllQueuesConfirmText: "å¯èƒ½ä¼šä½¿æœåŠ¡å™¨è´Ÿè·åœ¨ä¸€å®šæ—¶é—´å†…å¢žåŠ " enableChartsForRemoteUser: "生æˆè¿œç¨‹ç”¨æˆ·çš„图表" enableChartsForFederatedInstances: "生æˆè¿œç¨‹æœåŠ¡å™¨çš„图表" +enableStatsForFederatedInstances: "获å–远程æœåŠ¡å™¨çš„ä¿¡æ¯" showClipButtonInNoteFooter: "在贴文下方显示便ç¾æŒ‰é’®" reactionsDisplaySize: "回应显示大å°" limitWidthOfReaction: "é™åˆ¶å›žåº”的最大宽度,并将其缩å°æ˜¾ç¤º" @@ -1189,10 +1200,10 @@ followingOrFollower: "关注ä¸æˆ–关注者" fileAttachedOnly: "ä»…é™åª’体" showRepliesToOthersInTimeline: "在时间线ä¸åŒ…å«ç»™åˆ«äººçš„回å¤" hideRepliesToOthersInTimeline: "在时间线ä¸éšè—给别人的回å¤" -showRepliesToOthersInTimelineAll: "在时间线ä¸åŒ…å«çŽ°åœ¨å…³æ³¨çš„所有人的回å¤" -hideRepliesToOthersInTimelineAll: "在时间线ä¸éšè—现在关注的所有人的回å¤" -confirmShowRepliesAll: "æ¤æ“作ä¸å¯æ’¤é”€ã€‚确认è¦åœ¨æ—¶é—´çº¿ä¸åŒ…å«çŽ°åœ¨å…³æ³¨çš„所有人的回å¤å—?" -confirmHideRepliesAll: "æ¤æ“作ä¸å¯æ’¤é”€ã€‚确认è¦åœ¨æ—¶é—´çº¿ä¸éšè—现在关注的所有人的回å¤å—?" +showRepliesToOthersInTimelineAll: "在时间线ä¸æ˜¾ç¤ºæ‰€æœ‰çŽ°åœ¨å…³æ³¨çš„人的回å¤" +hideRepliesToOthersInTimelineAll: "在时间线ä¸éšè—所有现在关注的人的回å¤" +confirmShowRepliesAll: "æ¤æ“作ä¸å¯æ’¤é”€ã€‚确认è¦åœ¨æ—¶é—´çº¿ä¸æ˜¾ç¤ºæ‰€æœ‰çŽ°åœ¨å…³æ³¨çš„人的回å¤å—?" +confirmHideRepliesAll: "æ¤æ“作ä¸å¯æ’¤é”€ã€‚确认è¦åœ¨æ—¶é—´çº¿ä¸éšè—所有现在关注的人的回å¤å—?" externalServices: "外部æœåŠ¡" sourceCode: "æºä»£ç " sourceCodeIsNotYetProvided: "还未æä¾›æºä»£ç 。è¦è§£å†³æ¤é—®é¢˜è¯·è”系管ç†å‘˜ã€‚" @@ -1263,6 +1274,32 @@ confirmWhenRevealingSensitiveMedia: "显示æ•æ„Ÿå†…容å‰éœ€è¦ç¡®è®¤" sensitiveMediaRevealConfirm: "这是æ•æ„Ÿå†…容。是å¦æ˜¾ç¤ºï¼Ÿ" createdLists: "已创建的列表" createdAntennas: "已创建的天线" +fromX: "从 {x}" +genEmbedCode: "生æˆåµŒå…¥ä»£ç " +noteOfThisUser: "æ¤ç”¨æˆ·çš„帖å" +clipNoteLimitExceeded: "æ— æ³•å†å¾€æ¤ä¾¿ç¾å†…æ·»åŠ æ›´å¤šå¸–å" +performance: "性能" +modified: "有å˜æ›´" +discard: "å–消" +thereAreNChanges: "有 {n} 处更改" +signinWithPasskey: "使用通行密钥登录" +unknownWebAuthnKey: "æ¤é€šè¡Œå¯†é’¥æœªæ³¨å†Œã€‚" +passkeyVerificationFailed: "验è¯é€šè¡Œå¯†é’¥å¤±è´¥ã€‚" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验è¯æˆåŠŸï¼Œä½†è´¦æˆ·æœªå¼€å¯æ— 密ç 登录。" +messageToFollower: "给关注者的消æ¯" +target: "对象" +testCaptchaWarning: "æ¤åŠŸèƒ½ä¸ºæµ‹è¯• CAPTCHA 用。<strong>请勿在æ£å¼çŽ¯å¢ƒä¸ä½¿ç”¨ã€‚</strong>" +prohibitedWordsForNameOfUser: "用户åä¸ç¦æ¢çš„è¯" +prohibitedWordsForNameOfUserDescription: "更改用户å时,如果用户åä¸åŒ…å«æ¤åˆ—表里的è¯æ±‡ï¼Œç”¨æˆ·çš„改å请求将被拒ç»ã€‚æŒæœ‰ç®¡ç†å‘˜æƒé™çš„用户ä¸å—æ¤é™åˆ¶ã€‚" +yourNameContainsProhibitedWords: "ç›®æ ‡ç”¨æˆ·å包å«è¿ç¦è¯" +yourNameContainsProhibitedWordsDescription: "用户å内å«æœ‰è¿ç¦è¯ã€‚若想使用æ¤ç”¨æˆ·å,请è”ç³»æœåŠ¡å™¨ç®¡ç†å‘˜ã€‚" +_abuseUserReport: + forward: "转å‘" + forwardDescription: "ç›®æ ‡æ˜¯åŒ¿å系统账户,将把举报转å‘给远程æœåŠ¡å™¨ã€‚" + resolve: "解决" + accept: "确认" + reject: "æ‹’ç»" + resolveTutorial: "如果举报内容有ç†ä¸”已解决,选择「确认ã€å°†æ¡ˆä»¶ä»¥è‚¯å®šçš„æ€åº¦æ ‡è®°ä¸ºå·²è§£å†³ã€‚\n如果举报内容站ä¸ä½è„šï¼Œé€‰æ‹©ã€Œæ‹’ç»ã€å°†æ¡ˆä»¶ä»¥å¦å®šçš„æ€åº¦æ ‡è®°ä¸ºå·²è§£å†³ã€‚" _delivery: status: "投递状æ€" stop: "åœæ¢æŠ•é€’" @@ -1334,10 +1371,10 @@ _initialTutorial: _reaction: title: "什么是回应?" description: "您å¯ä»¥åœ¨å¸–åä¸æ·»åŠ “回应â€ã€‚ 您å¯ä»¥ä½¿ç”¨å应轻æ¾åœ°è¡¨è¾¾ç‚¹â€œèµžâ€æ‰€æ— æ³•ä¼ è¾¾çš„ç»†å¾®å·®åˆ«ã€‚" - letsTryReacting: "回应å¯ä»¥é€šè¿‡ç‚¹å‡»å¸–åä¸çš„「{reaction}ã€æŒ‰é’®æ¥æ·»åŠ 。试ç€ç»™è¿™ä¸ªç¤ºä¾‹å¸–åæ·»åŠ ä¸€ä¸ªå›žåº”ï¼" + letsTryReacting: "回应å¯ä»¥é€šè¿‡ç‚¹å‡»å¸–åä¸çš„「+ã€æŒ‰é’®æ¥æ·»åŠ 。试ç€ç»™è¿™ä¸ªç¤ºä¾‹å¸–åæ·»åŠ ä¸€ä¸ªå›žåº”ï¼" reactToContinue: "æ·»åŠ ä¸€ä¸ªå›žåº”æ¥ç»§ç»" reactNotification: "当您的帖å被æŸäººæ·»åŠ 了回应时,将实时收到通知。" - reactDone: "通过按下「{undo}ã€æŒ‰é’®ï¼Œå¯ä»¥å–消已ç»æ·»åŠ 的回应" + reactDone: "通过按下「ーã€æŒ‰é’®ï¼Œå¯ä»¥å–消已ç»æ·»åŠ 的回应" _timeline: title: "时间线的è¿ä½œæ–¹å¼" description1: "Misskey æ ¹æ®ä½¿ç”¨æ–¹å¼æä¾›äº†å¤šä¸ªæ—¶é—´çº¿ï¼ˆæ ¹æ®æœåŠ¡å™¨çš„设定,å¯èƒ½æœ‰ä¸€äº›è¢«ç¦ç”¨ï¼‰ã€‚" @@ -1397,8 +1434,10 @@ _serverSettings: fanoutTimelineDescription: "当å¯ç”¨æ—¶ï¼Œå¯æ˜¾è‘—æ高获å–å„ç§æ—¶é—´çº¿æ—¶çš„性能,并å‡è½»æ•°æ®åº“çš„è´Ÿè·ã€‚但是相对的 Redis 的内å˜ä½¿ç”¨é‡å°†ä¼šå¢žåŠ 。如果æœåŠ¡å™¨çš„内å˜ä¸æ˜¯å¾ˆå¤§ï¼Œåˆæˆ–者è¿è¡Œä¸ç¨³å®šçš„è¯å¯ä»¥æŠŠå®ƒå…³æŽ‰ã€‚" fanoutTimelineDbFallback: "回退到数æ®åº“" fanoutTimelineDbFallbackDescription: "当å¯ç”¨æ—¶ï¼Œè‹¥æ—¶é—´çº¿æœªè¢«ç¼“å˜ï¼Œåˆ™å°†é¢å¤–查询数æ®åº“。ç¦ç”¨è¯¥åŠŸèƒ½å¯é€šè¿‡ä¸æ‰§è¡Œå›žé€€å¤„ç†è¿›ä¸€æ¥å‡å°‘æœåŠ¡å™¨è´Ÿè½½ï¼Œä½†ä¼šé™åˆ¶å¯æ£€ç´¢çš„时间线范围。" + reactionsBufferingDescription: "å¼€å¯æ—¶å¯æ˜¾è‘—æ高å‘é€å›žåº”时的性能,åŠå‡è½»æ•°æ®åº“è´Ÿè·ã€‚但 Redis 的内å˜ç”¨é‡ä¼šç›¸åº”å¢žåŠ ã€‚" inquiryUrl: "è”络地å€" inquiryUrlDescription: "用æ¥æŒ‡å®šè¯¸å¦‚å‘æœåŠ¡è¿è¥å•†å’¨è¯¢çš„论å›åœ°å€ï¼Œæˆ–记载了è¿è¥å•†è”系方å¼ä¹‹ç±»çš„网页地å€ã€‚" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管ç†æ´»åŠ¨ï¼Œä¸ºé˜²æ¢åžƒåœ¾ä¿¡æ¯ï¼Œæ¤è®¾å®šå°†è‡ªåŠ¨å…³é—。" _accountMigration: moveFrom: "从别的账å·è¿ç§»åˆ°æ¤è´¦æˆ·" moveFromSub: "为å¦ä¸€ä¸ªè´¦æˆ·å»ºç«‹åˆ«å" @@ -1598,7 +1637,7 @@ _achievements: _postedAt0min0sec: title: "报时" description: "在 0 点å‘布一篇帖å" - flavor: "嘣 嘣 嘣 Biu——ï¼" + flavor: "嘟 · 嘟 · 嘟 · 哔——" _selfQuote: title: "自我引用" description: "引用了自己的帖å" @@ -1647,8 +1686,8 @@ _achievements: flavor: "今年也请对本æœåŠ¡å™¨å¤šå¤šæŒ‡æ•™ï¼" _cookieClicked: title: "点击饼干å°æ¸¸æˆ" - description: "点击了å¯ç–‘的饼干" - flavor: "是ä¸æ˜¯è½¯ä»¶æœ‰é—®é¢˜ï¼Ÿ" + description: "点击了饼干" + flavor: "用错软件了?" _brainDiver: title: "Brain Diver" description: "å‘å¸ƒäº†åŒ…å« Brain Diver 链接的帖å" @@ -1665,7 +1704,7 @@ _achievements: _bubbleGameDoubleExplodingHead: title: "两个🤯" description: "ä½ åˆæˆå‡ºäº†2个游æˆé‡Œæœ€å¤§çš„Emoji" - flavor: "" + flavor: "大约能 装满 这些便当盒 🤯 🤯 (比划)" _role: new: "创建角色" edit: "编辑角色" @@ -1730,6 +1769,11 @@ _role: canSearchNotes: "是å¦å¯ä»¥æœç´¢å¸–å" canUseTranslator: "使用翻译功能" avatarDecorationLimit: "å¯æ·»åŠ 头åƒæŒ‚件的最大个数" + canImportAntennas: "å…许导入天线" + canImportBlocking: "å…许导入拉黑列表" + canImportFollowing: "å…许导入关注列表" + canImportMuting: "å…许导入å±è”½åˆ—表" + canImportUserLists: "å…许导入用户列表" _condition: roleAssignedTo: "已分é…给手动角色" isLocal: "是本地用户" @@ -1947,7 +1991,6 @@ _theme: buttonBg: "按钮背景" buttonHoverBg: "按钮背景(悬åœï¼‰" inputBorder: "输入框边框" - listItemHoverBg: "下拉列表项目背景(悬åœï¼‰" driveFolderBg: "网盘的文件夹背景" wallpaperOverlay: "å£çº¸å åŠ å±‚" badge: "å¾½ç« " @@ -2224,6 +2267,9 @@ _profile: changeBanner: "修改横幅" verifiedLinkDescription: "如果将内容设置为 URL,当链接所指å‘的网页内包å«è‡ªå·±çš„个人资料链接时,å¯ä»¥æ˜¾ç¤ºä¸€ä¸ªå·²éªŒè¯å›¾æ ‡ã€‚" avatarDecorationMax: "最多å¯æ·»åŠ {max} 个挂件" + followedMessage: "被关注时显示的消æ¯" + followedMessageDescription: "å¯ä»¥è®¾ç½®è¢«å…³æ³¨æ—¶å‘对方显示的çŸæ¶ˆæ¯ã€‚" + followedMessageDescriptionForLockedAccount: "需è¦æ‰¹å‡†æ‰èƒ½å…³æ³¨çš„情况下,消æ¯æ˜¯åœ¨è¯·æ±‚被批准åŽæ˜¾ç¤ºã€‚" _exportOrImport: allNotes: "所有帖å" favoritedNotes: "收è—的帖å" @@ -2362,6 +2408,8 @@ _notification: renotedBySomeUsers: "{n} 人转å‘了" followedBySomeUsers: "被 {n} 人关注" flushNotification: "é‡ç½®é€šçŸ¥åŽ†å²" + exportOfXCompleted: "å·²å®Œæˆ {x} 个导出" + login: "有新的登录" _types: all: "全部" note: "用户的新帖å" @@ -2376,6 +2424,9 @@ _notification: followRequestAccepted: "关注请求已通过" roleAssigned: "授予的角色" achievementEarned: "å–å¾—çš„æˆå°±" + exportCompleted: "已完æˆå¯¼å‡º" + login: "登录" + test: "测试通知" app: "å…³è”应用的通知" _actions: followBack: "回关" @@ -2441,7 +2492,10 @@ _webhookSettings: abuseReport: "当收到举报时" abuseReportResolved: "当举报被处ç†æ—¶" userCreated: "当用户被创建时" + inactiveModeratorsWarning: "当管ç†å‘˜åœ¨ä¸€æ®µæ—¶é—´å†…ä¸æ´»è·ƒæ—¶" + inactiveModeratorsInvitationOnlyChanged: "å½“å› ä¸ºç®¡ç†å‘˜åœ¨ä¸€æ®µæ—¶é—´å†…ä¸æ´»è·ƒï¼Œå¯¼è‡´æœåŠ¡å™¨å˜ä¸ºé‚€è¯·åˆ¶æ—¶" deleteConfirm: "è¦åˆ 除 webhook å—?" + testRemarks: "点击开关å³ä¾§çš„按钮,å¯ä»¥å‘é€ä½¿ç”¨å‡æ•°æ®çš„测试 Webhook。" _abuseReport: _notificationRecipient: createRecipient: "新建举报通知" @@ -2485,6 +2539,8 @@ _moderationLogTypes: markSensitiveDriveFile: "æ ‡è®°ç½‘ç›˜æ–‡ä»¶ä¸ºæ•æ„Ÿåª’体" unmarkSensitiveDriveFile: "å–æ¶ˆæ ‡è®°ç½‘ç›˜æ–‡ä»¶ä¸ºæ•æ„Ÿåª’体" resolveAbuseReport: "处ç†ä¸¾æŠ¥" + forwardAbuseReport: "转å‘举报" + updateAbuseReportNote: "更新举报用管ç†ç¬”è®°" createInvitation: "生æˆé‚€è¯·ç " createAd: "创建了广告" deleteAd: "åˆ é™¤äº†å¹¿å‘Š" @@ -2640,3 +2696,17 @@ _contextMenu: app: "应用" appWithShift: "Shift 键应用" native: "æµè§ˆå™¨çš„用户界é¢" +_embedCodeGen: + title: "自定义嵌入代ç " + header: "æ˜¾ç¤ºæ ‡é¢˜" + autoload: "è¿žç»åŠ 载(ä¸æŽ¨è)" + maxHeight: "最大高度" + maxHeightDescription: "若将最大值设为 0 则ä¸é™åˆ¶æœ€å¤§é«˜åº¦ã€‚为防æ¢å°å·¥å…·æ— é™å¢žé«˜ï¼Œå»ºè®®è®¾ç½®ä¸€ä¸‹ã€‚" + maxHeightWarn: "最大高度é™åˆ¶å·²ç¦ç”¨ï¼ˆ0)。若这ä¸æ˜¯æ‚¨æƒ³è¦çš„效果,请将最大高度设一个值。" + previewIsNotActual: "由于超出了预览画é¢å¯æ˜¾ç¤ºçš„èŒƒå›´ï¼Œå› æ¤æ˜¾ç¤ºå†…容会与实际嵌入时有所ä¸åŒã€‚" + rounded: "圆角" + border: "外边框" + applyToPreview: "应用预览" + generateCode: "生æˆåµŒå…¥ä»£ç " + codeGenerated: "已生æˆä»£ç " + codeGeneratedDescription: "将生æˆçš„代ç 贴到网站上æ¥ä½¿ç”¨ã€‚" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 80ad2eaf027c31a3589a028bb82d5d89ff990582..de18342bbf6354aa915209d2b65fee58aa578627 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -8,6 +8,9 @@ search: "æœå°‹" notifications: "通知" username: "使用者å稱" password: "密碼" +initialPasswordForSetup: "åˆå§‹è¨å®šç”¨çš„密碼" +initialPasswordIsIncorrect: "åˆå§‹è¨å®šç”¨çš„密碼錯誤。" +initialPasswordForSetupDescription: "如果您自己安è£äº† Misskey,請使用您在è¨å®šæª”ä¸è¼¸å…¥çš„密碼。\n如果您使用 Misskey 的託管æœå‹™ä¹‹é¡žçš„æœå‹™ï¼Œè«‹ä½¿ç”¨æ供的密碼。\n如果您尚未è¨å®šå¯†ç¢¼ï¼Œè«‹å°‡å…¶ç•™ç©ºä¸¦ç¹¼çºŒã€‚" forgotPassword: "忘記密碼" fetchingAsApObject: "從è¯é‚¦å®‡å®™å–å¾—ä¸..." ok: "OK" @@ -236,6 +239,8 @@ silencedInstances: "被ç¦è¨€çš„伺æœå™¨" silencedInstancesDescription: "è¨å®šè¦ç¦è¨€çš„伺æœå™¨ä¸»æ©Ÿå稱,以æ›è¡Œåˆ†éš”。隸屬於ç¦è¨€ä¼ºæœå™¨çš„所有帳戶都將被視為「ç¦è¨€å¸³æˆ¶ã€ï¼Œåªèƒ½ç™¼å‡ºã€Œè¿½éš¨è«‹æ±‚ã€ï¼Œè€Œä¸”無法æåŠæœªè¿½éš¨çš„本地帳戶。這ä¸æœƒå½±éŸ¿å·²å°éŽ–的實例。" mediaSilencedInstances: "媒體被ç¦è¨€çš„伺æœå™¨" mediaSilencedInstancesDescription: "è¨å®šæ‚¨æƒ³è¦å°åª’é«”è¨å®šç¦è¨€çš„伺æœå™¨ï¼Œä»¥æ›è¡Œç¬¦è™Ÿå€éš”。來自被媒體ç¦è¨€çš„伺æœå™¨æ‰€å±¬å¸³æˆ¶çš„所有檔案都會被視為æ•æ„Ÿæª”案,且自訂表情符號ä¸èƒ½ä½¿ç”¨ã€‚被å°éŽ–的伺æœå™¨ä¸å—影響。" +federationAllowedHosts: "å…許è¯é‚¦é€šè¨Šçš„伺æœå™¨" +federationAllowedHostsDescription: "è¨å®šå…許è¯é‚¦é€šè¨Šçš„伺æœå™¨ä¸»æ©Ÿï¼Œä»¥æ›è¡Œç¬¦è™Ÿåˆ†éš”。" muteAndBlock: "éœéŸ³å’Œå°éŽ–" mutedUsers: "被éœéŸ³çš„使用者" blockedUsers: "被å°éŽ–的使用者" @@ -334,6 +339,7 @@ renameFolder: "é‡æ–°å‘½å資料夾" deleteFolder: "刪除資料夾" folder: "資料夾" addFile: "åŠ å…¥é™„ä»¶" +showFile: "ç€è¦½æ–‡ä»¶" emptyDrive: "雲端硬碟為空" emptyFolder: "資料夾為空" unableToDelete: "無法刪除" @@ -448,6 +454,7 @@ totpDescription: "以驗è‰æ‡‰ç”¨ç¨‹å¼è¼¸å…¥ä¸€æ¬¡æ€§å¯†ç¢¼" moderator: "審查員" moderation: "審查" moderationNote: "管ç†ç†è¨˜" +moderationNoteDescription: "您å¯ä»¥ç·¨å¯«åƒ…在審查員之間共用的註解。" addModerationNote: "新增管ç†ç†è¨˜" moderationLogs: "管ç†æ—¥èªŒ" nUsersMentioned: "被 {n} 個人æåŠ" @@ -509,8 +516,11 @@ uiLanguage: "介é¢èªžè¨€" aboutX: "關於{x}" emojiStyle: "è¡¨æƒ…ç¬¦è™Ÿçš„é¢¨æ ¼" native: "原生" -disableDrawer: "ä¸é¡¯ç¤ºä¸‹æ‹‰å¼é¸å–®" -showNoteActionsOnlyHover: "僅在游標åœç•™æ™‚顯示貼文的æ“作é¸é …" +menuStyle: "é¸å–®é¢¨æ ¼" +style: "é¢¨æ ¼" +drawer: "å´é‚Šæ¬„" +popup: "彈出å¼è¦–窗" +showNoteActionsOnlyHover: "僅在游標åœç•™æ™‚顯示貼文的" showReactionsCount: "顯示貼文的å應數目" noHistory: "沒有æ·å²ç´€éŒ„" signinHistory: "登入æ·å²" @@ -592,6 +602,8 @@ ascendingOrder: "昇冪" descendingOrder: "é™å†ª" scratchpad: "æš«å˜è¨˜æ†¶é«”" scratchpadDescription: "AiScript 控制臺為 AiScript 的實驗環境。您å¯ä»¥åœ¨æ¤ç·¨å¯«ã€åŸ·è¡Œå’Œç¢ºèªç¨‹å¼ç¢¼èˆ‡ Misskey 互動的çµæžœã€‚" +uiInspector: "UI 檢查" +uiInspectorDescription: "您å¯ä»¥çœ‹åˆ°è¨˜æ†¶é«”ä¸å˜åœ¨çš„ UI 元件實例的清單。 UI 元件由 Ui:C: 系列函數產生。" output: "輸出" script: "腳本" disablePagesScript: "åœç”¨é é¢çš„ AiScript 腳本" @@ -708,10 +720,7 @@ abuseReported: "檢舉完æˆã€‚æ„Ÿè¬æ‚¨çš„å ±å‘Šã€‚" reporter: "檢舉者" reporteeOrigin: "檢舉來æº" reporterOrigin: "檢舉者來æº" -forwardReport: "å°‡å ±å‘Šè½‰é€çµ¦é 端伺æœå™¨" -forwardReportIsAnonymous: "在é 端實例上看ä¸åˆ°æ‚¨çš„è³‡è¨Šï¼Œé¡¯ç¤ºçš„å ±å‘Šè€…æ˜¯åŒ¿å的系统帳戶。" send: "發é€" -abuseMarkAsResolved: "處ç†å®Œç•¢" openInNewTab: "在新分é ä¸é–‹å•Ÿ" openInSideView: "在å´æ¬„ä¸é–‹å•Ÿ" defaultNavigationBehaviour: "é è¨å°Žèˆª" @@ -913,6 +922,7 @@ followersVisibility: "追隨者的å¯è¦‹æ€§" continueThread: "查看更多貼文" deleteAccountConfirm: "å°‡è¦åˆªé™¤å¸³æˆ¶ã€‚是å¦ç¢ºå®šï¼Ÿ" incorrectPassword: "密碼錯誤。" +incorrectTotp: "一次性密碼錯誤,或者已éŽæœŸã€‚" voteConfirm: "確定投給「{choice}ã€ï¼Ÿ" hide: "éš±è—" useDrawerReactionPickerForMobile: "在移動è¨å‚™ä¸Šä½¿ç”¨æŠ½å±œé¡¯ç¤º" @@ -1009,7 +1019,7 @@ show: "檢視" neverShow: "ä¸å†é¡¯ç¤º" remindMeLater: "以後å†èªª" didYouLikeMisskey: "æ‚¨å–œæ¡ Misskey 嗎?" -pleaseDonate: "Misskey 是由 {host} 使用的å…費軟體。請贊助我們,讓開發得以æŒçºŒï¼" +pleaseDonate: "Misskey是由{host}使用的å…è²»è»Ÿé«”ã€‚è«‹è´ŠåŠ©æˆ‘å€‘ï¼Œè®“é–‹ç™¼çš„å·¥ä½œèƒ½å¤ æŒçºŒï¼" correspondingSourceIsAvailable: "å°æ‡‰çš„原始碼å¯ä»¥åœ¨ {anchor} 處找到。" roles: "角色" role: "角色" @@ -1077,6 +1087,7 @@ retryAllQueuesConfirmTitle: "è¦ç¾åœ¨é‡è©¦å—Žï¼Ÿ" retryAllQueuesConfirmText: "伺æœå™¨çš„è² è·å¯èƒ½æœƒæš«æ™‚å¢žåŠ ã€‚" enableChartsForRemoteUser: "生æˆé 端使用者的圖表" enableChartsForFederatedInstances: "生æˆé 端伺æœå™¨çš„圖表" +enableStatsForFederatedInstances: "å–å¾—é 端伺æœå™¨è³‡è¨Š" showClipButtonInNoteFooter: "新增摘錄按鈕至貼文" reactionsDisplaySize: "å應的顯示尺寸" limitWidthOfReaction: "é™åˆ¶å應的最大寬度,並縮å°é¡¯ç¤ºå°ºå¯¸ã€‚" @@ -1185,8 +1196,8 @@ showRenotes: "顯示其他人的轉發貼文" edited: "已編輯" notificationRecieveConfig: "接å—通知的è¨å®š" mutualFollow: "互相追隨" -followingOrFollower: "追隨ä¸æˆ–追隨者" -fileAttachedOnly: "顯示包å«é™„件的貼文" +followingOrFollower: "追隨ä¸æˆ–者追隨者" +fileAttachedOnly: "åªé¡¯ç¤ºåŒ…å«é™„件的貼文" showRepliesToOthersInTimeline: "顯示給其他人的回覆" hideRepliesToOthersInTimeline: "在時間軸上隱è—給其他人的回覆" showRepliesToOthersInTimelineAll: "在時間軸包å«è¿½éš¨ä¸æ‰€æœ‰äººçš„回覆" @@ -1256,13 +1267,39 @@ useNativeUIForVideoAudioPlayer: "使用ç€è¦½å™¨çš„ UI æ’放影片與音訊" keepOriginalFilename: "ä¿ç•™åŽŸå§‹æª”å" keepOriginalFilenameDescription: "如果關閉æ¤è¨ç½®ï¼Œä¸Šå‚³æ™‚檔案å稱會自動替æ›ç‚ºéš¨æ©Ÿå—串。" noDescription: "沒有說明文å—" -alwaysConfirmFollow: "點擊追隨時總是顯示確èªè¨Šæ¯" +alwaysConfirmFollow: "跟隨時總是確èª" inquiry: "è¯çµ¡æˆ‘們" tryAgain: "è«‹å†è©¦ä¸€æ¬¡ã€‚" confirmWhenRevealingSensitiveMedia: "è¦é¡¯ç¤ºæ•æ„Ÿåª’體時需確èª" sensitiveMediaRevealConfirm: "這是æ•æ„Ÿåª’體。確定è¦é¡¯ç¤ºå—Žï¼Ÿ" createdLists: "已建立的清單" createdAntennas: "已建立的天線" +fromX: "自 {x}" +genEmbedCode: "產生嵌入程å¼ç¢¼" +noteOfThisUser: "這個使用者的貼文列表" +clipNoteLimitExceeded: "沒辦法在這個摘錄ä¸å¢žåŠ 更多貼文了。" +performance: "性能" +modified: "已變更" +discard: "å–消" +thereAreNChanges: "有 {n} 處的變更" +signinWithPasskey: "使用密碼金鑰登入" +unknownWebAuthnKey: "未註冊的金鑰。" +passkeyVerificationFailed: "é©—è‰é‡‘鑰失敗。" +passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗è‰é‡‘é‘°æˆåŠŸï¼Œä½†æ˜¯ç„¡å¯†ç¢¼ç™»å…¥çš„æ–¹å¼æ˜¯åœç”¨çš„。" +messageToFollower: "給追隨者的訊æ¯" +target: "目標 " +testCaptchaWarning: "æ¤åŠŸèƒ½ç”¨æ–¼ CAPTCHA 的測試。<strong>請勿在æ£å¼ç’°å¢ƒä¸ä½¿ç”¨ã€‚</strong>" +prohibitedWordsForNameOfUser: "ç¦æ¢ä½¿ç”¨çš„å—詞(使用者å稱)" +prohibitedWordsForNameOfUserDescription: "如果使用者å稱包å«æ¤æ¸…å–®ä¸çš„任何å—串,則拒絕é‡æ–°å‘½å使用者。 具有審查員權é™çš„使用者ä¸å—æ¤é™åˆ¶çš„影響。" +yourNameContainsProhibitedWords: "您嘗試更改的å稱包å«ç¦æ¢çš„å—串" +yourNameContainsProhibitedWordsDescription: "å稱ä¸åŒ…å«ç¦æ¢ä½¿ç”¨çš„å—串。 如果您想使用æ¤å稱,請è¯çµ¡æ‚¨çš„伺æœå™¨ç®¡ç†å“¡ã€‚" +_abuseUserReport: + forward: "轉發" + forwardDescription: "以匿å系統帳戶將檢舉轉發至é 端伺æœå™¨ã€‚" + resolve: "解決" + accept: "接å—" + reject: "拒絕" + resolveTutorial: "如果您已回覆æ£ç•¶çš„檢舉,請é¸æ“‡ã€ŒæŽ¥å—ã€ä»¥å°‡æ¡ˆä»¶æ¨™è¨˜ç‚ºå·²è§£æ±ºã€‚\n 如果檢舉的內容ä¸æ£ç•¶ï¼Œè«‹é¸æ“‡ã€Œæ‹’絕ã€å°‡æ¡ˆä»¶æ¨™è¨˜ç‚ºå·²è§£æ±ºã€‚" _delivery: status: "傳é€ç‹€æ…‹" stop: "åœæ¢ç™¼é€" @@ -1334,10 +1371,10 @@ _initialTutorial: _reaction: title: "什麼是å應?" description: "您å¯ä»¥åœ¨è²¼æ–‡ä¸æ·»åŠ 「å應ã€ã€‚您å¯ä»¥ä½¿ç”¨å應輕鬆隨æ„地表é”「最愛/大心ã€æ‰€ç„¡æ³•å‚³é”的細微差別。" - letsTryReacting: "å¯ä»¥é€éŽé»žæ“Šè²¼æ–‡ä¸Šçš„「{reaction}ã€æŒ‰éˆ•ä¾†æ·»åŠ å應。請嘗試在æ¤ç¯„ä¾‹è²¼æ–‡æ·»åŠ å應ï¼" + letsTryReacting: "å¯ä»¥é€éŽé»žæ“Šè²¼æ–‡ä¸Šçš„「+ã€æŒ‰éˆ•ä¾†æ·»åŠ å應。請嘗試在æ¤ç¯„ä¾‹è²¼æ–‡æ·»åŠ å應ï¼" reactToContinue: "æ·»åŠ å應以繼續教å¸èª²ç¨‹ã€‚" reactNotification: "當有人å°æ‚¨çš„貼文åšå‡ºå應時會å³æ™‚接收到通知。" - reactDone: "按下「{undo}ã€æŒ‰éˆ•å¯ä»¥å–消å應。" + reactDone: "按下「-ã€æŒ‰éˆ•å¯ä»¥å–消å應。" _timeline: title: "時間軸如何é‹ä½œ" description1: "Misskeyæ ¹æ“šä½¿ç”¨æ–¹å¼æ供了多個時間軸(伺æœå™¨å¯èƒ½æœƒå°‡éƒ¨ä»½æ™‚間軸åœç”¨ï¼‰ã€‚" @@ -1397,8 +1434,10 @@ _serverSettings: fanoutTimelineDescription: "如果啟用的話,檢索å„個時間軸的性能會顯著ææ˜‡ï¼Œè³‡æ–™åº«çš„è² è·ä¹Ÿæœƒæ¸›å°‘。ä¸éŽï¼ŒRedis 的記憶體使用é‡æœƒå¢žåŠ 。如果伺æœå™¨çš„記憶體容é‡æ¯”較少或者é‹è¡Œä¸ç©©å®šï¼Œå¯ä»¥åœç”¨ã€‚" fanoutTimelineDbFallback: "資料庫的回退" fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快å–的情æ³ä¸‹å°‡åŸ·è¡Œå›žé€€è™•ç†ä»¥é¡å¤–查詢資料庫。若åœç”¨ï¼Œå¯ä»¥é€éŽä¸åŸ·è¡Œå›žé€€è™•ç†ä¾†é€²ä¸€æ¥æ¸›å°‘伺æœå™¨çš„è² è·ï¼Œä½†æœƒé™åˆ¶å¯å–得的時間軸範åœã€‚" + reactionsBufferingDescription: "啟用時,å¯ä»¥é¡¯è‘—æ高建立åæ‡‰æ™‚çš„æ•ˆèƒ½ä¸¦æ¸›å°‘è³‡æ–™åº«çš„è² è¼‰ã€‚ 但是,Redis 記憶體使用é‡æœƒå¢žåŠ 。" inquiryUrl: "è¯çµ¡è¡¨å–®ç¶²å€" inquiryUrlDescription: "指定伺æœå™¨é‹ç‡Ÿè€…çš„è¯çµ¡è¡¨å–®ç¶²å€ï¼Œæˆ–包å«é‹ç‡Ÿè€…è¯çµ¡è³‡è¨Šç¶²é 的網å€ã€‚" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "ç‚ºäº†é˜²æ¢ spam,如果一段期間內沒有åµæ¸¬åˆ°å¯©æŸ¥å“¡çš„活動,æ¤è¨å®šå°‡è‡ªå‹•é—œé–‰ã€‚" _accountMigration: moveFrom: "從其他帳戶é·ç§»åˆ°é€™å€‹å¸³æˆ¶" moveFromSub: "為å¦ä¸€å€‹å¸³æˆ¶å»ºç«‹åˆ¥å" @@ -1412,7 +1451,7 @@ _accountMigration: startMigration: "é·ç§»" migrationConfirm: "確定è¦å°‡é€™å€‹å¸³æˆ¶é·ç§»è‡³ {account} 嗎?一旦é·ç§»å°±ç„¡æ³•æ’¤éŠ·ï¼Œä¹Ÿå°±ç„¡æ³•ä»¥åŽŸä¾†çš„狀態使用這個帳戶。\nå¦å¤–,請確èªåœ¨è¦é·ç§»åˆ°çš„帳戶已經建立了一個別å。" movedAndCannotBeUndone: "帳戶已é·ç§»ã€‚\né·ç§»ç„¡æ³•æ’¤æ¶ˆã€‚" - postMigrationNote: "在完æˆé·ç§»çš„ 24 å°æ™‚後解除æ¤å¸³æˆ¶çš„追隨。æ¤å¸³æˆ¶çš„追隨ä¸ã€è¿½éš¨è€…數é‡è®Šç‚º 0。由於ä¸æœƒè§£é™¤è¿½éš¨è€…ï¼Œä½ çš„è¿½éš¨è€…ä»ç„¶å¯ä»¥ç¹¼çºŒæª¢è¦–這個帳戶發布給追隨者的貼文。" + postMigrationNote: "å–消追蹤æ¤å¸³æˆ¶å°‡åœ¨é·ç§»æ“作後 24 å°æ™‚執行。\n æ¤å¸³æˆ¶æœ‰ 0 個關注者/關注者。 您的關注者ä»ç„¶å¯ä»¥çœ‹åˆ°æ¤å¸³æˆ¶çš„關注者帖åï¼Œå› ç‚ºæ‚¨ä¸æœƒè¢«å–消關注。" movedTo: "è¦é·ç§»åˆ°çš„帳戶:" _achievements: earnedAt: "ç²å¾—日期" @@ -1532,7 +1571,7 @@ _achievements: _markedAsCat: title: "我是貓" description: "已將帳戶è¨å®šç‚ºè²“" - flavor: "還沒有åå—。" + flavor: "沒有åå—。" _following1: title: "首次追隨" description: "首次追隨了" @@ -1546,7 +1585,7 @@ _achievements: title: "一百ä½æœ‹å‹" description: "追隨超éŽ100人了" _following300: - title: "朋å‹éŽå¤š" + title: "朋å‹å¤ªå¤š" description: "追隨超éŽ300人了" _followers1: title: "第一個追隨者" @@ -1730,6 +1769,11 @@ _role: canSearchNotes: "å¯å¦æœå°‹è²¼æ–‡" canUseTranslator: "使用翻è¯åŠŸèƒ½" avatarDecorationLimit: "é åƒè£é£¾çš„最大è¨ç½®é‡" + canImportAntennas: "å…許匯入天線" + canImportBlocking: "å…許匯入å°éŽ–åå–®" + canImportFollowing: "å…許匯入跟隨åå–®" + canImportMuting: "å…許匯入éœéŸ³åå–®" + canImportUserLists: "å…許匯入清單" _condition: roleAssignedTo: "手動指派角色完æˆ" isLocal: "本地使用者" @@ -1867,7 +1911,7 @@ _channel: following: "追隨ä¸" usersCount: "有 {n} 人åƒèˆ‡" notesCount: "有 {n} 篇貼文" - nameAndDescription: "å稱與說明" + nameAndDescription: "å稱" nameOnly: "僅å稱" allowRenoteToExternal: "å…è¨±åœ¨é »é“外轉發和引用" _menuDisplay: @@ -1947,7 +1991,6 @@ _theme: buttonBg: "按鈕背景" buttonHoverBg: "按鈕背景 (漂浮)" inputBorder: "輸入框邊框" - listItemHoverBg: "列表物å“背景 (漂浮)" driveFolderBg: "雲端硬碟文件夾背景" wallpaperOverlay: "å£ç´™è¦†è“‹å±¤" badge: "å¾½ç« " @@ -2224,6 +2267,9 @@ _profile: changeBanner: "變更橫幅圖åƒ" verifiedLinkDescription: "如果輸入包å«æ‚¨å€‹äººè³‡æ–™çš„網站 URL,欄ä½æ—邊將出ç¾é©—è‰åœ–示。" avatarDecorationMax: "最多å¯ä»¥è¨ç½® {max} 個è£é£¾ã€‚" + followedMessage: "被追隨時的訊æ¯" + followedMessageDescription: "å¯ä»¥è¨å®šè¢«è¿½éš¨æ™‚顯示給å°æ–¹çš„訊æ¯ã€‚" + followedMessageDescriptionForLockedAccount: "如果追隨是需è¦å¯©æ ¸çš„話,在å…許追隨請求之後顯示。" _exportOrImport: allNotes: "所有貼文" favoritedNotes: "「我的最愛ã€è²¼æ–‡" @@ -2362,6 +2408,8 @@ _notification: renotedBySomeUsers: "{n}人åšäº†è½‰ç™¼" followedBySomeUsers: "被{n}人追隨了" flushNotification: "é‡ç½®é€šçŸ¥æ·å²ç´€éŒ„" + exportOfXCompleted: "{x} 的匯出已完æˆã€‚" + login: "已登入" _types: all: "全部 " note: "使用者的最新貼文" @@ -2376,6 +2424,9 @@ _notification: followRequestAccepted: "追隨請求已接å—" roleAssigned: "已授予角色" achievementEarned: "ç²å¾—æˆå°±" + exportCompleted: "已完æˆåŒ¯å‡ºã€‚" + login: "登入" + test: "通知測試" app: "應用程å¼é€šçŸ¥" _actions: followBack: "追隨回去" @@ -2441,7 +2492,10 @@ _webhookSettings: abuseReport: "當使用者檢舉時" abuseReportResolved: "當處ç†äº†ä½¿ç”¨è€…的檢舉時" userCreated: "使用者被新增時" + inactiveModeratorsWarning: "當審查員在一段時間內沒有活動時" + inactiveModeratorsInvitationOnlyChanged: "當審查員在一段時間內ä¸æ´»å‹•æ™‚,系統會將模å¼è®Šæ›´ç‚ºé‚€è«‹åˆ¶" deleteConfirm: "è«‹å•æ˜¯å¦è¦åˆªé™¤ Webhook?" + testRemarks: "按下切æ›é–‹é—œå³å´çš„按鈕,就會將å‡è³‡æ–™ç™¼é€è‡³ Webhook。" _abuseReport: _notificationRecipient: createRecipient: "新增接收檢舉的通知å°è±¡" @@ -2454,7 +2508,7 @@ _abuseReport: mail: "寄é€åˆ°æ“有監察員權é™çš„使用者電å郵件地å€ï¼ˆåƒ…在收到檢舉時)" webhook: "å‘指定的 SystemWebhook 發é€é€šçŸ¥ï¼ˆåœ¨æ”¶åˆ°æª¢èˆ‰å’Œè§£æ±ºæª¢èˆ‰æ™‚發é€ï¼‰" keywords: "é—œéµå—" - notifiedUser: "被通知的使用者" + notifiedUser: "通知的使用者" notifiedWebhook: "使用的 Webhook" deleteConfirm: "確定è¦åˆªé™¤é€šçŸ¥å°è±¡å—Žï¼Ÿ" _moderationLogTypes: @@ -2485,6 +2539,8 @@ _moderationLogTypes: markSensitiveDriveFile: "標記為æ•æ„Ÿæª”案" unmarkSensitiveDriveFile: "撤銷標記為æ•æ„Ÿæª”案" resolveAbuseReport: "解決檢舉" + forwardAbuseReport: "轉發檢舉" + updateAbuseReportNote: "更新檢舉的審查備註" createInvitation: "建立邀請碼" createAd: "建立廣告" deleteAd: "刪除廣告" @@ -2640,3 +2696,17 @@ _contextMenu: app: "應用程å¼" appWithShift: "Shift éµæ‡‰ç”¨ç¨‹å¼" native: "ç€è¦½å™¨çš„使用者介é¢" +_embedCodeGen: + title: "自訂嵌入程å¼ç¢¼" + header: "檢視標é " + autoload: "自動繼續載入(ä¸å»ºè°ï¼‰" + maxHeight: "最大高度" + maxHeightDescription: "è¨å®šç‚º 0 時代表沒有最大值。請指定æŸå€‹å€¼ä»¥é¿å…å°å·¥å…·æŒçºŒåœ¨ç¸±å‘延伸。" + maxHeightWarn: "最大高度é™åˆ¶å·²åœç”¨ï¼ˆ0)。如果這個變更ä¸æ˜¯æ‚¨æƒ³è¦çš„,請將最大高度è¨å®šç‚ºæŸå€‹å€¼ã€‚" + previewIsNotActual: "由於超出了é 覽畫é¢å¯é¡¯ç¤ºçš„範åœï¼Œå› æ¤é¡¯ç¤ºå…§å®¹æœƒèˆ‡å¯¦éš›åµŒå…¥æ™‚有所ä¸åŒã€‚" + rounded: "圓角" + border: "çµ¦å¤–æ¡†åŠ ä¸Šé‚Šæ¡†" + applyToPreview: "åæ˜ åœ¨é 覽ä¸" + generateCode: "建立嵌入程å¼ç¢¼" + codeGenerated: "已產生程å¼ç¢¼" + codeGeneratedDescription: "請將產生的程å¼ç¢¼è²¼åˆ°æ‚¨çš„網站上。" diff --git a/package.json b/package.json index 2877204540d67741e54e98424815154b3b3f3105..b312044921972cb04bd522b29bfd6c0eb911807a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2024.8.2", + "version": "2024.9.1", "codename": "shonk", "repository": { "type": "git", @@ -8,7 +8,9 @@ }, "packageManager": "pnpm@9.6.0", "workspaces": [ + "packages/frontend-shared", "packages/frontend", + "packages/frontend-embed", "packages/backend", "packages/sw", "packages/misskey-js", @@ -32,9 +34,13 @@ "watch": "pnpm dev", "dev": "node scripts/dev.mjs", "lint": "pnpm -r lint", + "lint-all": "pnpm -r --no-bail lint", + "eslint": "pnpm -r eslint", + "eslint-all": "pnpm -r --no-bail eslint", "cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts", "cy:run": "pnpm cypress run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", + "e2e-dev-container": "cp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run", "jest": "cd packages/backend && pnpm jest", "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", "test": "pnpm -r test", @@ -53,11 +59,11 @@ "fast-glob": "3.3.2", "ignore-walk": "6.0.5", "js-yaml": "4.1.0", - "postcss": "8.4.40", + "postcss": "8.4.47", "tar": "6.2.1", - "terser": "5.31.3", - "typescript": "5.5.4", - "esbuild": "0.23.0", + "terser": "5.33.0", + "typescript": "5.6.2", + "esbuild": "0.23.1", "glob": "11.0.0" }, "devDependencies": { @@ -66,10 +72,10 @@ "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "cross-env": "7.0.3", - "cypress": "13.13.1", + "cypress": "13.14.2", "eslint": "9.8.0", - "globals": "15.8.0", + "globals": "15.9.0", "ncp": "2.0.0", - "start-server-and-test": "2.0.4" + "start-server-and-test": "2.0.8" } } diff --git a/packages/backend/assets/embed.js b/packages/backend/assets/embed.js new file mode 100644 index 0000000000000000000000000000000000000000..24fccc1b6c1f4554e3c25e7327a7ee49e50d16db --- /dev/null +++ b/packages/backend/assets/embed.js @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: MIT + */ +//@ts-check +(() => { + /** @type {NodeListOf<HTMLIFrameElement>} */ + const els = document.querySelectorAll('iframe[data-misskey-embed-id]'); + + window.addEventListener('message', function (event) { + els.forEach((el) => { + if (event.source !== el.contentWindow) { + return; + } + + const id = el.dataset.misskeyEmbedId; + + if (event.data.type === 'misskey:embed:ready') { + el.contentWindow?.postMessage({ + type: 'misskey:embedParent:registerIframeId', + payload: { + iframeId: id, + } + }, '*'); + } + if (event.data.type === 'misskey:embed:changeHeight' && event.data.iframeId === id) { + el.style.height = event.data.payload.height + 'px'; + } + }); + }); +})(); diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js index 4fd9f0cd51b6819c6f0a9bb17c7b068027bf3626..452045bc3ecb155d6ba6ee65f5501385fd5c122a 100644 --- a/packages/backend/eslint.config.js +++ b/packages/backend/eslint.config.js @@ -1,5 +1,6 @@ import tsParser from '@typescript-eslint/parser'; import sharedConfig from '../shared/eslint.config.js'; +import globals from 'globals'; export default [ ...sharedConfig, @@ -43,4 +44,25 @@ export default [ }], }, }, + { + files: ['src/server/web/**/*.js', 'src/server/web/**/*.ts'], + languageOptions: { + globals: { + ...globals.browser, + LANGS: true, + CLIENT_ENTRY: true, + LANGS_VERSION: true, + }, + }, + }, + { + ignores: [ + "**/lib/", + "**/temp/", + "**/built/", + "**/coverage/", + "**/node_modules/", + "**/migration/", + ] + }, ]; diff --git a/packages/backend/migration/1711008460816-external-website-warn.js b/packages/backend/migration/1711008460816-external-website-warn.js new file mode 100644 index 0000000000000000000000000000000000000000..d36639459b8f68c4299d07ec3d4f199b8020f7d9 --- /dev/null +++ b/packages/backend/migration/1711008460816-external-website-warn.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ExternalWebsiteWarn1711008460816 { + name = 'ExternalWebsiteWarn1711008460816' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "trustedLinkUrlPatterns" character varying(3072) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "trustedLinkUrlPatterns"`); + } +} \ No newline at end of file diff --git a/packages/backend/migration/1723944246767-followedMessage.js b/packages/backend/migration/1723944246767-followedMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..fc9ad1cb8538a25f30258d172737af1d7b88cd78 --- /dev/null +++ b/packages/backend/migration/1723944246767-followedMessage.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class FollowedMessage1723944246767 { + name = 'FollowedMessage1723944246767'; + + async up(queryRunner) { + await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)'); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"'); + } +} diff --git a/packages/backend/migration/1726804538569-reactions-buffering.js b/packages/backend/migration/1726804538569-reactions-buffering.js new file mode 100644 index 0000000000000000000000000000000000000000..bc19e9cc8aa67d9444496c5e8f1591809693ac03 --- /dev/null +++ b/packages/backend/migration/1726804538569-reactions-buffering.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ReactionsBuffering1726804538569 { + name = 'ReactionsBuffering1726804538569' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableReactionsBuffering" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableReactionsBuffering"`); + } +} diff --git a/packages/backend/migration/1727027985575-SidebarLogo.js b/packages/backend/migration/1727027985575-SidebarLogo.js new file mode 100644 index 0000000000000000000000000000000000000000..03344a367fb66af15b138caadae28c3ce81adbbc --- /dev/null +++ b/packages/backend/migration/1727027985575-SidebarLogo.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: piuvas and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SidebarLogo1727027985575 { + name = 'SidebarLogo1727027985575'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "sidebarLogoUrl" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sidebarLogoUrl"`); + } +} diff --git a/packages/backend/migration/1727491883993-user-score.js b/packages/backend/migration/1727491883993-user-score.js new file mode 100644 index 0000000000000000000000000000000000000000..7292d5363c53c1f5808fd067bcf07486c31a9cd8 --- /dev/null +++ b/packages/backend/migration/1727491883993-user-score.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UserScore1727491883993 { + name = 'UserScore1727491883993' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`); + } +} diff --git a/packages/backend/migration/1727512908322-meta-federation.js b/packages/backend/migration/1727512908322-meta-federation.js new file mode 100644 index 0000000000000000000000000000000000000000..52c24df4f76a03f2e2f0a5ded7012d86755338f1 --- /dev/null +++ b/packages/backend/migration/1727512908322-meta-federation.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MetaFederation1727512908322 { + name = 'MetaFederation1727512908322' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`); + await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`); + } +} diff --git a/packages/backend/migration/1727659258948-add_latest_note.js b/packages/backend/migration/1727659258948-add_latest_note.js new file mode 100644 index 0000000000000000000000000000000000000000..0ab2b3f0d1cea7801e02e2ee461c455b01e8752d --- /dev/null +++ b/packages/backend/migration/1727659258948-add_latest_note.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddLatestNote1727659258948 { + name = 'AddLatestNote1727659258948'; + + async up(queryRunner) { + await queryRunner.query('CREATE TABLE "latest_note" ("user_id" character varying(32) NOT NULL, "note_id" character varying(32) NOT NULL, CONSTRAINT "PK_f619b62bfaafabe68f52fb50c9a" PRIMARY KEY ("user_id"))'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_20e346fffe4a2174585005d6d80" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd" FOREIGN KEY ("note_id") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "latest_note" DROP CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd"'); + await queryRunner.query('ALTER TABLE "latest_note" DROP CONSTRAINT "FK_20e346fffe4a2174585005d6d80"'); + await queryRunner.query('DROP TABLE "latest_note"'); + } +} diff --git a/packages/backend/migration/1727998351561-increase_character_limits.js b/packages/backend/migration/1727998351561-increase_character_limits.js new file mode 100644 index 0000000000000000000000000000000000000000..87541638460c96de11cfee9b1cc5d81a5678f1ff --- /dev/null +++ b/packages/backend/migration/1727998351561-increase_character_limits.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class IncreaseCharacterLimits1727998351561 { + name = 'IncreaseCharacterLimits1727998351561' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE varchar(100000)`); + await queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "cw" TYPE text`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "cw" TYPE varchar(512)`); + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE varchar(8192)`); + } +} diff --git a/packages/backend/migration/1728177700920-add-reject-reports.js b/packages/backend/migration/1728177700920-add-reject-reports.js new file mode 100644 index 0000000000000000000000000000000000000000..ed5f6bc559ef5c998678d0110947e2771ae9380a --- /dev/null +++ b/packages/backend/migration/1728177700920-add-reject-reports.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddRejectReports1728177700920 { + name = 'AddRejectReports1728177700920' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" ADD "rejectReports" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "rejectReports"`); + } +} diff --git a/packages/backend/migration/1728348353115-soft-limit-drive-comment.js b/packages/backend/migration/1728348353115-soft-limit-drive-comment.js new file mode 100644 index 0000000000000000000000000000000000000000..4eb04432c268faf67268e4a0280e08e8b0cab5ac --- /dev/null +++ b/packages/backend/migration/1728348353115-soft-limit-drive-comment.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SoftLimitDriveComment1728348353115 { + name = 'SoftLimitDriveComment1728348353115' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE text`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE varchar(100000)`); + } +} diff --git a/packages/backend/migration/1728420772835-track-latest-note-type.js b/packages/backend/migration/1728420772835-track-latest-note-type.js new file mode 100644 index 0000000000000000000000000000000000000000..4c9b4ca5949e8708eb5342cdff014145dc7c9adc --- /dev/null +++ b/packages/backend/migration/1728420772835-track-latest-note-type.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class TrackLatestNoteType1728420772835 { + name = 'TrackLatestNoteType1728420772835' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "latest_note" DROP CONSTRAINT "PK_f619b62bfaafabe68f52fb50c9a"`); + await queryRunner.query(`ALTER TABLE "latest_note" ADD "is_public" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "latest_note" ADD "is_reply" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "latest_note" ADD "is_quote" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "latest_note" ADD CONSTRAINT "PK_a44ac8ca9cb916faeefc0912abd" PRIMARY KEY ("user_id", is_public, is_reply, is_quote)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "latest_note" DROP CONSTRAINT "PK_a44ac8ca9cb916faeefc0912abd"`); + await queryRunner.query(`ALTER TABLE "latest_note" DROP COLUMN is_quote`); + await queryRunner.query(`ALTER TABLE "latest_note" DROP COLUMN is_reply`); + await queryRunner.query(`ALTER TABLE "latest_note" DROP COLUMN is_public`); + await queryRunner.query(`ALTER TABLE "latest_note" ADD CONSTRAINT "PK_f619b62bfaafabe68f52fb50c9a" PRIMARY KEY ("user_id")`); + } +} diff --git a/packages/backend/migration/1729414690009-defaultSensitive.js b/packages/backend/migration/1729414690009-defaultSensitive.js new file mode 100644 index 0000000000000000000000000000000000000000..47debf05a70a3c4ca9f154dfe10db4a063ca9a78 --- /dev/null +++ b/packages/backend/migration/1729414690009-defaultSensitive.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: marie and sharkey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class DefaultSensitive1729414690009 { + name = 'DefaultSensitive1729414690009' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "defaultSensitive" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "defaultSensitive"`); + } +} diff --git a/packages/backend/migration/1730505338000-friendlyCaptcha.js b/packages/backend/migration/1730505338000-friendlyCaptcha.js new file mode 100644 index 0000000000000000000000000000000000000000..94a4f22dfa4f726788bef0723b18619188eef266 --- /dev/null +++ b/packages/backend/migration/1730505338000-friendlyCaptcha.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class friendlyCaptcha1730505338000 { + name = 'friendlyCaptcha1730505338000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableFC" boolean NOT NULL DEFAULT false`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ADD "fcSiteKey" character varying(1024)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ADD "fcSecretKey" character varying(1024)`, undefined); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "fcSecretKey"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "fcSiteKey"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFC"`, undefined); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 353613bb575d49a9033210c4f8a5ecbd87337416..19547c503358d1de42b12871df89493aae32256c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -20,7 +20,7 @@ "restart": "pnpm build && pnpm start", "dev": "node ./scripts/dev.mjs", "typecheck": "pnpm --filter megalodon build && tsc --noEmit && tsc -p test --noEmit", - "eslint": "eslint --quiet \"src/**/*.ts\" --cache", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", "lint": "pnpm typecheck && pnpm eslint", "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs", "jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs", @@ -65,24 +65,24 @@ "dependencies": { "@aws-sdk/client-s3": "3.620.0", "@aws-sdk/lib-storage": "3.620.0", - "@bull-board/api": "5.21.1", - "@bull-board/fastify": "5.21.1", - "@bull-board/ui": "5.21.1", - "@discordapp/twemoji": "15.0.3", - "@fastify/accepts": "4.3.0", - "@fastify/cookie": "9.3.1", - "@fastify/cors": "9.0.1", - "@fastify/express": "3.0.0", - "@fastify/http-proxy": "9.5.0", - "@fastify/multipart": "8.3.0", - "@fastify/static": "7.0.4", - "@fastify/view": "9.1.0", + "@bull-board/api": "6.0.0", + "@bull-board/fastify": "6.0.0", + "@bull-board/ui": "6.0.0", + "@discordapp/twemoji": "15.1.0", + "@fastify/accepts": "5.0.0", + "@fastify/cookie": "10.0.0", + "@fastify/cors": "10.0.0", + "@fastify/express": "4.0.0", + "@fastify/http-proxy": "10.0.0", + "@fastify/multipart": "9.0.0", + "@fastify/static": "8.0.0", + "@fastify/view": "10.0.0", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.1.0", - "@napi-rs/canvas": "^0.1.53", - "@nestjs/common": "10.3.10", - "@nestjs/core": "10.3.10", - "@nestjs/testing": "10.3.10", + "@napi-rs/canvas": "0.1.56", + "@nestjs/common": "10.4.3", + "@nestjs/core": "10.4.3", + "@nestjs/testing": "10.4.3", "@peertube/http-signature": "1.7.0", "@sentry/node": "8.20.0", "@sentry/profiling-node": "8.20.0", @@ -100,8 +100,8 @@ "async-mutex": "0.5.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", - "body-parser": "1.20.2", - "bullmq": "5.10.4", + "body-parser": "1.20.3", + "bullmq": "5.13.2", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.3.0", @@ -112,12 +112,12 @@ "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fast-xml-parser": "^4.4.0", - "fastify": "4.28.1", + "fast-xml-parser": "4.4.1", + "fastify": "5.0.0", "fastify-multer": "^2.0.3", - "fastify-raw-body": "4.3.0", + "fastify-raw-body": "5.0.0", "feed": "4.2.2", - "file-type": "19.3.0", + "file-type": "19.5.0", "fluent-ffmpeg": "2.1.3", "form-data": "4.0.0", "glob": "11.0.0", @@ -127,16 +127,17 @@ "htmlescape": "1.1.1", "http-link-header": "1.1.3", "ioredis": "5.4.1", - "ip-cidr": "4.0.1", + "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", - "is-svg": "5.0.1", + "is-svg": "5.1.0", "js-yaml": "4.1.0", "jsdom": "24.1.1", "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", "megalodon": "workspace:*", - "meilisearch": "0.41.0", + "meilisearch": "0.42.0", + "juice": "11.0.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", @@ -145,24 +146,24 @@ "nanoid": "5.0.7", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.14", + "nodemailer": "6.9.15", "oauth": "0.10.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.3.1", + "otpauth": "9.3.2", "parse5": "7.1.2", - "pg": "8.12.0", + "pg": "8.13.0", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "proxy-addr": "^2.0.7", "pug": "3.0.3", "punycode": "2.3.1", - "qrcode": "1.5.3", + "qrcode": "1.5.4", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.21.3", + "re2": "1.21.4", "redis-lock": "0.1.4", "reflect-metadata": "0.2.2", "rename": "1.0.4", @@ -170,17 +171,17 @@ "rxjs": "7.8.1", "sanitize-html": "2.13.0", "secure-json-parse": "2.7.0", - "sharp": "0.33.4", + "sharp": "0.33.5", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.22.11", + "systeminformation": "5.23.5", "tinycolor2": "1.6.0", "tmp": "0.2.3", "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", "typeorm": "0.3.20", - "typescript": "5.5.4", + "typescript": "5.6.2", "ulid": "2.3.0", "uuid": "^9.0.1", "vary": "1.1.2", @@ -190,7 +191,7 @@ }, "devDependencies": { "@jest/globals": "29.7.0", - "@nestjs/platform-express": "10.3.10", + "@nestjs/platform-express": "10.4.3", "@simplewebauthn/types": "10.0.0", "@swc/jest": "0.2.36", "@types/accepts": "1.3.7", @@ -199,10 +200,10 @@ "@types/body-parser": "1.19.5", "@types/color-convert": "2.0.3", "@types/content-disposition": "0.5.8", - "@types/fluent-ffmpeg": "2.1.24", + "@types/fluent-ffmpeg": "2.1.26", "@types/htmlescape": "1.1.3", "@types/http-link-header": "1.0.7", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/js-yaml": "4.0.9", "@types/jsdom": "21.1.7", "@types/jsonld": "1.5.15", @@ -210,11 +211,11 @@ "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", "@types/node": "20.14.12", - "@types/nodemailer": "6.4.15", + "@types/nodemailer": "6.4.16", "@types/oauth": "0.9.5", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.6", + "@types/pg": "8.11.10", "@types/proxy-addr": "^2.0.3", "@types/pug": "2.0.10", "@types/punycode": "2.1.4", @@ -222,7 +223,7 @@ "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.11.0", + "@types/sanitize-html": "2.13.0", "@types/semver": "7.5.8", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.5", @@ -231,17 +232,17 @@ "@types/uuid": "^9.0.4", "@types/vary": "1.1.3", "@types/web-push": "3.6.3", - "@types/ws": "8.5.11", + "@types/ws": "8.5.12", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "aws-sdk-client-mock": "4.0.1", "cross-env": "7.0.3", - "eslint-plugin-import": "2.29.1", - "execa": "9.3.0", + "eslint-plugin-import": "2.30.0", + "execa": "9.4.0", "fkill": "9.0.0", "jest": "29.7.0", "jest-mock": "29.7.0", - "nodemon": "3.1.4", + "nodemon": "3.1.7", "pid-port": "1.0.0", "simple-oauth2": "5.1.0" } diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index d4bf4baf43cbf2048eda4db33ece2f82921718ee..f33a450325a644cfe8681820cb51daaf4325c3a6 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -12,26 +12,49 @@ const config = loadConfig(); // createPostgresDataSource handels primaries and replicas automatically. // usually, it only opens connections first use, so we force it using // .initialize() -createPostgresDataSource(config) - .initialize() - .then(c => { c.destroy() }) - .catch(e => { throw e }); - +async function connectToPostgres(){ + const source = createPostgresDataSource(config); + await source.initialize(); + await source.destroy(); +} // Connect to all redis servers -function connectToRedis(redisOptions) { - const redis = new Redis(redisOptions); - redis.on('connect', () => redis.disconnect()); - redis.on('error', (e) => { - throw e; +async function connectToRedis(redisOptions) { + return await new Promise(async (resolve, reject) => { + const redis = new Redis({ + ...redisOptions, + lazyConnect: true, + reconnectOnError: false, + showFriendlyErrorStack: true, + }); + redis.on('error', e => reject(e)); + + try { + await redis.connect(); + resolve(); + + } catch (e) { + reject(e); + + } finally { + redis.disconnect(false); + } }); } // If not all of these are defined, the default one gets reused. // so we use a Set to only try connecting once to each **uniq** redis. -(new Set([ - config.redis, - config.redisForPubsub, - config.redisForJobQueue, - config.redisForTimelines, -])).forEach(connectToRedis); +const promises = Array + .from(new Set([ + config.redis, + config.redisForPubsub, + config.redisForJobQueue, + config.redisForTimelines, + config.redisForReactions, + ])) + .map(connectToRedis) + .concat([ + connectToPostgres() + ]); + +await Promise.allSettled(promises); diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 09971e8ca022c2a20fa4fd08ed0f3d730cea753e..6ae8ccfbb32ca49338e608c5bd16acdd0baf5965 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -13,6 +13,8 @@ import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; import { allSettled } from './misc/promise-tracker.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; +import { MiMeta } from '@/models/Meta.js'; +import { GlobalEvents } from './core/GlobalEventService.js'; const $config: Provider = { provide: DI.config, @@ -78,11 +80,76 @@ const $redisForTimelines: Provider = { inject: [DI.config], }; +const $redisForReactions: Provider = { + provide: DI.redisForReactions, + useFactory: (config: Config) => { + return new Redis.Redis(config.redisForReactions); + }, + inject: [DI.config], +}; + +const $meta: Provider = { + provide: DI.meta, + useFactory: async (db: DataSource, redisForSub: Redis.Redis) => { + const meta = await db.transaction(async transactionalEntityManager => { + // éŽåŽ»ã®ãƒã‚°ã§ãƒ¬ã‚³ãƒ¼ãƒ‰ãŒè¤‡æ•°å‡ºæ¥ã¦ã—ã¾ã£ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§æ–°ã—ã„IDを優先ã™ã‚‹ + const metas = await transactionalEntityManager.find(MiMeta, { + order: { + id: 'DESC', + }, + }); + + const meta = metas[0]; + + if (meta) { + return meta; + } else { + // metaãŒç©ºã®ã¨ãfetchMetaãŒåŒæ™‚ã«å‘¼ã°ã‚Œã‚‹ã¨ã“ã“ãŒåŒæ™‚ã«å‘¼ã°ã‚Œã¦ã—ã¾ã†ã“ã¨ãŒã‚ã‚‹ã®ã§ãƒ•ã‚§ã‚¤ãƒ«ã‚»ãƒ¼ãƒ•ãªupsertを使ㆠ+ const saved = await transactionalEntityManager + .upsert( + MiMeta, + { + id: 'x', + }, + ['id'], + ) + .then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0])); + + return saved; + } + }); + + async function onMessage(_: string, data: string): Promise<void> { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'metaUpdated': { + for (const key in body.after) { + (meta as any)[key] = (body.after as any)[key]; + } + meta.proxyAccount = null; // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ + break; + } + default: + break; + } + } + } + + redisForSub.on('message', onMessage); + + return meta; + }, + inject: [DI.db, DI.redisForSub], +}; + @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines], - exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule], + providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions], + exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( @@ -91,6 +158,7 @@ export class GlobalModule implements OnApplicationShutdown { @Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, + @Inject(DI.redisForReactions) private redisForReactions: Redis.Redis, ) { } public async dispose(): Promise<void> { @@ -103,6 +171,7 @@ export class GlobalModule implements OnApplicationShutdown { this.redisForPub.disconnect(), this.redisForSub.disconnect(), this.redisForTimelines.disconnect(), + this.redisForReactions.disconnect(), ]); } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 3f9967ad19ea6d1972aa2cf6e8bbde2d1b1cf98a..3dc49c7eb6c41f14f30b4fe30b0e13bf446c8bf5 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -51,6 +51,7 @@ type Source = { redisForPubsub?: RedisOptionsSource; redisForJobQueue?: RedisOptionsSource; redisForTimelines?: RedisOptionsSource; + redisForReactions?: RedisOptionsSource; meilisearch?: { host: string; port: string; @@ -72,6 +73,11 @@ type Source = { maxFileSize?: number; maxNoteLength?: number; + maxCwLength?: number; + maxRemoteCwLength?: number; + maxRemoteNoteLength?: number; + maxAltTextLength?: number; + maxRemoteAltTextLength?: number; clusterLimit?: number; @@ -146,8 +152,13 @@ export type Config = { proxySmtp: string | undefined; proxyBypassHosts: string[] | undefined; allowedPrivateNetworks: string[] | undefined; - maxFileSize: number | undefined; + maxFileSize: number; maxNoteLength: number; + maxRemoteNoteLength: number; + maxCwLength: number; + maxRemoteCwLength: number; + maxAltTextLength: number; + maxRemoteAltTextLength: number; clusterLimit: number | undefined; id: string; outgoingAddress: string | undefined; @@ -177,8 +188,10 @@ export type Config = { authUrl: string; driveUrl: string; userAgent: string; - clientEntry: string; - clientManifestExists: boolean; + frontendEntry: string; + frontendManifestExists: boolean; + frontendEmbedEntry: string; + frontendEmbedManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; videoThumbnailGenerator: string | null; @@ -186,6 +199,7 @@ export type Config = { redisForPubsub: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource; redisForTimelines: RedisOptions & RedisOptionsSource; + redisForReactions: RedisOptions & RedisOptionsSource; sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined; sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined; perChannelMaxNoteCacheCount: number; @@ -219,10 +233,15 @@ const path = process.env.MISSKEY_CONFIG_YML export function loadConfig(): Config { const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8')); - const clientManifestExists = fs.existsSync(`${_dirname}/../../../built/_vite_/manifest.json`); - const clientManifest = clientManifestExists ? - JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) + + const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json'); + const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json'); + const frontendManifest = frontendManifestExists ? + JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8')) : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; + const frontendEmbedManifest = frontendEmbedManifestExists ? + JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8')) + : { 'src/boot.ts': { file: 'src/boot.ts' } }; const configFiles = globSync(path).sort(); @@ -282,6 +301,7 @@ export function loadConfig(): Config { redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, + redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis, sentryForBackend: config.sentryForBackend, sentryForFrontend: config.sentryForFrontend, id: config.id, @@ -289,8 +309,13 @@ export function loadConfig(): Config { proxySmtp: config.proxySmtp, proxyBypassHosts: config.proxyBypassHosts, allowedPrivateNetworks: config.allowedPrivateNetworks, - maxFileSize: config.maxFileSize, + maxFileSize: config.maxFileSize ?? 262144000, maxNoteLength: config.maxNoteLength ?? 3000, + maxRemoteNoteLength: config.maxRemoteNoteLength ?? 100000, + maxCwLength: config.maxCwLength ?? 500, + maxRemoteCwLength: config.maxRemoteCwLength ?? 5000, + maxAltTextLength: config.maxAltTextLength ?? 20000, + maxRemoteAltTextLength: config.maxRemoteAltTextLength ?? 100000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, outgoingAddressFamily: config.outgoingAddressFamily, @@ -313,8 +338,10 @@ export function loadConfig(): Config { config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator : null, userAgent: `Misskey/${version} (${config.url})`, - clientEntry: clientManifest['src/_boot_.ts'], - clientManifestExists: clientManifestExists, + frontendEntry: frontendManifest['src/_boot_.ts'], + frontendManifestExists: frontendManifestExists, + frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'], + frontendEmbedManifestExists: frontendEmbedManifestExists, perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), @@ -455,7 +482,7 @@ function applyEnvOverrides(config: Source) { _apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]); _apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]); _apply_top([ - ['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines'], + ['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines', 'redisForReactions'], ['host', 'port', 'username', 'pass', 'db', 'prefix'], ]); _apply_top(['meilisearch', ['host', 'port', 'apikey', 'ssl', 'index', 'scope']]); @@ -463,7 +490,7 @@ function applyEnvOverrides(config: Source) { _apply_top(['sentryForBackend', 'enableNodeProfiling']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); - _apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]); + _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]); } diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 02c27779cab436a594488a631d193970b30b46e3..adb0a63ad7a8f11a130474db014b4427241c1d71 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -3,26 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export const MAX_NOTE_TEXT_LENGTH = 3000; - export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days -//#region hard limits -// If you change DB_* values, you must also change the DB schema. - -/** - * Maximum note text length that can be stored in DB. - * Surrogate pairs count as one - */ -export const DB_MAX_NOTE_TEXT_LENGTH = 8192; - -/** - * Maximum image description length that can be stored in DB. - * Surrogate pairs count as one - */ -export const DB_MAX_IMAGE_COMMENT_LENGTH = 8192; -//#endregion +export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; // ブラウザã§ç›´æŽ¥è¡¨ç¤ºã™ã‚‹ã“ã¨ã‚’許å¯ã™ã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã®ç¨®é¡žã®ãƒªã‚¹ãƒˆ // ã“ã“ã«å«ã¾ã‚Œãªã„ã‚‚ã®ã¯ application/octet-stream ã¨ã—ã¦ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã•ã‚Œã‚‹ diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts index 7be53358856454c3bacd81b45cc9a09bd4d99020..fe2c63e7d6467d81b24e76e09243e8723820900b 100644 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -14,10 +14,10 @@ import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient, MiAbuseUserReport, + MiMeta, MiUser, } from '@/models/_.js'; import { EmailService } from '@/core/EmailService.js'; -import { MetaService } from '@/core/MetaService.js'; import { RoleService } from '@/core/RoleService.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -27,15 +27,19 @@ import { IdService } from './IdService.js'; @Injectable() export class AbuseReportNotificationService implements OnApplicationShutdown { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.abuseReportNotificationRecipientRepository) private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, + private idService: IdService, private roleService: RoleService, private systemWebhookService: SystemWebhookService, private emailService: EmailService, - private metaService: MetaService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { @@ -93,10 +97,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { .filter(x => x != null), ); - // é€ä¿¡å…ˆã®é®®åº¦ã‚’ä¿ã¤ãŸã‚ã€æ¯Žå›žå–å¾—ã™ã‚‹ - const meta = await this.metaService.fetch(true); recipientEMailAddresses.push( - ...(meta.email ? [meta.email] : []), + ...(this.meta.email ? [this.meta.email] : []), ); if (recipientEMailAddresses.length <= 0) { diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index b6b591d24036e7106134f1612a400cf815318872..6e3125044c6845b693344530c09e80c7a513acaf 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import { IdService } from '@/core/IdService.js'; @@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { MetaService } from '@/core/MetaService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; @Injectable() export class AccountMoveService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -57,7 +59,6 @@ export class AccountMoveService { private perUserFollowingChart: PerUserFollowingChart, private federatedInstanceService: FederatedInstanceService, private instanceChart: InstanceChart, - private metaService: MetaService, private relayService: RelayService, private queueService: QueueService, ) { @@ -276,7 +277,7 @@ export class AccountMoveService { if (this.userEntityService.isRemoteUser(oldAccount)) { this.federatedInstanceService.fetch(oldAccount.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, false); } }); diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 89e475b5f1d3d428942529cb5fa8e664e787412a..ec9ace417e858e7c03dd74bc64ac7fbf8bb872e9 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown { if (antenna.src === 'home') { // TODO } else if (antenna.src === 'list') { - const listUsers = (await this.userListMembershipsRepository.findBy({ - userListId: antenna.userListId!, - })).map(x => x.userId); - - if (!listUsers.includes(note.userId)) return false; + if (antenna.userListId == null) return false; + const exists = await this.userListMembershipsRepository.exists({ + where: { + userListId: antenna.userListId, + userId: note.userId, + }, + }); + if (!exists) return false; } else if (antenna.src === 'users') { const accts = antenna.users.map(x => { const { username, host } = Acct.parse(x); diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index f6b7955cd20778afd69b8e7e4015a4deb2f4ff45..4be45dabb8f6f74e646a97b6f2805972de6bd5bd 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -10,6 +10,7 @@ import { bindThis } from '@/decorators.js'; type CaptchaResponse = { success: boolean; 'error-codes'?: string[]; + 'errors'?: string[]; }; @Injectable() @@ -73,6 +74,35 @@ export class CaptchaService { } } + @bindThis + public async verifyFriendlyCaptcha(secret: string, response: string | null | undefined): Promise<void> { + if (response == null) { + throw new Error('frc-failed: no response provided'); + } + + const result = await this.httpRequestService.send('https://api.friendlycaptcha.com/api/v1/siteverify', { + method: 'POST', + body: JSON.stringify({ + secret: secret, + solution: response, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (result.status !== 200) { + throw new Error('frc-failed: frc didn\'t return 200 OK'); + } + + const resp = await result.json() as CaptchaResponse; + + if (resp.success !== true) { + const errorCodes = resp['errors'] ? resp['errors'].join(', ') : ''; + throw new Error(`frc-failed: ${errorCodes}`); + } + } + // https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go @bindThis public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> { diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 7e51d3afa468093455f38edf4db0de285491d2ba..c083068392a98ae76ec0bdc0b096b1c56963ffbd 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -13,6 +13,7 @@ import { import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { UserSearchService } from '@/core/UserSearchService.js'; +import { WebhookTestService } from '@/core/WebhookTestService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AnnouncementService } from './AnnouncementService.js'; @@ -42,6 +43,7 @@ import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; import { NoteEditService } from './NoteEditService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; +import { LatestNoteService } from './LatestNoteService.js'; import { NotePiningService } from './NotePiningService.js'; import { NoteReadService } from './NoteReadService.js'; import { NotificationService } from './NotificationService.js'; @@ -49,6 +51,7 @@ import { PollService } from './PollService.js'; import { PushNotificationService } from './PushNotificationService.js'; import { QueryService } from './QueryService.js'; import { ReactionService } from './ReactionService.js'; +import { ReactionsBufferingService } from './ReactionsBufferingService.js'; import { RelayService } from './RelayService.js'; import { RoleService } from './RoleService.js'; import { S3Service } from './S3Service.js'; @@ -149,6 +152,7 @@ import { ApQuestionService } from './activitypub/models/ApQuestionService.js'; import { QueueModule } from './QueueModule.js'; import { QueueService } from './QueueService.js'; import { LoggerService } from './LoggerService.js'; +import { SponsorsService } from './SponsorsService.js'; import type { Provider } from '@nestjs/common'; //#region æ–‡å—列ベースã§ã®injection用(循環å‚照対応ã®ãŸã‚) @@ -184,6 +188,7 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; const $NoteEditService: Provider = { provide: 'NoteEditService', useExisting: NoteEditService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; +const $LatestNoteService: Provider = { provide: 'LatestNoteService', useExisting: LatestNoteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; @@ -192,6 +197,7 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService }; const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService }; const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService }; +const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService }; const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService }; const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService }; const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service }; @@ -211,6 +217,7 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService }; const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService }; +const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; @@ -295,6 +302,8 @@ const $ApPersonService: Provider = { provide: 'ApPersonService', useExisting: Ap const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting: ApQuestionService }; //#endregion +const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: SponsorsService }; + @Module({ imports: [ QueueModule, @@ -332,6 +341,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteEditService, NoteDeleteService, + LatestNoteService, NotePiningService, NoteReadService, NotificationService, @@ -340,6 +350,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PushNotificationService, QueryService, ReactionService, + ReactionsBufferingService, RelayService, RoleService, S3Service, @@ -359,6 +370,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting VideoProcessingService, UserWebhookService, SystemWebhookService, + WebhookTestService, UtilityService, FileInfoService, SearchService, @@ -443,6 +455,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApQuestionService, QueueService, + SponsorsService, + //#region æ–‡å—列ベースã§ã®injection用(循環å‚照対応ã®ãŸã‚) $LoggerService, $AbuseReportService, @@ -476,6 +490,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteEditService, $NoteDeleteService, + $LatestNoteService, $NotePiningService, $NoteReadService, $NotificationService, @@ -484,6 +499,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PushNotificationService, $QueryService, $ReactionService, + $ReactionsBufferingService, $RelayService, $RoleService, $S3Service, @@ -503,6 +519,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $VideoProcessingService, $UserWebhookService, $SystemWebhookService, + $WebhookTestService, $UtilityService, $FileInfoService, $SearchService, @@ -586,6 +603,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApPersonService, $ApQuestionService, //#endregion + + $SponsorsService, ], exports: [ QueueModule, @@ -621,6 +640,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteCreateService, NoteEditService, NoteDeleteService, + LatestNoteService, NotePiningService, NoteReadService, NotificationService, @@ -629,6 +649,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PushNotificationService, QueryService, ReactionService, + ReactionsBufferingService, RelayService, RoleService, S3Service, @@ -648,6 +669,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting VideoProcessingService, UserWebhookService, SystemWebhookService, + WebhookTestService, UtilityService, FileInfoService, SearchService, @@ -731,6 +753,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ApQuestionService, QueueService, + SponsorsService, + //#region æ–‡å—列ベースã§ã®injection用(循環å‚照対応ã®ãŸã‚) $LoggerService, $AbuseReportService, @@ -764,6 +788,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteCreateService, $NoteEditService, $NoteDeleteService, + $LatestNoteService, $NotePiningService, $NoteReadService, $NotificationService, @@ -772,6 +797,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PushNotificationService, $QueryService, $ReactionService, + $ReactionsBufferingService, $RelayService, $RoleService, $S3Service, @@ -791,6 +817,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $VideoProcessingService, $UserWebhookService, $SystemWebhookService, + $WebhookTestService, $UtilityService, $FileInfoService, $SearchService, @@ -873,6 +900,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $ApPersonService, $ApQuestionService, //#endregion + + $SponsorsService, ], }) export class CoreModule { } diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 7f1b8f3efb0feb917919d0f4b3346fb25a9d2e19..8408e958636ca6ea3149b821b1588385ed264d25 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -13,6 +13,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; @Injectable() export class DeleteAccountService { @@ -38,6 +39,7 @@ export class DeleteAccountService { }, moderator?: MiUser): Promise<void> { const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); if (_user.isRoot) throw new Error('cannot delete a root account'); + if (isSystemAccount(_user)) throw new Error('cannot delete a system account'); if (moderator != null) { this.moderationLogService.log(moderator, 'deleteAccount', { diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 83452845d46348ca8aa33d202482c3738f029794..0e992f05ded5618f325a0dd6695a5f617c8caf05 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -42,7 +42,7 @@ export class DownloadService { const timeout = options.timeout ?? 30 * 1000; const operationTimeout = options.operationTimeout ?? 60 * 1000; - const maxSize = options.maxSize ?? this.config.maxFileSize ?? 262144000; + const maxSize = options.maxSize ?? this.config.maxFileSize; const urlObj = new URL(url); let filename = urlObj.pathname.split('/').pop() ?? 'untitled'; diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 46fa4243a7d518aa613d1c04f718f39916023c71..086f2f94d54ba0b93364c1f88ade5a5599940870 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiDriveFile } from '@/models/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @@ -99,6 +98,9 @@ export class DriveService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -115,7 +117,6 @@ export class DriveService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private idService: IdService, - private metaService: MetaService, private downloadService: DownloadService, private internalStorageService: InternalStorageService, private s3Service: S3Service, @@ -149,9 +150,7 @@ export class DriveService { // thunbnail, webpublic ã‚’å¿…è¦ãªã‚‰ç”Ÿæˆ const alts = await this.generateAlts(path, type, !file.uri); - const meta = await this.metaService.fetch(); - - if (meta.useObjectStorage) { + if (this.meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); @@ -170,11 +169,11 @@ export class DriveService { ext = ''; } - const baseUrl = meta.objectStorageBaseUrl - ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; + const baseUrl = this.meta.objectStorageBaseUrl + ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`; // for original - const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`; + const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -191,7 +190,7 @@ export class DriveService { ]; if (alts.webpublic) { - webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; + webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); @@ -199,7 +198,7 @@ export class DriveService { } if (alts.thumbnail) { - thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; + thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -228,25 +227,33 @@ export class DriveService { const thumbnailAccessKey = 'thumbnail-' + randomUUID(); const webpublicAccessKey = 'webpublic-' + randomUUID(); - const url = this.internalStorageService.saveFromPath(accessKey, path); - - let thumbnailUrl: string | null = null; - let webpublicUrl: string | null = null; + // Ugly type is just to help TS figure out that 2nd / 3rd promises are optional. + const promises: [Promise<string>, ...(Promise<string> | undefined)[]] = [ + this.internalStorageService.saveFromPath(accessKey, path), + ]; if (alts.thumbnail) { - thumbnailUrl = this.internalStorageService.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data); - this.registerLogger.info(`thumbnail stored: ${thumbnailAccessKey}`); + promises.push(this.internalStorageService.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data)); } if (alts.webpublic) { - webpublicUrl = this.internalStorageService.saveFromBuffer(webpublicAccessKey, alts.webpublic.data); + promises.push(this.internalStorageService.saveFromBuffer(webpublicAccessKey, alts.webpublic.data)); + } + + const [url, thumbnailUrl, webpublicUrl] = await Promise.all(promises); + + if (thumbnailUrl) { + this.registerLogger.info(`thumbnail stored: ${thumbnailAccessKey}`); + } + + if (webpublicUrl) { this.registerLogger.info(`web stored: ${webpublicAccessKey}`); } file.storedInternal = true; file.url = url; - file.thumbnailUrl = thumbnailUrl; - file.webpublicUrl = webpublicUrl; + file.thumbnailUrl = thumbnailUrl ?? null; + file.webpublicUrl = webpublicUrl ?? null; file.accessKey = accessKey; file.thumbnailAccessKey = thumbnailAccessKey; file.webpublicAccessKey = webpublicAccessKey; @@ -376,10 +383,8 @@ export class DriveService { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; - const meta = await this.metaService.fetch(); - const params = { - Bucket: meta.objectStorageBucket, + Bucket: this.meta.objectStorageBucket, Key: key, Body: stream, ContentType: type, @@ -392,9 +397,9 @@ export class DriveService { // 許å¯ã•ã‚Œã¦ã„るファイル形å¼ã§ã—ã‹æ‹¡å¼µåã‚’ã¤ã‘ãªã„ ext ? correctFilename(filename, ext) : filename, ); - if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; + if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - await this.s3Service.upload(meta, params) + await this.s3Service.upload(this.meta, params) .then( result => { if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput @@ -463,9 +468,7 @@ export class DriveService { requestHeaders = null, ext = null, }: AddFileArgs): Promise<MiDriveFile> { - const instance = await this.metaService.fetch(); const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; - const info = await this.fileInfoService.getFileInfo(path); this.registerLogger.info(`${JSON.stringify(info)}`); @@ -565,11 +568,11 @@ export class DriveService { file.maybeSensitive = info.sensitive; file.maybePorn = info.porn; file.isSensitive = user - ? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : + ? this.userEntityService.isLocalUser(user) && (profile!.alwaysMarkNsfw || profile!.defaultSensitive) ? true : sensitive ?? false : false; - if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true; + if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; @@ -631,7 +634,7 @@ export class DriveService { // ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ this.perUserDriveChart.update(file, true); } else { - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, true); } } @@ -725,19 +728,19 @@ export class DriveService { @bindThis public async deleteFileSync(file: MiDriveFile, isExpired = false, deleter?: MiUser) { + const promises = []; + if (file.storedInternal) { - this.internalStorageService.del(file.accessKey!); + promises.push(this.internalStorageService.del(file.accessKey!)); if (file.thumbnailUrl) { - this.internalStorageService.del(file.thumbnailAccessKey!); + promises.push(this.internalStorageService.del(file.thumbnailAccessKey!)); } if (file.webpublicUrl) { - this.internalStorageService.del(file.webpublicAccessKey!); + promises.push(this.internalStorageService.del(file.webpublicAccessKey!)); } } else if (!file.isLink) { - const promises = []; - promises.push(this.deleteObjectStorageFile(file.accessKey!)); if (file.thumbnailUrl) { @@ -747,10 +750,10 @@ export class DriveService { if (file.webpublicUrl) { promises.push(this.deleteObjectStorageFile(file.webpublicAccessKey!)); } - - await Promise.all(promises); } + await Promise.all(promises); + this.deletePostProcess(file, isExpired, deleter); } @@ -778,7 +781,7 @@ export class DriveService { // ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ this.perUserDriveChart.update(file, false); } else { - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, false); } } @@ -800,14 +803,13 @@ export class DriveService { @bindThis public async deleteObjectStorageFile(key: string) { - const meta = await this.metaService.fetch(); try { const param = { - Bucket: meta.objectStorageBucket, + Bucket: this.meta.objectStorageBucket, Key: key, } as DeleteObjectCommandInput; - await this.s3Service.delete(meta, param); + await this.s3Service.delete(this.meta, param); } catch (err: any) { if (err.name === 'NoSuchKey') { this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 435dbbae28a95f62e645aac738605e2eaea45d1c..a176474b953f6a14ecdd7a68645be8c4cee5aba6 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -5,18 +5,17 @@ import { URLSearchParams } from 'node:url'; import * as nodemailer from 'nodemailer'; +import juice from 'juice'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; -import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { QueueService } from '@/core/QueueService.js'; @Injectable() export class EmailService { @@ -26,49 +25,41 @@ export class EmailService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, private loggerService: LoggerService, private utilityService: UtilityService, private httpRequestService: HttpRequestService, - private queueService: QueueService, ) { this.logger = this.loggerService.getLogger('email'); } @bindThis public async sendEmail(to: string, subject: string, html: string, text: string) { - const meta = await this.metaService.fetch(true); - - if (!meta.enableEmail) return; + if (!this.meta.enableEmail) return; const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${this.config.url}/settings/email`; - const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; + const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== ''; const transporter = nodemailer.createTransport({ - host: meta.smtpHost, - port: meta.smtpPort, - secure: meta.smtpSecure, + host: this.meta.smtpHost, + port: this.meta.smtpPort, + secure: this.meta.smtpSecure, ignoreTLS: !enableAuth, proxy: this.config.proxySmtp, auth: enableAuth ? { - user: meta.smtpUser, - pass: meta.smtpPass, + user: this.meta.smtpUser, + pass: this.meta.smtpPass, } : undefined, } as any); - try { - // TODO: htmlサニタイズ - const info = await transporter.sendMail({ - from: meta.email!, - to: to, - subject: subject, - text: text, - html: `<!doctype html> + const htmlContent = `<!doctype html> <html> <head> <meta charset="utf-8"> @@ -133,7 +124,7 @@ export class EmailService { <body> <main> <header> - <img src="${ meta.logoImageUrl ?? meta.iconUrl ?? iconUrl }"/> + <img src="${ this.meta.logoImageUrl ?? this.meta.iconUrl ?? iconUrl }"/> </header> <article> <h1>${ subject }</h1> @@ -147,7 +138,18 @@ export class EmailService { <a href="${ this.config.url }">${ this.config.host }</a> </nav> </body> -</html>`, +</html>`; + + const inlinedHtml = juice(htmlContent); + + try { + // TODO: htmlサニタイズ + const info = await transporter.sendMail({ + from: this.meta.email!, + to: to, + subject: subject, + text: text, + html: inlinedHtml, }); this.logger.info(`Message sent: ${info.messageId}`); @@ -162,8 +164,6 @@ export class EmailService { available: boolean; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; }> { - const meta = await this.metaService.fetch(); - const exist = await this.userProfilesRepository.countBy({ emailVerified: true, email: emailAddress, @@ -181,11 +181,11 @@ export class EmailService { reason?: string | null, } = { valid: true, reason: null }; - if (meta.enableActiveEmailValidation) { - if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { - validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); - } else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) { - validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey); + if (this.meta.enableActiveEmailValidation) { + if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) { + validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey); + } else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) { + validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey); } else { validated = await validateEmail({ email: emailAddress, @@ -215,7 +215,7 @@ export class EmailService { } const emailDomain: string = emailAddress.split('@')[1]; - const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain); + const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain); if (isBanned) { return { diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index 7aeeb781786c8b60d7d00b091eebcf0902ee73cd..7ec565557c5dd1bc702f4086b1d121893be9e281 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -12,6 +12,8 @@ import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { QueryFailedError } from 'typeorm'; +import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @Injectable() export class FederatedInstanceService implements OnApplicationShutdown { @@ -56,11 +58,24 @@ export class FederatedInstanceService implements OnApplicationShutdown { const index = await this.instancesRepository.findOneBy({ host }); if (index == null) { - const i = await this.instancesRepository.insertOne({ - id: this.idService.gen(), - host, - firstRetrievedAt: new Date(), - }); + let i; + try { + i = await this.instancesRepository.insertOne({ + id: this.idService.gen(), + host, + firstRetrievedAt: new Date(), + }); + } catch (e: unknown) { + if (e instanceof QueryFailedError) { + if (isDuplicateKeyValueError(e)) { + i = await this.instancesRepository.findOneBy({ host }); + } + } + + if (i == null) { + throw e; + } + } this.federatedInstanceCache.set(host, i); return i; diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 753011cded8983ada947cb70c5d469a157dab18d..211c22bfaf6fbd5335ac617da3a69f266a1a4f4d 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -246,7 +246,7 @@ export interface InternalEventTypes { avatarDecorationCreated: MiAvatarDecoration; avatarDecorationDeleted: MiAvatarDecoration; avatarDecorationUpdated: MiAvatarDecoration; - metaUpdated: MiMeta; + metaUpdated: { before?: MiMeta; after: MiMeta; }; followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; updateUserProfile: MiUserProfile; diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index eb192ee6dafca28d2ce0945975daf520c1657778..793bbeecb16242a765e27e316e608051d6f5d1b6 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; import type { MiHashtag } from '@/models/Hashtag.js'; -import type { HashtagsRepository } from '@/models/_.js'; +import type { HashtagsRepository, MiMeta } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; @Injectable() export class HashtagService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.redis) private redisClient: Redis.Redis, // TODO: 専用ã®Redisサーãƒãƒ¼ã‚’è¨å®šã§ãるよã†ã«ã™ã‚‹ @@ -29,7 +31,6 @@ export class HashtagService { private userEntityService: UserEntityService, private featuredService: FeaturedService, private idService: IdService, - private metaService: MetaService, private utilityService: UtilityService, ) { } @@ -160,10 +161,9 @@ export class HashtagService { @bindThis public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise<void> { - const instance = await this.metaService.fetch(); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); + const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t)); if (hiddenTags.includes(hashtag)) return; - if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return; + if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 4fb8a93e4916400bdf153493c69b0c1505562fd8..f7371f8e79defbcd165af4df8cecb4eda2008459 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -4,6 +4,7 @@ */ import * as fs from 'node:fs'; +import { copyFile, mkdir, unlink, writeFile } from 'node:fs/promises'; import * as Path from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; @@ -23,6 +24,8 @@ export class InternalStorageService { @Inject(DI.config) private config: Config, ) { + // No one should erase the working directory *while the server is running*. + fs.mkdirSync(path, { recursive: true }); } @bindThis @@ -36,21 +39,19 @@ export class InternalStorageService { } @bindThis - public saveFromPath(key: string, srcPath: string) { - fs.mkdirSync(path, { recursive: true }); - fs.copyFileSync(srcPath, this.resolvePath(key)); + public async saveFromPath(key: string, srcPath: string): Promise<string> { + await copyFile(srcPath, this.resolvePath(key)); return `${this.config.url}/files/${key}`; } @bindThis - public saveFromBuffer(key: string, data: Buffer) { - fs.mkdirSync(path, { recursive: true }); - fs.writeFileSync(this.resolvePath(key), data); + public async saveFromBuffer(key: string, data: Buffer): Promise<string> { + await writeFile(this.resolvePath(key), data); return `${this.config.url}/files/${key}`; } @bindThis - public del(key: string) { - fs.unlink(this.resolvePath(key), () => {}); + public async del(key: string): Promise<void> { + await unlink(this.resolvePath(key)); } } diff --git a/packages/backend/src/core/LatestNoteService.ts b/packages/backend/src/core/LatestNoteService.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3798055066fd14ae5245eb85c1d27c8f0fd025c --- /dev/null +++ b/packages/backend/src/core/LatestNoteService.ts @@ -0,0 +1,139 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Not } from 'typeorm'; +import { MiNote } from '@/models/Note.js'; +import { isPureRenote } from '@/misc/is-renote.js'; +import { SkLatestNote } from '@/models/LatestNote.js'; +import { DI } from '@/di-symbols.js'; +import type { LatestNotesRepository, NotesRepository } from '@/models/_.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import Logger from '@/logger.js'; + +@Injectable() +export class LatestNoteService { + private readonly logger: Logger; + + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.latestNotesRepository) + private latestNotesRepository: LatestNotesRepository, + + loggerService: LoggerService, + ) { + this.logger = loggerService.getLogger('LatestNoteService'); + } + + handleUpdatedNoteBG(before: MiNote, after: MiNote): void { + this + .handleUpdatedNote(before, after) + .catch(err => this.logger.error('Unhandled exception while updating latest_note (after update):', err)); + } + + async handleUpdatedNote(before: MiNote, after: MiNote): Promise<void> { + // If the key didn't change, then there's nothing to update + if (SkLatestNote.areEquivalent(before, after)) return; + + // Simulate update as delete + create + await this.handleDeletedNote(before); + await this.handleCreatedNote(after); + } + + handleCreatedNoteBG(note: MiNote): void { + this + .handleCreatedNote(note) + .catch(err => this.logger.error('Unhandled exception while updating latest_note (after create):', err)); + } + + async handleCreatedNote(note: MiNote): Promise<void> { + // Ignore DMs. + // Followers-only posts are *included*, as this table is used to back the "following" feed. + if (note.visibility === 'specified') return; + + // Ignore pure renotes + if (isPureRenote(note)) return; + + // Compute the compound key of the entry to check + const key = SkLatestNote.keyFor(note); + + // Make sure that this isn't an *older* post. + // We can get older posts through replies, lookups, updates, etc. + const currentLatest = await this.latestNotesRepository.findOneBy(key); + if (currentLatest != null && currentLatest.noteId >= note.id) return; + + // Record this as the latest note for the given user + const latestNote = new SkLatestNote({ + ...key, + noteId: note.id, + }); + await this.latestNotesRepository.upsert(latestNote, ['userId', 'isPublic', 'isReply', 'isQuote']); + } + + handleDeletedNoteBG(note: MiNote): void { + this + .handleDeletedNote(note) + .catch(err => this.logger.error('Unhandled exception while updating latest_note (after delete):', err)); + } + + async handleDeletedNote(note: MiNote): Promise<void> { + // If it's a DM, then it can't possibly be the latest note so we can safely skip this. + if (note.visibility === 'specified') return; + + // If it's a pure renote, then it can't possibly be the latest note so we can safely skip this. + if (isPureRenote(note)) return; + + // Compute the compound key of the entry to check + const key = SkLatestNote.keyFor(note); + + // Check if the deleted note was possibly the latest for the user + const existingLatest = await this.latestNotesRepository.findOneBy(key); + if (existingLatest == null || existingLatest.noteId !== note.id) return; + + // Find the newest remaining note for the user. + // We exclude DMs and pure renotes. + const nextLatest = await this.notesRepository + .createQueryBuilder('note') + .select() + .where({ + userId: key.userId, + visibility: key.isPublic + ? 'public' + : Not('specified'), + replyId: key.isReply + ? Not(null) + : null, + renoteId: key.isQuote + ? Not(null) + : null, + }) + .andWhere(` + ( + note."renoteId" IS NULL + OR note.text IS NOT NULL + OR note.cw IS NOT NULL + OR note."replyId" IS NOT NULL + OR note."hasPoll" + OR note."fileIds" != '{}' + ) + `) + .orderBy({ id: 'DESC' }) + .getOne(); + if (!nextLatest) return; + + // Record it as the latest + const latestNote = new SkLatestNote({ + ...key, + noteId: nextLatest.id, + }); + + // When inserting the latest note, it's possible that another worker has "raced" the insert and already added a newer note. + // We must use orIgnore() to ensure that the query ignores conflicts, otherwise an exception may be thrown. + await this.latestNotesRepository + .createQueryBuilder('latest') + .insert() + .into(SkLatestNote) + .values(latestNote) + .orIgnore() + .execute(); + } +} diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index ec630f804e7cf3406fde4e9a314560f32745ad95..3d88d0aefe97a23e999ac6d6afe43404cc279f54 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -52,7 +52,7 @@ export class MetaService implements OnApplicationShutdown { switch (type) { case 'metaUpdated': { this.cache = { // TODO: ã“ã®ã‚ãŸã‚Šã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºå‡¦ç†ã¯å„modelファイル内ã«é–¢æ•°ã¨ã—ã¦exportã—ãŸã„ - ...body, + ...(body.after), proxyAccount: null, // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ }; break; @@ -141,7 +141,7 @@ export class MetaService implements OnApplicationShutdown { }); } - this.globalEventService.publishInternalEvent('metaUpdated', updated); + this.globalEventService.publishInternalEvent('metaUpdated', { before, after: updated }); return updated; } diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 94bb5af6b5402a845340df691d623193c203e2ee..42676d6f981bf425abf441700d32dc988fb11b87 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -6,7 +6,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { Window, DocumentFragment, XMLSerializer } from 'happy-dom'; +import { Window, XMLSerializer } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; @@ -412,8 +412,10 @@ export class MfmService { mention: (node) => { const a = doc.createElement('a'); const { username, host, acct } = node.props; - const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); - a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); + const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase()); + a.setAttribute('href', remoteUserInfo + ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) + : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`); a.className = 'u-url mention'; a.textContent = acct; return a; @@ -465,7 +467,7 @@ export class MfmService { const serialized = new XMLSerializer().serializeToString(body); - happyDOM.close().catch(e => {}); + happyDOM.close().catch(err => {}); return serialized; } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index c252336f995c7dde574c1b64e1e85092c8b321bb..1bc4599a602200a1bede36d9381aed4e04400188 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -8,13 +8,12 @@ import * as mfm from '@transfem-org/sfm-js'; import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import RE2 from 're2'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -23,11 +22,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { IPoll } from '@/models/Poll.js'; import { MiPoll } from '@/models/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; import type { MiChannel } from '@/models/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MemorySingleCache } from '@/misc/cache.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -49,9 +45,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; @@ -62,6 +56,8 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { LatestNoteService } from '@/core/LatestNoteService.js'; +import { CollapsedQueue } from '@/misc/collapsed-queue.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -153,11 +149,15 @@ type Option = { @Injectable() export class NoteCreateService implements OnApplicationShutdown { #shutdownController = new AbortController(); + private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>; constructor( @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.db) private db: DataSource, @@ -212,7 +212,6 @@ export class NoteCreateService implements OnApplicationShutdown { private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, - private metaService: MetaService, private searchService: SearchService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, @@ -221,7 +220,10 @@ export class NoteCreateService implements OnApplicationShutdown { private utilityService: UtilityService, private userBlockingService: UserBlockingService, private cacheService: CacheService, - ) { } + private latestNoteService: LatestNoteService, + ) { + this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); + } @bindThis public async create(user: { @@ -254,10 +256,8 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.visibleUsers = []; if (data.channel != null) data.localOnly = true; - const meta = await this.metaService.fetch(); - if (data.visibility === 'public' && data.channel == null) { - const sensitiveWords = meta.sensitiveWords; + const sensitiveWords = this.meta.sensitiveWords; if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { @@ -265,17 +265,17 @@ export class NoteCreateService implements OnApplicationShutdown { } } - const hasProhibitedWords = await this.checkProhibitedWordsContain({ + const hasProhibitedWords = this.checkProhibitedWordsContain({ cw: data.cw, text: data.text, pollChoices: data.poll?.choices, - }, meta.prohibitedWords); + }, this.meta.prohibitedWords); if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } - const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); + const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host); if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { data.visibility = 'home'; @@ -334,9 +334,13 @@ export class NoteCreateService implements OnApplicationShutdown { data.localOnly = true; } + const maxTextLength = user.host == null + ? this.config.maxNoteLength + : this.config.maxRemoteNoteLength; + if (data.text) { - if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { - data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + if (data.text.length > maxTextLength) { + data.text = data.text.slice(0, maxTextLength); } data.text = data.text.trim(); if (data.text === '') { @@ -346,6 +350,22 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = null; } + const maxCwLength = user.host == null + ? this.config.maxCwLength + : this.config.maxRemoteCwLength; + + if (data.cw) { + if (data.cw.length > maxCwLength) { + data.cw = data.cw.slice(0, maxCwLength); + } + data.cw = data.cw.trim(); + if (data.cw === '') { + data.cw = null; + } + } else { + data.cw = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -368,7 +388,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // if the host is media-silenced, custom emojis are not allowed - if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; + if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = []; tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); @@ -537,10 +557,8 @@ export class NoteCreateService implements OnApplicationShutdown { isBot: MiUser['isBot']; noindex: MiUser['noindex']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { - const meta = await this.metaService.fetch(); - this.notesChart.update(note, true); - if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) { + if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) { this.perUserNotesChart.update(user, note, true); } @@ -548,11 +566,11 @@ export class NoteCreateService implements OnApplicationShutdown { if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { if (note.renote && note.text) { - this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); + this.updateNotesCountQueue.enqueue(i.id, 1); } else if (!note.renote) { - this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); + this.updateNotesCountQueue.enqueue(i.id, 1); } - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, true); } }); @@ -560,7 +578,7 @@ export class NoteCreateService implements OnApplicationShutdown { // ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°æ›´æ–° if (data.visibility === 'public' || data.visibility === 'home') { - if (user.isBot && meta.enableBotTrending) { + if (user.isBot && this.meta.enableBotTrending) { this.hashtagService.updateHashtags(user, tags); } else if (!user.isBot) { this.hashtagService.updateHashtags(user, tags); @@ -796,6 +814,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); } + // Update the Latest Note index / following feed + this.latestNoteService.handleCreatedNoteBG(note); + // Register to search database if (!user.noindex) this.index(note); } @@ -934,15 +955,14 @@ export class NoteCreateService implements OnApplicationShutdown { @bindThis private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { - const meta = await this.metaService.fetch(); - if (!meta.enableFanoutTimeline) return; + if (!this.meta.enableFanoutTimeline) return; const r = this.redisForTimelines.pipeline(); if (note.channelId) { this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -952,9 +972,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -992,9 +1012,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -1011,25 +1031,25 @@ export class NoteCreateService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r); } } // 自分自身ã®HTL if (note.userHost == null) { if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } // 自分自身以外ã¸ã®è¿”ä¿¡ if (isReply(note)) { - this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); @@ -1038,9 +1058,9 @@ export class NoteCreateService implements OnApplicationShutdown { } } } else { - this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { @@ -1099,9 +1119,9 @@ export class NoteCreateService implements OnApplicationShutdown { } } - public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) { + public checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) { if (prohibitedWords == null) { - prohibitedWords = (await this.metaService.fetch()).prohibitedWords; + prohibitedWords = this.meta.prohibitedWords; } if ( @@ -1117,12 +1137,23 @@ export class NoteCreateService implements OnApplicationShutdown { } @bindThis - public dispose(): void { + private collapseNotesCount(oldValue: number, newValue: number) { + return oldValue + newValue; + } + + @bindThis + private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) { + await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy); + } + + @bindThis + public async dispose(): Promise<void> { this.#shutdownController.abort(); + await this.updateNotesCountQueue.performAllNow(); } @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); + public async onApplicationShutdown(signal?: string | undefined): Promise<void> { + await this.dispose(); } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index 7ce6d7c605a73da210d37f9e5999812ddb81e219..285db9f152d33bd0be57a84e342c864365bf6f2c 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -6,8 +6,8 @@ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; -import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; +import { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; +import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -19,12 +19,11 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { LatestNoteService } from '@/core/LatestNoteService.js'; @Injectable() export class NoteDeleteService { @@ -32,6 +31,9 @@ export class NoteDeleteService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -42,18 +44,17 @@ export class NoteDeleteService { private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, - private noteEntityService: NoteEntityService, private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, - private metaService: MetaService, private searchService: SearchService, private moderationLogService: ModerationLogService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, private instanceChart: InstanceChart, + private latestNoteService: LatestNoteService, ) {} /** @@ -109,10 +110,8 @@ export class NoteDeleteService { } //#endregion - const meta = await this.metaService.fetch(); - this.notesChart.update(note, false); - if (meta.enableChartsForRemoteUser || (user.host == null)) { + if (this.meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserNotesChart.update(user, note, false); } @@ -131,7 +130,7 @@ export class NoteDeleteService { } else if (!note.renoteId) { this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); } - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, false); } }); @@ -148,6 +147,8 @@ export class NoteDeleteService { userId: user.id, }); + this.latestNoteService.handleDeletedNoteBG(note); + if (deleter && (note.userId !== deleter.id)) { const user = await this.usersRepository.findOneByOrFail({ id: note.userId }); this.moderationLogService.log(deleter, 'deleteNote', { diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 5ff0f26e2bac1fdaa965159130f25a4121a2febc..d31958e5d4abd3ac2ce5757d2e5b94377b94d009 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -8,13 +8,12 @@ import * as mfm from '@transfem-org/sfm-js'; import { DataSource, In, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import RE2 from 're2'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository, PollsRepository } from '@/models/_.js'; +import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository, PollsRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -40,9 +39,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -52,6 +49,9 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { LatestNoteService } from '@/core/LatestNoteService.js'; +import { CollapsedQueue } from '@/misc/collapsed-queue.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited'; @@ -145,11 +145,15 @@ type Option = { @Injectable() export class NoteEditService implements OnApplicationShutdown { #shutdownController = new AbortController(); + private updateNotesCountQueue: CollapsedQueue<MiNote['id'], number>; constructor( @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.db) private db: DataSource, @@ -207,14 +211,17 @@ export class NoteEditService implements OnApplicationShutdown { private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, - private metaService: MetaService, private searchService: SearchService, private activeUsersChart: ActiveUsersChart, private instanceChart: InstanceChart, private utilityService: UtilityService, private userBlockingService: UserBlockingService, private cacheService: CacheService, - ) { } + private latestNoteService: LatestNoteService, + private noteCreateService: NoteCreateService, + ) { + this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); + } @bindThis public async edit(user: { @@ -247,6 +254,11 @@ export class NoteEditService implements OnApplicationShutdown { data.reply = undefined; } + // changing visibility on an edit is ill-defined, let's try to + // keep the same visibility as the original note + data.visibility = oldnote.visibility; + data.localOnly = oldnote.localOnly; + // ãƒãƒ£ãƒ³ãƒãƒ«å¤–ã«ãƒªãƒ—ライã—ãŸã‚‰å¯¾è±¡ã®ã‚¹ã‚³ãƒ¼ãƒ—ã«åˆã‚ã›ã‚‹ // (クライアントサイドã§ã‚„ã£ã¦ã‚‚良ã„処ç†ã ã¨æ€ã†ã‘ã©ã¨ã‚Šã‚ãˆãšã‚µãƒ¼ãƒãƒ¼ã‚µã‚¤ãƒ‰ã§) if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { @@ -270,10 +282,8 @@ export class NoteEditService implements OnApplicationShutdown { if (data.channel != null) data.localOnly = true; if (data.updatedAt == null) data.updatedAt = new Date(); - const meta = await this.metaService.fetch(); - if (data.visibility === 'public' && data.channel == null) { - const sensitiveWords = meta.sensitiveWords; + const sensitiveWords = this.meta.sensitiveWords; if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { @@ -281,17 +291,17 @@ export class NoteEditService implements OnApplicationShutdown { } } - const hasProhibitedWords = await this.checkProhibitedWordsContain({ + const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw: data.cw, text: data.text, pollChoices: data.poll?.choices, - }, meta.prohibitedWords); + }, this.meta.prohibitedWords); if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } - const inSilencedInstance = this.utilityService.isSilencedHost((meta).silencedHosts, user.host); + const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host); if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { data.visibility = 'home'; @@ -354,9 +364,13 @@ export class NoteEditService implements OnApplicationShutdown { data.localOnly = true; } + const maxTextLength = user.host == null + ? this.config.maxNoteLength + : this.config.maxRemoteNoteLength; + if (data.text) { - if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { - data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + if (data.text.length > maxTextLength) { + data.text = data.text.slice(0, maxTextLength); } data.text = data.text.trim(); if (data.text === '') { @@ -366,6 +380,22 @@ export class NoteEditService implements OnApplicationShutdown { data.text = null; } + const maxCwLength = user.host == null + ? this.config.maxCwLength + : this.config.maxRemoteCwLength; + + if (data.cw) { + if (data.cw.length > maxCwLength) { + data.cw = data.cw.slice(0, maxCwLength); + } + data.cw = data.cw.trim(); + if (data.cw === '') { + data.cw = null; + } + } else { + data.cw = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -388,7 +418,7 @@ export class NoteEditService implements OnApplicationShutdown { } // if the host is media-silenced, custom emojis are not allowed - if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; + if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = []; tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32); @@ -429,9 +459,6 @@ export class NoteEditService implements OnApplicationShutdown { if (data.cw !== oldnote.cw) { update.cw = data.cw; } - if (data.localOnly !== oldnote.localOnly) { - update.localOnly = data.localOnly; - } if (oldnote.hasPoll !== !!data.poll) { update.hasPoll = !!data.poll; } @@ -496,6 +523,7 @@ export class NoteEditService implements OnApplicationShutdown { renoteUserId: data.renote ? data.renote.userId : null, renoteUserHost: data.renote ? data.renote.userHost : null, userHost: user.host, + reactionAndUserPairCache: oldnote.reactionAndUserPairCache, }); if (data.uri != null) note.uri = data.uri; @@ -544,7 +572,7 @@ export class NoteEditService implements OnApplicationShutdown { } setImmediate('post edited', { signal: this.#shutdownController.signal }).then( - () => this.postNoteEdited(note, user, data, silent, tags!, mentionedUsers!), + () => this.postNoteEdited(note, oldnote, user, data, silent, tags!, mentionedUsers!), () => { /* aborted, ignore this */ }, ); @@ -555,7 +583,7 @@ export class NoteEditService implements OnApplicationShutdown { } @bindThis - private async postNoteEdited(note: MiNote, user: { + private async postNoteEdited(note: MiNote, oldNote: MiNote, user: { id: MiUser['id']; username: MiUser['username']; host: MiUser['host']; @@ -565,8 +593,8 @@ export class NoteEditService implements OnApplicationShutdown { // Register host if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { - this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + this.updateNotesCountQueue.enqueue(i.id, 1); + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, true); } }); @@ -752,6 +780,9 @@ export class NoteEditService implements OnApplicationShutdown { }); } + // Update the Latest Note index / following feed + this.latestNoteService.handleUpdatedNoteBG(oldNote, note); + // Register to search database if (!user.noindex) this.index(note); } @@ -852,15 +883,14 @@ export class NoteEditService implements OnApplicationShutdown { @bindThis private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { - const meta = await this.metaService.fetch(); - if (!meta.enableFanoutTimeline) return; + if (!this.meta.enableFanoutTimeline) return; const r = this.redisForTimelines.pipeline(); if (note.channelId) { this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -870,9 +900,9 @@ export class NoteEditService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -910,9 +940,9 @@ export class NoteEditService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -929,25 +959,25 @@ export class NoteEditService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r); } } // 自分自身ã®HTL if (note.userHost == null) { if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } // 自分自身以外ã¸ã®è¿”ä¿¡ if (isReply(note)) { - this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); @@ -956,9 +986,9 @@ export class NoteEditService implements OnApplicationShutdown { } } } else { - this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { @@ -1017,30 +1047,24 @@ export class NoteEditService implements OnApplicationShutdown { } } - public async checkProhibitedWordsContain(content: Parameters<UtilityService['concatNoteContentsForKeyWordCheck']>[0], prohibitedWords?: string[]) { - if (prohibitedWords == null) { - prohibitedWords = (await this.metaService.fetch()).prohibitedWords; - } - - if ( - this.utilityService.isKeyWordIncluded( - this.utilityService.concatNoteContentsForKeyWordCheck(content), - prohibitedWords, - ) - ) { - return true; - } + @bindThis + private collapseNotesCount(oldValue: number, newValue: number) { + return oldValue + newValue; + } - return false; + @bindThis + private async performUpdateNotesCount(id: MiNote['id'], incrBy: number) { + await this.instancesRepository.increment({ id: id }, 'notesCount', incrBy); } @bindThis - public dispose(): void { + public async dispose(): Promise<void> { this.#shutdownController.abort(); + await this.updateNotesCountQueue.performAllNow(); } @bindThis - public onApplicationShutdown(signal?: string | undefined): void { - this.dispose(); + public async onApplicationShutdown(signal?: string | undefined): Promise<void> { + await this.dispose(); } } diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 71d663bf90607b225166505636ee61b8206cea52..c3ff2a68d3357fe13f5b168a4333bed2d892b5d6 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -4,26 +4,25 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsersRepository } from '@/models/_.js'; import type { MiLocalUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class ProxyAccountService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, - - private metaService: MetaService, ) { } @bindThis public async fetch(): Promise<MiLocalUser | null> { - const meta = await this.metaService.fetch(); - if (meta.proxyAccountId == null) return null; - return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser; + if (this.meta.proxyAccountId == null) return null; + return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser; } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 6a845b951de65335336f953c9f4a5b346f1fa9f0..1479bb00d9b814848dff916e5ff13ea486183b84 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -10,8 +10,7 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; -import { MetaService } from '@/core/MetaService.js'; +import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RedisKVCache } from '@/misc/cache.js'; @@ -54,13 +53,14 @@ export class PushNotificationService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, - - private metaService: MetaService, ) { this.subscriptionsCache = new RedisKVCache<MiSwSubscription[]>(this.redisClient, 'userSwSubscriptions', { lifetime: 1000 * 60 * 60 * 1, // 1h @@ -73,14 +73,12 @@ export class PushNotificationService implements OnApplicationShutdown { @bindThis public async pushNotification<T extends keyof PushNotificationsTypes>(userId: string, type: T, body: PushNotificationsTypes[T]) { - const meta = await this.metaService.fetch(); - - if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; + if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return; // アプリケーションã®é€£çµ¡å…ˆã¨ã€ã‚µãƒ¼ãƒãƒ¼ã‚µã‚¤ãƒ‰ã®éµãƒšã‚¢ã®æƒ…å ±ã‚’ç™»éŒ² push.setVapidDetails(this.config.url, - meta.swPublicKey, - meta.swPrivateKey); + this.meta.swPublicKey, + this.meta.swPrivateKey); const subscriptions = await this.subscriptionsCache.fetch(userId); diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index be5f10771a9f4e89c6a4e13c64041b5097a5a1dd..dc13aa21bfd948eae043d5e363168cf666cd710f 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -88,6 +88,12 @@ export class QueueService { repeat: { pattern: '*/5 * * * *' }, removeOnComplete: true, }); + + this.systemQueue.add('bakeBufferedReactions', { + }, { + repeat: { pattern: '0 0 * * *' }, + removeOnComplete: true, + }); } @bindThis @@ -511,10 +517,15 @@ export class QueueService { /** * @see UserWebhookDeliverJobData - * @see WebhookDeliverProcessorService + * @see UserWebhookDeliverProcessorService */ @bindThis - public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) { + public userWebhookDeliver( + webhook: MiWebhook, + type: typeof webhookEventTypes[number], + content: unknown, + opts?: { attempts?: number }, + ) { const data: UserWebhookDeliverJobData = { type, content, @@ -527,7 +538,7 @@ export class QueueService { }; return this.userWebhookDeliverQueue.add(webhook.id, data, { - attempts: 4, + attempts: opts?.attempts ?? 4, backoff: { type: 'custom', }, @@ -538,10 +549,15 @@ export class QueueService { /** * @see SystemWebhookDeliverJobData - * @see WebhookDeliverProcessorService + * @see SystemWebhookDeliverProcessorService */ @bindThis - public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) { + public systemWebhookDeliver( + webhook: MiSystemWebhook, + type: SystemWebhookEventType, + content: unknown, + opts?: { attempts?: number }, + ) { const data: SystemWebhookDeliverJobData = { type, content, @@ -553,7 +569,7 @@ export class QueueService { }; return this.systemWebhookDeliverQueue.add(webhook.id, data, { - attempts: 4, + attempts: opts?.attempts ?? 4, backoff: { type: 'custom', }, diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 17ff1687868ff17e6fc018d68ed5550146660679..0179b0680faa4463ddd1272b8d4edac5649586c0 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -4,9 +4,8 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js'; +import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository, MiMeta } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -21,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; @@ -30,9 +28,10 @@ import { RoleService } from '@/core/RoleService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; const FALLBACK = '\u2764'; -const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; const legacies: Record<string, string> = { 'like': 'ðŸ‘', @@ -71,8 +70,8 @@ const decodeCustomEmojiRegexp = /^:([\p{Letter}\p{Number}\p{Mark}_+-]+)(?:@([\w. @Injectable() export class ReactionService { constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.meta) + private meta: MiMeta, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -90,12 +89,12 @@ export class ReactionService { private emojisRepository: EmojisRepository, private utilityService: UtilityService, - private metaService: MetaService, private customEmojiService: CustomEmojiService, private roleService: RoleService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private userBlockingService: UserBlockingService, + private reactionsBufferingService: ReactionsBufferingService, private idService: IdService, private featuredService: FeaturedService, private globalEventService: GlobalEventService, @@ -108,8 +107,6 @@ export class ReactionService { @bindThis public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { - const meta = await this.metaService.fetch(); - // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -155,7 +152,7 @@ export class ReactionService { } // for media silenced host, custom emoji reactions are not allowed - if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) { + if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) { reaction = FALLBACK; } } else { @@ -177,7 +174,6 @@ export class ReactionService { reaction, }; - // Create reaction try { await this.noteReactionsRepository.insert(record); } catch (e) { @@ -201,16 +197,20 @@ export class ReactionService { } // Increment reactions count - const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; - await this.notesRepository.createQueryBuilder().update() - .set({ - reactions: () => sql, - ...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? { - reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`, - } : {}), - }) - .where('id = :id', { id: note.id }) - .execute(); + if (this.meta.enableReactionsBuffering) { + await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache); + } else { + const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; + await this.notesRepository.createQueryBuilder().update() + .set({ + reactions: () => sql, + ...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? { + reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`, + } : {}), + }) + .where('id = :id', { id: note.id }) + .execute(); + } // 30%ã®ç¢ºçŽ‡ã€ã‚»ãƒ«ãƒ•ã§ã¯ãªã„ã€3日以内ã«æŠ•ç¨¿ã•ã‚ŒãŸãƒŽãƒ¼ãƒˆã®å ´åˆãƒã‚¤ãƒ©ã‚¤ãƒˆç”¨ãƒ©ãƒ³ã‚ング更新 if ( @@ -230,7 +230,7 @@ export class ReactionService { } } - if (meta.enableChartsForRemoteUser || (user.host == null)) { + if (this.meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserReactionsChart.update(user, note); } @@ -317,14 +317,18 @@ export class ReactionService { } // Decrement reactions count - const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; - await this.notesRepository.createQueryBuilder().update() - .set({ - reactions: () => sql, - reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`, - }) - .where('id = :id', { id: note.id }) - .execute(); + if (this.meta.enableReactionsBuffering) { + await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction); + } else { + const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; + await this.notesRepository.createQueryBuilder().update() + .set({ + reactions: () => sql, + reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`, + }) + .where('id = :id', { id: note.id }) + .execute(); + } this.globalEventService.publishNoteStream(note.id, 'unreacted', { reaction: this.decodeReaction(exist.reaction).reaction, @@ -346,8 +350,21 @@ export class ReactionService { } /** - * æ–‡å—列タイプã®ãƒ¬ã‚¬ã‚·ãƒ¼ãªå½¢å¼ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ç¾åœ¨ã®å½¢å¼ã«å¤‰æ›ã—ã¤ã¤ã€ - * データベース上ã«ã¯å˜åœ¨ã™ã‚‹ã€Œ0個ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ãŒã¤ã„ã¦ã„ã‚‹ã€ã¨ã„ã†æƒ…å ±ã‚’å‰Šé™¤ã™ã‚‹ã€‚ + * - æ–‡å—列タイプã®ãƒ¬ã‚¬ã‚·ãƒ¼ãªå½¢å¼ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ç¾åœ¨ã®å½¢å¼ã«å¤‰æ›ã™ã‚‹ + * - ãƒãƒ¼ã‚«ãƒ«ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®ãƒ›ã‚¹ãƒˆã‚’ `@.` ã«ã™ã‚‹ï¼ˆ`decodeReaction()`ã®åŠ¹æžœï¼‰ + */ + @bindThis + public convertLegacyReaction(reaction: string): string { + reaction = this.decodeReaction(reaction).reaction; + if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; + return reaction; + } + + // TODO: å»ƒæ¢ + /** + * - æ–‡å—列タイプã®ãƒ¬ã‚¬ã‚·ãƒ¼ãªå½¢å¼ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ç¾åœ¨ã®å½¢å¼ã«å¤‰æ›ã™ã‚‹ + * - ãƒãƒ¼ã‚«ãƒ«ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®ãƒ›ã‚¹ãƒˆã‚’ `@.` ã«ã™ã‚‹ï¼ˆ`decodeReaction()`ã®åŠ¹æžœï¼‰ + * - データベース上ã«ã¯å˜åœ¨ã™ã‚‹ã€Œ0個ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ãŒã¤ã„ã¦ã„ã‚‹ã€ã¨ã„ã†æƒ…å ±ã‚’å‰Šé™¤ã™ã‚‹ */ @bindThis public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] { @@ -360,10 +377,7 @@ export class ReactionService { return count > 0; }) .map(([reaction, count]) => { - // unchecked indexed access - const convertedReaction = legacies[reaction] as string | undefined; - - const key = this.decodeReaction(convertedReaction ?? reaction).reaction; + const key = this.convertLegacyReaction(reaction); return [key, count] as const; }) @@ -418,11 +432,4 @@ export class ReactionService { host: undefined, }; } - - @bindThis - public convertLegacyReaction(reaction: string): string { - reaction = this.decodeReaction(reaction).reaction; - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - return reaction; - } } diff --git a/packages/backend/src/core/ReactionsBufferingService.ts b/packages/backend/src/core/ReactionsBufferingService.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4207c51062c6eec25c5f7938810095d3d6a2ada --- /dev/null +++ b/packages/backend/src/core/ReactionsBufferingService.ts @@ -0,0 +1,211 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import { DI } from '@/di-symbols.js'; +import type { MiNote } from '@/models/Note.js'; +import { bindThis } from '@/decorators.js'; +import type { MiUser, NotesRepository } from '@/models/_.js'; +import type { Config } from '@/config.js'; +import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas'; +const REDIS_PAIR_PREFIX = 'reactionsBufferPairs'; + +@Injectable() +export class ReactionsBufferingService implements OnApplicationShutdown { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + + @Inject(DI.redisForReactions) + private redisForReactions: Redis.Redis, // TODO: 専用ã®Redisインスタンスã«ã™ã‚‹ + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + ) { + this.redisForSub.on('message', this.onMessage); + } + + @bindThis + private async onMessage(_: string, data: string) { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'metaUpdated': { + // リアクションãƒãƒƒãƒ•ã‚¡ãƒªãƒ³ã‚°ãŒæœ‰åŠ¹â†’無効ã«ãªã£ãŸã‚‰å³bake + if (body.before != null && body.before.enableReactionsBuffering && !body.after.enableReactionsBuffering) { + this.bake(); + } + break; + } + default: + break; + } + } + } + + @bindThis + public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise<void> { + const pipeline = this.redisForReactions.pipeline(); + pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1); + for (let i = 0; i < currentPairs.length; i++) { + pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]); + } + pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`); + pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1)); + await pipeline.exec(); + } + + @bindThis + public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise<void> { + const pipeline = this.redisForReactions.pipeline(); + pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1); + pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`); + // TODO: 「消ã—ãŸè¦ç´ 一覧ã€ã‚‚æŒã£ã¦ãŠã‹ãªã„ã¨createã•ã‚ŒãŸæ™‚ã«ä¸Šæ›¸ãã•ã‚Œã¦å¾©æ´»ã™ã‚‹ + await pipeline.exec(); + } + + @bindThis + public async get(noteId: MiNote['id']): Promise<{ + deltas: Record<string, number>; + pairs: ([MiUser['id'], string])[]; + }> { + const pipeline = this.redisForReactions.pipeline(); + pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`); + pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1); + const results = await pipeline.exec(); + + const resultDeltas = results![0][1] as Record<string, string>; + const resultPairs = results![1][1] as string[]; + + const deltas = {} as Record<string, number>; + for (const [name, count] of Object.entries(resultDeltas)) { + deltas[name] = parseInt(count); + } + + const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]); + + return { + deltas, + pairs, + }; + } + + @bindThis + public async getMany(noteIds: MiNote['id'][]): Promise<Map<MiNote['id'], { + deltas: Record<string, number>; + pairs: ([MiUser['id'], string])[]; + }>> { + const map = new Map<MiNote['id'], { + deltas: Record<string, number>; + pairs: ([MiUser['id'], string])[]; + }>(); + + const pipeline = this.redisForReactions.pipeline(); + for (const noteId of noteIds) { + pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`); + pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1); + } + const results = await pipeline.exec(); + + const opsForEachNotes = 2; + for (let i = 0; i < noteIds.length; i++) { + const noteId = noteIds[i]; + const resultDeltas = results![i * opsForEachNotes][1] as Record<string, string>; + const resultPairs = results![i * opsForEachNotes + 1][1] as string[]; + + const deltas = {} as Record<string, number>; + for (const [name, count] of Object.entries(resultDeltas)) { + deltas[name] = parseInt(count); + } + + const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]); + + map.set(noteId, { + deltas, + pairs, + }); + } + + return map; + } + + // TODO: scanã¯é‡ã„å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§ã€åˆ¥é€” bufferedNoteIds を直接Redis上ã«æŒã£ã¦ãŠã„ã¦ã‚‚ã„ã„ã‹ã‚‚ã—ã‚Œãªã„ + @bindThis + public async bake(): Promise<void> { + const bufferedNoteIds = []; + let cursor = '0'; + do { + // https://github.com/redis/ioredis#transparent-key-prefixing + const result = await this.redisForReactions.scan( + cursor, + 'MATCH', + `${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:*`, + 'COUNT', + '1000'); + + cursor = result[0]; + bufferedNoteIds.push(...result[1].map(x => x.replace(`${this.config.redis.prefix}:${REDIS_DELTA_PREFIX}:`, ''))); + } while (cursor !== '0'); + + const bufferedMap = await this.getMany(bufferedNoteIds); + + // clear + const pipeline = this.redisForReactions.pipeline(); + for (const noteId of bufferedNoteIds) { + pipeline.del(`${REDIS_DELTA_PREFIX}:${noteId}`); + pipeline.del(`${REDIS_PAIR_PREFIX}:${noteId}`); + } + await pipeline.exec(); + + // TODO: SQL一個ã«ã¾ã¨ã‚ãŸã„ + for (const [noteId, buffered] of bufferedMap) { + const sql = Object.entries(buffered.deltas) + .map(([reaction, count]) => + `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + ${count})::text::jsonb)`) + .join(' || '); + + this.notesRepository.createQueryBuilder().update() + .set({ + reactions: () => sql, + reactionAndUserPairCache: buffered.pairs.map(x => x.join('/')), + }) + .where('id = :id', { id: noteId }) + .execute(); + } + } + + @bindThis + public mergeReactions(src: MiNote['reactions'], delta: Record<string, number>): MiNote['reactions'] { + const reactions = { ...src }; + for (const [name, count] of Object.entries(delta)) { + if (reactions[name] != null) { + reactions[name] += count; + } else { + reactions[name] = count; + } + } + return reactions; + } + + @bindThis + public dispose(): void { + this.redisForSub.off('message', this.onMessage); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 7984dc56279ed58314f0c2560618475673c82f85..64f75390311fa1425f8d95193dd0619df98033d5 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -8,6 +8,7 @@ import * as Redis from 'ioredis'; import { In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import type { + MiMeta, MiRole, MiRoleAssignment, RoleAssignmentsRepository, @@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -60,6 +60,11 @@ export type RolePolicies = { rateLimitFactor: number; canImportNotes: boolean; avatarDecorationLimit: number; + canImportAntennas: boolean; + canImportBlocking: boolean; + canImportFollowing: boolean; + canImportMuting: boolean; + canImportUserLists: boolean; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -91,6 +96,11 @@ export const DEFAULT_POLICIES: RolePolicies = { rateLimitFactor: 1, canImportNotes: true, avatarDecorationLimit: 1, + canImportAntennas: true, + canImportBlocking: true, + canImportFollowing: true, + canImportMuting: true, + canImportUserLists: true, }; @Injectable() @@ -105,8 +115,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { constructor( private moduleRef: ModuleRef, - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.meta) + private meta: MiMeta, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @@ -123,7 +133,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @Inject(DI.roleAssignmentsRepository) private roleAssignmentsRepository: RoleAssignmentsRepository, - private metaService: MetaService, private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, @@ -343,8 +352,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @bindThis public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> { - const meta = await this.metaService.fetch(); - const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; + const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies }; if (userId == null) return basePolicies; @@ -393,6 +401,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), canImportNotes: calc('canImportNotes', vs => vs.some(v => v === true)), avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), + canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)), + canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)), + canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)), + canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), + canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), }; } diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 80907a89214a8afed443cd68009abbf0ebf9e876..1b0b1e5bbd2a779a9ae0f40ef42412f587be0972 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -9,7 +9,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as argon2 from 'argon2'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import { MiUser } from '@/models/User.js'; import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; @@ -21,7 +21,6 @@ import { InstanceActorService } from '@/core/InstanceActorService.js'; import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { UserService } from '@/core/UserService.js'; @Injectable() @@ -30,6 +29,9 @@ export class SignupService { @Inject(DI.db) private db: DataSource, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -40,7 +42,6 @@ export class SignupService { private userService: UserService, private userEntityService: UserEntityService, private idService: IdService, - private metaService: MetaService, private instanceActorService: InstanceActorService, private usersChart: UsersChart, ) { @@ -54,10 +55,10 @@ export class SignupService { host?: string | null; reason?: string | null; ignorePreservedUsernames?: boolean; + approved?: boolean; }) { const { username, password, passwordHash, host, reason } = opts; let hash = passwordHash; - const instance = await this.metaService.fetch(true); // Validate username if (!this.userEntityService.validateLocalUsername(username)) { @@ -91,7 +92,7 @@ export class SignupService { const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); if (!opts.ignorePreservedUsernames && !isTheFirstUser) { - const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new Error('USED_USERNAME'); } @@ -115,9 +116,6 @@ export class SignupService { )); let account!: MiUser; - let defaultApproval = false; - - if (!instance.approvalRequiredForSignup) defaultApproval = true; // Start transaction await this.db.transaction(async transactionalEntityManager => { @@ -135,7 +133,7 @@ export class SignupService { host: this.utilityService.toPunyNullable(host), token: secret, isRoot: isTheFirstUser, - approved: defaultApproval, + approved: isTheFirstUser || (opts.approved ?? !this.meta.approvalRequiredForSignup), signupReason: reason, })); @@ -163,4 +161,3 @@ export class SignupService { return { account, secret }; } } - diff --git a/packages/backend/src/core/SponsorsService.ts b/packages/backend/src/core/SponsorsService.ts new file mode 100644 index 0000000000000000000000000000000000000000..77dd6f81a44d05d0158452dbc9d7271834a7a0ea --- /dev/null +++ b/packages/backend/src/core/SponsorsService.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import type { MiMeta } from '@/models/_.js'; +import * as Redis from 'ioredis'; +import { DI } from '@/di-symbols.js'; +import { RedisKVCache } from '@/misc/cache.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class SponsorsService implements OnApplicationShutdown { + private cache: RedisKVCache<void[]>; + + constructor( + @Inject(DI.meta) + private meta: MiMeta, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + ) { + this.cache = new RedisKVCache<void[]>(this.redisClient, 'sponsors', { + lifetime: 1000 * 60 * 60, + memoryCacheLifetime: 1000 * 60, + fetcher: (key) => { + if (key === 'instance') return this.fetchInstanceSponsors(); + return this.fetchSharkeySponsors(); + }, + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), + }); + } + + @bindThis + private async fetchInstanceSponsors() { + if (!(this.meta.donationUrl && this.meta.donationUrl.includes('opencollective.com'))) { + return []; + } + + try { + const backers = await fetch(`${this.meta.donationUrl}/members/users.json`).then((response) => response.json()); + + // Merge both together into one array and make sure it only has Active subscriptions + const allSponsors = [...backers].filter(sponsor => sponsor.isActive === true && sponsor.role === 'BACKER' && sponsor.tier); + + // Remove possible duplicates + return [...new Map(allSponsors.map(v => [v.profile, v])).values()]; + } catch (error) { + return []; + } + } + + @bindThis + private async fetchSharkeySponsors() { + try { + const backers = await fetch('https://opencollective.com/sharkey/tiers/backer/all.json').then((response) => response.json()); + const sponsorsOC = await fetch('https://opencollective.com/sharkey/tiers/sponsor/all.json').then((response) => response.json()); + + // Merge both together into one array and make sure it only has Active subscriptions + const allSponsors = [...sponsorsOC, ...backers].filter(sponsor => sponsor.isActive === true); + + // Remove possible duplicates + return [...new Map(allSponsors.map(v => [v.profile, v])).values()]; + } catch (error) { + return []; + } + } + + @bindThis + public async instanceSponsors(forceUpdate: boolean) { + if (forceUpdate) this.cache.refresh('instance'); + return this.cache.fetch('instance'); + } + + @bindThis + public async sharkeySponsors(forceUpdate: boolean) { + if (forceUpdate) this.cache.refresh('sharkey'); + return this.cache.fetch('sharkey'); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.cache.dispose(); + } +} diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts index bc6851f788da65298a47095a79c2a4e5239fe811..bb7c6b8c0e027a11919b712b0759b4546262ec98 100644 --- a/packages/backend/src/core/SystemWebhookService.ts +++ b/packages/backend/src/core/SystemWebhookService.ts @@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown { * SystemWebhook ã®ä¸€è¦§ã‚’å–å¾—ã™ã‚‹. */ @bindThis - public async fetchSystemWebhooks(params?: { + public fetchSystemWebhooks(params?: { ids?: MiSystemWebhook['id'][]; isActive?: MiSystemWebhook['isActive']; on?: MiSystemWebhook['on']; @@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown { /** * SystemWebhook ã‚’Webhooké…é€ã‚ューã«è¿½åŠ ã™ã‚‹ * @see QueueService.systemWebhookDeliver + * // TODO: contentã®åž‹ã‚’åŽ³æ ¼åŒ–ã™ã‚‹ */ @bindThis - public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) { + public async enqueueSystemWebhook<T extends SystemWebhookEventType>( + webhook: MiSystemWebhook | MiSystemWebhook['id'], + type: T, + content: unknown, + ) { const webhookEntity = typeof webhook === 'string' ? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook) : webhook; if (!webhookEntity || !webhookEntity.isActive) { - this.logger.info(`Webhook is not active or not found : ${webhook}`); + this.logger.info(`SystemWebhook is not active or not found : ${webhook}`); return; } if (!webhookEntity.on.includes(type)) { - this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`); + this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`); return; } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 6aab8fde70c2c39387e8a4b7e7ab9cb49ceb381f..77e7b60beac647005a6446caacde149a7d5696f2 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import type { Packed } from '@/misc/json-schema.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { UserWebhookService } from '@/core/UserWebhookService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { ThinUser } from '@/queue/types.js'; import Logger from '../logger.js'; @@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: UserWebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, - private fanoutTimelineService: FanoutTimelineService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit { followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') || - (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host)) + (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host)) ) { let autoAccept = false; @@ -277,16 +275,19 @@ export class UserFollowingService implements OnModuleInit { followeeId: followee.id, followerId: follower.id, }); - - // é€šçŸ¥ã‚’ä½œæˆ - if (follower.host === null) { - this.notificationService.createNotification(follower.id, 'followRequestAccepted', { - }, followee.id); - } } if (alreadyFollowed) return; + // é€šçŸ¥ã‚’ä½œæˆ + if (follower.host === null) { + const profile = await this.cacheService.userProfileCache.fetch(followee.id); + + this.notificationService.createNotification(follower.id, 'followRequestAccepted', { + message: profile.followedMessage, + }, followee.id); + } + this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); const [followeeUser, followerUser] = await Promise.all([ @@ -307,14 +308,14 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowing(i.host, true); } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, true); } }); @@ -439,14 +440,14 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowing(i.host, false); } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, false); } }); diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 7920e58e367856d702d040cc3663babbbd64c630..30dcaa6f7dd9bf480eab1622caabffa3c598f0be 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { RelationshipJobData } from '@/queue/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; @Injectable() export class UserSuspendService { @@ -38,6 +39,8 @@ export class UserSuspendService { @bindThis public async suspend(user: MiUser, moderator: MiUser): Promise<void> { + if (isSystemAccount(user)) throw new Error('cannot suspend a system account'); + await this.usersRepository.update(user.id, { isSuspended: true, }); diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts index e96bfeea9581ab900067379e8e1e227724f835bd..8a40a5368804e2f4538dc2068c42c4e5a0e17020 100644 --- a/packages/backend/src/core/UserWebhookService.ts +++ b/packages/backend/src/core/UserWebhookService.ts @@ -5,8 +5,8 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import type { WebhooksRepository } from '@/models/_.js'; -import type { MiWebhook } from '@/models/Webhook.js'; +import { type WebhooksRepository } from '@/models/_.js'; +import { MiWebhook } from '@/models/Webhook.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { GlobalEvents } from '@/core/GlobalEventService.js'; @@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown { return this.activeWebhooks; } + /** + * UserWebhook ã®ä¸€è¦§ã‚’å–å¾—ã™ã‚‹. + */ + @bindThis + public fetchWebhooks(params?: { + ids?: MiWebhook['id'][]; + isActive?: MiWebhook['active']; + on?: MiWebhook['on']; + }): Promise<MiWebhook[]> { + const query = this.webhooksRepository.createQueryBuilder('webhook'); + if (params) { + if (params.ids && params.ids.length > 0) { + query.andWhere('webhook.id IN (:...ids)', { ids: params.ids }); + } + if (params.isActive !== undefined) { + query.andWhere('webhook.active = :isActive', { isActive: params.isActive }); + } + if (params.on && params.on.length > 0) { + query.andWhere(':on <@ webhook.on', { on: params.on }); + } + } + + return query.getMany(); + } + @bindThis private async onMessage(_: string, data: string): Promise<void> { const obj = JSON.parse(data); diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 22871e44f8fe142cf7002ad0a3893c4cf5a38cf5..009dd4665fff2f4b0b9cdb933e9fd06a3ef1ea59 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -10,12 +10,16 @@ import RE2 from 're2'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; +import { MiMeta } from '@/models/Meta.js'; @Injectable() export class UtilityService { constructor( @Inject(DI.config) private config: Config, + + @Inject(DI.meta) + private meta: MiMeta, ) { } @@ -112,4 +116,18 @@ export class UtilityService { const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; return host; } + + public isFederationAllowedHost(host: string): boolean { + if (this.meta.federation === 'none') return false; + if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false; + if (this.isBlockedHost(this.meta.blockedHosts, host)) return false; + + return true; + } + + @bindThis + public isFederationAllowedUri(uri: string): boolean { + const host = this.extractDbHost(uri); + return this.isFederationAllowedHost(host); + } } diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index ec9f4484a4c0fc870099e99d6c09a19b5ff51445..75ab0a207c4518ae3738a53e1c449667b598e1ea 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -12,10 +12,9 @@ import { } from '@simplewebauthn/server'; import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers'; import { DI } from '@/di-symbols.js'; -import type { UserSecurityKeysRepository } from '@/models/_.js'; +import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiUser } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { @@ -23,7 +22,6 @@ import type { AuthenticatorTransportFuture, CredentialDeviceType, PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialDescriptorFuture, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, } from '@simplewebauthn/types'; @@ -31,33 +29,33 @@ import type { @Injectable() export class WebAuthnService { constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, - - private metaService: MetaService, ) { } @bindThis - public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> { - const instance = await this.metaService.fetch(); + public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } { return { origin: this.config.url, rpId: this.config.hostname, - rpName: instance.name ?? this.config.host, - rpIcon: instance.iconUrl ?? undefined, + rpName: this.meta.name ?? this.config.host, + rpIcon: this.meta.iconUrl ?? undefined, }; } @bindThis public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise<PublicKeyCredentialCreationOptionsJSON> { - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -104,7 +102,7 @@ export class WebAuthnService { await this.redisClient.del(`webauthn:challenge:${userId}`); - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); let verification; try { @@ -143,7 +141,7 @@ export class WebAuthnService { @bindThis public async initiateAuthentication(userId: MiUser['id']): Promise<PublicKeyCredentialRequestOptionsJSON> { - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -166,6 +164,86 @@ export class WebAuthnService { return authenticationOptions; } + /** + * Initiate Passkey Auth (Without specifying user) + * @returns authenticationOptions + */ + @bindThis + public async initiateSignInWithPasskeyAuthentication(context: string): Promise<PublicKeyCredentialRequestOptionsJSON> { + const relyingParty = await this.getRelyingParty(); + + const authenticationOptions = await generateAuthenticationOptions({ + rpID: relyingParty.rpId, + userVerification: 'preferred', + }); + + await this.redisClient.setex(`webauthn:challenge:${context}`, 90, authenticationOptions.challenge); + + return authenticationOptions; + } + + /** + * Verify Webauthn AuthenticationCredential + * @throws IdentifiableError + * @returns If the challenge is successful, return the user ID. Otherwise, return null. + */ + @bindThis + public async verifySignInWithPasskeyAuthentication(context: string, response: AuthenticationResponseJSON): Promise<MiUser['id'] | null> { + const challenge = await this.redisClient.get(`webauthn:challenge:${context}`); + + if (!challenge) { + throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', `challenge '${context}' not found`); + } + + await this.redisClient.del(`webauthn:challenge:${context}`); + + const key = await this.userSecurityKeysRepository.findOneBy({ + id: response.id, + }); + + if (!key) { + throw new IdentifiableError('36b96a7d-b547-412d-aeed-2d611cdc8cdc', 'Unknown Webauthn key'); + } + + const relyingParty = await this.getRelyingParty(); + + let verification; + try { + verification = await verifyAuthenticationResponse({ + response: response, + expectedChallenge: challenge, + expectedOrigin: relyingParty.origin, + expectedRPID: relyingParty.rpId, + authenticator: { + credentialID: key.id, + credentialPublicKey: Buffer.from(key.publicKey, 'base64url'), + counter: key.counter, + transports: key.transports ? key.transports as AuthenticatorTransportFuture[] : undefined, + }, + requireUserVerification: true, + }); + } catch (error) { + throw new IdentifiableError('b18c89a7-5b5e-4cec-bb5b-0419f332d430', `verification failed: ${error}`); + } + + const { verified, authenticationInfo } = verification; + + if (!verified) { + return null; + } + + await this.userSecurityKeysRepository.update({ + id: response.id, + }, { + lastUsed: new Date(), + counter: authenticationInfo.newCounter, + credentialDeviceType: authenticationInfo.credentialDeviceType, + credentialBackedUp: authenticationInfo.credentialBackedUp, + }); + + return key.userId; + } + @bindThis public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> { const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`); @@ -209,7 +287,7 @@ export class WebAuthnService { } } - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); let verification; try { diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a31c1d17ad0117c5ba157ee0e375a04dd3ba96b --- /dev/null +++ b/packages/backend/src/core/WebhookTestService.ts @@ -0,0 +1,454 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { Packed } from '@/misc/json-schema.js'; +import { type WebhookEventTypes } from '@/models/Webhook.js'; +import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { QueueService } from '@/core/QueueService.js'; + +const oneDayMillis = 24 * 60 * 60 * 1000; + +function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport { + return { + id: 'dummy-abuse-report1', + targetUserId: 'dummy-target-user', + targetUser: null, + reporterId: 'dummy-reporter-user', + reporter: null, + assigneeId: null, + assignee: null, + resolved: false, + forwarded: false, + comment: 'This is a dummy report for testing purposes.', + targetUserHost: null, + reporterHost: null, + ...override, + }; +} + +function generateDummyUser(override?: Partial<MiUser>): MiUser { + return { + id: 'dummy-user-1', + updatedAt: new Date(Date.now() - oneDayMillis * 7), + lastFetchedAt: new Date(Date.now() - oneDayMillis * 5), + lastActiveDate: new Date(Date.now() - oneDayMillis * 3), + hideOnlineStatus: false, + username: 'dummy1', + usernameLower: 'dummy1', + name: 'DummyUser1', + followersCount: 10, + followingCount: 5, + movedToUri: null, + movedAt: null, + alsoKnownAs: null, + notesCount: 30, + avatarId: null, + avatar: null, + bannerId: null, + banner: null, + backgroundId: null, + background: null, + avatarUrl: null, + bannerUrl: null, + backgroundUrl: null, + avatarBlurhash: null, + bannerBlurhash: null, + backgroundBlurhash: null, + avatarDecorations: [], + tags: [], + isSuspended: false, + isLocked: false, + isSilenced: false, + isBot: false, + isCat: true, + speakAsCat: true, + isRoot: false, + isExplorable: true, + isHibernated: false, + isDeleted: false, + emojis: [], + score: 0, + host: null, + inbox: null, + sharedInbox: null, + featured: null, + uri: null, + followersUri: null, + token: null, + approved: true, + signupReason: null, + noindex: false, + ...override, + }; +} + +function generateDummyNote(override?: Partial<MiNote>): MiNote { + return { + id: 'dummy-note-1', + replyId: null, + reply: null, + renoteId: null, + renote: null, + threadId: null, + text: 'This is a dummy note for testing purposes.', + name: null, + cw: null, + userId: 'dummy-user-1', + user: null, + localOnly: true, + reactionAcceptance: 'likeOnly', + renoteCount: 10, + repliesCount: 5, + clippedCount: 0, + reactions: {}, + visibility: 'public', + uri: null, + url: null, + fileIds: [], + attachedFileTypes: [], + visibleUserIds: [], + mentions: [], + mentionedRemoteUsers: '[]', + reactionAndUserPairCache: [], + emojis: [], + tags: [], + hasPoll: false, + channelId: null, + channel: null, + userHost: null, + replyUserId: null, + replyUserHost: null, + renoteUserId: null, + renoteUserHost: null, + updatedAt: null, + ...override, + }; +} + +function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> { + return { + id: note.id, + createdAt: new Date().toISOString(), + deletedAt: null, + text: note.text, + cw: note.cw, + userId: note.userId, + user: toPackedUserLite(note.user ?? generateDummyUser()), + replyId: note.replyId, + renoteId: note.renoteId, + isHidden: false, + visibility: note.visibility, + mentions: note.mentions, + visibleUserIds: note.visibleUserIds, + fileIds: note.fileIds, + files: [], + tags: note.tags, + poll: null, + emojis: note.emojis, + channelId: note.channelId, + channel: note.channel, + localOnly: note.localOnly, + reactionAcceptance: note.reactionAcceptance, + reactionEmojis: {}, + reactions: {}, + reactionCount: 0, + renoteCount: note.renoteCount, + repliesCount: note.repliesCount, + uri: note.uri ?? undefined, + url: note.url ?? undefined, + reactionAndUserPairCache: note.reactionAndUserPairCache, + ...(detail ? { + clippedCount: note.clippedCount, + reply: note.reply ? toPackedNote(note.reply, false) : null, + renote: note.renote ? toPackedNote(note.renote, true) : null, + myReaction: null, + } : {}), + ...override, + }; +} + +function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> { + return { + id: user.id, + name: user.name, + username: user.username, + host: user.host, + avatarUrl: user.avatarUrl, + avatarBlurhash: user.avatarBlurhash, + avatarDecorations: user.avatarDecorations.map(it => ({ + id: it.id, + angle: it.angle, + flipH: it.flipH, + url: 'https://example.com/dummy-image001.png', + offsetX: it.offsetX, + offsetY: it.offsetY, + })), + isBot: user.isBot, + isCat: user.isCat, + speakAsCat: user.speakAsCat, + emojis: user.emojis, + onlineStatus: 'active', + badgeRoles: [], + noindex: user.noindex, + isModerator: false, + isAdmin: false, + isSystem: false, + isSilenced: user.isSilenced, + ...override, + }; +} + +function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> { + return { + ...toPackedUserLite(user), + url: null, + uri: null, + movedTo: null, + alsoKnownAs: [], + createdAt: new Date().toISOString(), + updatedAt: user.updatedAt?.toISOString() ?? null, + lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null, + bannerUrl: user.bannerUrl, + bannerBlurhash: user.bannerBlurhash, + backgroundUrl: user.backgroundUrl, + backgroundBlurhash: user.backgroundBlurhash, + isLocked: user.isLocked, + isSilenced: false, + isSuspended: user.isSuspended, + description: null, + location: null, + birthday: null, + lang: null, + fields: [], + verifiedLinks: [], + followersCount: user.followersCount, + followingCount: user.followingCount, + notesCount: user.notesCount, + pinnedNoteIds: [], + pinnedNotes: [], + pinnedPageId: null, + pinnedPage: null, + publicReactions: true, + followersVisibility: 'public', + followingVisibility: 'public', + twoFactorEnabled: false, + usePasswordLessLogin: false, + securityKeys: false, + roles: [], + memo: null, + moderationNote: undefined, + isFollowing: false, + isFollowed: false, + hasPendingFollowRequestFromYou: false, + hasPendingFollowRequestToYou: false, + isBlocking: false, + isBlocked: false, + isMuted: false, + isRenoteMuted: false, + notify: 'none', + withReplies: true, + listenbrainz: null, + ...override, + }; +} + +const dummyUser1 = generateDummyUser(); +const dummyUser2 = generateDummyUser({ + id: 'dummy-user-2', + updatedAt: new Date(Date.now() - oneDayMillis * 30), + lastFetchedAt: new Date(Date.now() - oneDayMillis), + lastActiveDate: new Date(Date.now() - oneDayMillis), + username: 'dummy2', + usernameLower: 'dummy2', + name: 'DummyUser2', + followersCount: 40, + followingCount: 50, + notesCount: 900, +}); +const dummyUser3 = generateDummyUser({ + id: 'dummy-user-3', + updatedAt: new Date(Date.now() - oneDayMillis * 15), + lastFetchedAt: new Date(Date.now() - oneDayMillis * 2), + lastActiveDate: new Date(Date.now() - oneDayMillis * 2), + username: 'dummy3', + usernameLower: 'dummy3', + name: 'DummyUser3', + followersCount: 60, + followingCount: 70, + notesCount: 15900, +}); + +@Injectable() +export class WebhookTestService { + public static NoSuchWebhookError = class extends Error {}; + + constructor( + private userWebhookService: UserWebhookService, + private systemWebhookService: SystemWebhookService, + private queueService: QueueService, + ) { + } + + /** + * UserWebhookã®ãƒ†ã‚¹ãƒˆé€ä¿¡ã‚’è¡Œã†. + * é€ä¿¡ã•ã‚Œã‚‹ãƒšã‚¤ãƒãƒ¼ãƒ‰ã¯ã„ãšã‚Œã‚‚ダミーã®å€¤ã§ã€å®Ÿéš›ã«ã¯ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ä¸Šã«å˜åœ¨ã—ãªã„. + * + * ã¾ãŸã€ã“ã®é–¢æ•°çµŒç”±ã§é€ä¿¡ã•ã‚Œã‚‹Webhookã¯ä»¥ä¸‹ã®è¨å®šã‚’無視ã™ã‚‹. + * - Webhookãã®ã‚‚ã®ã®æœ‰åŠ¹ãƒ»ç„¡åŠ¹è¨å®šï¼ˆactive) + * - é€ä¿¡å¯¾è±¡ã‚¤ãƒ™ãƒ³ãƒˆï¼ˆon)ã«é–¢ã™ã‚‹è¨å®š + */ + @bindThis + public async testUserWebhook( + params: { + webhookId: MiWebhook['id'], + type: WebhookEventTypes, + override?: Partial<Omit<MiWebhook, 'id'>>, + }, + sender: MiUser | null, + ) { + const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] }) + .then(it => it.filter(it => it.userId === sender?.id)); + if (webhooks.length === 0) { + throw new WebhookTestService.NoSuchWebhookError(); + } + + const webhook = webhooks[0]; + const send = (contents: unknown) => { + const merged = { + ...webhook, + ...params.override, + }; + + // テスト目的ãªã®ã§UserWebhookServiceã®æ©Ÿèƒ½ã‚’経由ã›ãšç›´æŽ¥ã‚ューã«è¿½åŠ ã™ã‚‹ï¼ˆãƒã‚§ãƒƒã‚¯å‡¦ç†ãªã©ã‚’スã‚ップã™ã‚‹æ„図). + // ã¾ãŸã€Jobã®è©¦è¡Œå›žæ•°ã‚‚1回ã ã‘. + this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 }); + }; + + const dummyNote1 = generateDummyNote({ + userId: dummyUser1.id, + user: dummyUser1, + }); + const dummyReply1 = generateDummyNote({ + id: 'dummy-reply-1', + replyId: dummyNote1.id, + reply: dummyNote1, + userId: dummyUser1.id, + user: dummyUser1, + }); + const dummyRenote1 = generateDummyNote({ + id: 'dummy-renote-1', + renoteId: dummyNote1.id, + renote: dummyNote1, + userId: dummyUser2.id, + user: dummyUser2, + text: null, + }); + const dummyMention1 = generateDummyNote({ + id: 'dummy-mention-1', + userId: dummyUser1.id, + user: dummyUser1, + text: `@${dummyUser2.username} This is a mention to you.`, + mentions: [dummyUser2.id], + }); + + switch (params.type) { + case 'note': { + send(toPackedNote(dummyNote1)); + break; + } + case 'reply': { + send(toPackedNote(dummyReply1)); + break; + } + case 'renote': { + send(toPackedNote(dummyRenote1)); + break; + } + case 'mention': { + send(toPackedNote(dummyMention1)); + break; + } + case 'follow': { + send(toPackedUserDetailedNotMe(dummyUser1)); + break; + } + case 'followed': { + send(toPackedUserLite(dummyUser2)); + break; + } + case 'unfollow': { + send(toPackedUserDetailedNotMe(dummyUser3)); + break; + } + } + } + + /** + * SystemWebhookã®ãƒ†ã‚¹ãƒˆé€ä¿¡ã‚’è¡Œã†. + * é€ä¿¡ã•ã‚Œã‚‹ãƒšã‚¤ãƒãƒ¼ãƒ‰ã¯ã„ãšã‚Œã‚‚ダミーã®å€¤ã§ã€å®Ÿéš›ã«ã¯ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ä¸Šã«å˜åœ¨ã—ãªã„. + * + * ã¾ãŸã€ã“ã®é–¢æ•°çµŒç”±ã§é€ä¿¡ã•ã‚Œã‚‹Webhookã¯ä»¥ä¸‹ã®è¨å®šã‚’無視ã™ã‚‹. + * - Webhookãã®ã‚‚ã®ã®æœ‰åŠ¹ãƒ»ç„¡åŠ¹è¨å®šï¼ˆisActive) + * - é€ä¿¡å¯¾è±¡ã‚¤ãƒ™ãƒ³ãƒˆï¼ˆon)ã«é–¢ã™ã‚‹è¨å®š + */ + @bindThis + public async testSystemWebhook( + params: { + webhookId: MiSystemWebhook['id'], + type: SystemWebhookEventType, + override?: Partial<Omit<MiSystemWebhook, 'id'>>, + }, + ) { + const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] }); + if (webhooks.length === 0) { + throw new WebhookTestService.NoSuchWebhookError(); + } + + const webhook = webhooks[0]; + const send = (contents: unknown) => { + const merged = { + ...webhook, + ...params.override, + }; + + // テスト目的ãªã®ã§SystemWebhookServiceã®æ©Ÿèƒ½ã‚’経由ã›ãšç›´æŽ¥ã‚ューã«è¿½åŠ ã™ã‚‹ï¼ˆãƒã‚§ãƒƒã‚¯å‡¦ç†ãªã©ã‚’スã‚ップã™ã‚‹æ„図). + // ã¾ãŸã€Jobã®è©¦è¡Œå›žæ•°ã‚‚1回ã ã‘. + this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 }); + }; + + switch (params.type) { + case 'abuseReport': { + send(generateAbuseReport({ + targetUserId: dummyUser1.id, + targetUser: dummyUser1, + reporterId: dummyUser2.id, + reporter: dummyUser2, + })); + break; + } + case 'abuseReportResolved': { + send(generateAbuseReport({ + targetUserId: dummyUser1.id, + targetUser: dummyUser1, + reporterId: dummyUser2.id, + reporter: dummyUser2, + assigneeId: dummyUser3.id, + assignee: dummyUser3, + resolved: true, + })); + break; + } + case 'userCreated': { + send(toPackedUserLite(dummyUser1)); + break; + } + } + } +} diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 062af397321e55e5eafd389250165627f087991d..8c97cc8ce838bb6dd81f3d81ee3270285f2c2920 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -16,6 +16,7 @@ import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; +import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; export type UriParseResult = { /** wether the URI was generated by us */ @@ -53,13 +54,14 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, + private apLoggerService: ApLoggerService, ) { this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h } @bindThis - public parseUri(value: string | IObject): UriParseResult { + public parseUri(value: string | IObject | [string | IObject]): UriParseResult { const separator = '/'; const uri = new URL(getApId(value)); @@ -78,7 +80,7 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Note => Misskey Note in DB */ @bindThis - public async getNoteFromApId(value: string | IObject): Promise<MiNote | null> { + public async getNoteFromApId(value: string | IObject | [string | IObject]): Promise<MiNote | null> { const parsed = this.parseUri(value); if (parsed.local) { @@ -98,7 +100,7 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Person => Misskey User in DB */ @bindThis - public async getUserFromApId(value: string | IObject): Promise<MiLocalUser | MiRemoteUser | null> { + public async getUserFromApId(value: string | IObject | [string | IObject]): Promise<MiLocalUser | MiRemoteUser | null> { const parsed = this.parseUri(value); if (parsed.local) { @@ -174,10 +176,16 @@ export class ApDbResolverService implements OnApplicationShutdown { */ @bindThis public async refetchPublicKeyForApId(user: MiRemoteUser): Promise<MiUserPublickey | null> { + this.apLoggerService.logger.debug('Re-fetching public key for user', { userId: user.id, uri: user.uri }); await this.apPersonService.updatePerson(user.uri); + const key = await this.userPublickeysRepository.findOneBy({ userId: user.id }); - if (key != null) { - await this.publicKeyByUserIdCache.set(user.id, key); + this.publicKeyByUserIdCache.set(user.id, key); + + if (key) { + this.apLoggerService.logger.info('Re-fetched public key for user', { userId: user.id, uri: user.uri }); + } else { + this.apLoggerService.logger.warn('Failed to re-fetch key for user', { userId: user.id, uri: user.uri }); } return key; } diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 6a28cbad15083d3dd42ac11a52207390024a08be..d54c9544c34a7f4297298bd71d7847eda41d615b 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; +import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -17,18 +18,18 @@ import { NoteCreateService } from '@/core/NoteCreateService.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; -import { MetaService } from '@/core/MetaService.js'; import { IdService } from '@/core/IdService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { MiRemoteUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; @@ -39,6 +40,7 @@ import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; +import { fromTuple } from '@/misc/from-tuple.js'; @Injectable() export class ApInboxService { @@ -48,6 +50,9 @@ export class ApInboxService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -64,7 +69,6 @@ export class ApInboxService { private noteEntityService: NoteEntityService, private utilityService: UtilityService, private idService: IdService, - private metaService: MetaService, private abuseReportService: AbuseReportService, private userFollowingService: UserFollowingService, private apAudienceService: ApAudienceService, @@ -83,6 +87,7 @@ export class ApInboxService { private apQuestionService: ApQuestionService, private queueService: QueueService, private globalEventService: GlobalEventService, + private federatedInstanceService: FederatedInstanceService, ) { this.logger = this.apLoggerService.logger; } @@ -250,7 +255,8 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object); + const object = fromTuple(activity.object); + const note = await this.apNoteService.resolveNote(object); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -267,11 +273,12 @@ export class ApInboxService { const resolver = this.apResolverService.createResolver(); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); + const activityObject = fromTuple(activity.object); + if (!activityObject) return 'skip: activity has no object property'; + const targetUri = getApId(activityObject); if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; - const target = await resolver.resolve(activity.object).catch(e => { + const target = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); return e; }); @@ -289,9 +296,8 @@ export class ApInboxService { return; } - // アナウンス先をブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; + // アナウンス先ãŒè¨±å¯ã•ã‚Œã¦ã„ã‚‹ã‹ãƒã‚§ãƒƒã‚¯ + if (!this.utilityService.isFederationAllowedUri(uri)) return; const unlock = await this.appLockService.getApLock(uri); @@ -367,29 +373,30 @@ export class ApInboxService { this.logger.info(`Create: ${uri}`); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); + const activityObject = fromTuple(activity.object); + if (!activityObject) return 'skip: activity has no object property'; + const targetUri = getApId(activityObject); if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; // copy audiences between activity <=> object. - if (typeof activity.object === 'object') { - const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); - const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)])); + if (typeof activityObject === 'object') { + const to = unique(concat([toArray(activity.to), toArray(activityObject.to)])); + const cc = unique(concat([toArray(activity.cc), toArray(activityObject.cc)])); activity.to = to; activity.cc = cc; - activity.object.to = to; - activity.object.cc = cc; + activityObject.to = to; + activityObject.cc = cc; } // If there is no attributedTo, use Activity actor. - if (typeof activity.object === 'object' && !activity.object.attributedTo) { - activity.object.attributedTo = activity.actor; + if (typeof activityObject === 'object' && !activityObject.attributedTo) { + activityObject.attributedTo = activity.actor; } const resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(activity.object).catch(e => { + const object = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); throw e; }); @@ -445,15 +452,15 @@ export class ApInboxService { // 削除対象objectã®type let formerType: string | undefined; - if (typeof activity.object === 'string') { + const activityObject = fromTuple(activity.object); + if (typeof activityObject === 'string') { // typeãŒä¸æ˜Žã ã‘ã©ã€ã©ã†ã›æ¶ˆãˆã¦ã‚‹ã®ã§remote resolveã—ãªã„ formerType = undefined; } else { - const object = activity.object; - if (isTombstone(object)) { - formerType = toSingle(object.formerType); + if (isTombstone(activityObject)) { + formerType = toSingle(activityObject.formerType); } else { - formerType = toSingle(object.type); + formerType = toSingle(activityObject.type); } } @@ -530,6 +537,12 @@ export class ApInboxService { @bindThis private async flag(actor: MiRemoteUser, activity: IFlag): Promise<string> { + // Make sure the source instance is allowed to send reports. + const instance = await this.federatedInstanceService.fetch(actor.host); + if (instance.rejectReports) { + throw new Bull.UnrecoverableError(`Rejecting report from instance: ${actor.host}`); + } + // object㯠`(User|Note) | (User|Note)[]` ã ã‘ã©ã€å…¨ãƒ‘ターンDBスã‚ーマã¨å¯¾å¿œã•ã›ã‚‰ã‚Œãªã„ã®ã§ // 対象ユーザーã¯ä¸€ç•ªæœ€åˆã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ ã¨ã—㦠ã‚ã¨ã¯ã‚³ãƒ¡ãƒ³ãƒˆã¨ã—ã¦æ ¼ç´ã™ã‚‹ const uris = getApIds(activity.object); @@ -607,7 +620,8 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object); + const activityObject = fromTuple(activity.object); + const note = await this.apNoteService.resolveNote(activityObject); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); return; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 55d1054de91190ab0a484a93160090f52ebda659..42ee5bc58ade2d3389b55f5850db2f9f3e522fa2 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -23,11 +23,10 @@ import { MfmService } from '@/core/MfmService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; -import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, InstancesRepository } from '@/models/_.js'; +import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, InstancesRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { IdService } from '@/core/IdService.js'; -import { MetaService } from '../MetaService.js'; import { JsonLdService } from './JsonLdService.js'; import { ApMfmService } from './ApMfmService.js'; import { CONTEXT } from './misc/contexts.js'; @@ -39,6 +38,9 @@ export class ApRendererService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -65,7 +67,6 @@ export class ApRendererService { private apMfmService: ApMfmService, private mfmService: MfmService, private idService: IdService, - private metaService: MetaService, ) { } @@ -199,7 +200,8 @@ export class ApRendererService { type: 'Flag', actor: this.userEntityService.genLocalUserUri(user.id), content, - object, + // This MUST be an array for Pleroma compatibility: https://activitypub.software/TransFem-org/Sharkey/-/issues/641#note_7301 + object: [object], }; } @@ -272,10 +274,9 @@ export class ApRendererService { @bindThis public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> { const reaction = noteReaction.reaction; - const meta = await this.metaService.fetch(true); let isMastodon = false; - if (meta.defaultLike && reaction.replaceAll(':', '') === meta.defaultLike.replaceAll(':', '')) { + if (this.meta.defaultLike && reaction.replaceAll(':', '') === this.meta.defaultLike.replaceAll(':', '')) { const note = await this.notesRepository.findOneBy({ id: noteReaction.noteId }); if (note && note.userHost) { @@ -419,7 +420,7 @@ export class ApRendererService { const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); + const { content } = this.apMfmService.getNoteHtml(note, apAppend); const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); @@ -449,13 +450,11 @@ export class ApRendererService { attributedTo, summary: summary ?? undefined, content: content ?? undefined, - ...(noMisskeyContent ? {} : { - _misskey_content: text, - source: { - content: text, - mediaType: 'text/x.misskeymarkdown', - }, - }), + _misskey_content: text, + source: { + content: text, + mediaType: 'text/x.misskeymarkdown', + }, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, @@ -517,6 +516,7 @@ export class ApRendererService { name: user.name, summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null, _misskey_summary: profile.description, + _misskey_followedMessage: profile.followedMessage, icon: avatar ? this.renderImage(avatar) : null, image: banner ? this.renderImage(banner) : null, backgroundUrl: background ? this.renderImage(background) : null, @@ -526,6 +526,7 @@ export class ApRendererService { publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, noindex: user.noindex, + indexable: !user.noindex, speakAsCat: user.speakAsCat, attachment: attachment.length ? attachment : undefined, }; @@ -710,7 +711,7 @@ export class ApRendererService { const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); + const { content } = this.apMfmService.getNoteHtml(note, apAppend); const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); @@ -741,13 +742,11 @@ export class ApRendererService { summary: summary ?? undefined, content: content ?? undefined, updated: note.updatedAt?.toISOString(), - ...(noMisskeyContent ? {} : { - _misskey_content: text, - source: { - content: text, - mediaType: 'text/x.misskeymarkdown', - }, - }), + _misskey_content: text, + source: { + content: text, + mediaType: 'text/x.misskeymarkdown', + }, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 0b9139db902a25b1c0dac8d75f5ea5dd30ba462c..38c78cf900d7d9cf2b3dabe272856fba31f64511 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -208,12 +208,12 @@ export class ApRequestService { const contentType = res.headers.get('content-type'); if ( - res.ok - && (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' - && _followAlternate === true + res.ok && + (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && + _followAlternate === true ) { const html = await res.text(); - const window = new Window({ + const { window, happyDOM } = new Window({ settings: { disableJavaScriptEvaluation: true, disableJavaScriptFileLoading: true, @@ -247,7 +247,7 @@ export class ApRequestService { } catch (e) { // something went wrong parsing the HTML, ignore the whole thing } finally { - await window.happyDOM.close(); + happyDOM.close().catch(err => {}); } } //#endregion diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index b047a6c59ba581cc22e528f7ac0f6755de2f1e83..5d5c61ce2cfe73d32b07bd5e6272d801ff89095f 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; -import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -21,6 +20,7 @@ import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; +import { fromTuple } from '@/misc/from-tuple.js'; export class Resolver { private history: Set<string>; @@ -29,6 +29,7 @@ export class Resolver { constructor( private config: Config, + private meta: MiMeta, private usersRepository: UsersRepository, private notesRepository: NotesRepository, private pollsRepository: PollsRepository, @@ -36,7 +37,6 @@ export class Resolver { private followRequestsRepository: FollowRequestsRepository, private utilityService: UtilityService, private instanceActorService: InstanceActorService, - private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, @@ -67,7 +67,10 @@ export class Resolver { } @bindThis - public async resolve(value: string | IObject): Promise<IObject> { + public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + if (typeof value !== 'string') { return value; } @@ -94,8 +97,7 @@ export class Resolver { return await this.resolveLocal(value); } - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { + if (!this.utilityService.isFederationAllowedHost(host)) { throw new Error('Instance is blocked'); } @@ -186,6 +188,9 @@ export class ApResolverService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -203,7 +208,6 @@ export class ApResolverService { private utilityService: UtilityService, private instanceActorService: InstanceActorService, - private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, @@ -216,6 +220,7 @@ export class ApResolverService { public createResolver(): Resolver { return new Resolver( this.config, + this.meta, this.usersRepository, this.notesRepository, this.pollsRepository, @@ -223,7 +228,6 @@ export class ApResolverService { this.followRequestsRepository, this.utilityService, this.instanceActorService, - this.metaService, this.apRequestService, this.httpRequestService, this.apRendererService, diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 815b20b910d89b977da73a47249d4048da471e9f..da75fc1d4214eb98235c4e0a6860d46c6783d09b 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -545,6 +545,7 @@ const extension_context_definition = { Emoji: 'toot:Emoji', featured: 'toot:featured', discoverable: 'toot:discoverable', + indexable: 'toot:indexable', // schema schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', @@ -556,6 +557,7 @@ const extension_context_definition = { '_misskey_reaction': 'misskey:_misskey_reaction', '_misskey_votes': 'misskey:_misskey_votes', '_misskey_summary': 'misskey:_misskey_summary', + '_misskey_followedMessage': 'misskey:_misskey_followedMessage', 'isCat': 'misskey:isCat', // Firefish firefish: 'https://joinfirefish.org/ns#', diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index b281ac9728fe6e96daff02cb026cc9bc5f021e00..259889d94526a7882e64bedaf2862eb01e75799a 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -5,17 +5,16 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; -import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DriveService } from '@/core/DriveService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import type { Config } from '@/config.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { isDocument, type IObject } from '../type.js'; @@ -25,10 +24,14 @@ export class ApImageService { private logger: Logger; constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.config) + private config: Config, - private metaService: MetaService, private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, @@ -65,12 +68,10 @@ export class ApImageService { this.logger.info(`Creating the Image: ${image.url}`); - const instance = await this.metaService.fetch(); - // Cache if remote file cache is on AND either // 1. remote sensitive file is also on // 2. or the image is not sensitive - const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); + const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive); await this.federatedInstanceService.fetch(actor.host).then(async i => { if (i.isNSFW) { @@ -84,7 +85,7 @@ export class ApImageService { uri: image.url, sensitive: !!(image.sensitive), isLink: !shouldBeCached, - comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), + comment: truncate(image.name ?? undefined, this.config.maxRemoteAltTextLength), }); if (!file.isLink || file.url === image.url) return file; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 7b7a7921fbca40a1692b86a9c46db189ed552f6d..f404a77fbb11d5e42c60105887c2665a278f7095 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -6,13 +6,12 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js'; +import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import type { MiEmoji } from '@/models/Emoji.js'; -import { MetaService } from '@/core/MetaService.js'; import { AppLockService } from '@/core/AppLockService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; @@ -47,6 +46,9 @@ export class ApNoteService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -69,7 +71,6 @@ export class ApNoteService { private apMentionService: ApMentionService, private apImageService: ApImageService, private apQuestionService: ApQuestionService, - private metaService: MetaService, private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, @@ -187,7 +188,7 @@ export class ApNoteService { /** * ç¦æ¢ãƒ¯ãƒ¼ãƒ‰ãƒã‚§ãƒƒã‚¯ */ - const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); + const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } @@ -565,9 +566,7 @@ export class ApNoteService { public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> { const uri = getApId(value); - // ブãƒãƒƒã‚¯ã—ã¦ã„ãŸã‚‰ä¸æ– - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { + if (!this.utilityService.isFederationAllowedUri(uri)) { throw new StatusError('blocked host', 451); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 10b1fc0bf4b9e70bb11072f45e4195f6533b4cc1..2046dad0995a76187a12f5ce22d6416b53415fcd 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js'; @@ -35,7 +35,6 @@ import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; @@ -46,7 +45,7 @@ import type { ApNoteService } from './ApNoteService.js'; import type { ApMfmService } from '../ApMfmService.js'; import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports + import type { ApImageService } from './ApImageService.js'; import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; @@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private idService: IdService; private globalEventService: GlobalEventService; - private metaService: MetaService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; private cacheService: CacheService; @@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.db) private db: DataSource, @@ -112,7 +113,6 @@ export class ApPersonService implements OnModuleInit { this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.idService = this.moduleRef.get('IdService'); this.globalEventService = this.moduleRef.get('GlobalEventService'); - this.metaService = this.moduleRef.get('MetaService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.cacheService = this.moduleRef.get('CacheService'); @@ -319,8 +319,8 @@ export class ApPersonService implements OnModuleInit { this.logger.error('error occurred while fetching following/followers collection', { stack: err }); } return 'private'; - }) - ) + }), + ), ); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -395,6 +395,7 @@ export class ApPersonService implements OnModuleInit { await transactionalEntityManager.save(new MiUserProfile({ userId: user.id, description: _description, + followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, url, fields, followingVisibility, @@ -433,10 +434,10 @@ export class ApPersonService implements OnModuleInit { this.cacheService.uriPersonCache.set(user.uri, user); // Register host - this.federatedInstanceService.fetch(host).then(async i => { + this.federatedInstanceService.fetch(host).then(i => { this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.newUser(i.host); } }); @@ -520,8 +521,8 @@ export class ApPersonService implements OnModuleInit { return undefined; } return 'private'; - }) - ) + }), + ), ); const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); @@ -595,6 +596,7 @@ export class ApPersonService implements OnModuleInit { url, fields, description: _description, + followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null, followingVisibility, followersVisibility, birthday: bday?.[0] ?? null, diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index 73004d10b0c5eedb59bb865cd6e4d72f012c53e3..9246398fdebf3aefa98af61506c1fa17d5dc9e91 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -98,7 +98,7 @@ export class ApQuestionService { const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems; if (newCount == null) throw new Error('invalid newCount: ' + newCount); - if (oldCount !== newCount) { + if (oldCount <= newCount) { changed = true; poll.votes[poll.choices.indexOf(choice)] = newCount; } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 2f58825de1a36819a7de1c3e6e6b4acf40651e08..af5aba9c162817180c05af4a999c93747e73f8a2 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { fromTuple } from '@/misc/from-tuple.js'; + export type Obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; @@ -13,6 +15,7 @@ export interface IObject { name?: string | null; summary?: string | null; _misskey_summary?: string; + _misskey_followedMessage?: string | null; published?: string; cc?: ApObject; to?: ApObject; @@ -52,10 +55,13 @@ export function getOneApId(value: ApObject): string { /** * Get ActivityStreams Object id */ -export function getApId(value: string | IObject): string { +export function getApId(value: string | IObject | [string | IObject]): string { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error('cannot detemine id'); + throw new Error('cannot determine id'); } /** @@ -84,7 +90,9 @@ export function getApHrefNullable(value: string | IObject | undefined): string | export interface IActivity extends IObject { //type: 'Activity'; actor: IObject | string; - object: IObject | string; + // ActivityPub spec allows for arrays: https://www.w3.org/TR/activitystreams-vocabulary/#properties + // Misskey can only handle one value, so we use a tuple for that case. + object: IObject | string | [IObject | string] ; target?: IObject | string; /** LD-Signature */ signature?: { diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index c2329a2f7357be5b095a1d99d119db41f694016e..c9b43cc66d7c3e72ecc1f10342a0122910a4f74d 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -5,10 +5,9 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { FollowingsRepository, InstancesRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -24,13 +23,15 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di @Inject(DI.db) private db: DataSource, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private metaService: MetaService, private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { @@ -43,8 +44,6 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di } protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> { - const meta = await this.metaService.fetch(); - const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') .select('instance.host') .where('instance.suspensionState != \'none\''); @@ -65,21 +64,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -88,7 +87,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() @@ -96,7 +95,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts index d2dbaf227030a108f879f99523fb3bfd0772840b..d54c954bf29ec8c46f98d38445d2d8d60291a332 100644 --- a/packages/backend/src/core/entities/FollowingEntityService.ts +++ b/packages/backend/src/core/entities/FollowingEntityService.ts @@ -8,11 +8,14 @@ import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/_.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiFollowing } from '@/models/Following.js'; +import { MiBlocking } from '@/models/Blocking.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import { MiFollowing } from '@/models/Following.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { RoleService } from '@/core/RoleService.js'; import { UserEntityService } from './UserEntityService.js'; type LocalFollowerFollowing = MiFollowing & { @@ -47,6 +50,8 @@ export class FollowingEntityService { private userEntityService: UserEntityService, private idService: IdService, + private queryService: QueryService, + private roleService: RoleService, ) { } @@ -70,6 +75,53 @@ export class FollowingEntityService { return following.followeeHost != null; } + @bindThis + public async getFollowing(me: MiLocalUser, params: FollowsQueryParams) { + return await this.getFollows(me, params, 'following.followerHost = :host'); + } + + @bindThis + public async getFollowers(me: MiLocalUser, params: FollowsQueryParams) { + return await this.getFollows(me, params, 'following.followeeHost = :host'); + } + + private async getFollows(me: MiLocalUser, params: FollowsQueryParams, condition: string) { + const builder = this.followingsRepository.createQueryBuilder('following'); + const query = this.queryService + .makePaginationQuery(builder, params.sinceId, params.untilId) + .andWhere(condition, { host: params.host }) + .limit(params.limit); + + if (!await this.roleService.isModerator(me)) { + query.setParameter('me', me.id); + + // Make sure that the followee doesn't block us, if their profile will be included. + if (params.includeFollowee) { + query.leftJoin(MiBlocking, 'followee_blocking', 'followee_blocking."blockerId" = following."followeeId" AND followee_blocking."blockeeId" = :me'); + query.andWhere('followee_blocking.id IS NULL'); + } + + // Make sure that the follower doesn't block us, if their profile will be included. + if (params.includeFollower) { + query.leftJoin(MiBlocking, 'follower_blocking', 'follower_blocking."blockerId" = following."followerId" AND follower_blocking."blockeeId" = :me'); + query.andWhere('follower_blocking.id IS NULL'); + } + + // Make sure that the followee hasn't hidden this connection. + query.leftJoin(MiUserProfile, 'followee', 'followee."userId" = following."followeeId"'); + query.leftJoin(MiFollowing, 'me_following_followee', 'me_following_followee."followerId" = :me AND me_following_followee."followeeId" = following."followerId"'); + query.andWhere('(followee."userId" = :me OR followee."followersVisibility" = \'public\' OR (followee."followersVisibility" = \'followers\' AND me_following_followee.id IS NOT NULL))'); + + // Make sure that the follower hasn't hidden this connection. + query.leftJoin(MiUserProfile, 'follower', 'follower."userId" = following."followerId"'); + query.leftJoin(MiFollowing, 'me_following_follower', 'me_following_follower."followerId" = :me AND me_following_follower."followeeId" = following."followerId"'); + query.andWhere('(follower."userId" = :me OR follower."followingVisibility" = \'public\' OR (follower."followingVisibility" = \'followers\' AND me_following_follower.id IS NOT NULL))'); + } + + const followings = await query.getMany(); + return await this.packMany(followings, me, { populateFollowee: params.includeFollowee, populateFollower: params.includeFollower }); + } + @bindThis public async pack( src: MiFollowing['id'] | MiFollowing, @@ -124,3 +176,12 @@ export class FollowingEntityService { } } +interface FollowsQueryParams { + readonly host: string; + readonly limit: number; + readonly includeFollower: boolean; + readonly includeFollowee: boolean; + + readonly sinceId?: string; + readonly untilId?: string; +} diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 7695e6dfa73e1f4cd5286fc5ddda7d9d401220ed..63e5923255b6f64a3c5cb39850826c08fc99fa76 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -3,19 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import type { MiInstance } from '@/models/Instance.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { RoleService } from '@/core/RoleService.js'; import { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import { MiMeta } from '@/models/_.js'; @Injectable() export class InstanceEntityService { constructor( - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, + private roleService: RoleService, private utilityService: UtilityService, @@ -27,7 +30,6 @@ export class InstanceEntityService { instance: MiInstance, me?: { id: MiUser['id']; } | null | undefined, ): Promise<Packed<'FederationInstance'>> { - const meta = await this.metaService.fetch(); const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; return { @@ -41,7 +43,7 @@ export class InstanceEntityService { isNotResponding: instance.isNotResponding, isSuspended: instance.suspensionState !== 'none', suspensionState: instance.suspensionState, - isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), + isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, @@ -49,14 +51,15 @@ export class InstanceEntityService { description: instance.description, maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, - isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), - isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host), + isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host), + isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, isNSFW: instance.isNSFW, + rejectReports: instance.rejectReports, moderationNote: iAmModerator ? instance.moderationNote : null, }; } diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 3128b762f4f56feeb579a1c8eedddf316efa46bf..b2b9aebb79154ff70ba36318448ec8c52873a708 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -9,7 +9,6 @@ import JSON5 from 'json5'; import type { Packed } from '@/misc/json-schema.js'; import type { MiMeta } from '@/models/Meta.js'; import type { AdsRepository } from '@/models/_.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; @@ -23,11 +22,13 @@ export class MetaEntityService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.adsRepository) private adsRepository: AdsRepository, private userEntityService: UserEntityService, - private metaService: MetaService, private instanceActorService: InstanceActorService, ) { } @@ -36,7 +37,7 @@ export class MetaEntityService { let instance = meta; if (!instance) { - instance = await this.metaService.fetch(); + instance = this.meta; } const ads = await this.adsRepository.createQueryBuilder('ads') @@ -97,6 +98,8 @@ export class MetaEntityService { recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + enableFC: instance.enableFC, + fcSiteKey: instance.fcSiteKey, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png', @@ -105,9 +108,15 @@ export class MetaEntityService { serverErrorImageUrl: instance.serverErrorImageUrl, notFoundImageUrl: instance.notFoundImageUrl, iconUrl: instance.iconUrl, + sidebarLogoUrl: instance.sidebarLogoUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, maxNoteTextLength: this.config.maxNoteLength, + maxRemoteNoteTextLength: this.config.maxRemoteNoteLength, + maxCwLength: this.config.maxCwLength, + maxRemoteCwLength: this.config.maxRemoteCwLength, + maxAltTextLength: this.config.maxAltTextLength, + maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, defaultLightTheme, defaultDarkTheme, defaultLike: instance.defaultLike, @@ -119,6 +128,7 @@ export class MetaEntityService { imageUrl: ad.imageUrl, dayOfWeek: ad.dayOfWeek, })), + trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns, notesPerOneAd: instance.notesPerOneAd, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, @@ -132,6 +142,7 @@ export class MetaEntityService { mediaProxy: this.config.mediaProxy, enableUrlPreview: instance.urlPreviewEnabled, noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', + maxFileSize: this.config.maxFileSize, }; return packed; @@ -142,7 +153,7 @@ export class MetaEntityService { let instance = meta; if (!instance) { - instance = await this.metaService.fetch(); + instance = this.meta; } const packed = await this.pack(instance); diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 493723ac453261bb0bb66e5254bcf113d0fdbda3..4dd17c5af3900c89298c268357a583e4351b2fa7 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -11,11 +11,11 @@ import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import type { MiNoteReaction } from '@/models/NoteReaction.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; +import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; @@ -29,12 +29,16 @@ export class NoteEntityService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private customEmojiService: CustomEmojiService; private reactionService: ReactionService; + private reactionsBufferingService: ReactionsBufferingService; private idService: IdService; private noteLoader = new DebounceLoader(this.findNoteOrFail); constructor( private moduleRef: ModuleRef, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -63,6 +67,8 @@ export class NoteEntityService implements OnModuleInit { //private driveFileEntityService: DriveFileEntityService, //private customEmojiService: CustomEmojiService, //private reactionService: ReactionService, + //private reactionsBufferingService: ReactionsBufferingService, + //private idService: IdService, ) { } @@ -71,6 +77,7 @@ export class NoteEntityService implements OnModuleInit { this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.reactionService = this.moduleRef.get('ReactionService'); + this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService'); this.idService = this.moduleRef.get('IdService'); } @@ -119,7 +126,7 @@ export class NoteEntityService implements OnModuleInit { followerId: meId, }, }); - + hide = !isFollowing; } else { // フォãƒãƒ¯ãƒ¼ã‹ã©ã†ã‹ @@ -304,6 +311,7 @@ export class NoteEntityService implements OnModuleInit { skipHide?: boolean; withReactionAndUserPairCache?: boolean; _hint_?: { + bufferedReactions: Map<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null; myReactions: Map<MiNote['id'], string | null>; packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>; packedUsers: Map<MiUser['id'], Packed<'UserLite'>> @@ -320,6 +328,15 @@ export class NoteEntityService implements OnModuleInit { const note = typeof src === 'object' ? src : await this.noteLoader.load(src); const host = note.userHost; + const bufferedReactions = opts._hint_?.bufferedReactions != null + ? (opts._hint_.bufferedReactions.get(note.id) ?? { deltas: {}, pairs: [] }) + : this.meta.enableReactionsBuffering + ? await this.reactionsBufferingService.get(note.id) + : { deltas: {}, pairs: [] }; + const reactions = this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {})); + + const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/'))); + let text = note.text; if (note.name && (note.url ?? note.uri)) { @@ -332,7 +349,7 @@ export class NoteEntityService implements OnModuleInit { : await this.channelsRepository.findOneBy({ id: note.channelId }) : null; - const reactionEmojiNames = Object.keys(note.reactions) + const reactionEmojiNames = Object.keys(reactions) .filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文å—ã®ã¿ .map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', '')); const packedFiles = options?._hint_?.packedFiles; @@ -352,10 +369,10 @@ export class NoteEntityService implements OnModuleInit { visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, renoteCount: note.renoteCount, repliesCount: note.repliesCount, - reactionCount: Object.values(note.reactions).reduce((a, b) => a + b, 0), - reactions: this.reactionService.convertLegacyReactions(note.reactions), + reactionCount: Object.values(reactions).reduce((a, b) => a + b, 0), + reactions: reactions, reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host), - reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined, + reactionAndUserPairCache: opts.withReactionAndUserPairCache ? reactionAndUserPairCache : undefined, emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined, tags: note.tags.length > 0 ? note.tags : undefined, fileIds: note.fileIds, @@ -375,8 +392,12 @@ export class NoteEntityService implements OnModuleInit { uri: note.uri ?? undefined, url: note.url ?? undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, - ...(meId && Object.keys(note.reactions).length > 0 ? { - myReaction: this.populateMyReaction(note, meId, options?._hint_), + ...(meId && Object.keys(reactions).length > 0 ? { + myReaction: this.populateMyReaction({ + id: note.id, + reactions: reactions, + reactionAndUserPairCache: reactionAndUserPairCache, + }, meId, options?._hint_), } : {}), ...(opts.detail ? { @@ -416,6 +437,8 @@ export class NoteEntityService implements OnModuleInit { ) { if (notes.length === 0) return []; + const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null; + const meId = me ? me.id : null; const myReactionsMap = new Map<MiNote['id'], string | null>(); if (meId) { @@ -426,23 +449,33 @@ export class NoteEntityService implements OnModuleInit { for (const note of notes) { if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote - const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0); + const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); if (reactionsCount === 0) { myReactionsMap.set(note.renote.id, null); - } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) { - const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId)); - myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null); + } else if (reactionsCount <= note.renote.reactionAndUserPairCache.length + (bufferedReactions?.get(note.renote.id)?.pairs.length ?? 0)) { + const pairInBuffer = bufferedReactions?.get(note.renote.id)?.pairs.find(p => p[0] === meId); + if (pairInBuffer) { + myReactionsMap.set(note.renote.id, pairInBuffer[1]); + } else { + const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId)); + myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null); + } } else { idsNeedFetchMyReaction.add(note.renote.id); } } else { if (note.id < oldId) { - const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); + const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions?.get(note.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); if (reactionsCount === 0) { myReactionsMap.set(note.id, null); - } else if (reactionsCount <= note.reactionAndUserPairCache.length) { - const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); - myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null); + } else if (reactionsCount <= note.reactionAndUserPairCache.length + (bufferedReactions?.get(note.id)?.pairs.length ?? 0)) { + const pairInBuffer = bufferedReactions?.get(note.id)?.pairs.find(p => p[0] === meId); + if (pairInBuffer) { + myReactionsMap.set(note.id, pairInBuffer[1]); + } else { + const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId)); + myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null); + } } else { idsNeedFetchMyReaction.add(note.id); } @@ -477,6 +510,7 @@ export class NoteEntityService implements OnModuleInit { return await Promise.all(notes.map(n => this.pack(n, me, { ...options, _hint_: { + bufferedReactions, myReactions: myReactionsMap, packedFiles, packedUsers, diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index e2de450756c5325213637ac931eacb5a48943a7c..bbaf0cb7c846da23049e1c9079a379d4b81bce41 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit { async #packInternal <T extends MiNotification | MiGroupedNotification> ( src: T, meId: MiUser['id'], - // eslint-disable-next-line @typescript-eslint/ban-types + options: { checkValidNotifier?: boolean; }, @@ -159,9 +159,16 @@ export class NotificationEntityService implements OnModuleInit { ...(notification.type === 'roleAssigned' ? { role: role, } : {}), + ...(notification.type === 'followRequestAccepted' ? { + message: notification.message, + } : {}), ...(notification.type === 'achievementEarned' ? { achievement: notification.achievement, } : {}), + ...(notification.type === 'exportCompleted' ? { + exportedEntity: notification.exportedEntity, + fileId: notification.fileId, + } : {}), ...(notification.type === 'app' ? { body: notification.customBody, header: notification.customHeader, @@ -229,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit { public async pack( src: MiNotification | MiGroupedNotification, meId: MiUser['id'], - // eslint-disable-next-line @typescript-eslint/ban-types + options: { checkValidNotifier?: boolean; }, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 2b1c4d5c63b71841cb07565075fdce72f2ef9b6e..b1832ca0f51ed5042e83d3acc01f40a36149acf0 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -53,6 +53,7 @@ import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; const Ajv = _Ajv.default; const ajv = new Ajv(); @@ -373,6 +374,13 @@ export class UserEntityService implements OnModuleInit { return count > 0; } + @bindThis + public async getHasPendingSentFollowRequest(userId: MiUser['id']): Promise<boolean> { + return this.followRequestsRepository.existsBy({ + followerId: userId, + }); + } + @bindThis public getOnlineStatus(user: MiUser): 'unknown' | 'online' | 'active' | 'offline' { if (user.hideOnlineStatus) return 'unknown'; @@ -525,6 +533,7 @@ export class UserEntityService implements OnModuleInit { flipH: ud.flipH || undefined, offsetX: ud.offsetX || undefined, offsetY: ud.offsetY || undefined, + showBelow: ud.showBelow || undefined, url: decorations.find(d => d.id === ud.id)!.url, }))) : [], isBot: user.isBot, @@ -554,7 +563,7 @@ export class UserEntityService implements OnModuleInit { name: r.name, iconUrl: r.iconUrl, displayOrder: r.displayOrder, - })) + })), ) : undefined, ...(isDetailed ? { @@ -611,11 +620,14 @@ export class UserEntityService implements OnModuleInit { avatarId: user.avatarId, bannerId: user.bannerId, backgroundId: user.backgroundId, + followedMessage: profile!.followedMessage, isModerator: isModerator, isAdmin: isAdmin, + isSystem: isSystemAccount(user), injectFeaturedNote: profile!.injectFeaturedNote, receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, alwaysMarkNsfw: profile!.alwaysMarkNsfw, + defaultSensitive: profile!.defaultSensitive, autoSensitive: profile!.autoSensitive, carefulBot: profile!.carefulBot, autoAcceptFollowed: profile!.autoAcceptFollowed, @@ -639,6 +651,7 @@ export class UserEntityService implements OnModuleInit { hasUnreadChannel: false, // 後方互æ›æ€§ã®ãŸã‚ hasUnreadNotification: notificationsInfo?.hasUnread, // 後方互æ›æ€§ã®ãŸã‚ hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), + hasPendingSentFollowRequest: this.getHasPendingSentFollowRequest(user.id), unreadNotificationsCount: notificationsInfo?.unreadCount, mutedWords: profile!.mutedWords, hardMutedWords: profile!.hardMutedWords, @@ -680,6 +693,7 @@ export class UserEntityService implements OnModuleInit { isRenoteMuted: relation.isRenoteMuted, notify: relation.following?.notify ?? 'none', withReplies: relation.following?.withReplies ?? false, + followedMessage: relation.isFollowing ? profile!.followedMessage : undefined, } : {}), } as Promiseable<Packed<S>>; diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 0be2149a0a18a33b43e7a1a6834dcd837160f499..6e9d29dcbd26e8e7abbc02394cb511e5cb8cdf99 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; const ev = new Xev(); @@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown { private intervalId: NodeJS.Timeout | null = null; constructor( - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, ) { } @@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown { */ @bindThis public async start(): Promise<void> { - if (!(await this.metaService.fetch(true)).enableServerMachineStats) return; + if (!this.meta.enableServerMachineStats) return; const log = [] as any[]; diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts index 21777657d185ad450048f654b5df5d11459814b0..42f925e1251cf8d5eb48caeb96d5a72fb203c017 100644 --- a/packages/backend/src/decorators.ts +++ b/packages/backend/src/decorators.ts @@ -10,8 +10,9 @@ * The getter will return a .bind version of the function * and memoize the result against a symbol on the instance */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function bindThis(target: any, key: string, descriptor: any) { - let fn = descriptor.value; + const fn = descriptor.value; if (typeof fn !== 'function') { throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`); @@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) { configurable: true, get() { // eslint-disable-next-line no-prototype-builtins - if (this === target.prototype || this.hasOwnProperty(key) || - typeof fn !== 'function') { + if (this === target.prototype || this.hasOwnProperty(key)) { return fn; } const boundFn = fn.bind(this); - Object.defineProperty(this, key, { + Reflect.defineProperty(this, key, { + value: boundFn, configurable: true, - get() { - return boundFn; - }, - set(value) { - fn = value; - delete this[key]; - }, + writable: true, }); + return boundFn; }, - set(value: any) { - fn = value; - }, }; } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index d4a21ab625a7f7dff67762f76d21e5b90db15fe5..5ea500ac775d499bb04d85286196040c20506252 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -6,11 +6,13 @@ export const DI = { config: Symbol('config'), db: Symbol('db'), + meta: Symbol('meta'), meilisearch: Symbol('meilisearch'), redis: Symbol('redis'), redisForPub: Symbol('redisForPub'), redisForSub: Symbol('redisForSub'), redisForTimelines: Symbol('redisForTimelines'), + redisForReactions: Symbol('redisForReactions'), //#region Repositories usersRepository: Symbol('usersRepository'), @@ -19,6 +21,7 @@ export const DI = { announcementReadsRepository: Symbol('announcementReadsRepository'), appsRepository: Symbol('appsRepository'), avatarDecorationsRepository: Symbol('avatarDecorationsRepository'), + latestNotesRepository: Symbol('latestNotesRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'), diff --git a/packages/backend/src/misc/collapsed-queue.ts b/packages/backend/src/misc/collapsed-queue.ts new file mode 100644 index 0000000000000000000000000000000000000000..5bc20a78ae29a0ddeb22e949599b8ff66250fb7c --- /dev/null +++ b/packages/backend/src/misc/collapsed-queue.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +type Job<V> = { + value: V; + timer: NodeJS.Timeout; +}; + +// TODO: redis使ãˆã‚‹ã‚ˆã†ã«ã™ã‚‹ +export class CollapsedQueue<K, V> { + private jobs: Map<K, Job<V>> = new Map(); + + constructor( + private timeout: number, + private collapse: (oldValue: V, newValue: V) => V, + private perform: (key: K, value: V) => Promise<void>, + ) {} + + enqueue(key: K, value: V) { + if (this.jobs.has(key)) { + const old = this.jobs.get(key)!; + const merged = this.collapse(old.value, value); + this.jobs.set(key, { ...old, value: merged }); + } else { + const timer = setTimeout(() => { + const job = this.jobs.get(key)!; + this.jobs.delete(key); + this.perform(key, job.value); + }, this.timeout); + this.jobs.set(key, { value, timer }); + } + } + + async performAllNow() { + const entries = [...this.jobs.entries()]; + this.jobs.clear(); + for (const [_key, job] of entries) { + clearTimeout(job.timer); + } + await Promise.allSettled(entries.map(([key, job]) => this.perform(key, job.value))); + } +} diff --git a/packages/backend/src/misc/fastify-hook-handlers.ts b/packages/backend/src/misc/fastify-hook-handlers.ts index 3e1c099e000c3c33a1aeb9221c7bd7b1386251bd..fa3ef0a267d6f36025fab2801f353e8ee04c36df 100644 --- a/packages/backend/src/misc/fastify-hook-handlers.ts +++ b/packages/backend/src/misc/fastify-hook-handlers.ts @@ -8,7 +8,7 @@ import type { onRequestHookHandler } from 'fastify'; export const handleRequestRedirectToOmitSearch: onRequestHookHandler = (request, reply, done) => { const index = request.url.indexOf('?'); if (~index) { - reply.redirect(301, request.url.slice(0, index)); + reply.redirect(request.url.slice(0, index), 301); } done(); }; diff --git a/packages/backend/src/misc/from-tuple.ts b/packages/backend/src/misc/from-tuple.ts new file mode 100644 index 0000000000000000000000000000000000000000..366b1e310fa45f1288a5a4cce5979940af456a1e --- /dev/null +++ b/packages/backend/src/misc/from-tuple.ts @@ -0,0 +1,7 @@ +export function fromTuple<T>(value: T | [T]): T { + if (Array.isArray(value)) { + return value[0]; + } + + return value; +} diff --git a/packages/backend/src/misc/get-note-summary.ts b/packages/backend/src/misc/get-note-summary.ts index 1a07139a50e5a8d210450d6956f77c67657ac753..60dddee9a2887a3e3cb975d5c331457e03f4fd9f 100644 --- a/packages/backend/src/misc/get-note-summary.ts +++ b/packages/backend/src/misc/get-note-summary.ts @@ -22,14 +22,14 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { // 本文 if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ''; + summary += `CW: ${note.cw}`; + } else if (note.text) { + summary += note.text; } // ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ã‚‹ã¨ã - if ((note.files ?? []).length !== 0) { - summary += ` (📎${note.files!.length})`; + if (note.files && note.files.length !== 0) { + summary += ` (📎${note.files.length})`; } // 投票ãŒæ·»ä»˜ã•ã‚Œã¦ã„ã‚‹ã¨ã @@ -39,7 +39,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { // 返信ã®ã¨ã if (note.replyId) { - if (note.reply) { + if (note.reply && !note.cw) { summary += `\n\nRE: ${getNoteSummary(note.reply)}`; } else { summary += '\n\nRE: ...'; @@ -48,7 +48,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => { // Renoteã®ã¨ã if (note.renoteId) { - if (note.renote) { + if (note.renote && !note.cw) { summary += `\n\nRN: ${getNoteSummary(note.renote)}`; } else { summary += '\n\nRN: ...'; diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index 48f821806c1a55cf4870fbfc2802cfa3d90b1841..c128fded143efeeb703c0c6d75641c68087ec141 100644 --- a/packages/backend/src/misc/is-renote.ts +++ b/packages/backend/src/misc/is-renote.ts @@ -23,6 +23,17 @@ type Quote = hasPoll: true }); +type PureRenote = + Renote & { + text: null, + cw: null, + replyId: null, + hasPoll: false, + fileIds: { + length: 0, + }, + }; + export function isRenote(note: MiNote): note is Renote { return note.renoteId != null; } @@ -36,6 +47,10 @@ export function isQuote(note: Renote): note is Quote { note.fileIds.length > 0; } +export function isPureRenote(note: MiNote): note is PureRenote { + return isRenote(note) && !isQuote(note); +} + type PackedRenote = Packed<'Note'> & { renoteId: NonNullable<Packed<'Note'>['renoteId']> diff --git a/packages/backend/src/misc/is-system-account.ts b/packages/backend/src/misc/is-system-account.ts new file mode 100644 index 0000000000000000000000000000000000000000..a34882f4944afaf8101af8f1575101b878c6f959 --- /dev/null +++ b/packages/backend/src/misc/is-system-account.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +interface UserLike { + readonly username: string; + readonly host: string | null; +} + +/** + * Checks if the given user represents a system account, such as instance.actor. + */ +export function isSystemAccount(user: UserLike): boolean { + return user.host == null && user.username.includes('.'); +} diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index a721b8663c3c32328047f3d7150b8eab8b0d2ae0..040e36228ccfca52a3c940155f07811b0b4c7434 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -144,7 +144,9 @@ export interface Schema extends OfSchema { readonly type?: TypeStringef; readonly nullable?: boolean; readonly optional?: boolean; + readonly prefixItems?: ReadonlyArray<Schema>; readonly items?: Schema; + readonly unevaluatedItems?: Schema | boolean; readonly properties?: Obj; readonly required?: ReadonlyArray<Extract<keyof NonNullable<this['properties']>, string>>; readonly description?: string; @@ -198,6 +200,7 @@ type UnionSchemaType<a extends readonly any[], X extends Schema = a[number]> = X //type UnionObjectSchemaType<a extends readonly any[], X extends Schema = a[number]> = X extends any ? ObjectSchemaType<X> : never; type UnionObjType<s extends Obj, a extends readonly any[], X extends ReadonlyArray<keyof s> = a[number]> = X extends any ? ObjType<s, X> : never; type ArrayUnion<T> = T extends any ? Array<T> : never; +type ArrayToTuple<X extends ReadonlyArray<Schema>> = { [K in keyof X]: SchemaType<X[K]> }; type ObjectSchemaTypeDef<p extends Schema> = p['ref'] extends keyof typeof refs ? Packed<p['ref']> : @@ -232,6 +235,12 @@ export type SchemaTypeDef<p extends Schema> = p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] : never ) : + p['prefixItems'] extends ReadonlyArray<Schema> ? ( + p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] : + p['items'] extends false ? ArrayToTuple<p['prefixItems']> : + p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> : + [...ArrayToTuple<p['prefixItems']>, ...unknown[]] + ) : p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] : any[] ) : diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts index dd810681c5334bd5800d42aeb679fd6d058e43c9..12d7b31e005c8f70cfc48886ee8762ba75eb39fc 100644 --- a/packages/backend/src/models/DriveFile.ts +++ b/packages/backend/src/models/DriveFile.ts @@ -60,8 +60,7 @@ export class MiDriveFile { }) public size: number; - @Column('varchar', { - length: 8192, + @Column('text', { nullable: true, comment: 'The comment of the DriveFile.', }) diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts index dd625f95d31d02c749245a1a621eb47b6db86516..ba93190c570ce4751aa30b2c629aee48e5667bbf 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -158,7 +158,12 @@ export class MiInstance { default: false, }) public isNSFW: boolean; - + + @Column('boolean', { + default: false, + }) + public rejectReports: boolean; + @Column('varchar', { length: 16384, default: '', }) diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts new file mode 100644 index 0000000000000000000000000000000000000000..064fcccc0a469ae3d0ac7099b1a0cb5e2626ef2f --- /dev/null +++ b/packages/backend/src/models/LatestNote.ts @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { MiUser } from '@/models/User.js'; +import { MiNote } from '@/models/Note.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; + +/** + * Maps a user to the most recent post by that user. + * Public, home-only, and followers-only posts are included. + * DMs are not counted. + */ +@Entity('latest_note') +export class SkLatestNote { + @PrimaryColumn({ + name: 'user_id', + type: 'varchar' as const, + length: 32, + }) + public userId: string; + + @PrimaryColumn('boolean', { + name: 'is_public', + default: false, + }) + public isPublic: boolean; + + @PrimaryColumn('boolean', { + name: 'is_reply', + default: false, + }) + public isReply: boolean; + + @PrimaryColumn('boolean', { + name: 'is_quote', + default: false, + }) + public isQuote: boolean; + + @ManyToOne(() => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn({ + name: 'user_id', + }) + public user: MiUser | null; + + @Column({ + name: 'note_id', + type: 'varchar' as const, + length: 32, + }) + public noteId: string; + + @ManyToOne(() => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn({ + name: 'note_id', + }) + public note: MiNote | null; + + constructor(data?: Partial<SkLatestNote>) { + if (!data) return; + + for (const [k, v] of Object.entries(data)) { + (this as Record<string, unknown>)[k] = v; + } + } + + /** + * Generates a compound key matching a provided note. + */ + static keyFor(note: MiNote) { + return { + userId: note.userId, + isPublic: note.visibility === 'public', + isReply: note.replyId != null, + isQuote: isRenote(note) && isQuote(note), + }; + } + + /** + * Checks if two notes would produce equivalent compound keys. + */ + static areEquivalent(first: MiNote, second: MiNote): boolean { + const firstKey = SkLatestNote.keyFor(first); + const secondKey = SkLatestNote.keyFor(second); + + return ( + firstKey.userId === secondKey.userId && + firstKey.isPublic === secondKey.isPublic && + firstKey.isReply === secondKey.isReply && + firstKey.isQuote === secondKey.isQuote + ); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 07c4e28b3a6d30d1346993a371de41aadc9aee24..0ea6765d6ac78950a721b7afe033f3efc33afa68 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -139,6 +139,12 @@ export class MiMeta { }) public app512IconUrl: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public sidebarLogoUrl: string | null; + @Column('varchar', { length: 1024, nullable: true, @@ -263,6 +269,23 @@ export class MiMeta { }) public turnstileSecretKey: string | null; + @Column('boolean', { + default: false, + }) + public enableFC: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public fcSiteKey: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public fcSecretKey: string | null; + // chaptchaç³»ã‚’è¿½åŠ ã—ãŸéš›ã«ã¯nodeinfoã®ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã«è¿½åŠ ã™ã‚‹ã®ã‚’忘れãªã„よã†ã«ã™ã‚‹ã“㨠@Column('enum', { @@ -621,6 +644,11 @@ export class MiMeta { }) public perUserListTimelineCacheMax: number; + @Column('boolean', { + default: false, + }) + public enableReactionsBuffering: boolean; + @Column('integer', { default: 0, }) @@ -668,4 +696,25 @@ export class MiMeta { nullable: true, }) public urlPreviewUserAgent: string | null; + + @Column('varchar', { + length: 3072, + array: true, + default: '{}', + comment: 'An array of URL strings or regex that can be used to omit warnings about redirects to external sites. Separate them with spaces to specify AND, and enclose them with slashes to specify regular expressions. Each item is regarded as an OR.', + }) + public trustedLinkUrlPatterns: string[]; + + @Column('varchar', { + length: 128, + default: 'all', + }) + public federation: 'all' | 'specified' | 'none'; + + @Column('varchar', { + length: 1024, + array: true, + default: '{}', + }) + public federationHosts: string[]; } diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index b11e2ec62b76817cbea0a7e0d90ca006642240f9..408e023ff70375f74794615274c9456dc700be7d 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -66,8 +66,8 @@ export class MiNote { }) public name: string | null; - @Column('varchar', { - length: 512, nullable: true, + @Column('text', { + nullable: true, }) public cw: string | null; diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 4ed71a106cb1b2d6eaf7ee776fb837bc801c0139..c4f046c5657203812d5ffc40142be369f282196b 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -7,6 +7,8 @@ import { MiUser } from './User.js'; import { MiNote } from './Note.js'; import { MiAccessToken } from './AccessToken.js'; import { MiRole } from './Role.js'; +import { MiDriveFile } from './DriveFile.js'; +import { userExportableEntities } from '@/types.js'; export type MiNotification = { type: 'note'; @@ -67,6 +69,7 @@ export type MiNotification = { id: string; createdAt: string; notifierId: MiUser['id']; + message: string | null; } | { type: 'roleAssigned'; id: string; @@ -77,6 +80,12 @@ export type MiNotification = { id: string; createdAt: string; achievement: string; +} | { + type: 'exportCompleted'; + id: string; + createdAt: string; + exportedEntity: typeof userExportableEntities[number]; + fileId: MiDriveFile['id']; } | { type: 'app'; id: string; @@ -85,7 +94,7 @@ export type MiNotification = { /** * アプリ通知ã®body */ - customBody: string | null; + customBody: string; /** * アプリ通知ã®header diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 1eaeb86df686bd8b9cba4a902db6c9c436ada4f2..eb45b9a6311e9c1565b724143c79a30384b92df7 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -7,6 +7,7 @@ import type { Provider } from '@nestjs/common'; import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import { + SkLatestNote, MiAbuseReportNotificationRecipient, MiAbuseUserReport, MiAccessToken, @@ -118,6 +119,12 @@ const $avatarDecorationsRepository: Provider = { inject: [DI.db], }; +const $latestNotesRepository: Provider = { + provide: DI.latestNotesRepository, + useFactory: (db: DataSource) => db.getRepository(SkLatestNote).extend(miRepository as MiRepository<SkLatestNote>), + inject: [DI.db], +}; + const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>), @@ -511,6 +518,7 @@ const $reversiGamesRepository: Provider = { $announcementReadsRepository, $appsRepository, $avatarDecorationsRepository, + $latestNotesRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, @@ -583,6 +591,7 @@ const $reversiGamesRepository: Provider = { $announcementReadsRepository, $appsRepository, $avatarDecorationsRepository, + $latestNotesRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index b0910133c9bbe6e1c35904dc67c9de638bc31eba..c7ecccf1cf62d876b1b73476f29341aca72bfc1d 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -170,6 +170,7 @@ export class MiUser { flipH?: boolean; offsetX?: number; offsetY?: number; + showBelow?: boolean; }[]; @Index() @@ -178,6 +179,11 @@ export class MiUser { }) public tags: string[]; + @Column('integer', { + default: 0, + }) + public score: number; + @Column('boolean', { default: false, comment: 'Whether the User is suspended.', @@ -340,6 +346,7 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const; +export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const; export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } as const; export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 40ea26f6107d90021e186ac95e8746039dfe86a1..751b1aff08a33297744d2533fcfee6db34130f33 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -49,6 +49,14 @@ export class MiUserProfile { }) public description: string | null; + // フォãƒãƒ¼ã•ã‚ŒãŸéš›ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ + @Column('varchar', { + length: 256, nullable: true, + }) + public followedMessage: string | null; + + // TODO: éµã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®å ´åˆã®ã€ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆå—信時ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚‚è¨å®šã§ãるよã†ã«ã™ã‚‹ + @Column('jsonb', { default: [], }) @@ -188,6 +196,11 @@ export class MiUserProfile { }) public alwaysMarkNsfw: boolean; + @Column('boolean', { + default: false, + }) + public defaultSensitive: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts index 2a727f86fd93e1a764382da41332177de6f2d895..8ef73fa143da5b67aeca2e5d1c3906d60285f44b 100644 --- a/packages/backend/src/models/Webhook.ts +++ b/packages/backend/src/models/Webhook.ts @@ -8,6 +8,7 @@ import { id } from './util/id.js'; import { MiUser } from './User.js'; export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const; +export type WebhookEventTypes = typeof webhookEventTypes[number]; @Entity('webhook') export class MiWebhook { diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index f7646dce2ae0b928117e1d1c773863f2545573d4..ac2dd62aa29ae233bb44b18cf1e4b058f5a2a064 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -10,6 +10,7 @@ import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLo import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; import { ObjectUtils } from 'typeorm/util/ObjectUtils.js'; import { OrmUtils } from 'typeorm/util/OrmUtils.js'; +import { SkLatestNote } from '@/models/LatestNote.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; @@ -126,6 +127,7 @@ export const miRepository = { } satisfies MiRepository<ObjectLiteral>; export { + SkLatestNote, MiAbuseUserReport, MiAbuseReportNotificationRecipient, MiAccessToken, @@ -224,6 +226,7 @@ export type GalleryPostsRepository = Repository<MiGalleryPost> & MiRepository<Mi export type HashtagsRepository = Repository<MiHashtag> & MiRepository<MiHashtag>; export type InstancesRepository = Repository<MiInstance> & MiRepository<MiInstance>; export type MetasRepository = Repository<MiMeta> & MiRepository<MiMeta>; +export type LatestNotesRepository = Repository<SkLatestNote> & MiRepository<SkLatestNote>; export type ModerationLogsRepository = Repository<MiModerationLog> & MiRepository<MiModerationLog>; export type MutingsRepository = Repository<MiMuting> & MiRepository<MiMuting>; export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<MiRenoteMuting>; diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 062dba9bad6449427749e733fdaf31f043a36e1f..7960e748e9a5e63b4669c78552c130544ed40b93 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -121,6 +121,11 @@ export const packedFederationInstanceSchema = { optional: false, nullable: false, }, + rejectReports: { + type: 'boolean', + optional: false, + nullable: false, + }, moderationNote: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 1d620f16fdcfd37f9fe3fd413a7c553df7280eeb..decdbd56506c04512f16ef204d3e7499a2c4a96b 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -127,6 +127,14 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + enableFC: { + type: 'boolean', + optional: false, nullable: false, + }, + fcSiteKey: { + type: 'string', + optional: false, nullable: true, + }, enableAchievements: { type: 'boolean', optional: false, nullable: true, @@ -160,10 +168,34 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + sidebarLogoUrl: { + type: 'string', + optional: false, nullable: true, + }, maxNoteTextLength: { type: 'number', optional: false, nullable: false, }, + maxRemoteNoteTextLength: { + type: 'number', + optional: false, nullable: false, + }, + maxCwLength: { + type: 'number', + optional: false, nullable: false, + }, + maxRemoteCwLength: { + type: 'number', + optional: false, nullable: false, + }, + maxAltTextLength: { + type: 'number', + optional: false, nullable: false, + }, + maxRemoteAltTextLength: { + type: 'number', + optional: false, nullable: false, + }, ads: { type: 'array', optional: false, nullable: false, @@ -269,6 +301,18 @@ export const packedMetaLiteSchema = { optional: false, nullable: false, default: 'local', }, + trustedLinkUrlPatterns: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + maxFileSize: { + type: 'number', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 3f31cc47ee13120ad734ab1108e9b73f1b59bd28..990e8957cfb32360f76e226837d774de1a7b8d6a 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { notificationTypes } from '@/types.js'; +import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; +import { notificationTypes, userExportableEntities } from '@/types.js'; const baseSchema = { type: 'object', @@ -266,6 +267,10 @@ export const packedNotificationSchema = { optional: false, nullable: false, format: 'id', }, + message: { + type: 'string', + optional: false, nullable: true, + }, }, }, { type: 'object', @@ -294,6 +299,27 @@ export const packedNotificationSchema = { achievement: { type: 'string', optional: false, nullable: false, + enum: ACHIEVEMENT_TYPES, + }, + }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['exportCompleted'], + }, + exportedEntity: { + type: 'string', + optional: false, nullable: false, + enum: userExportableEntities, + }, + fileId: { + type: 'string', + optional: false, nullable: false, + format: 'id', }, }, }, { @@ -311,11 +337,11 @@ export const packedNotificationSchema = { }, header: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, }, icon: { type: 'string', - optional: false, nullable: false, + optional: false, nullable: true, }, }, }, { diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 504b9b122fb0dbdb8d995a8ff630b3923b6640f6..19ea6263c9c63c5f8d35558161a8d82fe62f8e56 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -276,6 +276,26 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, + canImportAntennas: { + type: 'boolean', + optional: false, nullable: false, + }, + canImportBlocking: { + type: 'boolean', + optional: false, nullable: false, + }, + canImportFollowing: { + type: 'boolean', + optional: false, nullable: false, + }, + canImportMuting: { + type: 'boolean', + optional: false, nullable: false, + }, + canImportUserLists: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 33a3efd453016a0488dbcfdd0643304b30932db4..d5e847cc40493d0e985629fb407cd657bf82b488 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -104,6 +104,10 @@ export const packedUserLiteSchema = { type: 'number', nullable: false, optional: true, }, + showBelow: { + type: 'boolean', + nullable: false, optional: true, + }, }, }, }, @@ -117,9 +121,10 @@ export const packedUserLiteSchema = { nullable: false, optional: true, default: false, }, - isSilenced: { + isSystem: { type: 'boolean', - nullable: false, optional: false, + nullable: false, optional: true, + default: false, }, noindex: { type: 'boolean', @@ -137,6 +142,10 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: true, }, + isSilenced: { + type: 'boolean', + nullable: false, optional: false, + }, instance: { type: 'object', nullable: false, optional: true, @@ -268,6 +277,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + isSilenced: { + type: 'boolean', + nullable: false, optional: false, + }, isSuspended: { type: 'boolean', nullable: false, optional: false, @@ -287,7 +300,7 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: true, optional: false, example: '2018-03-12', }, - ListenBrainz: { + listenbrainz: { type: 'string', nullable: true, optional: false, @@ -403,6 +416,10 @@ export const packedUserDetailedNotMeOnlySchema = { ref: 'RoleLite', }, }, + followedMessage: { + type: 'string', + nullable: true, optional: true, + }, memo: { type: 'string', nullable: true, optional: false, @@ -475,6 +492,10 @@ export const packedMeDetailedOnlySchema = { nullable: true, optional: false, format: 'id', }, + followedMessage: { + type: 'string', + nullable: true, optional: false, + }, isModerator: { type: 'boolean', nullable: true, optional: false, @@ -495,6 +516,10 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + defaultSensitive: { + type: 'boolean', + nullable: false, optional: false, + }, autoSensitive: { type: 'boolean', nullable: false, optional: false, @@ -569,6 +594,10 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: false, optional: false, }, + hasPendingSentFollowRequest: { + type: 'boolean', + nullable: false, optional: false, + }, unreadNotificationsCount: { type: 'number', nullable: false, optional: false, diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 047b7f8ae9ff43b8a96b1db83dd97f30767958a7..2d66e6e4454cdef2debc54d9138dbb9260893b26 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -83,6 +83,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { SkLatestNote } from '@/models/LatestNote.js'; pg.types.setTypeParser(20, Number); @@ -130,6 +131,7 @@ class MyCustomLogger implements Logger { } export const entities = [ + SkLatestNote, MiAnnouncement, MiAnnouncementRead, MiMeta, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 7daca687a1a903a9af3697c8979d3ccad4e6a935..7c6675b15d8a9e781848b4ca43ec4ead87cf4625 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -14,6 +14,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; +import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; @@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor ResyncChartsProcessorService, CleanChartsProcessorService, CheckExpiredMutingsProcessorService, + BakeBufferedReactionsProcessorService, CleanProcessorService, DeleteDriveFilesProcessorService, ExportAccountDataProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 7a6169bf9cfaffe27efd1d2ec0d0b3a09ea7d469..eaeb6d58dfe9777a37931b0c0a9e1849a2b8d83a 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -40,6 +40,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; +import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; @@ -122,24 +123,35 @@ export class QueueProcessorService implements OnApplicationShutdown { private cleanChartsProcessorService: CleanChartsProcessorService, private aggregateRetentionProcessorService: AggregateRetentionProcessorService, private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, + private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private cleanProcessorService: CleanProcessorService, ) { this.logger = this.queueLoggerService.logger; - function renderError(e: Error): any { - if (e) { // 何故ã‹eãŒundefinedã§æ¥ã‚‹ã“ã¨ãŒã‚ã‚‹ - return { - stack: e.stack, - message: e.message, - name: e.name, - }; - } else { - return { - stack: '?', - message: '?', - name: '?', - }; + function renderError(e?: Error) { + // 何故ã‹eãŒundefinedã§æ¥ã‚‹ã“ã¨ãŒã‚ã‚‹ + if (!e) return '?'; + + if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') { + return `${e.name}: ${e.message}`; } + + return { + stack: e.stack, + message: e.message, + name: e.name, + }; + } + + function renderJob(job?: Bull.Job) { + if (!job) return '?'; + + return { + name: job.name || undefined, + info: getJobInfo(job), + failedReason: job.failedReason || undefined, + data: job.data, + }; } //#region system @@ -151,6 +163,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'cleanCharts': return this.cleanChartsProcessorService.process(); case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); + case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); default: throw new Error(`unrecognized job type ${job.name} for system`); } @@ -173,15 +186,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err: Error) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -238,15 +251,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -278,15 +291,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Deliver: ${err.message}`, { + Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -318,15 +331,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Inbox: ${err.message}`, { + Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -358,15 +371,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, { + Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -398,15 +411,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, { + Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -445,15 +458,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -486,15 +499,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts new file mode 100644 index 0000000000000000000000000000000000000000..d49c99f694af950f10ee9d9f3d94062f690f7bdd --- /dev/null +++ b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; + +@Injectable() +export class BakeBufferedReactionsProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.meta) + private meta: MiMeta, + + private reactionsBufferingService: ReactionsBufferingService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions'); + } + + @bindThis + public async process(): Promise<void> { + if (!this.meta.enableReactionsBuffering) { + this.logger.info('Reactions buffering is disabled. Skipping...'); + return; + } + + this.logger.info('Baking buffered reactions...'); + + await this.reactionsBufferingService.bake(); + + this.logger.succ('All buffered reactions baked.'); + } +} diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 4076e9da9038f8ca7f5d5c66b882745bc6127dfd..9590a4fe71c5ad5c2865a32a5a88fa23938f5126 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Bull from 'bullmq'; import { Not } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { InstancesRepository } from '@/models/_.js'; +import type { InstancesRepository, MiMeta } from '@/models/_.js'; import type Logger from '@/logger.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; @@ -31,10 +30,12 @@ export class DeliverProcessorService { private latest: string | null; constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private metaService: MetaService, private utilityService: UtilityService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, @@ -52,9 +53,7 @@ export class DeliverProcessorService { public async process(job: Bull.Job<DeliverJobData>): Promise<string> { const { host } = new URL(job.data.to); - // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) { + if (!this.utilityService.isFederationAllowedUri(job.data.to)) { return 'skip (blocked)'; } @@ -88,7 +87,7 @@ export class DeliverProcessorService { this.apRequestChart.deliverSucc(); this.federationChart.deliverd(i.host, true); - if (meta.enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.requestSent(i.host, true); } }); @@ -120,7 +119,7 @@ export class DeliverProcessorService { this.apRequestChart.deliverFail(); this.federationChart.deliverd(i.host, false); - if (meta.enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.requestSent(i.host, false); } }); diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index 88c4ea29c0e630fab61b78a4912713ca068b3a7d..b3111865adc6e238cb8191a46967cc10a3c615fd 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js'; import { bindThis } from '@/decorators.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { DBExportAntennasData } from '../types.js'; import type * as Bull from 'bullmq'; @@ -35,6 +36,7 @@ export class ExportAntennasProcessorService { private driveService: DriveService, private utilityService: UtilityService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas'); } @@ -95,6 +97,11 @@ export class ExportAntennasProcessorService { const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); this.logger.succ('Exported to: ' + driveFile.id); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'antenna', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts index 6ec3c18786fee8e5d6d51947e7932b62f7d76f80..ecc439db69ce5040906180178c6ce785030467bf 100644 --- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts @@ -13,6 +13,7 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -30,6 +31,7 @@ export class ExportBlockingProcessorService { private blockingsRepository: BlockingsRepository, private utilityService: UtilityService, + private notificationService: NotificationService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, ) { @@ -109,6 +111,11 @@ export class ExportBlockingProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'blocking', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts index 01eab26e96abc55ee65242262750efacc275a6ce..583ddbb745a47e9654da5d13e3a678f07d5b39cf 100644 --- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -19,6 +19,7 @@ import { bindThis } from '@/decorators.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @@ -43,6 +44,7 @@ export class ExportClipsProcessorService { private driveService: DriveService, private queueLoggerService: QueueLoggerService, private idService: IdService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-clips'); } @@ -79,6 +81,11 @@ export class ExportClipsProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'clip', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index 45087927a55629e09fc920fb0864dda8ae174c44..14d32e78b3fca95e854b3fc0b286aa21128b0a35 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -16,6 +16,7 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { DownloadService } from '@/core/DownloadService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -37,6 +38,7 @@ export class ExportCustomEmojisProcessorService { private driveService: DriveService, private downloadService: DownloadService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis'); } @@ -134,6 +136,12 @@ export class ExportCustomEmojisProcessorService { const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'customEmoji', + fileId: driveFile.id, + }); + cleanup(); archiveCleanup(); resolve(); diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index 7bb626dd31bf43cce4cce1867bdf4331bd4f263a..b81feece018e31acda879f24596cc6a93abede4e 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -16,6 +16,7 @@ import type { MiPoll } from '@/models/Poll.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService { private driveService: DriveService, private queueLoggerService: QueueLoggerService, private idService: IdService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites'); } @@ -123,6 +125,11 @@ export class ExportFavoritesProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'favorite', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts index 1cc80e66d7189f552431ffd30dabb958bc1c0185..903f96251587f6943746f1ea5033c8e043ac100c 100644 --- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts @@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import type { MiFollowing } from '@/models/Following.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -36,6 +37,7 @@ export class ExportFollowingProcessorService { private utilityService: UtilityService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-following'); } @@ -113,6 +115,11 @@ export class ExportFollowingProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'following', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts index 243b74f2c2be99c63736800f058df92ff6f4f158..f9867ade292a30d4d0e5f81809992415df9ec163 100644 --- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts @@ -13,6 +13,7 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -32,6 +33,7 @@ export class ExportMutingProcessorService { private utilityService: UtilityService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-muting'); } @@ -110,6 +112,11 @@ export class ExportMutingProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'muting', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index c7611012d74b8aacc838bd5a0004d769a1a04b9f..9e2b678219cfd4ceb16976fcf78d978fe13adcc6 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { JsonArrayStream } from '@/misc/JsonArrayStream.js'; import { FileWriterStream } from '@/misc/FileWriterStream.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; @@ -112,6 +113,7 @@ export class ExportNotesProcessorService { private queueLoggerService: QueueLoggerService, private driveFileEntityService: DriveFileEntityService, private idService: IdService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-notes'); } @@ -150,6 +152,11 @@ export class ExportNotesProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'note', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts index ee87cff5d360e37aa945dd113e04761a17c33417..c483d79854d0f028cb26833172b25b560fc41a33 100644 --- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts @@ -13,6 +13,7 @@ import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import { UtilityService } from '@/core/UtilityService.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; @@ -35,6 +36,7 @@ export class ExportUserListsProcessorService { private utilityService: UtilityService, private driveService: DriveService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists'); } @@ -89,6 +91,11 @@ export class ExportUserListsProcessorService { const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); this.logger.succ(`Exported to: ${driveFile.id}`); + + this.notificationService.createNotification(user.id, 'exportCompleted', { + exportedEntity: 'userList', + fileId: driveFile.id, + }); } finally { cleanup(); } diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 04ad74ee01a68594b150cd15c649ca0da6f0578a..17ba71df3dad690b434df53e992403a68eafb5a2 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -88,23 +88,30 @@ export class ImportCustomEmojisProcessorService { await this.emojisRepository.delete({ name: nameNfc, }); - const driveFile = await this.driveService.addFile({ - user: null, - path: emojiPath, - name: record.fileName, - force: true, - }); - await this.customEmojiService.add({ - name: nameNfc, - category: emojiInfo.category?.normalize('NFC'), - host: null, - aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')), - driveFile, - license: emojiInfo.license, - isSensitive: emojiInfo.isSensitive, - localOnly: emojiInfo.localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: [], - }); + try { + const driveFile = await this.driveService.addFile({ + user: null, + path: emojiPath, + name: record.fileName, + force: true, + }); + await this.customEmojiService.add({ + name: nameNfc, + category: emojiInfo.category?.normalize('NFC'), + host: null, + aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')), + driveFile, + license: emojiInfo.license, + isSensitive: emojiInfo.isSensitive, + localOnly: emojiInfo.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + }); + } catch (e) { + if (e instanceof Error || typeof e === 'string') { + this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`); + } + continue; + } } cleanup(); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 641b8b860717b7c736f54a01365e97513e040f17..11b00bb6832472a20d759b09b4f651b3af39917e 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -4,11 +4,10 @@ */ import { URL } from 'node:url'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; import type Logger from '@/logger.js'; -import { MetaService } from '@/core/MetaService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; @@ -26,16 +25,28 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js'; import { ApInboxService } from '@/core/activitypub/ApInboxService.js'; import { bindThis } from '@/decorators.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { CollapsedQueue } from '@/misc/collapsed-queue.js'; +import { MiNote } from '@/models/Note.js'; +import { MiMeta } from '@/models/Meta.js'; +import { DI } from '@/di-symbols.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; +type UpdateInstanceJob = { + latestRequestReceivedAt: Date, + shouldUnsuspend: boolean, +}; + @Injectable() -export class InboxProcessorService { +export class InboxProcessorService implements OnApplicationShutdown { private logger: Logger; + private updateInstanceQueue: CollapsedQueue<MiNote['id'], UpdateInstanceJob>; constructor( + @Inject(DI.meta) + private meta: MiMeta, + private utilityService: UtilityService, - private metaService: MetaService, private apInboxService: ApInboxService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, @@ -48,6 +59,7 @@ export class InboxProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); + this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance); } @bindThis @@ -63,9 +75,7 @@ export class InboxProcessorService { const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); - // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { + if (!this.utilityService.isFederationAllowedHost(host)) { return `Blocked request: ${host}`; } @@ -108,19 +118,16 @@ export class InboxProcessorService { // HTTP-Signatureã®æ¤œè¨¼ let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); - // ã¾ãŸã€signatureã®signerã¯ã€activity.actorã¨ä¸€è‡´ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ - if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { - let renewKeyFailed = true; - - if (!httpSignatureValidated) { - authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); - - if (authUser.key != null) { - httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); - renewKeyFailed = false; - } + // maybe they changed their key? refetch it + if (!httpSignatureValidated) { + authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); + if (authUser.key != null) { + httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); } + } + // ã¾ãŸã€signatureã®signerã¯ã€activity.actorã¨ä¸€è‡´ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ + if (!httpSignatureValidated || authUser.user.uri !== getApId(activity.actor)) { // 一致ã—ãªãã¦ã‚‚ã€ã§ã‚‚LD-SignatureãŒã‚ã‚Šãã†ãªã‚‰ãã£ã¡ã‚‚見る const ldSignature = activity.signature; if (ldSignature) { @@ -169,9 +176,8 @@ export class InboxProcessorService { throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); } - // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– const ldHost = this.utilityService.extractDbHost(authUser.user.uri); - if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { + if (!this.utilityService.isFederationAllowedHost(ldHost)) { throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { @@ -190,11 +196,9 @@ export class InboxProcessorService { // Update stats this.federatedInstanceService.fetch(authUser.user.host).then(i => { - this.federatedInstanceService.update(i.id, { + this.updateInstanceQueue.enqueue(i.id, { latestRequestReceivedAt: new Date(), - isNotResponding: false, - // ã‚‚ã—サーãƒãƒ¼ãŒæ»ã‚“ã§ã‚‹ãŸã‚ã«é…ä¿¡ãŒæ¢ã¾ã£ã¦ã„ãŸå ´åˆã«ã¯è‡ªå‹•çš„ã«å¾©æ´»ã•ã›ã¦ã‚ã’ã‚‹ - suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined, + shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding', }); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); @@ -202,7 +206,7 @@ export class InboxProcessorService { this.apRequestChart.inbox(); this.federationChart.inbox(i.host); - if (meta.enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.requestReceived(i.host); } }); @@ -230,4 +234,36 @@ export class InboxProcessorService { } return 'ok'; } + + @bindThis + public collapseUpdateInstanceJobs(oldJob: UpdateInstanceJob, newJob: UpdateInstanceJob) { + const latestRequestReceivedAt = oldJob.latestRequestReceivedAt < newJob.latestRequestReceivedAt + ? newJob.latestRequestReceivedAt + : oldJob.latestRequestReceivedAt; + const shouldUnsuspend = oldJob.shouldUnsuspend || newJob.shouldUnsuspend; + return { + latestRequestReceivedAt, + shouldUnsuspend, + }; + } + + @bindThis + public async performUpdateInstance(id: string, job: UpdateInstanceJob) { + await this.federatedInstanceService.update(id, { + latestRequestReceivedAt: new Date(), + isNotResponding: false, + // ã‚‚ã—サーãƒãƒ¼ãŒæ»ã‚“ã§ã‚‹ãŸã‚ã«é…ä¿¡ãŒæ¢ã¾ã£ã¦ã„ãŸå ´åˆã«ã¯è‡ªå‹•çš„ã«å¾©æ´»ã•ã›ã¦ã‚ã’ã‚‹ + suspensionState: job.shouldUnsuspend ? 'none' : undefined, + }); + } + + @bindThis + public async dispose(): Promise<void> { + await this.updateInstanceQueue.performAllNow(); + } + + @bindThis + async onApplicationShutdown(signal?: string) { + await this.dispose(); + } } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 398ad54f6af6bc0821af3a1f400ad95834df0d42..52592c47c64912f5bc5afd54ef15f48c7699e6e8 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -21,7 +21,6 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { QueueService } from '@/core/QueueService.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { UserKeypairService } from '@/core/UserKeypairService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; @@ -75,7 +74,6 @@ export class ActivityPubServerService { @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, - private metaService: MetaService, private utilityService: UtilityService, private userEntityService: UserEntityService, private instanceActorService: InstanceActorService, @@ -175,8 +173,7 @@ export class ActivityPubServerService { return true; } - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, keyHost)) { + if (!this.utilityService.isFederationAllowedHost(keyHost)) { /* blocked instance: refuse (we don't care if the signature is good, if they even pretend to be from a blocked instance, they're out) */ @@ -208,15 +205,11 @@ export class ActivityPubServerService { let httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + // maybe they changed their key? refetch it if (!httpSignatureValidated) { - this.authlogger.info(`${logPrefix} failed to validate signature, re-fetching the key for ${authUser.user.uri}`); - // maybe they changed their key? refetch it authUser.key = await this.apDbResolverService.refetchPublicKeyForApId(authUser.user); - if (authUser.key != null) { httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); - } else { - this.authlogger.warn(`${logPrefix} failed to re-fetch key for ${authUser.user}`); } } @@ -795,7 +788,7 @@ export class ActivityPubServerService { fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; - + vary(reply.raw, 'Accept'); const userId = request.params.user; @@ -811,7 +804,7 @@ export class ActivityPubServerService { fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return; - + vary(reply.raw, 'Accept'); const user = await this.usersRepository.findOneBy({ diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 65a82181740999d72da472184857035a2a757aaf..1a4d0cb48f29136577cef3a9d0bc890d90ca0138 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -80,7 +80,7 @@ export class FileServerService { .catch(err => this.errorHandler(request, reply, err)); }); fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => { - return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`); + return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301); }); done(); }); @@ -145,12 +145,12 @@ export class FileServerService { url.searchParams.set('static', '1'); file.cleanup(); - return await reply.redirect(301, url.toString()); + return await reply.redirect(url.toString(), 301); } else if (file.mime.startsWith('video/')) { const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url); if (externalThumbnail) { file.cleanup(); - return await reply.redirect(301, externalThumbnail); + return await reply.redirect(externalThumbnail, 301); } image = await this.videoProcessingService.generateVideoThumbnail(file.path); @@ -165,7 +165,7 @@ export class FileServerService { url.searchParams.set('url', file.url); file.cleanup(); - return await reply.redirect(301, url.toString()); + return await reply.redirect(url.toString(), 301); } } @@ -312,11 +312,17 @@ export class FileServerService { } return await reply.redirect( - 301, url.toString(), + 301, ); } + if (!request.headers['user-agent']) { + throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); + } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { + throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); + } + // Create temp file const file = await this.getStreamAndTypeFromUrl(url); if (file === '404') { diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts index 2c3ed85925c93781df739b033cdeebfd816b604d..5980609f02bd94e19178f5d4d17d679847845625 100644 --- a/packages/backend/src/server/HealthServerService.ts +++ b/packages/backend/src/server/HealthServerService.ts @@ -27,6 +27,9 @@ export class HealthServerService { @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, + @Inject(DI.redisForReactions) + private redisForReactions: Redis.Redis, + @Inject(DI.db) private db: DataSource, @@ -43,6 +46,7 @@ export class HealthServerService { this.redisForPub.ping(), this.redisForSub.ping(), this.redisForTimelines.ping(), + this.redisForReactions.ping(), this.db.query('SELECT 1'), ...(this.meilisearch ? [this.meilisearch.health()] : []), ]).then(() => 200, () => 503)); diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index bc8d3c0411d00418958ebb96c776daa286db6706..6dee6ecd780c2abac0432f7485e224bcb6b3b53f 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -121,7 +121,13 @@ export class NodeinfoServerService { enableRecaptcha: meta.enableRecaptcha, enableMcaptcha: meta.enableMcaptcha, enableTurnstile: meta.enableTurnstile, + enableFC: meta.enableFC, maxNoteTextLength: this.config.maxNoteLength, + maxRemoteNoteTextLength: this.config.maxRemoteNoteLength, + maxCwLength: this.config.maxCwLength, + maxRemoteCwLength: this.config.maxRemoteCwLength, + maxAltTextLength: this.config.maxAltTextLength, + maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, proxyAccountName: proxyAccount ? proxyAccount.username : null, diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index 39c8f67b8e9f0b7fe01c6d408823ab692fa71eff..216e6b4fb81d217f2d8440d745266508ad2e813f 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -49,6 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; import { ReversiChannelService } from './api/stream/channels/reversi.js'; import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js'; +import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js'; @Module({ imports: [ @@ -74,6 +75,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js AuthenticateService, RateLimiterService, SigninApiService, + SigninWithPasskeyApiService, SigninService, SignupApiService, StreamingApiServerService, diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 30c133d9ece5dc59f252c7909e2c535e5b7a5393..43a2a3a2b03b3a5af6bf34655c129dd89d416b46 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; -import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; @@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ApiServerService } from './api/ApiServerService.js'; @@ -45,6 +44,9 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -54,7 +56,6 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - private metaService: MetaService, private userEntityService: UserEntityService, private apiServerService: ApiServerService, private openApiServerService: OpenApiServerService, @@ -167,8 +168,8 @@ export class ServerService implements OnApplicationShutdown { } return await reply.redirect( - 301, url.toString(), + 301, ); }); @@ -195,7 +196,7 @@ export class ServerService implements OnApplicationShutdown { reply.header('Content-Type', 'image/png'); reply.header('Cache-Control', 'public, max-age=86400'); - if ((await this.metaService.fetch()).enableIdenticonGeneration) { + if (this.meta.enableIdenticonGeneration) { return await genIdenticon(request.params.x); } else { return reply.redirect('/static-assets/avatar.png'); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 808795fdac300b9dd22f36bf759ea1e41332cf62..016db6ac194bfbc1535f7c3748fe16978b521583 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -13,8 +13,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiAccessToken } from '@/models/AccessToken.js'; import type Logger from '@/logger.js'; -import type { UserIpsRepository } from '@/models/_.js'; -import { MetaService } from '@/core/MetaService.js'; +import type { MiMeta, UserIpsRepository } from '@/models/_.js'; import { createTemp } from '@/misc/create-temp.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; @@ -40,13 +39,15 @@ export class ApiCallService implements OnApplicationShutdown { private userIpHistoriesClearIntervalId: NodeJS.Timeout; constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.config) private config: Config, @Inject(DI.userIpsRepository) private userIpsRepository: UserIpsRepository, - private metaService: MetaService, private authenticateService: AuthenticateService, private rateLimiterService: RateLimiterService, private roleService: RoleService, @@ -199,9 +200,18 @@ export class ApiCallService implements OnApplicationShutdown { return; } - const [path] = await createTemp(); + const [path, cleanup] = await createTemp(); await stream.pipeline(multipartData.file, fs.createWriteStream(path)); + // ファイルサイズãŒåˆ¶é™ã‚’超ãˆã¦ã„ãŸå ´åˆ + // ãªãŠ truncated ã¯ã‚¹ãƒˆãƒªãƒ¼ãƒ ã‚’èªã¿åˆ‡ã£ã¦ã‹ã‚‰ã§ãªã„ã¨æ©Ÿèƒ½ã—ãªã„ãŸã‚ã€stream.pipeline より後ã«ã‚ã‚‹å¿…è¦ãŒã‚ã‚‹ + if (multipartData.file.truncated) { + cleanup(); + reply.code(413); + reply.send(); + return; + } + const fields = {} as Record<string, unknown>; for (const [k, v] of Object.entries(multipartData.fields)) { fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined; @@ -256,10 +266,14 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - private async logIp(request: FastifyRequest, user: MiLocalUser) { - const meta = await this.metaService.fetch(); - if (!meta.enableIpLogging) return; + private logIp(request: FastifyRequest, user: MiLocalUser) { + if (!this.meta.enableIpLogging) return; const ip = request.ip; + if (!ip) { + this.logger.warn(`user ${user.id} has a null IP address; please check your network configuration.`); + return; + } + const ips = this.userIpHistories.get(user.id); if (ips == null || !ips.has(ip)) { if (ips == null) { diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 4a5935f93092dd91c631c50c0eeb498cc80defe1..ac3b982742e6d8fe6f1c868cf0ad2d097dcc2fd4 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -8,6 +8,7 @@ import cors from '@fastify/cors'; import multipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; import { ModuleRef } from '@nestjs/core'; +import { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { Config } from '@/config.js'; import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -17,6 +18,7 @@ import endpoints from './endpoints.js'; import { ApiCallService } from './ApiCallService.js'; import { SignupApiService } from './SignupApiService.js'; import { SigninApiService } from './SigninApiService.js'; +import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @Injectable() @@ -37,6 +39,7 @@ export class ApiServerService { private apiCallService: ApiCallService, private signupApiService: SignupApiService, private signinApiService: SigninApiService, + private signinWithPasskeyApiService: SigninWithPasskeyApiService, ) { //this.createServer = this.createServer.bind(this); } @@ -49,7 +52,7 @@ export class ApiServerService { fastify.register(multipart, { limits: { - fileSize: this.config.maxFileSize ?? 262144000, + fileSize: this.config.maxFileSize, files: 1, }, }); @@ -115,6 +118,7 @@ export class ApiServerService { 'hcaptcha-response'?: string; 'g-recaptcha-response'?: string; 'turnstile-response'?: string; + 'frc-captcha-solution'?: string; } }>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); @@ -131,6 +135,12 @@ export class ApiServerService { }; }>('/signin', (request, reply) => this.signinApiService.signin(request, reply)); + fastify.post<{ + Body: { + credential?: AuthenticationResponseJSON; + }; + }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply)); + fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply)); fastify.get('/v1/instance/peers', async (request, reply) => { diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 4a08410cebe57f1b556d2fb3df7cdeccf3f56664..5bdd7cf65088496816b95f4d06bd79b07152696d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -79,6 +79,7 @@ import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; import * as ep___admin_approveUser from './endpoints/admin/approve-user.js'; +import * as ep___admin_declineUser from './endpoints/admin/decline-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; @@ -97,6 +98,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js'; import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js'; import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js'; +import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___announcements_show from './endpoints/announcements/show.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; @@ -189,6 +191,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js' import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; import * as ep___following_requests_list from './endpoints/following/requests/list.js'; +import * as ep___following_requests_sent from './endpoints/following/requests/sent.js'; import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; import * as ep___gallery_featured from './endpoints/gallery/featured.js'; import * as ep___gallery_popular from './endpoints/gallery/popular.js'; @@ -266,6 +269,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; +import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js'; import * as ep___invite_create from './endpoints/invite/create.js'; import * as ep___invite_delete from './endpoints/invite/delete.js'; import * as ep___invite_list from './endpoints/invite/list.js'; @@ -290,6 +294,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_following from './endpoints/notes/following.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -297,6 +302,7 @@ import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; +import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js'; import * as ep___notes_reactions from './endpoints/notes/reactions.js'; import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js'; import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; @@ -475,6 +481,7 @@ const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClas const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default }; const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default }; const $admin_approveUser: Provider = { provide: 'ep:admin/approve-user', useClass: ep___admin_approveUser.default }; +const $admin_declineUser: Provider = { provide: 'ep:admin/decline-user', useClass: ep___admin_declineUser.default }; const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default }; const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default }; const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default }; @@ -493,6 +500,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default }; const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default }; const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default }; +const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default }; const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default }; const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; @@ -585,6 +593,7 @@ const $following_invalidate: Provider = { provide: 'ep:following/invalidate', us const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default }; +const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default }; const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default }; const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default }; const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default }; @@ -662,6 +671,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default }; const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default }; +const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default }; const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default }; const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default }; const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default }; @@ -686,6 +696,7 @@ const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___not const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; +const $notes_following: Provider = { provide: 'ep:notes/following', useClass: ep___notes_following.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default }; const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; @@ -693,6 +704,7 @@ const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', use const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default }; const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default }; const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default }; +const $notes_polls_refresh: Provider = { provide: 'ep:notes/polls/refresh', useClass: ep___notes_polls_refresh.default }; const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default }; const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default }; const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default }; @@ -875,6 +887,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_unsilenceUser, $admin_suspendUser, $admin_approveUser, + $admin_declineUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, @@ -893,6 +906,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_systemWebhook_list, $admin_systemWebhook_show, $admin_systemWebhook_update, + $admin_systemWebhook_test, $announcements, $announcements_show, $antennas_create, @@ -985,6 +999,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $following_requests_accept, $following_requests_cancel, $following_requests_list, + $following_requests_sent, $following_requests_reject, $gallery_featured, $gallery_popular, @@ -1062,6 +1077,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $i_webhooks_show, $i_webhooks_update, $i_webhooks_delete, + $i_webhooks_test, $invite_create, $invite_delete, $invite_list, @@ -1086,6 +1102,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_create, $notes_favorites_delete, $notes_featured, + $notes_following, $notes_globalTimeline, $notes_bubbleTimeline, $notes_hybridTimeline, @@ -1093,6 +1110,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_mentions, $notes_polls_recommendation, $notes_polls_vote, + $notes_polls_refresh, $notes_reactions, $notes_reactions_create, $notes_reactions_delete, @@ -1269,6 +1287,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_unsilenceUser, $admin_suspendUser, $admin_approveUser, + $admin_declineUser, $admin_unsuspendUser, $admin_updateMeta, $admin_deleteAccount, @@ -1287,6 +1306,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_systemWebhook_list, $admin_systemWebhook_show, $admin_systemWebhook_update, + $admin_systemWebhook_test, $announcements, $announcements_show, $antennas_create, @@ -1456,6 +1476,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $i_webhooks_show, $i_webhooks_update, $i_webhooks_delete, + $i_webhooks_test, $invite_create, $invite_delete, $invite_list, @@ -1480,6 +1501,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_create, $notes_favorites_delete, $notes_featured, + $notes_following, $notes_globalTimeline, $notes_bubbleTimeline, $notes_hybridTimeline, @@ -1487,6 +1509,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_mentions, $notes_polls_recommendation, $notes_polls_vote, + $notes_polls_refresh, $notes_reactions, $notes_reactions_create, $notes_reactions_delete, diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 6fbcacbc1172f7ecf683b92b12f8b463e9250934..64af7da7a61b18ce9d49d1f26aa6bd302f300c02 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -21,11 +21,12 @@ import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { UserAuthService } from '@/core/UserAuthService.js'; -import { MetaService } from '@/core/MetaService.js'; import { RateLimiterService } from './RateLimiterService.js'; import { SigninService } from './SigninService.js'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { FastifyReply, FastifyRequest } from 'fastify'; +import { isSystemAccount } from '@/misc/is-system-account.js'; +import type { MiMeta } from '@/models/_.js'; @Injectable() export class SigninApiService { @@ -33,6 +34,9 @@ export class SigninApiService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -47,7 +51,6 @@ export class SigninApiService { private signinService: SigninService, private userAuthService: UserAuthService, private webAuthnService: WebAuthnService, - private metaService: MetaService, ) { } @@ -66,8 +69,6 @@ export class SigninApiService { reply.header('Access-Control-Allow-Origin', this.config.url); reply.header('Access-Control-Allow-Credentials', 'true'); - const instance = await this.metaService.fetch(true); - const body = request.body; const username = body['username']; const password = body['password']; @@ -125,9 +126,15 @@ export class SigninApiService { }); } + if (isSystemAccount(user)) { + return error(403, { + id: 's8dhsj9s-a93j-493j-ja9k-kas9sj20aml2', + }); + } + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (!user.approved && instance.approvalRequiredForSignup) { + if (!user.approved && this.meta.approvalRequiredForSignup) { reply.code(403); return { error: { @@ -162,7 +169,7 @@ export class SigninApiService { password: newHash }); } - if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); + if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); return this.signinService.signin(request, reply, user); } else { @@ -193,7 +200,7 @@ export class SigninApiService { }); } - if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); + if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); return this.signinService.signin(request, reply, user); } else if (body.credential) { @@ -206,7 +213,7 @@ export class SigninApiService { const authorized = await this.webAuthnService.verifyAuthentication(user.id, body.credential); if (authorized) { - if (!instance.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); + if (!this.meta.approvalRequiredForSignup && !user.approved) this.usersRepository.update(user.id, { approved: true }); return this.signinService.signin(request, reply, user); } else { return await fail(403, { diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ba23c54e22656c80f5d848eb3073eb29b959b8e --- /dev/null +++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts @@ -0,0 +1,173 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import { IsNull } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { + SigninsRepository, + UserProfilesRepository, + UsersRepository, +} from '@/models/_.js'; +import type { Config } from '@/config.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; +import { IdService } from '@/core/IdService.js'; +import { bindThis } from '@/decorators.js'; +import { WebAuthnService } from '@/core/WebAuthnService.js'; +import Logger from '@/logger.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type { IdentifiableError } from '@/misc/identifiable-error.js'; +import { RateLimiterService } from './RateLimiterService.js'; +import { SigninService } from './SigninService.js'; +import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; +import type { FastifyReply, FastifyRequest } from 'fastify'; + +@Injectable() +export class SigninWithPasskeyApiService { + private logger: Logger; + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.signinsRepository) + private signinsRepository: SigninsRepository, + + private idService: IdService, + private rateLimiterService: RateLimiterService, + private signinService: SigninService, + private webAuthnService: WebAuthnService, + private loggerService: LoggerService, + ) { + this.logger = this.loggerService.getLogger('PasskeyAuth'); + } + + @bindThis + public async signin( + request: FastifyRequest<{ + Body: { + credential?: AuthenticationResponseJSON; + context?: string; + }; + }>, + reply: FastifyReply, + ) { + reply.header('Access-Control-Allow-Origin', this.config.url); + reply.header('Access-Control-Allow-Credentials', 'true'); + + const body = request.body; + const credential = body['credential']; + + function error(status: number, error: { id: string }) { + reply.code(status); + return { error }; + } + + const fail = async (userId: MiUser['id'], status?: number, failure?: { id: string }) => { + // Append signin history + await this.signinsRepository.insert({ + id: this.idService.gen(), + userId: userId, + ip: request.ip, + headers: request.headers as any, + success: false, + }); + return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' }); + }; + + try { + // Not more than 1 API call per 250ms and not more than 100 attempts per 30min + // NOTE: 1 Sign-in require 2 API calls + await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip)); + } catch (err) { + reply.code(429); + return { + error: { + message: 'Too many failed attempts to sign in. Try again later.', + code: 'TOO_MANY_AUTHENTICATION_FAILURES', + id: '22d05606-fbcf-421a-a2db-b32610dcfd1b', + }, + }; + } + + // Initiate Passkey Auth challenge with context + if (!credential) { + const context = randomUUID(); + this.logger.info(`Initiate Passkey challenge: context: ${context}`); + const authChallengeOptions = { + option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context), + context: context, + }; + reply.code(200); + return authChallengeOptions; + } + + const context = body.context; + if (!context || typeof context !== 'string') { + // If try Authentication without context + return error(400, { + id: '1658cc2e-4495-461f-aee4-d403cdf073c1', + }); + } + + this.logger.debug(`Try Sign-in with Passkey: context: ${context}`); + + let authorizedUserId: MiUser['id'] | null; + try { + authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential); + } catch (err) { + this.logger.warn(`Passkey challenge Verify error! : ${err}`); + const errorId = (err as IdentifiableError).id; + return error(403, { + id: errorId, + }); + } + + if (!authorizedUserId) { + return error(403, { + id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', + }); + } + + // Fetch user + const user = await this.usersRepository.findOneBy({ + id: authorizedUserId, + host: IsNull(), + }) as MiLocalUser | null; + + if (user == null) { + return error(403, { + id: '652f899f-66d4-490e-993e-6606c8ec04c3', + }); + } + + if (user.isSuspended) { + return error(403, { + id: 'e03a5f46-d309-4865-9b69-56282d94e1eb', + }); + } + + const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + + // Authentication was successful, but passwordless login is not enabled + if (!profile.usePasswordLessLogin) { + return await fail(user.id, 403, { + id: '2d84773e-f7b7-4d0b-8f72-bb69b584c912', + }); + } + + const signinResponse = this.signinService.signin(request, reply, user); + return { + signinResponse: signinResponse, + }; + } +} diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index f89c3954f80e8a4d6c8320a143a9052115707285..db860d710a8f93bbb7ffeb06e53ee04dcc9518a2 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -8,9 +8,8 @@ import { Inject, Injectable } from '@nestjs/common'; import * as argon2 from 'argon2'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js'; +import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; import { IdService } from '@/core/IdService.js'; import { SignupService } from '@/core/SignupService.js'; @@ -31,6 +30,9 @@ export class SignupApiService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -48,7 +50,6 @@ export class SignupApiService { private userEntityService: UserEntityService, private idService: IdService, - private metaService: MetaService, private captchaService: CaptchaService, private signupService: SignupService, private signinService: SigninService, @@ -71,37 +72,42 @@ export class SignupApiService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'frc-captcha-solution'?: string; } }>, reply: FastifyReply, ) { const body = request.body; - const instance = await this.metaService.fetch(true); - // Verify *Captcha // ãŸã ã—テスト時ã¯ã“ã®æ©Ÿæ§‹ã¯éšœå®³ã¨ãªã‚‹ãŸã‚無効ã«ã™ã‚‹ if (process.env.NODE_ENV !== 'test') { - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { - await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { + if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { + await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) { - await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { + await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { - await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { + if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { + await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableTurnstile && instance.turnstileSecretKey) { - await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => { + if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { + await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { + throw new FastifyReplyError(400, err); + }); + } + + if (this.meta.enableFC && this.meta.fcSecretKey) { + await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => { throw new FastifyReplyError(400, err); }); } @@ -114,7 +120,7 @@ export class SignupApiService { const reason = body['reason']; const emailAddress = body['emailAddress']; - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { if (emailAddress == null || typeof emailAddress !== 'string') { reply.code(400); return; @@ -127,7 +133,7 @@ export class SignupApiService { } } - if (instance.approvalRequiredForSignup) { + if (this.meta.approvalRequiredForSignup) { if (reason == null || typeof reason !== 'string') { reply.code(400); return; @@ -136,7 +142,7 @@ export class SignupApiService { let ticket: MiRegistrationTicket | null = null; - if (instance.disableRegistration) { + if (this.meta.disableRegistration) { if (invitationCode == null || typeof invitationCode !== 'string') { reply.code(400); return; @@ -157,7 +163,7 @@ export class SignupApiService { } // メアドèªè¨¼ãŒæœ‰åŠ¹ã®å ´åˆ - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { // メアドèªè¨¼æ¸ˆã¿ãªã‚‰ã‚¨ãƒ©ãƒ¼ if (ticket.usedBy) { reply.code(400); @@ -175,7 +181,7 @@ export class SignupApiService { } } - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); } @@ -185,7 +191,7 @@ export class SignupApiService { throw new FastifyReplyError(400, 'USED_USERNAME'); } - const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new FastifyReplyError(400, 'DENIED_USERNAME'); } @@ -220,7 +226,7 @@ export class SignupApiService { reply.code(204); return; - } else if (instance.approvalRequiredForSignup) { + } else if (this.meta.approvalRequiredForSignup) { const { account } = await this.signupService.signup({ username, password, host, reason, }); @@ -288,8 +294,6 @@ export class SignupApiService { const code = body['code']; - const instance = await this.metaService.fetch(true); - try { const pendingUser = await this.userPendingsRepository.findOneByOrFail({ code }); @@ -324,7 +328,7 @@ export class SignupApiService { }); } - if (instance.approvalRequiredForSignup) { + if (this.meta.approvalRequiredForSignup) { if (pendingUser.email) { this.emailService.sendEmail(pendingUser.email, 'Approval pending', 'Congratulations! Your account is now pending approval. You will get notified when you have been accepted.', diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e2fcd1a9d03c0cf12e6922ae7575a1c09ba6c741..14e002929a41cc287f9f53849e354e9517508af9 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -85,6 +85,7 @@ import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js'; import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js'; import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js'; import * as ep___admin_approveUser from './endpoints/admin/approve-user.js'; +import * as ep___admin_declineUser from './endpoints/admin/decline-user.js'; import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; @@ -103,6 +104,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js'; import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js'; import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js'; +import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___announcements_show from './endpoints/announcements/show.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; @@ -195,6 +197,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js' import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; import * as ep___following_requests_list from './endpoints/following/requests/list.js'; +import * as ep___following_requests_sent from './endpoints/following/requests/sent.js'; import * as ep___following_requests_reject from './endpoints/following/requests/reject.js'; import * as ep___gallery_featured from './endpoints/gallery/featured.js'; import * as ep___gallery_popular from './endpoints/gallery/popular.js'; @@ -272,6 +275,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js'; import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js'; +import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js'; import * as ep___invite_create from './endpoints/invite/create.js'; import * as ep___invite_delete from './endpoints/invite/delete.js'; import * as ep___invite_list from './endpoints/invite/list.js'; @@ -296,6 +300,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_following from './endpoints/notes/following.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -303,6 +308,7 @@ import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js'; import * as ep___notes_mentions from './endpoints/notes/mentions.js'; import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js'; import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js'; +import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js'; import * as ep___notes_reactions from './endpoints/notes/reactions.js'; import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js'; import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js'; @@ -479,6 +485,7 @@ const eps = [ ['admin/unsilence-user', ep___admin_unsilenceUser], ['admin/suspend-user', ep___admin_suspendUser], ['admin/approve-user', ep___admin_approveUser], + ['admin/decline-user', ep___admin_declineUser], ['admin/unsuspend-user', ep___admin_unsuspendUser], ['admin/update-meta', ep___admin_updateMeta], ['admin/delete-account', ep___admin_deleteAccount], @@ -497,6 +504,7 @@ const eps = [ ['admin/system-webhook/list', ep___admin_systemWebhook_list], ['admin/system-webhook/show', ep___admin_systemWebhook_show], ['admin/system-webhook/update', ep___admin_systemWebhook_update], + ['admin/system-webhook/test', ep___admin_systemWebhook_test], ['announcements', ep___announcements], ['announcements/show', ep___announcements_show], ['antennas/create', ep___antennas_create], @@ -589,6 +597,7 @@ const eps = [ ['following/requests/accept', ep___following_requests_accept], ['following/requests/cancel', ep___following_requests_cancel], ['following/requests/list', ep___following_requests_list], + ['following/requests/sent', ep___following_requests_sent], ['following/requests/reject', ep___following_requests_reject], ['gallery/featured', ep___gallery_featured], ['gallery/popular', ep___gallery_popular], @@ -666,6 +675,7 @@ const eps = [ ['i/webhooks/show', ep___i_webhooks_show], ['i/webhooks/update', ep___i_webhooks_update], ['i/webhooks/delete', ep___i_webhooks_delete], + ['i/webhooks/test', ep___i_webhooks_test], ['invite/create', ep___invite_create], ['invite/delete', ep___invite_delete], ['invite/list', ep___invite_list], @@ -690,6 +700,7 @@ const eps = [ ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], + ['notes/following', ep___notes_following], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/bubble-timeline', ep___notes_bubbleTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], @@ -697,6 +708,7 @@ const eps = [ ['notes/mentions', ep___notes_mentions], ['notes/polls/recommendation', ep___notes_polls_recommendation], ['notes/polls/vote', ep___notes_polls_vote], + ['notes/polls/refresh', ep___notes_polls_refresh], ['notes/reactions', ep___notes_reactions], ['notes/reactions/create', ep___notes_reactions_create], ['notes/reactions/delete', ep___notes_reactions_delete], diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index a7e8a3b018cd30cd45dece1d17abcb407a5a68e7..7754899b95ba11a85d7ea6852c18258dcd0af351 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -3,16 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository } from '@/models/_.js'; +import { MiAccessToken, MiUser } from '@/models/_.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js'; -import { DI } from '@/di-symbols.js'; import { Packed } from '@/misc/json-schema.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '@/server/api/error.js'; export const meta = { tags: ['admin'], @@ -28,6 +28,35 @@ export const meta = { }, }, }, + + errors: { + // From ApiCallService.ts + noCredential: { + message: 'Credential required.', + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + httpStatusCode: 401, + }, + noAdmin: { + message: 'You are not assigned to an administrator role.', + code: 'ROLE_PERMISSION_DENIED', + kind: 'permission', + id: 'c3d38592-54c0-429d-be96-5636b0431a61', + }, + noPermission: { + message: 'Your app does not have the necessary permissions to use this endpoint.', + code: 'PERMISSION_DENIED', + kind: 'permission', + id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838', + }, + }, + + // Required token permissions, but we need to check them manually. + // ApiCallService checks access in a way that would prevent creating the first account. + softPermissions: [ + 'write:admin:account', + 'write:admin:approve-user', + ], } as const; export const paramDef = { @@ -42,22 +71,19 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - + private roleService: RoleService, private userEntityService: UserEntityService, private signupService: SignupService, private instanceActorService: InstanceActorService, ) { super(meta, paramDef, async (ps, _me, token) => { - const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null; - const realUsers = await this.instanceActorService.realLocalUsersPresent(); - if ((realUsers && !me?.isRoot) || token !== null) throw new Error('access denied'); + await this.ensurePermissions(_me, token); const { account, secret } = await this.signupService.signup({ username: ps.username, password: ps.password, ignorePreservedUsernames: true, + approved: true, }); const res = await this.userEntityService.pack(account, account, { @@ -70,4 +96,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return res; }); } + + private async ensurePermissions(me: MiUser | null, token: MiAccessToken | null): Promise<void> { + // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check. + if (token && !meta.softPermissions.every(p => token.permission.includes(p))) { + throw new ApiError(meta.errors.noPermission); + } + + // Only administrators (including root) can create users. + if (me && !await this.roleService.isAdministrator(me)) { + throw new ApiError(meta.errors.noAdmin); + } + + // Anonymous access is only allowed for initial instance setup. + if (!me && await this.instanceActorService.realLocalUsersPresent()) { + throw new ApiError(meta.errors.noCredential); + } + } } diff --git a/packages/backend/src/server/api/endpoints/admin/decline-user.ts b/packages/backend/src/server/api/endpoints/admin/decline-user.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a75dd977dc723d62f403f768fbb8c4c13f03d60 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/decline-user.ts @@ -0,0 +1,75 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { UsedUsernamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { DI } from '@/di-symbols.js'; +import { EmailService } from '@/core/EmailService.js'; +import { DeleteAccountService } from '@/core/DeleteAccountService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:decline-user', +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + @Inject(DI.usedUsernamesRepository) + private usedUsernamesRepository: UsedUsernamesRepository, + + private moderationLogService: ModerationLogService, + private emailService: EmailService, + private deleteAccountService: DeleteAccountService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null || user.isDeleted) { + throw new Error('user not found or already deleted'); + } + + if (user.approved) { + throw new Error('user is already approved'); + } + + if (user.host) { + throw new Error('user is not local'); + } + + const profile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); + + if (profile?.email) { + this.emailService.sendEmail(profile.email, 'Account Declined', + 'Your Account has been declined!', + 'Your Account has been declined!'); + } + + await this.usedUsernamesRepository.delete({ username: user.username }); + + await this.deleteAccountService.deleteAccount(user); + + this.moderationLogService.log(me, 'decline', { + userId: user.id, + userUsername: user.username, + userHost: user.host, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index 9e93310746c95900fdc1f7640dd603a726041963..601c898f52ab46f5c254871d950e18f08df5475b 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -31,15 +31,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.notesRepository) + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { - const followings = await this.followingsRepository.findBy({ - followerHost: ps.host, - }); + const followings = await this.followingsRepository.findBy([ + { + followeeHost: ps.host, + }, + { + followerHost: ps.host, + }, + ]); const pairs = await Promise.all(followings.map(f => Promise.all([ this.usersRepository.findOneByOrFail({ id: f.followerId }), diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index 8b142910a69b0a2c832ce0af599b1c3913d2e4a4..daf19c44353c08d75ce7e9c5504a72789d1512ef 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -25,6 +25,7 @@ export const paramDef = { host: { type: 'string' }, isSuspended: { type: 'boolean' }, isNSFW: { type: 'boolean' }, + rejectReports: { type: 'boolean' }, moderationNote: { type: 'string' }, }, required: ['host'], @@ -57,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- await this.federatedInstanceService.update(instance.id, { suspensionState, isNSFW: ps.isNSFW, + rejectReports: ps.rejectReports, moderationNote: ps.moderationNote, }); @@ -74,6 +76,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } + if (ps.isNSFW != null && instance.isNSFW !== ps.isNSFW) { + const message = ps.rejectReports ? 'setRemoteInstanceNSFW' : 'unsetRemoteInstanceNSFW'; + this.moderationLogService.log(me, message, { + id: instance.id, + host: instance.host, + }); + } + + if (ps.rejectReports != null && instance.rejectReports !== ps.rejectReports) { + const message = ps.rejectReports ? 'rejectRemoteInstanceReports' : 'acceptRemoteInstanceReports'; + this.moderationLogService.log(me, message, { + id: instance.id, + host: instance.host, + }); + } + if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) { this.moderationLogService.log(me, 'updateRemoteInstanceNote', { id: instance.id, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 063bb6751b290f3e955ffd6ba60f581f4916375e..6e368eff43f8a8deb31fca9faa7ee2f14fb3efa0 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -73,6 +73,14 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableFC: { + type: 'boolean', + optional: false, nullable: false, + }, + fcSiteKey: { + type: 'string', + optional: false, nullable: true, + }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -110,6 +118,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + sidebarLogoUrl: { + type: 'string', + optional: false, nullable: true, + }, enableEmail: { type: 'boolean', optional: false, nullable: false, @@ -124,7 +136,7 @@ export const meta = { }, silencedHosts: { type: 'array', - optional: true, + optional: false, nullable: false, items: { type: 'string', @@ -215,6 +227,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + fcSecretKey: { + type: 'string', + optional: false, nullable: true, + }, sensitiveMediaDetection: { type: 'string', optional: false, nullable: false, @@ -396,6 +412,10 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + enableReactionsBuffering: { + type: 'boolean', + optional: false, nullable: false, + }, notesPerOneAd: { type: 'number', optional: false, nullable: false, @@ -522,6 +542,26 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + trustedLinkUrlPatterns: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + federation: { + type: 'string', + optional: false, nullable: false, + }, + federationHosts: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, }, }, } as const; @@ -572,6 +612,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + enableFC: instance.enableFC, + fcSiteKey: instance.fcSiteKey, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, @@ -582,6 +624,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- iconUrl: instance.iconUrl, app192IconUrl: instance.app192IconUrl, app512IconUrl: instance.app512IconUrl, + sidebarLogoUrl: instance.sidebarLogoUrl, backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, defaultLightTheme: instance.defaultLightTheme, @@ -605,6 +648,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- mcaptchaSecretKey: instance.mcaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey, turnstileSecretKey: instance.turnstileSecretKey, + fcSecretKey: instance.fcSecretKey, sensitiveMediaDetection: instance.sensitiveMediaDetection, sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, @@ -656,6 +700,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, + enableReactionsBuffering: instance.enableReactionsBuffering, notesPerOneAd: instance.notesPerOneAd, summalyProxy: instance.urlPreviewSummaryProxyUrl, urlPreviewEnabled: instance.urlPreviewEnabled, @@ -664,6 +709,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, urlPreviewUserAgent: instance.urlPreviewUserAgent, urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, + trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns, + federation: instance.federation, + federationHosts: instance.federationHosts, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts index 7a3410ffa7519ca0af75458abe896da12c1a091f..f3e440b4cb516d2d4ec5ce9785f7bf78a25507f4 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts @@ -21,16 +21,15 @@ export const meta = { items: { type: 'array', optional: false, nullable: false, - items: { - anyOf: [ - { - type: 'string', - }, - { - type: 'number', - }, - ], - }, + prefixItems: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + unevaluatedItems: false, }, example: [[ 'example.com', diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts index 305ae1af1da5d404ca9b745b6291e645c274806e..e7589cba81128dc205cceaa3ee27be3207e4a3ff 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts @@ -21,16 +21,15 @@ export const meta = { items: { type: 'array', optional: false, nullable: false, - items: { - anyOf: [ - { - type: 'string', - }, - { - type: 'number', - }, - ], - }, + prefixItems: [ + { + type: 'string', + }, + { + type: 'number', + }, + ], + unevaluatedItems: false, }, example: [[ 'example.com', diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts index 828dbae7126d1a72b17ba38a3624803cc3356045..e4bb545f5d69b8b9b47b2f63c1612763c25b683c 100644 --- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts +++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts @@ -11,6 +11,7 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; export const meta = { tags: ['admin'], @@ -63,6 +64,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('cannot reset password of root'); } + if (isSystemAccount(user)) { + throw new Error('cannot reset password of system account'); + } + const passwd = secureRndstr(8); // Generate hash of password diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index a7ca7f9547fa8665d82466e2a54cc827763aa9e3..669bffe2dcdddea1d9d03558c0630101e0532ffc 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -11,6 +11,7 @@ import { RoleService } from '@/core/RoleService.js'; import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; import { IdService } from '@/core/IdService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; +import { isSystemAccount } from '@/misc/is-system-account.js'; export const meta = { tags: ['admin'], @@ -31,6 +32,14 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + approved: { + type: 'boolean', + optional: false, nullable: false, + }, + followedMessage: { + type: 'string', + optional: false, nullable: true, + }, autoAcceptFollowed: { type: 'boolean', optional: false, nullable: false, @@ -111,6 +120,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + isSystem: { + type: 'boolean', + optional: false, nullable: false, + }, isSilenced: { type: 'boolean', optional: false, nullable: false, @@ -228,6 +241,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- emailVerified: profile.emailVerified, approved: user.approved, signupReason: user.signupReason, + followedMessage: profile.followedMessage, autoAcceptFollowed: profile.autoAcceptFollowed, noCrawle: profile.noCrawle, preventAiLearning: profile.preventAiLearning, @@ -240,6 +254,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- mutedInstances: profile.mutedInstances, notificationRecieveConfig: profile.notificationRecieveConfig, isModerator: isModerator, + isSystem: isSystemAccount(user), isSilenced: isSilenced, isSuspended: user.isSuspended, isHibernated: user.isHibernated, diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb2ddf4b446d76e476603c539e83fc3786b25c42 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { WebhookTestService } from '@/core/WebhookTestService.js'; +import { ApiError } from '@/server/api/error.js'; +import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; + +export const meta = { + tags: ['webhooks'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'read:admin:system-webhook', + + limit: { + duration: ms('15min'), + max: 60, + }, + + errors: { + noSuchWebhook: { + message: 'No such webhook.', + code: 'NO_SUCH_WEBHOOK', + id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + webhookId: { + type: 'string', + format: 'misskey:id', + }, + type: { + type: 'string', + enum: systemWebhookEventTypes, + }, + override: { + type: 'object', + properties: { + url: { type: 'string', nullable: false }, + secret: { type: 'string', nullable: false }, + }, + }, + }, + required: ['webhookId', 'type'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private webhookTestService: WebhookTestService, + ) { + super(meta, paramDef, async (ps) => { + try { + await this.webhookTestService.testSystemWebhook({ + webhookId: ps.webhookId, + type: ps.type, + override: ps.override, + }); + } catch (e) { + if (e instanceof WebhookTestService.NoSuchWebhookError) { + throw new ApiError(meta.errors.noSuchWebhook); + } + throw e; + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 6bda1ae6adb08e3f344df40e1bc2041b09080d36..98760bbcc3fbd72ea06886263cbc9a71e21fcf57 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -55,6 +55,7 @@ export const paramDef = { iconUrl: { type: 'string', nullable: true }, app192IconUrl: { type: 'string', nullable: true }, app512IconUrl: { type: 'string', nullable: true }, + sidebarLogoUrl: { type: 'string', nullable: true }, backgroundImageUrl: { type: 'string', nullable: true }, logoImageUrl: { type: 'string', nullable: true }, name: { type: 'string', nullable: true }, @@ -80,6 +81,9 @@ export const paramDef = { enableTurnstile: { type: 'boolean' }, turnstileSiteKey: { type: 'string', nullable: true }, turnstileSecretKey: { type: 'string', nullable: true }, + enableFC: { type: 'boolean' }, + fcSiteKey: { type: 'string', nullable: true }, + fcSecretKey: { type: 'string', nullable: true }, sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, @@ -150,6 +154,7 @@ export const paramDef = { perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' }, + enableReactionsBuffering: { type: 'boolean' }, notesPerOneAd: { type: 'integer' }, silencedHosts: { type: 'array', @@ -175,6 +180,21 @@ export const paramDef = { urlPreviewRequireContentLength: { type: 'boolean' }, urlPreviewUserAgent: { type: 'string', nullable: true }, urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, + trustedLinkUrlPatterns: { + type: 'array', nullable: true, items: { + type: 'string', + }, + }, + federation: { + type: 'string', + enum: ['all', 'none', 'specified'], + }, + federationHosts: { + type: 'array', + items: { + type: 'string', + }, + }, }, required: [], } as const; @@ -250,6 +270,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.app512IconUrl = ps.app512IconUrl; } + if (ps.sidebarLogoUrl !== undefined) { + set.sidebarLogoUrl = ps.sidebarLogoUrl; + } + if (ps.serverErrorImageUrl !== undefined) { set.serverErrorImageUrl = ps.serverErrorImageUrl; } @@ -362,6 +386,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.turnstileSecretKey = ps.turnstileSecretKey; } + if (ps.enableFC !== undefined) { + set.enableFC = ps.enableFC; + } + + if (ps.fcSiteKey !== undefined) { + set.fcSiteKey = ps.fcSiteKey; + } + + if (ps.fcSecretKey !== undefined) { + set.fcSecretKey = ps.fcSecretKey; + } + if (ps.enableBotTrending !== undefined) { set.enableBotTrending = ps.enableBotTrending; } @@ -626,6 +662,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax; } + if (ps.enableReactionsBuffering !== undefined) { + set.enableReactionsBuffering = ps.enableReactionsBuffering; + } + if (ps.notesPerOneAd !== undefined) { set.notesPerOneAd = ps.notesPerOneAd; } @@ -660,6 +700,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.urlPreviewSummaryProxyUrl = value === '' ? null : value; } + if (Array.isArray(ps.trustedLinkUrlPatterns)) { + set.trustedLinkUrlPatterns = ps.trustedLinkUrlPatterns.filter(Boolean); + } + + if (ps.federation !== undefined) { + set.federation = ps.federation; + } + + if (Array.isArray(ps.federationHosts)) { + set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 577b9e1b1f8c347d342671c708b1b9869686f429..e0c8ddcc8478e8851089b4e222082fd2b03f26cb 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -34,6 +34,12 @@ export const meta = { code: 'TOO_MANY_ANTENNAS', id: 'faf47050-e8b5-438c-913c-db2b1576fde4', }, + + emptyKeyword: { + message: 'Either keywords or excludeKeywords is required.', + code: 'EMPTY_KEYWORD', + id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a', + }, }, res: { @@ -87,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); + throw new ApiError(meta.errors.emptyKeyword); } const currentAntennasCount = await this.antennasRepository.countBy({ diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 0c30bca9e0bf17b52d6825c80f8511644bc58485..10f26b19126ed0a8301504e042c5bd81449dd0fa 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -32,6 +32,12 @@ export const meta = { code: 'NO_SUCH_USER_LIST', id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }, + + emptyKeyword: { + message: 'Either keywords or excludeKeywords is required.', + code: 'EMPTY_KEYWORD', + id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4', + }, }, res: { @@ -85,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- super(meta, paramDef, async (ps, me) => { if (ps.keywords && ps.excludeKeywords) { if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); + throw new ApiError(meta.errors.emptyKeyword); } } // Fetch the antenna diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index ca6789a4642351caf70cd095b18e85152944dda1..a877d1ce0d034277902d5afb03195803ef43a9ad 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { MiNote } from '@/models/Note.js'; @@ -12,7 +12,6 @@ import { isActor, isPost, getApId } from '@/core/activitypub/type.js'; import type { SchemaType } from '@/misc/json-schema.js'; import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -91,7 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private utilityService: UtilityService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, - private metaService: MetaService, private apResolverService: ApResolverService, private apDbResolverService: ApDbResolverService, private apPersonService: ApPersonService, @@ -112,10 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- */ @bindThis private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> { - // ブãƒãƒƒã‚¯ã—ã¦ãŸã‚‰ä¸æ– - const host = this.utilityService.extractDbHost(uri); - const fetchedMeta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, host)) return null; + if (!this.utilityService.isFederationAllowedUri(uri)) return null; let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), @@ -123,6 +118,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ])); if (local != null) return local; + const host = this.utilityService.extractDbHost(uri); + // local object, not found in db? fail if (this.utilityService.isSelfHost(host)) return null; diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 936948164990cdc8ee12906b3e7e496f45d7c7a7..06130464a97f271fa2df6ae5152f75b462b72281 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -5,14 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { MiLocalUser } from '@/models/User.js'; import { ApiError } from '../../error.js'; @@ -65,6 +63,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -75,16 +76,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, - private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const serverSettings = await this.metaService.fetch(); - const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, }); @@ -95,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (me) this.activeUsersChart.read(me); - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me); } diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 7e9b0fa0e1e430f3831bbd0d691c128ebb0026a6..eb45e29f9e123bed218043a0de1842b9edfd4bf8 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { RoleService } from '@/core/RoleService.js'; @@ -41,14 +40,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - private metaService: MetaService, private driveFileEntityService: DriveFileEntityService, private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const instance = await this.metaService.fetch(true); - - // Calculate drive usage const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); const policies = await this.roleService.getUserPolicies(me.id); diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 4670392025242aec0958949c870715bfa969ac28..b86059b5e7ac988e9d4d928d7706150d9669e159 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -10,6 +10,7 @@ import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +import { RoleService } from '@/core/RoleService.js'; export const meta = { tags: ['drive', 'notes'], @@ -61,12 +62,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { // Fetch file const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId, - userId: me.id, + userId: await this.roleService.isModerator(me) ? undefined : me.id, }); if (file == null) { diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 9c17f93ab26bfbf280a97b89f25fd22752bab9a4..f67ff6ddc4b03b65ad1e304ea4c554143a757618 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -4,14 +4,15 @@ */ import ms from 'ms'; -import { Injectable } from '@nestjs/common'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; +import { Inject, Injectable } from '@nestjs/common'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { DriveService } from '@/core/DriveService.js'; +import type { Config } from '@/config.js'; import { ApiError } from '../../../error.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['drive'], @@ -55,6 +56,12 @@ export const meta = { code: 'NO_FREE_SPACE', id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064', }, + + commentTooLong: { + message: 'Cannot upload the file because the comment exceeds the instance limit.', + code: 'COMMENT_TOO_LONG', + id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5', + }, }, } as const; @@ -63,7 +70,7 @@ export const paramDef = { properties: { folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, name: { type: 'string', nullable: true, default: null }, - comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null }, + comment: { type: 'string', nullable: true, default: null }, isSensitive: { type: 'boolean', default: false }, force: { type: 'boolean', default: false }, }, @@ -73,8 +80,13 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + + @Inject(DI.config) + private config: Config, + private driveFileEntityService: DriveFileEntityService, - private metaService: MetaService, private driveService: DriveService, ) { super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => { @@ -91,7 +103,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - const instance = await this.metaService.fetch(); + if (ps.comment && ps.comment.length > this.config.maxAltTextLength) { + throw new ApiError(meta.errors.commentTooLong); + } try { // Create file @@ -103,8 +117,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive, - requestIp: instance.enableIpLogging ? ip : null, - requestHeaders: instance.enableIpLogging ? headers : null, + requestIp: this.serverSettings.enableIpLogging ? ip : null, + requestHeaders: this.serverSettings.enableIpLogging ? headers : null, }); return await this.driveFileEntityService.pack(driveFile, { self: true }); } catch (err) { diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 554101812694c8016bf5c67017c3916364b89fdd..1501abf9ced35d61fb5192e636045361f7c2ad51 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -9,8 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { DriveService } from '@/core/DriveService.js'; +import type { Config } from '@/config.js'; import { ApiError } from '../../../error.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; export const meta = { tags: ['drive'], @@ -51,6 +51,12 @@ export const meta = { code: 'RESTRICTED_BY_ROLE', id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7', }, + + commentTooLong: { + message: 'Cannot upload the file because the comment exceeds the instance limit.', + code: 'COMMENT_TOO_LONG', + id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5', + }, }, res: { type: 'object', @@ -66,7 +72,7 @@ export const paramDef = { folderId: { type: 'string', format: 'misskey:id', nullable: true }, name: { type: 'string' }, isSensitive: { type: 'boolean' }, - comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH }, + comment: { type: 'string', nullable: true }, }, required: ['fileId'], } as const; @@ -76,6 +82,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.config) + private config: Config, private driveService: DriveService, private roleService: RoleService, @@ -90,6 +98,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.accessDenied); } + if (ps.comment && ps.comment.length > this.config.maxAltTextLength) { + throw new ApiError(meta.errors.commentTooLong); + } + let packedFile; try { diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index 49d2e78d08bc24a36635ac35d00ed27aaf36dc5e..e20d482e7046243cf1da6de923c653083581054a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -4,12 +4,14 @@ */ import ms from 'ms'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveService } from '@/core/DriveService.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; export const meta = { tags: ['drive'], @@ -26,6 +28,14 @@ export const meta = { prohibitMoved: true, kind: 'write:drive', + + errors: { + commentTooLong: { + message: 'Cannot upload the file because the comment exceeds the instance limit.', + code: 'COMMENT_TOO_LONG', + id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5', + }, + }, } as const; export const paramDef = { @@ -34,7 +44,7 @@ export const paramDef = { url: { type: 'string' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, isSensitive: { type: 'boolean', default: false }, - comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null }, + comment: { type: 'string', nullable: true, default: null }, marker: { type: 'string', nullable: true, default: null }, force: { type: 'boolean', default: false }, }, @@ -44,11 +54,18 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + private driveFileEntityService: DriveFileEntityService, private driveService: DriveService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => { + if (ps.comment && ps.comment.length > this.config.maxAltTextLength) { + throw new ApiError(meta.errors.commentTooLong); + } + this.driveService.uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => { this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', { diff --git a/packages/backend/src/server/api/endpoints/federation/followers.ts b/packages/backend/src/server/api/endpoints/federation/followers.ts index ce4dd130670c99fda1842bbee6c3f568e717bfe8..d5b80035df8f3e04e0bd42e4e51ab21674f9ab33 100644 --- a/packages/backend/src/server/api/endpoints/federation/followers.ts +++ b/packages/backend/src/server/api/endpoints/federation/followers.ts @@ -3,17 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; -import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; -import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, + kind: 'read:account', res: { type: 'array', @@ -33,6 +31,8 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + includeFollower: { type: 'boolean', default: false }, + includeFollowee: { type: 'boolean', default: true }, }, required: ['host'], } as const; @@ -40,21 +40,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private followingEntityService: FollowingEntityService, - private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere('following.followeeHost = :host', { host: ps.host }); - - const followings = await query - .limit(ps.limit) - .getMany(); - - return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); + return this.followingEntityService.getFollowers(me, ps); }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/following.ts b/packages/backend/src/server/api/endpoints/federation/following.ts index 1a793889c7e9d478fec2de168b44bb3db96f3ec9..215f94fbcc4975e427f5c2d3aeca7440541058bc 100644 --- a/packages/backend/src/server/api/endpoints/federation/following.ts +++ b/packages/backend/src/server/api/endpoints/federation/following.ts @@ -3,17 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository } from '@/models/_.js'; -import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; -import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], - requireCredential: false, + requireCredential: true, + kind: 'read:account', res: { type: 'array', @@ -33,6 +31,8 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + includeFollower: { type: 'boolean', default: false }, + includeFollowee: { type: 'boolean', default: true }, }, required: ['host'], } as const; @@ -40,21 +40,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private followingEntityService: FollowingEntityService, - private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId) - .andWhere('following.followerHost = :host', { host: ps.host }); - - const followings = await query - .limit(ps.limit) - .getMany(); - - return await this.followingEntityService.packMany(followings, me, { populateFollowee: true }); + return this.followingEntityService.getFollowing(me, ps); }); } } diff --git a/packages/backend/src/server/api/endpoints/following/requests/sent.ts b/packages/backend/src/server/api/endpoints/following/requests/sent.ts new file mode 100644 index 0000000000000000000000000000000000000000..6325f01bb8c83b38b8b1130cf6be532fd5730e03 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/following/requests/sent.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import type { FollowRequestsRepository } from '@/models/_.js'; +import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['following', 'account'], + + requireCredential: true, + + kind: 'read:following', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + follower: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + followee: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + + private followRequestEntityService: FollowRequestEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId) + .andWhere('request.followerId = :meId', { meId: me.id }); + + const requests = await query + .limit(ps.limit) + .getMany(); + + return await this.followRequestEntityService.packMany(requests, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts index 73231e8e09ad14f03f2712ce44787dd76199600a..603df44733fd7ab57115ec339f9375cd013e67e1 100644 --- a/packages/backend/src/server/api/endpoints/i/claim-achievement.ts +++ b/packages/backend/src/server/api/endpoints/i/claim-achievement.ts @@ -3,10 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; -import { MetaService } from '@/core/MetaService.js'; +import type { MiMeta } from '@/models/_.js'; export const meta = { requireCredential: true, @@ -25,12 +26,13 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private achievementService: AchievementService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { - const meta = await this.metaService.fetch(); - if (!meta.enableAchievements) return; + if (!this.serverSettings.enableAchievements) return; await this.achievementService.create(me.id, ps.name); }); diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts index bc46163e3d27f6198fa91e4959eb58ce53f0a01a..bdf6c065e89c1d1c96a3061518819049b5237876 100644 --- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -16,6 +16,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, + requireRolePolicy: 'canImportAntennas', prohibitMoved: true, limit: { diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index 260610853930d7ba0a62bfe4c13758831ca11728..d7bb6bcd22562a8b2ec4c6961bf819aa54810154 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -15,6 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, + requireRolePolicy: 'canImportBlocking', prohibitMoved: true, limit: { diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index d5e824df274077c32521c319e57034fb5cf7ea76..e03192d8c67b7b6a35c0aca0d29d4a01f45179f5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -15,6 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, + requireRolePolicy: 'canImportFollowing', prohibitMoved: true, limit: { duration: ms('1hour'), diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index 0f5800404eaf688d67d9340921c6815ba47eaa3b..76b285bb7e3f9e76ba9b4c08df8ad136216660f5 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -15,6 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, + requireRolePolicy: 'canImportMuting', prohibitMoved: true, limit: { diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index bacdd5c88f285432445763abc6c17b023ee09685..76ecfd082ca3ca7e6d42489250de4f85233bfac7 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -15,6 +15,7 @@ import { ApiError } from '../../error.js'; export const meta = { secure: true, requireCredential: true, + requireRolePolicy: 'canImportUserLists', prohibitMoved: true, limit: { duration: ms('1hour'), diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 7332026d84efaa390c865ca2a1d897219b558096..0be8bfb6952cb660ae7cead83d020e14578bb038 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -8,7 +8,7 @@ import ms from 'ms'; //import bcrypt from 'bcryptjs'; import * as argon2 from 'argon2'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; import type { Config } from '@/config.js'; @@ -16,7 +16,6 @@ import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { UserAuthService } from '@/core/UserAuthService.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -71,10 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, private userEntityService: UserEntityService, private emailService: EmailService, private userAuthService: UserAuthService, @@ -106,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (!res.available) { throw new ApiError(meta.errors.unavailable); } - } else if ((await this.metaService.fetch()).emailRequiredForSignup) { + } else if (this.serverSettings.emailRequiredForSignup) { throw new ApiError(meta.errors.emailRequired); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 6cc22e79948da1c2daf0dd8ecac664a901e6d99b..8994c3fff6e8826c816664a6dfd719086661e181 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; -import { birthdaySchema, listenbrainzSchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; +import { birthdaySchema, listenbrainzSchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; -import { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; @@ -146,6 +145,7 @@ export const paramDef = { properties: { name: { ...nameSchema, nullable: true }, description: { ...descriptionSchema, nullable: true }, + followedMessage: { ...followedMessageSchema, nullable: true }, location: { ...locationSchema, nullable: true }, birthday: { ...birthdaySchema, nullable: true }, listenbrainz: { ...listenbrainzSchema, nullable: true }, @@ -159,6 +159,7 @@ export const paramDef = { flipH: { type: 'boolean', nullable: true }, offsetX: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, offsetY: { type: 'number', nullable: true, maximum: 0.25, minimum: -0.25 }, + showBelow: { type: 'boolean', nullable: true }, }, required: ['id'], } }, @@ -192,6 +193,7 @@ export const paramDef = { injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, + defaultSensitive: { type: 'boolean' }, autoSensitive: { type: 'boolean' }, followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, @@ -283,6 +285,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } if (ps.description !== undefined) profileUpdates.description = ps.description; + if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; @@ -347,6 +350,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; } + if (typeof ps.defaultSensitive === 'boolean') profileUpdates.defaultSensitive = ps.defaultSensitive; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.avatarId) { @@ -399,7 +403,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- updates.backgroundUrl = null; updates.backgroundBlurhash = null; } - + if (ps.avatarDecorations) { policies ??= await this.roleService.getUserPolicies(user.id); const decorations = await this.avatarDecorationService.getAll(true); @@ -417,6 +421,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- flipH: d.flipH ?? false, offsetX: d.offsetX ?? 0, offsetY: d.offsetY ?? 0, + showBelow: d.showBelow ?? false, })); } diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index 9eb7f5b3a0333f1eab499d8e64fa38d5c5aba40b..6e84603f7a454e5641627e7e5ede8b0a64466b4b 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '@/server/api/error.js'; +// TODO: UserWebhook schemaã®é©ç”¨ export const meta = { tags: ['webhooks'], diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts index fe07afb2d08965bdb690696b5ce52cdcb0c676d1..394c178f2adf1628a18de72ea96e43c56c447452 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts @@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js'; import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +// TODO: UserWebhook schemaã®é©ç”¨ export const meta = { tags: ['webhooks', 'account'], diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts index 5ddb79caf283c31b8378550a188be670f406b0f7..4a0c09ff0ccbf8850d4fd029eb937f7bbd8dbf3c 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts @@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '../../../error.js'; +// TODO: UserWebhook schemaã®é©ç”¨ export const meta = { tags: ['webhooks'], diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2bf6df9ce2f9b5bc7aa2dffc8c1d5186505fca2e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { webhookEventTypes } from '@/models/Webhook.js'; +import { WebhookTestService } from '@/core/WebhookTestService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['webhooks'], + + requireCredential: true, + secure: true, + kind: 'read:account', + + limit: { + duration: ms('15min'), + max: 60, + }, + + errors: { + noSuchWebhook: { + message: 'No such webhook.', + code: 'NO_SUCH_WEBHOOK', + id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + webhookId: { + type: 'string', + format: 'misskey:id', + }, + type: { + type: 'string', + enum: webhookEventTypes, + }, + override: { + type: 'object', + properties: { + url: { type: 'string' }, + secret: { type: 'string' }, + }, + }, + }, + required: ['webhookId', 'type'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private webhookTestService: WebhookTestService, + ) { + super(meta, paramDef, async (ps, me) => { + try { + await this.webhookTestService.testUserWebhook({ + webhookId: ps.webhookId, + type: ps.type, + override: ps.override, + }, me); + } catch (e) { + if (e instanceof WebhookTestService.NoSuchWebhookError) { + throw new ApiError(meta.errors.noSuchWebhook); + } + throw e; + } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/invite/limit.ts b/packages/backend/src/server/api/endpoints/invite/limit.ts index 2786bd98d5bf6a0c42e0b293c42c7667517c85f9..2ffd41ae28a3650f79c5f818fdea8106b6d6163e 100644 --- a/packages/backend/src/server/api/endpoints/invite/limit.ts +++ b/packages/backend/src/server/api/endpoints/invite/limit.ts @@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const policies = await this.roleService.getUserPolicies(me.id); const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({ - id: MoreThan(this.idService.gen(Date.now() - (policies.inviteExpirationTime * 60 * 1000))), + id: MoreThan(this.idService.gen(Date.now() - (policies.inviteLimitCycle * 60 * 1000))), createdById: me.id, }) : null; diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts index c5e3a5a5f74fcc49fd01813ec419a0ab4af42baf..94ec8c37ec589a2e2e8b3b026791ce0782f27a15 100644 --- a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -9,7 +9,6 @@ import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; import { CacheService } from '@/core/CacheService.js'; -import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['notes'], @@ -51,6 +50,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -59,11 +61,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private cacheService: CacheService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const policies = await this.roleService.getUserPolicies(me ? me.id : null); - const instance = await this.metaService.fetch(); if (!policies.btlAvailable) { throw new ApiError(meta.errors.btlDisabled); } @@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.visibility = \'public\'') .andWhere('note.channelId IS NULL') - .andWhere('note.userHost IN (:...hosts)', { hosts: instance.bubbleInstances }) + .andWhere('note.userHost IN (:...hosts)', { hosts: this.serverSettings.bubbleInstances }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('note.reply', 'reply') .leftJoinAndSelect('note.renote', 'renote') @@ -97,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } if (!ps.withBots) query.andWhere('user.isBot = FALSE'); - + if (ps.withRenotes === false) { query.andWhere(new Brackets(qb => { qb.where('note.renoteId IS NULL'); diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index f3d887bb200af9f3c8d00272c280d53b0b63fd0a..18d80e867ba222ddfaf806c6b8b30920136e5219 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -5,15 +5,12 @@ process.env.NODE_ENV = 'test'; -import { readFile } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; import { describe, test, expect } from '@jest/globals'; +import { loadConfig } from '@/config.js'; import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; import { paramDef } from './create.js'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); +const config = loadConfig(); const VALID = true; const INVALID = false; @@ -21,7 +18,12 @@ const INVALID = false; describe('api:notes/create', () => { describe('validation', () => { const v = getValidator(paramDef); - const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8'); + const tooLong = (limit: number) => { + const arr: string[] = ['']; + arr.length = limit + 1; + arr.fill('a'); + return arr.join(''); + }; test('reject empty', () => { const valid = v({ }); @@ -71,8 +73,8 @@ describe('api:notes/create', () => { .toBe(INVALID); }); - test('over 500 characters cw', async () => { - expect(v({ text: 'Body', cw: await tooLong })) + test('over max characters cw', async () => { + expect(v({ text: '', cw: tooLong(config.maxNoteLength) })) .toBe(INVALID); }); }); @@ -220,7 +222,7 @@ describe('api:notes/create', () => { }); test('reject poll with too long choice', async () => { - expect(v({ poll: { choices: [await tooLong, '2'] } })) + expect(v({ poll: { choices: [tooLong(config.maxNoteLength), '2'] } })) .toBe(INVALID); }); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 626f03b758690542d1301aa6adaaf467c0b55179..d1cf0123dc306950678468a644f2294ee7188fa7 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -17,8 +17,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; -import { MetaService } from '@/core/MetaService.js'; -import { UtilityService } from '@/core/UtilityService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; @@ -92,6 +90,12 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', }, + maxCwLength: { + message: 'You tried posting a content warning which is too long.', + code: 'MAX_CW_LENGTH', + id: '7004c478-bda3-4b4f-acb2-4316398c9d52', + }, + cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { message: 'You cannot reply to a specified visibility note with extended visibility.', code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', @@ -149,7 +153,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, - cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, + cw: { type: 'string', nullable: true, minLength: 1 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, @@ -252,9 +256,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.text && (ps.text.length > this.config.maxNoteLength)) { + if (ps.text && ps.text.length > this.config.maxNoteLength) { throw new ApiError(meta.errors.maxLength); } + if (ps.cw && ps.cw.length > this.config.maxCwLength) { + throw new ApiError(meta.errors.maxCwLength); + } let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 835cbc14facfea71eb2cbb9bda5d6871b4f1fcea..dc94c78e7570cd01cc11c4a5b59e55eb2c8d9476 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -86,6 +86,12 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', }, + maxCwLength: { + message: 'You tried posting a content warning which is too long.', + code: 'MAX_CW_LENGTH', + id: '7004c478-bda3-4b4f-acb2-4316398c9d52', + }, + cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { message: 'You cannot reply to a specified visibility note with extended visibility.', code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', @@ -197,7 +203,7 @@ export const paramDef = { format: 'misskey:id', }, }, - cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, + cw: { type: 'string', nullable: true, minLength: 1 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, @@ -297,9 +303,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEditService: NoteEditService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.text && (ps.text.length > this.config.maxNoteLength)) { + if (ps.text && ps.text.length > this.config.maxNoteLength) { throw new ApiError(meta.errors.maxLength); } + if (ps.cw && ps.cw.length > this.config.maxCwLength) { + throw new ApiError(meta.errors.maxCwLength); + } + let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6604b9798b958214c76fa2544faa17aa33de7d1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -0,0 +1,178 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; +import { SkLatestNote, MiFollowing } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'read:account', + allowGet: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, + + errors: { + bothWithRepliesAndWithFiles: { + message: 'Specifying both includeReplies and filesOnly is not supported', + code: 'BOTH_INCLUDE_REPLIES_AND_FILES_ONLY', + id: '91c8cb9f-36ed-46e7-9ca2-7df96ed6e222', + }, + bothWithFollowersAndIncludeNonPublic: { + message: 'Specifying both list:followers and includeNonPublic is not supported', + code: 'BOTH_LIST_FOLLOWERS_AND_INCLUDE_NON_PUBLIC', + id: '7a1b9cb6-235b-4e58-9c00-32c1796f502c', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + list: { type: 'string', enum: ['following', 'followers', 'mutuals'], default: 'following' }, + + filesOnly: { type: 'boolean', default: false }, + includeNonPublic: { type: 'boolean', default: false }, + includeReplies: { type: 'boolean', default: false }, + includeQuotes: { type: 'boolean', default: false }, + includeBots: { type: 'boolean', default: true }, + + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.includeReplies && ps.filesOnly) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); + if (ps.list === 'followers' && ps.includeNonPublic) throw new ApiError(meta.errors.bothWithFollowersAndIncludeNonPublic); + + const query = this.notesRepository + .createQueryBuilder('note') + .setParameter('me', me.id) + + // Limit to latest notes + .innerJoin( + (sub: SelectQueryBuilder<SkLatestNote>) => { + sub + .from(SkLatestNote, 'latest') + + // Return only one note per user + .addSelect('latest.user_id', 'user_id') + .addSelect('MAX(latest.note_id)', 'note_id') + .groupBy('latest.user_id'); + + // Match selected note types. + if (!ps.includeNonPublic) { + sub.andWhere('latest.is_public = true'); + } + if (!ps.includeReplies) { + sub.andWhere('latest.is_reply = false'); + } + if (!ps.includeQuotes) { + sub.andWhere('latest.is_quote = false'); + } + + return sub; + }, + 'latest', + 'note.id = latest.note_id', + ) + + // Avoid N+1 queries from the "pack" method + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel') + ; + + // Select the appropriate collection of users + if (ps.list === 'followers') { + addFollower(query); + } else if (ps.list === 'following') { + addFollowee(query); + } else { + addMutual(query); + } + + // Limit to files, if requested + if (ps.filesOnly) { + query.andWhere('note."fileIds" != \'{}\''); + } + + // Match selected user types. + if (!ps.includeBots) { + query.andWhere('"user"."isBot" = false'); + } + + // Respect blocks and mutes + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserQuery(query, me); + + // Support pagination + this.queryService + .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .orderBy('note.id', 'DESC') + .take(ps.limit); + + // Query and return the next page + const notes = await query.getMany(); + return await this.noteEntityService.packMany(notes, me); + }); + } +} + +/** + * Limit to followers (they follow us) + */ +function addFollower<T extends SelectQueryBuilder<ObjectLiteral>>(query: T): T { + return query.innerJoin(MiFollowing, 'follower', 'follower."followerId" = latest.user_id AND follower."followeeId" = :me'); +} + +/** + * Limit to followees (we follow them) + */ +function addFollowee<T extends SelectQueryBuilder<ObjectLiteral>>(query: T): T { + return query.innerJoin(MiFollowing, 'followee', 'followee."followerId" = :me AND followee."followeeId" = latest.user_id'); +} + +/** + * Limit to mutuals (they follow us AND we follow them) + */ +function addMutual<T extends SelectQueryBuilder<ObjectLiteral>>(query: T): T { + addFollower(query); + addFollowee(query); + return query; +} diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index fdc9a779564ba67ac5fd47e41d23aee91b1ffd6f..75be7b988887f8f923164ded3e9435e471a456ed 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -75,6 +74,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -88,7 +90,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private cacheService: CacheService, private queryService: QueryService, private userFollowingService: UserFollowingService, - private metaService: MetaService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { super(meta, paramDef, async (ps, me) => { @@ -102,9 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -158,7 +157,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- allowPartial: ps.allowPartial, me, redisTimelines: timelineConfig, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, excludeBots: !ps.withBots, diff --git a/packages/backend/src/server/api/endpoints/notes/like.ts b/packages/backend/src/server/api/endpoints/notes/like.ts index 17ee93736070a79ac0e8eb1c8606797e54d836c2..593463aea0ac919a101a884e348e171a3a1e64e0 100644 --- a/packages/backend/src/server/api/endpoints/notes/like.ts +++ b/packages/backend/src/server/api/endpoints/notes/like.ts @@ -1,8 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ReactionService } from '@/core/ReactionService.js'; -import { MetaService } from '@/core/MetaService.js'; +import type { MiMeta } from '@/models/_.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -26,6 +27,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '20ef5475-9f38-4e4c-bd33-de6d979498ec', }, + + cannotReactToRenote: { + message: 'You cannot like a Renote.', + code: 'CANNOT_REACT_TO_RENOTE', + id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', + }, }, } as const; @@ -41,13 +48,14 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private getterService: GetterService, private reactionService: ReactionService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { - const instance = await this.metaService.fetch(); - const like = ps.override ?? instance.defaultLike; + const like = ps.override ?? this.serverSettings.defaultLike; const note = await this.getterService.getNote(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; @@ -58,6 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return; } if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked); + if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote); throw err; }); return; diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index 5c3c7ae7d0c457f38c138d99889ba780f3804978..d4c806d7e2af430131fce6df7a6fe8f7d90bb86c 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,16 +5,14 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { MiMeta, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -67,6 +65,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -74,10 +75,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, - private cacheService: CacheService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -90,9 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -117,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] diff --git a/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts new file mode 100644 index 0000000000000000000000000000000000000000..b96691f894b906dfb7d4077eee923648bae6ac27 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/polls/refresh.ts @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: marie and sharkey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { ApQuestionService } from '@/core/activitypub/models/ApQuestionService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'read:federation', + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'ecafbd2e-c283-4d6d-aecb-1a0a33b75396', + }, + + noPoll: { + message: 'The note does not attach a poll.', + code: 'NO_POLL', + id: '5f979967-52d9-4314-a911-1c673727f92f', + }, + + noUri: { + message: 'The note has no URI defined.', + code: 'INVALID_URI', + id: 'e0cc9a04-f2e8-41e4-a5f1-4127293260ca', + }, + + youHaveBeenBlocked: { + message: 'You cannot refresh this poll because you have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: '85a5377e-b1e9-4617-b0b9-5bea73331e49', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['noteId'], +} as const; + +// TODO: ãƒã‚¸ãƒƒã‚¯ã‚’サービスã«åˆ‡ã‚Šå‡ºã™ + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private getterService: GetterService, + private userBlockingService: UserBlockingService, + private apQuestionService: ApQuestionService, + private noteEntityService: NoteEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // Get note + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + if (!note.hasPoll) { + throw new ApiError(meta.errors.noPoll); + } + + // Check blocking + if (note.userId !== me.id) { + const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id); + if (blocked) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + if (!note.uri) { + throw new ApiError(meta.errors.noUri); + } + + await this.apQuestionService.updateQuestion(note.uri); + + return await this.noteEntityService.pack(note, me, { + detail: true, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 55ff6771b1a2313f843fba3099b141a75c330ec5..2b4885a194f2123a167203f930e0d5538c05e0ab 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -5,14 +5,13 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { NotesRepository, MiMeta } from '@/models/_.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -69,18 +68,18 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, private noteEntityService: NoteEntityService, private queryService: QueryService, - private metaService: MetaService, private cacheService: CacheService, private utilityService: UtilityService, ) { super(meta, paramDef, async (ps, me) => { - const meta = await this.metaService.fetch(true); - const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere('note.visibility = \'public\'') .innerJoinAndSelect('note.user', 'user') @@ -89,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- .leftJoinAndSelect('reply.user', 'replyUser') .leftJoinAndSelect('renote.user', 'renoteUser'); - if (!meta.enableBotTrending) query.andWhere('user.isBot = FALSE'); + if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE'); this.queryService.generateVisibilityQuery(query, me); if (me) this.queryService.generateMutedUserQuery(query, me); @@ -156,8 +155,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- notes = notes.filter(note => { if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false; if (note.user?.isSuspended) return false; - if (this.utilityService.isBlockedHost(meta.blockedHosts, note.userHost)) return false; - if (this.utilityService.isSilencedHost(meta.silencedHosts, note.userHost)) return false; + if (note.userHost) { + if (!this.utilityService.isFederationAllowedHost(note.userHost)) return false; + if (this.utilityService.isSilencedHost(this.serverSettings.silencedHosts, note.userHost)) return false; + } return true; }); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 1a14703e6eb2647d54e568b20465b87830012454..d40a04c1b1f22006d042659960d4b36920645bde 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; export const meta = { @@ -57,6 +56,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -70,15 +72,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private userFollowingService: UserFollowingService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -110,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index d6ef655291f4a2382ff6eb67a049ece4b22f6866..234248db5c0740390ba252bf2906ec9a63606a11 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -4,14 +4,15 @@ */ import { URLSearchParams } from 'node:url'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -59,9 +60,11 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private noteEntityService: NoteEntityService, private getterService: GetterService, - private metaService: MetaService, private httpRequestService: HttpRequestService, private roleService: RoleService, ) { @@ -84,13 +87,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return; } - const instance = await this.metaService.fetch(); - - if (instance.deeplAuthKey == null && !instance.deeplFreeMode) { + if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode) { throw new ApiError(meta.errors.unavailable); } - if (instance.deeplFreeMode && !instance.deeplFreeInstance) { + if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance) { throw new ApiError(meta.errors.unavailable); } @@ -98,11 +99,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; const params = new URLSearchParams(); - if (instance.deeplAuthKey) params.append('auth_key', instance.deeplAuthKey); + if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey); params.append('text', note.text); params.append('target_lang', targetLang); - const endpoint = instance.deeplFreeMode && instance.deeplFreeInstance ? instance.deeplFreeInstance : instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + const endpoint = this.serverSettings.deeplFreeMode && this.serverSettings.deeplFreeInstance ? this.serverSettings.deeplFreeInstance : this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; const res = await this.httpRequestService.send(endpoint, { method: 'POST', @@ -112,7 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }, body: params.toString(), }); - if (instance.deeplAuthKey) { + if (this.serverSettings.deeplAuthKey) { const json = (await res.json()) as { translations: { detected_source_language: string; diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 43877e61efaf4fc0bcf5f6cc3a27b0c1415049ec..87f9b322a60289adebfbfdf706d8a276e8f14281 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -5,16 +5,14 @@ import { Inject, Injectable } from '@nestjs/common'; import { Brackets } from 'typeorm'; -import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; +import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; -import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { QueryService } from '@/core/QueryService.js'; import { MiLocalUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -69,6 +67,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -80,11 +81,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteEntityService: NoteEntityService, private activeUsersChart: ActiveUsersChart, - private cacheService: CacheService, private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -99,9 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.noSuchList); } - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb(list, { untilId, sinceId, @@ -115,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.activeUsersChart.read(me); - await this.noteEntityService.packMany(timeline, me); + return await this.noteEntityService.packMany(timeline, me); } const timeline = await this.fanoutTimelineEndpointService.timeline({ @@ -124,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`], alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 15832ef7f8d7b3b120ea8b57f8288efad44fa838..5b0b656c63fe069bb9392f20a9d32b6163bdfa7e 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -5,11 +5,10 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsersRepository } from '@/models/_.js'; import * as Acct from '@/misc/acct.js'; import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -38,16 +37,16 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, - private metaService: MetaService, private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - const meta = await this.metaService.fetch(); - - const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({ + const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({ usernameLower: acct.username.toLowerCase(), host: acct.host ?? IsNull(), }))); diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts index c13802eb0689d51da2936cbab5a143f964af9e30..8301c85f2eb6e61127a0f0aef328d6b4f6eea752 100644 --- a/packages/backend/src/server/api/endpoints/server-info.ts +++ b/packages/backend/src/server/api/endpoints/server-info.ts @@ -5,9 +5,10 @@ import * as os from 'node:os'; import si from 'systeminformation'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { requireCredential: false, @@ -73,10 +74,11 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - private metaService: MetaService, + @Inject(DI.meta) + private serverSettings: MiMeta, ) { super(meta, paramDef, async () => { - if (!(await this.metaService.fetch()).enableServerMachineStats) return { + if (!this.serverSettings.enableServerMachineStats) return { machine: '?', cpu: { model: '?', diff --git a/packages/backend/src/server/api/endpoints/sponsors.ts b/packages/backend/src/server/api/endpoints/sponsors.ts index 99414e739abd381853ce4c76e552bc9ae47ae7b4..2a8a461a8f2c7d607329273db5208c669d9df4c6 100644 --- a/packages/backend/src/server/api/endpoints/sponsors.ts +++ b/packages/backend/src/server/api/endpoints/sponsors.ts @@ -3,14 +3,13 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; -import * as Redis from 'ioredis'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { DI } from '@/di-symbols.js'; +import { SponsorsService } from '@/core/SponsorsService.js'; export const meta = { tags: ['meta'], - description: 'Get Sharkey Sponsors', + description: 'Get Sharkey Sponsors or Instance Sponsors', requireCredential: false, requireCredentialPrivateMode: false, @@ -20,6 +19,7 @@ export const paramDef = { type: 'object', properties: { forceUpdate: { type: 'boolean', default: false }, + instance: { type: 'boolean', default: false }, }, required: [], } as const; @@ -27,31 +27,14 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.redis) private redisClient: Redis.Redis, + private sponsorsService: SponsorsService, ) { super(meta, paramDef, async (ps, me) => { - let totalSponsors; - const cachedSponsors = await this.redisClient.get('sponsors'); - - if (!ps.forceUpdate && cachedSponsors) { - totalSponsors = JSON.parse(cachedSponsors); + if (ps.instance) { + return { sponsor_data: await this.sponsorsService.instanceSponsors(ps.forceUpdate) }; } else { - try { - const backers = await fetch('https://opencollective.com/sharkey/tiers/backer/all.json').then((response) => response.json()); - const sponsorsOC = await fetch('https://opencollective.com/sharkey/tiers/sponsor/all.json').then((response) => response.json()); - - // Merge both together into one array and make sure it only has Active subscriptions - const allSponsors = [...sponsorsOC, ...backers].filter(sponsor => sponsor.isActive === true); - - // Remove possible duplicates - totalSponsors = [...new Map(allSponsors.map(v => [v.profile, v])).values()]; - - await this.redisClient.set('sponsors', JSON.stringify(totalSponsors), 'EX', 3600); - } catch (error) { - totalSponsors = []; - } + return { sponsor_data: await this.sponsorsService.sharkeySponsors(ps.forceUpdate) }; } - return { sponsor_data: totalSponsors }; }); } } diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts index a9a33149f95ac7aada8a087acbcd7b7ea5aaa22b..fd76df2d3c0b1adb19e54aac2b1781adf1154a5a 100644 --- a/packages/backend/src/server/api/endpoints/sw/register.ts +++ b/packages/backend/src/server/api/endpoints/sw/register.ts @@ -5,9 +5,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IdService } from '@/core/IdService.js'; -import type { SwSubscriptionsRepository } from '@/models/_.js'; +import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; import { PushNotificationService } from '@/core/PushNotificationService.js'; @@ -62,11 +61,13 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, private idService: IdService, - private metaService: MetaService, private pushNotificationService: PushNotificationService, ) { super(meta, paramDef, async (ps, me) => { @@ -78,12 +79,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- publickey: ps.publickey, }); - const instance = await this.metaService.fetch(true); - if (exist != null) { return { state: 'already-subscribed' as const, - key: instance.swPublicKey, + key: this.serverSettings.swPublicKey, userId: me.id, endpoint: exist.endpoint, sendReadMessage: exist.sendReadMessage, @@ -103,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return { state: 'subscribed' as const, - key: instance.swPublicKey, + key: this.serverSettings.swPublicKey, userId: me.id, endpoint: ps.endpoint, sendReadMessage: ps.sendReadMessage, diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts index affb0996f182a477ebc02c4ac3f592b064951e97..4944be9b05417615afddb40466b1093739e307ca 100644 --- a/packages/backend/src/server/api/endpoints/username/available.ts +++ b/packages/backend/src/server/api/endpoints/username/available.ts @@ -5,11 +5,10 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { localUsernameSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; export const meta = { tags: ['users'], @@ -39,13 +38,14 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.usedUsernamesRepository) private usedUsernamesRepository: UsedUsernamesRepository, - - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const exist = await this.usersRepository.countBy({ @@ -55,8 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() }); - const meta = await this.metaService.fetch(); - const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase()); + const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase()); return { available: exist === 0 && exist2 === 0 && !isPreserved, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index cc76c12f1d897e3eb3ab3aa7188f6aee2af90474..263d0629617116b6f6e09fa5ca2432c1a58896c5 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -5,18 +5,18 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { MiMeta, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { IdService } from '@/core/IdService.js'; import { QueryService } from '@/core/QueryService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { ApiError } from '@/server/api/error.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; export const meta = { tags: ['users', 'notes'], @@ -51,7 +51,11 @@ export const paramDef = { properties: { userId: { type: 'string', format: 'misskey:id' }, withReplies: { type: 'boolean', default: false }, + withRepliesToSelf: { type: 'boolean', default: true }, + withQuotes: { type: 'boolean', default: true }, withRenotes: { type: 'boolean', default: true }, + withBots: { type: 'boolean', default: true }, + withNonPublic: { type: 'boolean', default: true }, withChannelNotes: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, @@ -67,6 +71,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -75,15 +82,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private cacheService: CacheService, private idService: IdService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); const isSelf = me && (me.id === ps.userId); - const serverSettings = await this.metaService.fetch(); - if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); // early return if me is blocked by requesting user @@ -94,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -103,6 +107,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withChannelNotes: ps.withChannelNotes, withFiles: ps.withFiles, withRenotes: ps.withRenotes, + withQuotes: ps.withQuotes, + withBots: ps.withBots, + withNonPublic: ps.withNonPublic, + withRepliesToOthers: ps.withReplies, + withRepliesToSelf: ps.withRepliesToSelf, }, me); return await this.noteEntityService.packMany(timeline, me); @@ -127,11 +136,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- excludeReplies: ps.withChannelNotes && !ps.withReplies, // userTimelineWithChannel may include replies excludeNoFiles: ps.withChannelNotes && ps.withFiles, // userTimelineWithChannel may include notes without files excludePureRenotes: !ps.withRenotes, + excludeBots: !ps.withBots, noteFilter: note => { if (note.channel?.isSensitive && !isSelf) return false; if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false; if (note.visibility === 'followers' && !isFollowing && !isSelf) return false; + // These are handled by DB fallback, but we duplicate them here in case a timeline was already populated with notes + if (!ps.withRepliesToSelf && note.reply?.userId === note.userId) return false; + if (!ps.withQuotes && isRenote(note) && isQuote(note)) return false; + if (!ps.withNonPublic && note.visibility !== 'public') return false; + return true; }, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ @@ -142,6 +157,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withChannelNotes: ps.withChannelNotes, withFiles: ps.withFiles, withRenotes: ps.withRenotes, + withQuotes: ps.withQuotes, + withBots: ps.withBots, + withNonPublic: ps.withNonPublic, + withRepliesToOthers: ps.withReplies, + withRepliesToSelf: ps.withRepliesToSelf, }, me), }); @@ -157,6 +177,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- withChannelNotes: boolean, withFiles: boolean, withRenotes: boolean, + withQuotes: boolean, + withBots: boolean, + withNonPublic: boolean, + withRepliesToOthers: boolean, + withRepliesToSelf: boolean, }, me: MiLocalUser | null) { const isSelf = me && (me.id === ps.userId); @@ -188,7 +213,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- query.andWhere('note.fileIds != \'{}\''); } - if (ps.withRenotes === false) { + if (!ps.withRenotes && !ps.withQuotes) { + query.andWhere('note.renoteId IS NULL'); + } else if (!ps.withRenotes) { query.andWhere(new Brackets(qb => { qb.orWhere('note.userId != :userId', { userId: ps.userId }); qb.orWhere('note.renoteId IS NULL'); @@ -196,6 +223,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- qb.orWhere('note.fileIds != \'{}\''); qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)'); })); + } else if (!ps.withQuotes) { + query.andWhere(` + ( + note."renoteId" IS NULL + OR ( + note.text IS NULL + AND note.cw IS NULL + AND note."replyId" IS NULL + AND note."hasPoll" IS FALSE + AND note."fileIds" = '{}' + ) + ) + `); + } + + if (!ps.withRepliesToOthers && !ps.withRepliesToSelf) { + query.andWhere('reply.id IS NULL'); + } else if (!ps.withRepliesToOthers) { + query.andWhere('(reply.id IS NULL OR reply."userId" = note."userId")'); + } else if (!ps.withRepliesToSelf) { + query.andWhere('(reply.id IS NULL OR reply."userId" != note."userId")'); + } + + if (!ps.withNonPublic) { + query.andWhere('note.visibility = \'public\''); + } + + if (!ps.withBots) { + query.andWhere('"user"."isBot" = false'); } return await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts index 8c9cca173066b8fbac1af36bf551a17794b5ef94..b40e4cdaa4590369e7c637cb81afbe79b2fdc0be 100644 --- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts +++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts @@ -8,11 +8,10 @@ import megalodon, { Entity, MegalodonInterface } from 'megalodon'; import querystring from 'querystring'; import { IsNull } from 'typeorm'; import multer from 'fastify-multer'; -import type { AccessTokensRepository, NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { AccessTokensRepository, NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository, MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList, MastoConverters } from './converters.js'; import { getInstance } from './endpoints/meta.js'; import { ApiAuthMastodon, ApiAccountMastodon, ApiFilterMastodon, ApiNotifyMastodon, ApiSearchMastodon, ApiTimelineMastodon, ApiStatusMastodon } from './endpoints.js'; @@ -31,6 +30,8 @@ export function getClient(BASE_URL: string, authorization: string | undefined): @Injectable() export class MastodonApiServerService { constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.notesRepository) @@ -43,7 +44,6 @@ export class MastodonApiServerService { private accessTokensRepository: AccessTokensRepository, @Inject(DI.config) private config: Config, - private metaService: MetaService, private userEntityService: UserEntityService, private driveService: DriveService, private mastoConverter: MastoConverters, @@ -112,7 +112,7 @@ export class MastodonApiServerService { order: { id: 'ASC' }, }); const contact = admin == null ? null : await this.mastoConverter.convertAccount((await client.getAccount(admin.id)).data); - reply.send(await getInstance(data.data, contact as Entity.Account, this.config, await this.metaService.fetch())); + reply.send(await getInstance(data.data, contact as Entity.Account, this.config, this.serverSettings)); } catch (e: any) { /* console.error(e); */ reply.code(401).send(e.response.data); diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index 79d2c62a241fb0b826b20c188ab877bb78a12c56..c9833b85d7c07d733c27f94751001a655b54d645 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -54,7 +54,7 @@ export async function getInstance( }, polls: { max_options: 10, - max_characters_per_option: 50, + max_characters_per_option: 150, min_expiration: 50, max_expiration: 2629746, }, diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts index 647e9cab8114787cef22d2b8ead602f1ca8a9051..8693f0c6ac0dc8aa97a53445d9adc68df043c6ad 100644 --- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; import type { Packed } from '@/misc/json-schema.js'; import { MetaService } from '@/core/MetaService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 226e161122fa92ae701e5e40d262f4d86440ff0f..9939aa49ee4f2e3f79831e5895db76d97ee2989b 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -30,7 +30,7 @@ class ChannelChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { if (typeof params.channelId !== 'string') return; this.channelId = params.channelId; this.withFiles = !!(params.withFiles ?? false); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 0af90a844b0c9fe0569c1b864f4c38aee2cf2fb9..c14f6c9123bb98b065e2eb724b288e770ab50bf0 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import { createBullBoard } from '@bull-board/api'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; -import { FastifyAdapter } from '@bull-board/fastify'; +import { FastifyAdapter as BullBoardFastifyAdapter } from '@bull-board/fastify'; import ms from 'ms'; import sharp from 'sharp'; import pug from 'pug'; @@ -24,7 +24,6 @@ import type { Config } from '@/config.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; import { DI } from '@/di-symbols.js'; import * as Acct from '@/misc/acct.js'; -import { MetaService } from '@/core/MetaService.js'; import type { DbQueue, DeliverQueue, @@ -61,7 +60,8 @@ const staticAssets = `${_dirname}/../../../assets/`; const clientAssets = `${_dirname}/../../../../frontend/assets/`; const assets = `${_dirname}/../../../../../built/_frontend_dist_/`; const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; -const viteOut = `${_dirname}/../../../../../built/_vite_/`; +const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`; +const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`; const tarball = `${_dirname}/../../../../../built/tarball/`; @Injectable() @@ -72,6 +72,9 @@ export class ClientServerService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -108,7 +111,6 @@ export class ClientServerService { private clipEntityService: ClipEntityService, private channelEntityService: ChannelEntityService, private reversiGameEntityService: ReversiGameEntityService, - private metaService: MetaService, private urlPreviewService: UrlPreviewService, private feedService: FeedService, private roleService: RoleService, @@ -128,39 +130,37 @@ export class ClientServerService { @bindThis private async manifestHandler(reply: FastifyReply) { - const instance = await this.metaService.fetch(true); - let manifest = { // 空文å—列ã®å ´åˆå³è¾ºã‚’使ã„ãŸã„ãŸã‚ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'short_name': instance.shortName || instance.name || this.config.host, + 'short_name': this.meta.shortName || this.meta.name || this.config.host, // 空文å—列ã®å ´åˆå³è¾ºã‚’使ã„ãŸã„ãŸã‚ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'name': instance.name || this.config.host, + 'name': this.meta.name || this.config.host, 'start_url': '/', 'display': 'standalone', 'background_color': '#313a42', // 空文å—列ã®å ´åˆå³è¾ºã‚’使ã„ãŸã„ãŸã‚ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'theme_color': instance.themeColor || '#86b300', + 'theme_color': this.meta.themeColor || '#86b300', 'icons': [{ // 空文å—列ã®å ´åˆå³è¾ºã‚’使ã„ãŸã„ãŸã‚ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'src': instance.app192IconUrl || '/static-assets/icons/192.png', + 'src': this.meta.app192IconUrl || '/static-assets/icons/192.png', 'sizes': '192x192', 'type': 'image/png', 'purpose': 'maskable', }, { // 空文å—列ã®å ´åˆå³è¾ºã‚’使ã„ãŸã„ãŸã‚ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'src': instance.app512IconUrl || '/static-assets/icons/512.png', + 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png', 'sizes': '512x512', 'type': 'image/png', 'purpose': 'maskable', }, { // 空文å—列ã®å ´åˆå³è¾ºã‚’使ã„ãŸã„ãŸã‚ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - 'src': instance.app512IconUrl || '/static-assets/icons/512.png', + 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png', 'sizes': '300x300', 'type': 'image/png', 'purpose': 'any', @@ -179,7 +179,7 @@ export class ClientServerService { manifest = { ...manifest, - ...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride), + ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride), }; reply.header('Cache-Control', 'max-age=300'); @@ -193,9 +193,9 @@ export class ClientServerService { icon: meta.iconUrl, appleTouchIcon: meta.app512IconUrl, themeColor: meta.themeColor, - serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://launcher.moe/error.png', - infoImageUrl: meta.infoImageUrl ?? 'https://launcher.moe/nothinghere.png', - notFoundImageUrl: meta.notFoundImageUrl ?? 'https://launcher.moe/missingpage.webp', + serverErrorImageUrl: meta.serverErrorImageUrl ?? '/client-assets/status/error.png', + infoImageUrl: meta.infoImageUrl ?? '/client-assets/status/nothinghere.png', + notFoundImageUrl: meta.notFoundImageUrl ?? '/client-assets/status/missingpage.webp', instanceUrl: this.config.url, randomMOTD: this.config.customMOTD ? this.config.customMOTD[Math.floor(Math.random() * this.config.customMOTD.length)] : undefined, metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)), @@ -242,7 +242,7 @@ export class ClientServerService { } }); - const serverAdapter = new FastifyAdapter(); + const bullBoardServerAdapter = new BullBoardFastifyAdapter(); createBullBoard({ queues: [ @@ -255,11 +255,11 @@ export class ClientServerService { this.userWebhookDeliverQueue, this.systemWebhookDeliverQueue, ].map(q => new BullMQAdapter(q)), - serverAdapter, + serverAdapter: bullBoardServerAdapter, }); - serverAdapter.setBasePath(bullBoardPath); - (fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath }); + bullBoardServerAdapter.setBasePath(bullBoardPath); + (fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath }); //#endregion fastify.register(fastifyView, { @@ -280,15 +280,22 @@ export class ClientServerService { }); //#region vite assets - if (this.config.clientManifestExists) { + if (this.config.frontendEmbedManifestExists) { fastify.register((fastify, options, done) => { fastify.register(fastifyStatic, { - root: viteOut, + root: frontendViteOut, prefix: '/vite/', maxAge: ms('30 days'), immutable: true, decorateReply: false, }); + fastify.register(fastifyStatic, { + root: frontendEmbedViteOut, + prefix: '/embed_vite/', + maxAge: ms('30 days'), + immutable: true, + decorateReply: false, + }); fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); done(); }); @@ -299,6 +306,13 @@ export class ClientServerService { prefix: '/vite', rewritePrefix: '/vite', }); + + const embedPort = (process.env.EMBED_VITE_PORT ?? '5174'); + fastify.register(fastifyProxy, { + upstream: 'http://localhost:' + embedPort, + prefix: '/embed_vite', + rewritePrefix: '/embed_vite', + }); } //#endregion @@ -443,15 +457,20 @@ export class ClientServerService { // Manifest fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply)); + // Embed Javascript + fastify.get('/embed.js', async (request, reply) => { + return await reply.sendFile('/embed.js', staticAssets, { + maxAge: ms('1 day'), + }); + }); + fastify.get('/robots.txt', async (request, reply) => { return await reply.sendFile('/robots.txt', staticAssets); }); // OpenSearch XML fastify.get('/opensearch.xml', async (request, reply) => { - const meta = await this.metaService.fetch(); - - const name = meta.name ?? 'Sharkey'; + const name = this.meta.name ?? 'Sharkey'; let content = ''; content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">'; content += `<ShortName>${name}</ShortName>`; @@ -468,14 +487,13 @@ export class ClientServerService { //#endregion const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => { - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { - img: meta.bannerUrl, + img: this.meta.bannerUrl, url: this.config.url, - title: meta.name ?? 'Misskey', - desc: meta.description, - ...await this.generateCommonPugData(meta), + title: this.meta.name ?? 'Sharkey', + desc: this.meta.description, + ...await this.generateCommonPugData(this.meta), ...data, }); }; @@ -553,7 +571,6 @@ export class ClientServerService { if (user != null) { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - const meta = await this.metaService.fetch(); const me = profile.fields ? profile.fields .filter(filed => filed.value != null && filed.value.match(/^https?:/)) @@ -569,7 +586,7 @@ export class ClientServerService { user, profile, me, avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { // リモートユーザーãªã®ã§ @@ -607,7 +624,6 @@ export class ClientServerService { if (note) { const _note = await this.noteEntityService.pack(note); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -619,7 +635,7 @@ export class ClientServerService { avatarUrl: _note.user.avatarUrl, // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -644,7 +660,6 @@ export class ClientServerService { if (page) { const _page = await this.pageEntityService.pack(page); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId }); - const meta = await this.metaService.fetch(); if (['public'].includes(page.visibility)) { reply.header('Cache-Control', 'public, max-age=15'); } else { @@ -658,7 +673,7 @@ export class ClientServerService { page: _page, profile, avatarUrl: _page.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -674,7 +689,6 @@ export class ClientServerService { if (flash) { const _flash = await this.flashEntityService.pack(flash); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -684,7 +698,7 @@ export class ClientServerService { flash: _flash, profile, avatarUrl: _flash.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -700,7 +714,6 @@ export class ClientServerService { if (clip && clip.isPublic) { const _clip = await this.clipEntityService.pack(clip); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -710,7 +723,7 @@ export class ClientServerService { clip: _clip, profile, avatarUrl: _clip.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -724,7 +737,6 @@ export class ClientServerService { if (post) { const _post = await this.galleryPostEntityService.pack(post); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -734,7 +746,7 @@ export class ClientServerService { post: _post, profile, avatarUrl: _post.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -749,11 +761,10 @@ export class ClientServerService { if (channel) { const _channel = await this.channelEntityService.pack(channel); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); return await reply.view('channel', { channel: _channel, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -768,11 +779,10 @@ export class ClientServerService { if (game) { const _game = await this.reversiGameEntityService.packDetail(game); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=3600'); return await reply.view('reversi-game', { game: _game, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -780,7 +790,7 @@ export class ClientServerService { }); //#endregion - //region noindex pages + //#region noindex pages // Tags fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => { return await renderBase(reply, { noindex: true }); @@ -790,21 +800,97 @@ export class ClientServerService { fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => { return await renderBase(reply, { noindex: true }); }); - //endregion + //#endregion - fastify.get('/_info_card_', async (request, reply) => { - const meta = await this.metaService.fetch(true); + //#region embed pages + fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const user = await this.usersRepository.findOneBy({ + id: request.params.user, + }); + + if (user == null) return; + if (user.host != null) return; + + const _user = await this.userEntityService.pack(user); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Sharkey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + user: _user, + }), + }); + }); + + fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + const note = await this.notesRepository.findOneBy({ + id: request.params.note, + }); + + if (note == null) return; + if (note.visibility !== 'public') return; + if (note.userHost != null) return; + + const _note = await this.noteEntityService.pack(note, null, { detail: true }); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Sharkey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + note: _note, + }), + }); + }); + + fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const clip = await this.clipsRepository.findOneBy({ + id: request.params.clip, + }); + + if (clip == null) return; + + const _clip = await this.clipEntityService.pack(clip); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Sharkey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + clip: _clip, + }), + }); + }); + + fastify.get('/embed/*', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Sharkey', + ...await this.generateCommonPugData(this.meta), + }); + }); + + fastify.get('/_info_card_', async (request, reply) => { reply.removeHeader('X-Frame-Options'); return await reply.view('info-card', { version: this.config.version, host: this.config.host, - meta: meta, + meta: this.meta, originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }), originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), }); }); + //#endregion fastify.get('/bios', async (request, reply) => { return await reply.view('bios', { diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index ef804b5bfdbc613814ae3e0e2b325b4c583cf6c9..981fbb435326c90541a2d4588a808779975f1dba 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly'; import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; @@ -32,7 +31,9 @@ export class UrlPreviewService { @Inject(DI.redis) private redisClient: Redis.Redis, - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, + private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { @@ -75,9 +76,7 @@ export class UrlPreviewService { return; } - const meta = await this.metaService.fetch(); - - if (!meta.urlPreviewEnabled) { + if (!this.meta.urlPreviewEnabled) { reply.code(403); return { error: new ApiError({ @@ -98,14 +97,14 @@ export class UrlPreviewService { return cached; } - this.logger.info(meta.urlPreviewSummaryProxyUrl + this.logger.info(this.meta.urlPreviewSummaryProxyUrl ? `(Proxy) Getting preview of ${key} ...` : `Getting preview of ${key} ...`); try { - const summary = meta.urlPreviewSummaryProxyUrl - ? await this.fetchSummaryFromProxy(url, meta, lang) - : await this.fetchSummary(url, meta, lang); + const summary = this.meta.urlPreviewSummaryProxyUrl + ? await this.fetchSummaryFromProxy(url, this.meta, lang) + : await this.fetchSummary(url, this.meta, lang); this.logger.succ(`Got preview of ${url}: ${summary.title}`); diff --git a/packages/backend/src/server/web/bios.js b/packages/backend/src/server/web/bios.js index 9ff5dca72ace8c8d9d1c4165722de5d321bb94fd..5dbb26f9e34dc9c21c981325f3a5eea72cc01db5 100644 --- a/packages/backend/src/server/web/bios.js +++ b/packages/backend/src/server/web/bios.js @@ -6,36 +6,6 @@ 'use strict'; window.onload = async () => { - const account = JSON.parse(localStorage.getItem('account')); - const i = account.token; - - const api = (endpoint, data = {}) => { - const promise = new Promise((resolve, reject) => { - // Append a credential - if (i) data.i = i; - - // Send request - window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `/api/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache' - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - }); - - return promise; - }; - const content = document.getElementById('content'); document.getElementById('ls').addEventListener('click', () => { diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js new file mode 100644 index 0000000000000000000000000000000000000000..b07dce3ac42220553fa5d8f8c36b9434a35f2e2e --- /dev/null +++ b/packages/backend/src/server/web/boot.embed.js @@ -0,0 +1,208 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +'use strict'; + +// ブãƒãƒƒã‚¯ã®ä¸ã«å…¥ã‚Œãªã„ã¨ã€å®šç¾©ã—ãŸå¤‰æ•°ãŒãƒ–ラウザã®ã‚°ãƒãƒ¼ãƒãƒ«ã‚¹ã‚³ãƒ¼ãƒ—ã«ç™»éŒ²ã•ã‚Œã¦ã—ã¾ã„邪é”ãªã®ã§ +(async () => { + window.onerror = (e) => { + console.error(e); + renderError('SOMETHING_HAPPENED'); + }; + window.onunhandledrejection = (e) => { + console.error(e); + renderError('SOMETHING_HAPPENED_IN_PROMISE'); + }; + + let forceError = localStorage.getItem('forceError'); + if (forceError != null) { + renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); + return; + } + + // パラメータã«å¿œã˜ã¦splashã®ã‚¹ã‚¿ã‚¤ãƒ«ã‚’変更 + const params = new URLSearchParams(location.search); + if (params.has('rounded') && params.get('rounded') === 'false') { + document.documentElement.classList.add('norounded'); + } + if (params.has('border') && params.get('border') === 'false') { + document.documentElement.classList.add('noborder'); + } + + // Force update when locales change + const langsVersion = LANGS_VERSION; + const localeVersion = localStorage.getItem('localeVersion'); + if (localeVersion !== langsVersion) { + console.info(`Updating locales from version ${localeVersion ?? 'N/A'} to ${langsVersion}`); + localStorage.removeItem('localeVersion'); + localStorage.removeItem('locale'); + } + + //#region Detect language & fetch translations + if (!localStorage.getItem('locale')) { + const supportedLangs = LANGS; + let lang = localStorage.getItem('lang'); + if (lang == null || !supportedLangs.includes(lang)) { + if (supportedLangs.includes(navigator.language)) { + lang = navigator.language; + } else { + lang = supportedLangs.find(x => x.split('-')[0] === navigator.language); + + // Fallback + if (lang == null) lang = 'en-US'; + } + } + + // for https://github.com/misskey-dev/misskey/issues/10202 + if (lang == null || lang.toString == null || lang.toString() === 'null') { + console.error('invalid lang value detected!!!', typeof lang, lang); + lang = 'en-US'; + } + + const localRes = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`); + if (localRes.status === 200) { + localStorage.setItem('lang', lang); + localStorage.setItem('locale', await localRes.text()); + localStorage.setItem('localeVersion', langsVersion); + } else { + renderError('LOCALE_FETCH'); + return; + } + } + //#endregion + + //#region Script + async function importAppScript() { + await import(`/embed_vite/${CLIENT_ENTRY}`) + .catch(async e => { + console.error(e); + renderError('APP_IMPORT'); + }); + } + + // タイミングã«ã‚ˆã£ã¦ã¯ã€ã“ã®æ™‚点ã§DOMã®æ§‹ç¯‰ãŒæ¸ˆã‚“ã§ã„ã‚‹å ´åˆã¨ãã†ã§ãªã„å ´åˆã¨ãŒã‚ã‚‹ + if (document.readyState !== 'loading') { + importAppScript(); + } else { + window.addEventListener('DOMContentLoaded', () => { + importAppScript(); + }); + } + //#endregion + + async function addStyle(styleText) { + let css = document.createElement('style'); + css.appendChild(document.createTextNode(styleText)); + document.head.appendChild(css); + } + + async function renderError(code) { + // Cannot set property 'innerHTML' of null ã‚’å›žé¿ + if (document.readyState === 'loading') { + await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); + } + document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg> + <div class="message">èªã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ</div> + <div class="submessage">Failed to initialize Sharkey</div> + <div class="submessage">Error Code: ${code}</div> + <button onclick="location.reload(!0)"> + <div>リãƒãƒ¼ãƒ‰</div> + <div><small>Reload</small></div> + </button>`; + addStyle(` + #sharkey_app, + #splash { + display: none !important; + } + + html, + body { + margin: 0; + } + + body { + position: relative; + color: #dee7e4; + font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; + line-height: 1.35; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + margin: 0; + padding: 24px; + box-sizing: border-box; + overflow: hidden; + + border-radius: var(--radius, 12px); + border: 1px solid rgba(231, 255, 251, 0.14); + } + + body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #192320; + border-radius: var(--radius, 12px); + z-index: -1; + } + + html.embed.norounded body, + html.embed.norounded body::before { + border-radius: 0; + } + + html.embed.noborder body { + border: none; + } + + .icon { + max-width: 60px; + width: 100%; + height: auto; + margin-bottom: 20px; + color: #dec340; + } + + .message { + text-align: center; + font-size: 20px; + font-weight: 700; + margin-bottom: 20px; + } + + .submessage { + text-align: center; + font-size: 90%; + margin-bottom: 7.5px; + } + + .submessage:last-of-type { + margin-bottom: 20px; + } + + button { + padding: 7px 14px; + min-width: 100px; + font-weight: 700; + font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; + line-height: 1.35; + border-radius: 99rem; + background-color: #b4e900; + color: #192320; + border: none; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + } + + button:hover { + background-color: #c6ff03; + }`); + } +})(); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 38e37ce093f46978a32ad656adf388bffdd06722..ad92480c1c2b943cc5e63ba2628dd2e291762748 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -3,17 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -/** - * BOOT LOADER - * サーãƒãƒ¼ã‹ã‚‰ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã•ã‚Œã‚‹HTMLã«åŸ‹ã‚è¾¼ã¾ã‚Œã‚‹ã‚¹ã‚¯ãƒªãƒ—トã§ã€ä»¥ä¸‹ã®å½¹å‰²ã‚’æŒã¡ã¾ã™ã€‚ - * - 翻訳ファイルをフェッãƒã™ã‚‹ã€‚ - * - ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«åŸºã¥ã„ã¦é©åˆ‡ãªãƒ¡ã‚¤ãƒ³ã‚¹ã‚¯ãƒªãƒ—トをèªã¿è¾¼ã‚€ã€‚ - * - ã‚ャッシュã•ã‚ŒãŸã‚³ãƒ³ãƒ‘イル済ã¿ãƒ†ãƒ¼ãƒžã‚’é©ç”¨ã™ã‚‹ã€‚ - * - クライアントã®è¨å®šå€¤ã«åŸºã¥ã„ã¦å¯¾å¿œã™ã‚‹HTMLクラスç‰ã‚’è¨å®šã™ã‚‹ã€‚ - * テーマをã“ã®æ®µéšŽã§è¨å®šã™ã‚‹ã®ã¯ã€ãƒ¡ã‚¤ãƒ³ã‚¹ã‚¯ãƒªãƒ—トãŒèªã¿è¾¼ã¾ã‚Œã‚‹é–“もテーマをé©ç”¨ã—ãŸã„ãŸã‚ã§ã™ã€‚ - * 注: webpackã¯ä»‹ã•ãªã„ãŸã‚ã€ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã§ã¯requireã‚„importã¯ä½¿ãˆã¾ã›ã‚“。 - */ - 'use strict'; // ブãƒãƒƒã‚¯ã®ä¸ã«å…¥ã‚Œãªã„ã¨ã€å®šç¾©ã—ãŸå¤‰æ•°ãŒãƒ–ラウザã®ã‚°ãƒãƒ¼ãƒãƒ«ã‚¹ã‚³ãƒ¼ãƒ—ã«ç™»éŒ²ã•ã‚Œã¦ã—ã¾ã„邪é”ãªã®ã§ @@ -33,8 +22,17 @@ return; } + // Force update when locales change + const langsVersion = LANGS_VERSION; + const localeVersion = localStorage.getItem('localeVersion'); + if (localeVersion !== langsVersion) { + console.info(`Updating locales from version ${localeVersion ?? 'N/A'} to ${langsVersion}`); + localStorage.removeItem('localeVersion'); + localStorage.removeItem('locale'); + } + //#region Detect language & fetch translations - if (!localStorage.hasOwnProperty('locale')) { + if (!localStorage.getItem('locale')) { const supportedLangs = LANGS; let lang = localStorage.getItem('lang'); if (lang == null || !supportedLangs.includes(lang)) { @@ -48,37 +46,17 @@ } } - const metaRes = await window.fetch('/api/meta', { - method: 'POST', - body: JSON.stringify({}), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - }); - if (metaRes.status !== 200) { - renderError('META_FETCH'); - return; - } - const meta = await metaRes.json(); - const v = meta.version; - if (v == null) { - renderError('META_FETCH_V'); - return; - } - // for https://github.com/misskey-dev/misskey/issues/10202 if (lang == null || lang.toString == null || lang.toString() === 'null') { console.error('invalid lang value detected!!!', typeof lang, lang); lang = 'en-US'; } - const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`); + const localRes = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`); if (localRes.status === 200) { localStorage.setItem('lang', lang); localStorage.setItem('locale', await localRes.text()); - localStorage.setItem('localeVersion', v); + localStorage.setItem('localeVersion', langsVersion); } else { renderError('LOCALE_FETCH'); return; @@ -110,7 +88,7 @@ const themeFontFaceName = 'sharkey-theme-font-face'; if (theme) { let existingFontFace; - document.fonts.forEach((v,k,s)=>{if (v.family === themeFontFaceName) existingFontFace=v;}); + document.fonts.forEach((v) => { if (v.family === themeFontFaceName) existingFontFace = v;}); if (existingFontFace) document.fonts.delete(existingFontFace); const themeProps = JSON.parse(theme); @@ -124,8 +102,8 @@ document.fonts.add(fontFace); fontFace.load().catch( (failure) => { - console.log(failure) - } + console.log(failure); + }, ); } for (const [k, v] of Object.entries(themeProps)) { @@ -192,7 +170,7 @@ if (!errorsElement) { document.body.innerHTML = ` - <svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-alert-triangle" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> + <svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 9v2m0 4v.01"></path> <path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path> @@ -202,10 +180,10 @@ <span class="button-label-big">Reload / リãƒãƒ¼ãƒ‰</span> </button> <p><b>The following actions may solve the problem. / 以下を行ã†ã¨è§£æ±ºã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚</b></p> - <p>Clear the browser cache / ブラウザã®ã‚ャッシュをクリアã™ã‚‹</p> <p>Update your os and browser / ブラウザãŠã‚ˆã³OSを最新ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«æ›´æ–°ã™ã‚‹</p> <p>Disable an adblocker / アドブãƒãƒƒã‚«ãƒ¼ã‚’無効ã«ã™ã‚‹</p> - <p>(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledã‚’trueã«è¨å®šã™ã‚‹</p> + <p>Clear the browser cache / ブラウザã®ã‚ャッシュをクリアã™ã‚‹</p> + <p>(Tor Browser) Set dom.webaudio.enabled to true / dom.webaudio.enabledã‚’trueã«è¨å®šã™ã‚‹</p> <details style="color: #86b300;"> <summary>Other options / ãã®ä»–ã®ã‚ªãƒ—ション</summary> <a href="/flush"> @@ -238,7 +216,7 @@ <summary> <code>ERROR CODE: ${code}</code> </summary> - <code>${JSON.stringify(details)}</code>`; + <code>${details.toString()} ${JSON.stringify(details)}</code>`; errorsElement.appendChild(detailsElement); addStyle(` * { @@ -346,6 +324,6 @@ #errorInfo { width: 50%; } - }`) + }`); } })(); diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index b60a6da49a222954b585f40cd23b9fcc77786b34..1cd9cadecf7fca0aafac11a44b2d7f5d34b42bc8 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -47,6 +47,7 @@ html { transform: translateY(80px); color: var(--accent); } + #splashSpinner > .spinner { position: absolute; top: 0; diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css new file mode 100644 index 0000000000000000000000000000000000000000..a7b110d80a1098e0b2db61174bccd85fe4e1f6a6 --- /dev/null +++ b/packages/backend/src/server/web/style.embed.css @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +html { + background-color: var(--bg); + color: var(--fg); +} + +html.embed { + box-sizing: border-box; + background-color: transparent; + color-scheme: light dark; + max-width: 500px; +} + +#splash { + position: fixed; + z-index: 10000; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + cursor: wait; + background-color: var(--bg); + opacity: 1; + transition: opacity 0.5s ease; +} + +html.embed #splash { + box-sizing: border-box; + min-height: 300px; + border-radius: var(--radius, 12px); + border: 1px solid var(--divider, #e8e8e8); +} + +html.embed.norounded #splash { + border-radius: 0; +} + +html.embed.noborder #splash { + border: none; +} + +#splashIcon { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + width: 64px; + height: 64px; + pointer-events: none; +} + +#splashSpinner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + display: inline-block; + width: 28px; + height: 28px; + transform: translateY(70px); + color: var(--accent); +} + +#splashSpinner > .spinner { + position: absolute; + top: 0; + left: 0; + width: 28px; + height: 28px; + fill-rule: evenodd; + clip-rule: evenodd; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 1.5; +} +#splashSpinner > .spinner.bg { + opacity: 0.275; +} +#splashSpinner > .spinner.fg { + animation: splashSpinner 0.5s linear infinite; +} + +@keyframes splashSpinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug new file mode 100644 index 0000000000000000000000000000000000000000..6ef9281b8fb6c37f37ede3e9ee9ca0a57ee6dde8 --- /dev/null +++ b/packages/backend/src/server/web/views/base-embed.pug @@ -0,0 +1,74 @@ +block vars + +block loadClientEntry + - const entry = config.frontendEmbedEntry; + +doctype html + +html(class='embed') + + head + meta(charset='utf-8') + meta(name='application-name' content='Sharkey') + meta(name='referrer' content='origin') + meta(name='theme-color' content= themeColor || '#86b300') + meta(name='theme-color-orig' content= themeColor || '#86b300') + meta(property='og:site_name' content= instanceName || 'Sharkey') + meta(property='instance_url' content= instanceUrl) + meta(name='viewport' content='width=device-width, initial-scale=1') + meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no') + link(rel='icon' href= icon || '/favicon.ico') + link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') + link(rel='stylesheet' href=`/assets/phosphor-icons/bold/style.css?version=${version}`) + link(rel='stylesheet' href=`/static-assets/fonts/sharkey-icons/style.css?version=${version}`) + link(rel='modulepreload' href=`/embed_vite/${entry.file}`) + + if !config.frontendEmbedManifestExists + script(type="module" src="/embed_vite/@vite/client") + + if Array.isArray(entry.css) + each href in entry.css + link(rel='stylesheet' href=`/embed_vite/${href}`) + + title + block title + = title || 'Sharkey' + + block meta + meta(name='robots' content='noindex') + + style + include ../style.embed.css + + script. + var VERSION = "#{version}"; + var CLIENT_ENTRY = "#{entry.file}"; + + script(type='application/json' id='misskey_meta' data-generated-at=now) + != metaJson + + script(type='application/json' id='misskey_embedCtx' data-generated-at=now) + != embedCtx + + script + include ../boot.embed.js + + body + noscript: p + | JavaScriptを有効ã«ã—ã¦ãã ã•ã„ + br + | Please turn on your JavaScript + div#splash + img#splashIcon(src= icon || '/static-assets/splash.png') + div#splashSpinner + <svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1,0,0,1,12,12)"> + <circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/> + </g> + </svg> + <svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1,0,0,1,12,12)"> + <path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/> + </g> + </svg> + block content diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 36cec20c857c92be0399a1ac794ccfbf0c472a3f..84c48328020f1708490085431348b8af112ea95c 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -1,7 +1,7 @@ block vars block loadClientEntry - - const clientEntry = config.clientEntry; + - const entry = config.frontendEntry; doctype html @@ -40,16 +40,15 @@ html link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=notFoundImageUrl) - //- https://github.com/misskey-dev/misskey/issues/9842 link(rel='stylesheet' href=`/assets/phosphor-icons/bold/style.css?version=${version}`) link(rel='stylesheet' href=`/static-assets/fonts/sharkey-icons/style.css?version=${version}`) - link(rel='modulepreload' href=`/vite/${clientEntry.file}`) + link(rel='modulepreload' href=`/vite/${entry.file}`) - if !config.clientManifestExists + if !config.frontendManifestExists script(type="module" src="/vite/@vite/client") - if Array.isArray(clientEntry.css) - each href in clientEntry.css + if Array.isArray(entry.css) + each href in entry.css link(rel='stylesheet' href=`/vite/${href}`) title @@ -75,7 +74,7 @@ html script. var VERSION = "#{version}"; - var CLIENT_ENTRY = "#{clientEntry.file}"; + var CLIENT_ENTRY = "#{entry.file}"; script(type='application/json' id='misskey_meta' data-generated-at=now) != metaJson diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index d83d4140960beaa11e79b608ae959c7d90eec404..2aa4f279ea9f5e87f930a073b8ba9526db03205c 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -16,6 +16,7 @@ * followRequestAccepted - 自分ã®é€ã£ãŸãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒæ‰¿èªã•ã‚ŒãŸ * roleAssigned - ãƒãƒ¼ãƒ«ãŒä»˜ä¸Žã•ã‚ŒãŸ * achievementEarned - 実績をç²å¾— + * exportCompleted - エクスãƒãƒ¼ãƒˆãŒå®Œäº† * app - アプリ通知 * test - テスト通知(サーãƒãƒ¼å´ï¼‰ */ @@ -33,6 +34,7 @@ export const notificationTypes = [ 'followRequestAccepted', 'roleAssigned', 'achievementEarned', + 'exportCompleted', 'app', 'test', ] as const; @@ -52,10 +54,25 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const; export const followingVisibilities = ['public', 'followers', 'private'] as const; export const followersVisibilities = ['public', 'followers', 'private'] as const; +/** + * ユーザーãŒã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã§ãã‚‹ã‚‚ã®ã®ç¨®é¡ž + * + * (主ã«ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆå®Œäº†é€šçŸ¥ã§ä½¿ç”¨ã™ã‚‹ã‚‚ã®ã§ã‚ã‚Šã€æ—¢å˜ã®DBã®å称ç‰ã¨å¿…ãšã—も一致ã—ãªã„) + */ +export const userExportableEntities = ['antenna', 'blocking', 'clip', 'customEmoji', 'favorite', 'following', 'muting', 'note', 'userList'] as const; + +/** + * ユーザーãŒã‚¤ãƒ³ãƒãƒ¼ãƒˆã§ãã‚‹ã‚‚ã®ã®ç¨®é¡ž + * + * (主ã«ã‚¤ãƒ³ãƒãƒ¼ãƒˆå®Œäº†é€šçŸ¥ã§ä½¿ç”¨ã™ã‚‹ã‚‚ã®ã§ã‚ã‚Šã€æ—¢å˜ã®DBã®å称ç‰ã¨å¿…ãšã—も一致ã—ãªã„) + */ +export const userImportableEntities = ['antenna', 'blocking', 'customEmoji', 'following', 'muting', 'userList'] as const; + export const moderationLogTypes = [ 'updateServerSettings', 'suspend', 'approve', + 'decline', 'unsuspend', 'updateUserNote', 'addCustomEmoji', @@ -77,8 +94,12 @@ export const moderationLogTypes = [ 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'resetPassword', + 'setRemoteInstanceNSFW', + 'unsetRemoteInstanceNSFW', 'suspendRemoteInstance', 'unsuspendRemoteInstance', + 'rejectRemoteInstanceReports', + 'acceptRemoteInstanceReports', 'updateRemoteInstanceNote', 'markSensitiveDriveFile', 'unmarkSensitiveDriveFile', @@ -119,6 +140,11 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + decline: { + userId: string; + userUsername: string; + userHost: string | null; + }; unsuspend: { userId: string; userUsername: string; @@ -227,6 +253,14 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + setRemoteInstanceNSFW: { + id: string; + host: string; + }; + unsetRemoteInstanceNSFW: { + id: string; + host: string; + }; suspendRemoteInstance: { id: string; host: string; @@ -235,6 +269,14 @@ export type ModerationLogPayloads = { id: string; host: string; }; + rejectRemoteInstanceReports: { + id: string; + host: string; + }; + acceptRemoteInstanceReports: { + id: string; + host: string; + }; updateRemoteInstanceNote: { id: string; host: string; diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts index 866a7e1f5bc5da05e5172ef021fae5daa501788c..04bf62d2096d25a40bc240618b01469516d3cb08 100644 --- a/packages/backend/test-server/entry.ts +++ b/packages/backend/test-server/entry.ts @@ -6,12 +6,16 @@ import { MainModule } from '@/MainModule.js'; import { ServerService } from '@/server/ServerService.js'; import { loadConfig } from '@/config.js'; import { NestLogger } from '@/NestLogger.js'; +import { INestApplicationContext } from '@nestjs/common'; const config = loadConfig(); const originEnv = JSON.stringify(process.env); process.env.NODE_ENV = 'test'; +let app: INestApplicationContext; +let serverService: ServerService; + /** * テスト用ã®ã‚µãƒ¼ãƒã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’èµ·å‹•ã™ã‚‹ */ @@ -20,10 +24,10 @@ async function launch() { console.log('starting application...'); - const app = await NestFactory.createApplicationContext(MainModule, { + app = await NestFactory.createApplicationContext(MainModule, { logger: new NestLogger(), }); - const serverService = app.get(ServerService); + serverService = app.get(ServerService); await serverService.launch(); await startControllerEndpoints(); @@ -71,6 +75,20 @@ async function startControllerEndpoints(port = config.port + 1000) { fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => { process.env = JSON.parse(originEnv); + + await serverService.dispose(); + await app.close(); + + await killTestServer(); + + console.log('starting application...'); + + app = await NestFactory.createApplicationContext(MainModule, { + logger: new NestLogger(), + }); + serverService = app.get(ServerService); + await serverService.launch(); + res.code(200).send({ success: true }); }); diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 06548fa7da039738829e9f0e28b7454b0460c209..48da6ba27f2703aab3b21db53cc542362649dbcf 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -123,12 +123,14 @@ describe('2è¦ç´ èªè¨¼', () => { password: string, 'g-recaptcha-response'?: string | null, 'hcaptcha-response'?: string | null, + 'frc-captcha-solution'?: string | null, } => { return { username, password, 'g-recaptcha-response': null, 'hcaptcha-response': null, + 'frc-captcha-solution': null, }; }; diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 6ac14cd8dcdefe503f4d49eb75b977cd37fff2a8..881e88d40f63767f9cf18b79ccf912a3a611c6de 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -228,6 +228,17 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); + test('を作æˆã™ã‚‹æ™‚ã‚ーワードãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ã¨ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹', async () => { + await failedApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] }, + user: alice + }, { + status: 400, + code: 'EMPTY_KEYWORD', + id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a' + }); + }); //#endregion //#region æ›´æ–°(antennas/update) @@ -255,6 +266,18 @@ describe('アンテナ', () => { id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }); }); + test('を変更ã™ã‚‹æ™‚ã‚ーワードãŒæŒ‡å®šã•ã‚Œã¦ã„ãªã„ã¨ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹', async () => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + await failedApiCall({ + endpoint: 'antennas/update', + parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] }, + user: alice + }, { + status: 400, + code: 'EMPTY_KEYWORD', + id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4' + }); + }); //#endregion //#region 表示(antennas/show) diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 7efd688ec279186c5c276ca96a2dd16b01bc7b88..c8f5debbc9b83837713ffcab58e59ea3f9632723 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -156,7 +156,7 @@ describe('Webリソース', () => { describe(' has entry such ', () => { beforeEach(() => { - post(alice, { text: "**a**" }) + post(alice, { text: "**a**" }); }); test('MFMã‚’å«ã¾ãªã„。', async () => { @@ -169,7 +169,7 @@ describe('Webリソース', () => { throw new Error("MFM shouldn't be included"); } }); - }) + }); }); describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 5937eb9b4924a7961d0b545dc466a15c5079fc38..7d91b604268e3ae248e6eb98f3a10cecc5c81c15 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -9,10 +9,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { MiNote } from '@/models/Note.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; +// Important: this must match the value of maxNoteLength in .config/ci.yml! +const MAX_NOTE_TEXT_LENGTH = 3000; + describe('Note', () => { let Notes: Repository<MiNote>; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index eb56f7bebf35db3ee7270d064c0e2b464a2dc2d4..7d2e14f85d6f3b1f2cbcd1535524fb436607c91f 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -7,9 +7,9 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { inspect } from 'node:util'; -import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; describe('ユーザー', () => { // エンティティã¨ã—ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’主眼ã«ãŠã„ãŸãƒ†ã‚¹ãƒˆã‚’記述ã™ã‚‹ @@ -108,6 +108,7 @@ describe('ユーザー', () => { isRenoteMuted: user.isRenoteMuted ?? false, notify: user.notify ?? 'none', withReplies: user.withReplies ?? false, + followedMessage: user.isFollowing ? (user.followedMessage ?? null) : undefined, }); }; @@ -117,6 +118,7 @@ describe('ユーザー', () => { ...userDetailedNotMe(user), avatarId: user.avatarId, bannerId: user.bannerId, + followedMessage: user.followedMessage, isModerator: user.isModerator, isAdmin: user.isAdmin, injectFeaturedNote: user.injectFeaturedNote, @@ -139,6 +141,7 @@ describe('ユーザー', () => { hasUnreadNotification: user.hasUnreadNotification, unreadNotificationsCount: user.unreadNotificationsCount, hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, + hasPendingSentFollowRequest: user.hasPendingSentFollowRequest, unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, hardMutedWords: user.hardMutedWords, @@ -356,6 +359,7 @@ describe('ユーザー', () => { // MeDetailedOnly assert.strictEqual(response.avatarId, null); assert.strictEqual(response.bannerId, null); + assert.strictEqual(response.followedMessage, null); assert.strictEqual(response.isModerator, false); assert.strictEqual(response.isAdmin, false); assert.strictEqual(response.injectFeaturedNote, true); @@ -378,6 +382,7 @@ describe('ユーザー', () => { assert.strictEqual(response.hasUnreadNotification, false); assert.strictEqual(response.unreadNotificationsCount, 0); assert.strictEqual(response.hasPendingReceivedFollowRequest, false); + assert.strictEqual(response.hasPendingSentFollowRequest, false); assert.deepStrictEqual(response.unreadAnnouncements, []); assert.deepStrictEqual(response.mutedWords, []); assert.deepStrictEqual(response.mutedInstances, []); @@ -419,6 +424,8 @@ describe('ユーザー', () => { { parameters: () => ({ description: 'x'.repeat(1500) }) }, { parameters: () => ({ description: 'x' }) }, { parameters: () => ({ description: 'My description' }) }, + { parameters: () => ({ followedMessage: null }) }, + { parameters: () => ({ followedMessage: 'Thank you' }) }, { parameters: () => ({ location: null }) }, { parameters: () => ({ location: 'x'.repeat(50) }) }, { parameters: () => ({ location: 'x' }) }, diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts index 861bc6db669b73edaf196464671e2fd69443fe21..7c6dd6a55f2055be30dcf05c9337a26f53b814be 100644 --- a/packages/backend/test/jest.setup.ts +++ b/packages/backend/test/jest.setup.ts @@ -6,8 +6,6 @@ import { initTestDb, sendEnvResetRequest } from './utils.js'; beforeAll(async () => { - await Promise.all([ - initTestDb(false), - sendEnvResetRequest(), - ]); + await initTestDb(false); + await sendEnvResetRequest(); }); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 3c7e796700da32b0b6846941005f60a7d7ebdbe7..c8f3db8aac392a75bced6b5809afe8d5bb260c90 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -17,6 +17,7 @@ import type { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { FollowRequestsRepository, + MiMeta, NoteReactionsRepository, NotesRepository, PollsRepository, @@ -35,6 +36,7 @@ export class MockResolver extends Resolver { constructor(loggerService: LoggerService) { super( {} as Config, + {} as MiMeta, {} as UsersRepository, {} as NotesRepository, {} as PollsRepository, @@ -42,7 +44,6 @@ export class MockResolver extends Resolver { {} as FollowRequestsRepository, {} as UtilityService, {} as InstanceActorService, - {} as MetaService, {} as ApRequestService, {} as HttpRequestService, {} as ApRendererService, diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index cf54ebd909e74d260776608ac5a31b785a1838af..89583e73045167d1572d559a0e0eed90577810d9 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -35,7 +35,7 @@ describe('FileInfoService', () => { delete fi.porn; return fi; - } + }; beforeAll(async () => { app = await Test.createTestingModule({ diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index b6cbe4c520d1ff20924509943349442ddb4b97b7..ef80d25f812549e4cbc2155d620572348c2a10c7 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -13,6 +13,7 @@ import * as lolex from '@sinonjs/fake-timers'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import { + MiMeta, MiRole, MiRoleAssignment, MiUser, @@ -41,7 +42,7 @@ describe('RoleService', () => { let usersRepository: UsersRepository; let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; - let metaService: jest.Mocked<MetaService>; + let meta: jest.Mocked<MiMeta>; let notificationService: jest.Mocked<NotificationService>; let clock: lolex.InstalledClock; @@ -142,7 +143,7 @@ describe('RoleService', () => { rolesRepository = app.get<RolesRepository>(DI.rolesRepository); roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository); - metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>; + meta = app.get<MiMeta>(DI.meta) as jest.Mocked<MiMeta>; notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>; await roleService.onModuleInit(); @@ -164,11 +165,9 @@ describe('RoleService', () => { describe('getUserPolicies', () => { test('instance default policies', async () => { const user = await createUser(); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); @@ -177,11 +176,9 @@ describe('RoleService', () => { test('instance default policies 2', async () => { const user = await createUser(); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: true, - }, - } as any); + meta.policies = { + canManageCustomEmojis: true, + }; const result = await roleService.getUserPolicies(user.id); @@ -201,11 +198,9 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); @@ -236,11 +231,9 @@ describe('RoleService', () => { }); await roleService.assign(user.id, role1.id); await roleService.assign(user.id, role2.id); - metaService.fetch.mockResolvedValue({ - policies: { - driveCapacityMb: 50, - }, - } as any); + meta.policies = { + driveCapacityMb: 50, + }; const result = await roleService.getUserPolicies(user.id); @@ -260,11 +253,9 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24))); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); expect(result.canManageCustomEmojis).toBe(true); diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d73ba5af1c1833efb38599b72b45bb1e0f29e93 --- /dev/null +++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IncomingHttpHeaders } from 'node:http'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { FastifyReply, FastifyRequest } from 'fastify'; +import { AuthenticationResponseJSON } from '@simplewebauthn/types'; +import { HttpHeader } from 'fastify/types/utils.js'; +import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; +import { MiUser } from '@/models/User.js'; +import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { DI } from '@/di-symbols.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js'; +import { RateLimiterService } from '@/server/api/RateLimiterService.js'; +import { WebAuthnService } from '@/core/WebAuthnService.js'; +import { SigninService } from '@/server/api/SigninService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; + +const moduleMocker = new ModuleMocker(global); + +class FakeLimiter { + public async limit() { + return; + } +} + +class FakeSigninService { + public signin(..._args: any): any { + return true; + } +} + +class DummyFastifyReply { + public statusCode: number; + code(num: number): void { + this.statusCode = num; + } + header(_key: HttpHeader, _value: any): void { + } +} +class DummyFastifyRequest { + public ip: string; + public body: {credential: any, context: string}; + public headers: IncomingHttpHeaders = { 'accept': 'application/json' }; + constructor(body?: any) { + this.ip = '0.0.0.0'; + this.body = body; + } +} + +type ApiFastifyRequestType = FastifyRequest<{ + Body: { + credential?: AuthenticationResponseJSON; + context?: string; + }; +}>; + +describe('SigninWithPasskeyApiService', () => { + let app: TestingModule; + let passkeyApiService: SigninWithPasskeyApiService; + let usersRepository: UsersRepository; + let userProfilesRepository: UserProfilesRepository; + let webAuthnService: WebAuthnService; + let idService: IdService; + let FakeWebauthnVerify: ()=>Promise<string>; + + async function createUser(data: Partial<MiUser> = {}) { + const user = await usersRepository + .save({ + ...data, + }); + return user; + } + + async function createUserProfile(data: Partial<MiUserProfile> = {}) { + const userProfile = await userProfilesRepository + .save({ ...data }, + ); + return userProfile; + } + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + providers: [ + SigninWithPasskeyApiService, + { provide: RateLimiterService, useClass: FakeLimiter }, + { provide: SigninService, useClass: FakeSigninService }, + ], + }).useMocker((token) => { + if (typeof token === 'function') { + const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>; + const Mock = moduleMocker.generateFromMetadata(mockMetadata); + return new Mock(); + } + }).compile(); + passkeyApiService = app.get<SigninWithPasskeyApiService>(SigninWithPasskeyApiService); + usersRepository = app.get<UsersRepository>(DI.usersRepository); + userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository); + webAuthnService = app.get<WebAuthnService>(WebAuthnService); + idService = app.get<IdService>(IdService); + }); + + beforeEach(async () => { + const uid = idService.gen(); + FakeWebauthnVerify = async () => { + return uid; + }; + jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify); + + const dummyUser = { + id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null, + }; + const dummyProfile = { + userId: uid, + password: 'qwerty', + usePasswordLessLogin: true, + }; + await createUser(dummyUser); + await createUserProfile(dummyProfile); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('Get Passkey Options', () => { + it('Should return passkey Auth Options', async () => { + const req = new DummyFastifyRequest({}) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as unknown as FastifyReply; + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(200); + expect((res_body as any).option).toBeDefined(); + expect(typeof (res_body as any).context).toBe('string'); + }); + }); + describe('Try Passkey Auth', () => { + it('Should Success', async () => { + const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + const res_body = await passkeyApiService.signin(req, res); + expect((res_body as any).signinResponse).toBeDefined(); + }); + + it('Should return 400 Without Auth Context', async () => { + const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(400); + expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1'); + }); + + it('Should return 403 When Challenge Verify fail', async () => { + const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication') + .mockImplementation(async () => { + throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); + }); + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(403); + expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); + }); + + it('Should return 403 When The user not Enabled Passwordless login', async () => { + const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; + const res = new DummyFastifyReply() as FastifyReply; + const userId = await FakeWebauthnVerify(); + const data = { userId: userId, usePasswordLessLogin: false }; + await userProfilesRepository.update({ userId: userId }, data); + const res_body = await passkeyApiService.signin(req, res); + expect(res.statusCode).toBe(403); + expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912'); + }); + }); +}); diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts index 790cd1490eb5c050b5b74c23e7fdacc18e3d6b85..5401dd74d80b10648e9719287458eea23052a046 100644 --- a/packages/backend/test/unit/SystemWebhookService.ts +++ b/packages/backend/test/unit/SystemWebhookService.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only @@ -6,6 +7,7 @@ import { setTimeout } from 'node:timers/promises'; import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; import { Test, TestingModule } from '@nestjs/testing'; +import { randomString } from '../utils.js'; import { MiUser } from '@/models/User.js'; import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js'; @@ -17,7 +19,6 @@ import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; -import { randomString } from '../utils.js'; describe('SystemWebhookService', () => { let app: TestingModule; @@ -313,7 +314,7 @@ describe('SystemWebhookService', () => { isActive: true, on: ['abuseReport'], }); - await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' }); + await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any); expect(queueService.systemWebhookDeliver).toHaveBeenCalled(); }); @@ -323,7 +324,7 @@ describe('SystemWebhookService', () => { isActive: false, on: ['abuseReport'], }); - await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' }); + await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any); expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); }); @@ -337,8 +338,8 @@ describe('SystemWebhookService', () => { isActive: true, on: ['abuseReportResolved'], }); - await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' }); - await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' }); + await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any); + await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any); expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); }); diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e88835a0243f93144fa31e1688e5f33eb6d7800 --- /dev/null +++ b/packages/backend/test/unit/UserWebhookService.ts @@ -0,0 +1,245 @@ + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { randomString } from '../utils.js'; +import { MiUser } from '@/models/User.js'; +import { MiWebhook, UsersRepository, WebhooksRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DI } from '@/di-symbols.js'; +import { QueueService } from '@/core/QueueService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import { UserWebhookService } from '@/core/UserWebhookService.js'; + +describe('UserWebhookService', () => { + let app: TestingModule; + let service: UserWebhookService; + + // -------------------------------------------------------------------------------------- + + let usersRepository: UsersRepository; + let userWebhooksRepository: WebhooksRepository; + let idService: IdService; + let queueService: jest.Mocked<QueueService>; + + // -------------------------------------------------------------------------------------- + + let root: MiUser; + + // -------------------------------------------------------------------------------------- + + async function createUser(data: Partial<MiUser> = {}) { + return await usersRepository + .insert({ + id: idService.gen(), + ...data, + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + } + + async function createWebhook(data: Partial<MiWebhook> = {}) { + return userWebhooksRepository + .insert({ + id: idService.gen(), + name: randomString(), + on: ['mention'], + url: 'https://example.com', + secret: randomString(), + userId: root.id, + ...data, + }) + .then(x => userWebhooksRepository.findOneByOrFail(x.identifiers[0])); + } + + // -------------------------------------------------------------------------------------- + + async function beforeAllImpl() { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + UserWebhookService, + IdService, + LoggerService, + GlobalEventService, + { + provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }), + }, + ], + }) + .compile(); + + usersRepository = app.get(DI.usersRepository); + userWebhooksRepository = app.get(DI.webhooksRepository); + + service = app.get(UserWebhookService); + idService = app.get(IdService); + queueService = app.get(QueueService) as jest.Mocked<QueueService>; + + app.enableShutdownHooks(); + } + + async function afterAllImpl() { + await app.close(); + } + + async function beforeEachImpl() { + root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' }); + } + + async function afterEachImpl() { + await usersRepository.delete({}); + await userWebhooksRepository.delete({}); + } + + // -------------------------------------------------------------------------------------- + + describe('アプリを毎回作り直ã™å¿…è¦ã®ãªã„グループ', () => { + beforeAll(beforeAllImpl); + afterAll(afterAllImpl); + beforeEach(beforeEachImpl); + afterEach(afterEachImpl); + + describe('fetchSystemWebhooks', () => { + test('フィルタãªã—', async () => { + const webhook1 = await createWebhook({ + active: true, + on: ['mention'], + }); + const webhook2 = await createWebhook({ + active: false, + on: ['mention'], + }); + const webhook3 = await createWebhook({ + active: true, + on: ['reply'], + }); + const webhook4 = await createWebhook({ + active: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchWebhooks(); + expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]); + }); + + test('activeã®ã¿', async () => { + const webhook1 = await createWebhook({ + active: true, + on: ['mention'], + }); + const webhook2 = await createWebhook({ + active: false, + on: ['mention'], + }); + const webhook3 = await createWebhook({ + active: true, + on: ['reply'], + }); + const webhook4 = await createWebhook({ + active: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchWebhooks({ isActive: true }); + expect(fetchedWebhooks).toEqual([webhook1, webhook3]); + }); + + test('特定ã®ã‚¤ãƒ™ãƒ³ãƒˆã®ã¿', async () => { + const webhook1 = await createWebhook({ + active: true, + on: ['mention'], + }); + const webhook2 = await createWebhook({ + active: false, + on: ['mention'], + }); + const webhook3 = await createWebhook({ + active: true, + on: ['reply'], + }); + const webhook4 = await createWebhook({ + active: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'] }); + expect(fetchedWebhooks).toEqual([webhook1, webhook2]); + }); + + test('activeãªç‰¹å®šã®ã‚¤ãƒ™ãƒ³ãƒˆã®ã¿', async () => { + const webhook1 = await createWebhook({ + active: true, + on: ['mention'], + }); + const webhook2 = await createWebhook({ + active: false, + on: ['mention'], + }); + const webhook3 = await createWebhook({ + active: true, + on: ['reply'], + }); + const webhook4 = await createWebhook({ + active: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'], isActive: true }); + expect(fetchedWebhooks).toEqual([webhook1]); + }); + + test('ID指定', async () => { + const webhook1 = await createWebhook({ + active: true, + on: ['mention'], + }); + const webhook2 = await createWebhook({ + active: false, + on: ['mention'], + }); + const webhook3 = await createWebhook({ + active: true, + on: ['reply'], + }); + const webhook4 = await createWebhook({ + active: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id] }); + expect(fetchedWebhooks).toEqual([webhook1, webhook4]); + }); + + test('ID指定(ä»–æ¡ä»¶ã¨ANDã«ãªã‚‹ã‹è¦‹ãŸã„)', async () => { + const webhook1 = await createWebhook({ + active: true, + on: ['mention'], + }); + const webhook2 = await createWebhook({ + active: false, + on: ['mention'], + }); + const webhook3 = await createWebhook({ + active: true, + on: ['reply'], + }); + const webhook4 = await createWebhook({ + active: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false }); + expect(fetchedWebhooks).toEqual([webhook4]); + }); + }); + }); +}); diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e63b86f8fb3815e55b6e13973df2afe66aab819 --- /dev/null +++ b/packages/backend/test/unit/WebhookTestService.ts @@ -0,0 +1,225 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { beforeAll, describe, jest } from '@jest/globals'; +import { WebhookTestService } from '@/core/WebhookTestService.js'; +import { UserWebhookService } from '@/core/UserWebhookService.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { DI } from '@/di-symbols.js'; +import { QueueService } from '@/core/QueueService.js'; + +describe('WebhookTestService', () => { + let app: TestingModule; + let service: WebhookTestService; + + // -------------------------------------------------------------------------------------- + + let usersRepository: UsersRepository; + let userProfilesRepository: UserProfilesRepository; + let queueService: jest.Mocked<QueueService>; + let userWebhookService: jest.Mocked<UserWebhookService>; + let systemWebhookService: jest.Mocked<SystemWebhookService>; + let idService: IdService; + + let root: MiUser; + let alice: MiUser; + + async function createUser(data: Partial<MiUser> = {}) { + const user = await usersRepository + .insert({ + id: idService.gen(), + ...data, + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + + await userProfilesRepository.insert({ + userId: user.id, + }); + + return user; + } + + // -------------------------------------------------------------------------------------- + + beforeAll(async () => { + app = await Test.createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + WebhookTestService, + IdService, + { + provide: QueueService, useFactory: () => ({ + systemWebhookDeliver: jest.fn(), + userWebhookDeliver: jest.fn(), + }), + }, + { + provide: UserWebhookService, useFactory: () => ({ + fetchWebhooks: jest.fn(), + }), + }, + { + provide: SystemWebhookService, useFactory: () => ({ + fetchSystemWebhooks: jest.fn(), + }), + }, + ], + }).compile(); + + usersRepository = app.get(DI.usersRepository); + userProfilesRepository = app.get(DI.userProfilesRepository); + + service = app.get(WebhookTestService); + idService = app.get(IdService); + queueService = app.get(QueueService) as jest.Mocked<QueueService>; + userWebhookService = app.get(UserWebhookService) as jest.Mocked<UserWebhookService>; + systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>; + + app.enableShutdownHooks(); + }); + + beforeEach(async () => { + root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); + alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false }); + + userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([ + { id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook, + ])); + systemWebhookService.fetchSystemWebhooks.mockReturnValue(Promise.resolve([ + { id: 'dummy-webhook', isActive: true } as MiSystemWebhook, + ])); + }); + + afterEach(async () => { + queueService.systemWebhookDeliver.mockClear(); + queueService.userWebhookDeliver.mockClear(); + userWebhookService.fetchWebhooks.mockClear(); + systemWebhookService.fetchSystemWebhooks.mockClear(); + + await usersRepository.delete({}); + await userProfilesRepository.delete({}); + }); + + afterAll(async () => { + await app.close(); + }); + + // -------------------------------------------------------------------------------------- + + describe('testUserWebhook', () => { + test('note', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('note'); + expect((calls[2] as any).id).toBe('dummy-note-1'); + }); + + test('reply', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reply' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('reply'); + expect((calls[2] as any).id).toBe('dummy-reply-1'); + }); + + test('renote', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'renote' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('renote'); + expect((calls[2] as any).id).toBe('dummy-renote-1'); + }); + + test('mention', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'mention' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('mention'); + expect((calls[2] as any).id).toBe('dummy-mention-1'); + }); + + test('follow', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'follow' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('follow'); + expect((calls[2] as any).id).toBe('dummy-user-1'); + }); + + test('followed', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'followed' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('followed'); + expect((calls[2] as any).id).toBe('dummy-user-2'); + }); + + test('unfollow', async () => { + await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'unfollow' }, alice); + + const calls = queueService.userWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('unfollow'); + expect((calls[2] as any).id).toBe('dummy-user-3'); + }); + + describe('NoSuchWebhookError', () => { + test('user not match', async () => { + userWebhookService.fetchWebhooks.mockClear(); + userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([ + { id: 'dummy-webhook', active: true } as MiWebhook, + ])); + + await expect(service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, root)) + .rejects.toThrow(WebhookTestService.NoSuchWebhookError); + }); + }); + }); + + describe('testSystemWebhook', () => { + test('abuseReport', async () => { + await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReport' }); + + const calls = queueService.systemWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('abuseReport'); + expect((calls[2] as any).id).toBe('dummy-abuse-report1'); + expect((calls[2] as any).resolved).toBe(false); + }); + + test('abuseReportResolved', async () => { + await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReportResolved' }); + + const calls = queueService.systemWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('abuseReportResolved'); + expect((calls[2] as any).id).toBe('dummy-abuse-report1'); + expect((calls[2] as any).resolved).toBe(true); + }); + + test('userCreated', async () => { + await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'userCreated' }); + + const calls = queueService.systemWebhookDeliver.mock.calls[0]; + expect((calls[0] as any).id).toBe('dummy-webhook'); + expect(calls[1]).toBe('userCreated'); + expect((calls[2] as any).id).toBe('dummy-user-1'); + }); + }); +}); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 889a6ac53f6b4effac727931ca66aa10e6a79ea0..53ced3dab360e153efff6098b9d90dd771afec53 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -24,7 +24,6 @@ import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DownloadService } from '@/core/DownloadService.js'; -import { MetaService } from '@/core/MetaService.js'; import type { MiRemoteUser } from '@/models/User.js'; import { genAidx } from '@/misc/id/aidx.js'; import { MockResolver } from '../misc/mock-resolver.js'; @@ -107,7 +106,14 @@ describe('ActivityPub', () => { sensitiveWords: [] as string[], prohibitedWords: [] as string[], } as MiMeta; - let meta = metaInitial; + const meta = { ...metaInitial }; + + function updateMeta(newMeta: Partial<MiMeta>): void { + for (const key in meta) { + delete (meta as any)[key]; + } + Object.assign(meta, newMeta); + } beforeAll(async () => { const app = await Test.createTestingModule({ @@ -120,11 +126,8 @@ describe('ActivityPub', () => { }; }, }) - .overrideProvider(MetaService).useValue({ - async fetch(): Promise<MiMeta> { - return meta; - }, - }).compile(); + .overrideProvider(DI.meta).useFactory({ factory: () => meta }) + .compile(); await app.init(); app.enableShutdownHooks(); @@ -311,7 +314,7 @@ describe('ActivityPub', () => { // actor2Note is from a different server and needs to be fetched again assert.deepStrictEqual( resolver.remoteGetTrials(), - [actor1.id, `${actor1.id}/outbox`, actor1.featured, actor2Note.id, actor2.id, `${actor2.id}/outbox` ], + [actor1.id, `${actor1.id}/outbox`, actor1.featured, actor2Note.id, actor2.id, `${actor2.id}/outbox`], ); const note = await noteService.fetchNote(actor2Note.id); @@ -367,7 +370,7 @@ describe('ActivityPub', () => { }); test('cacheRemoteFiles=false disables caching', async () => { - meta = { ...metaInitial, cacheRemoteFiles: false }; + updateMeta({ ...metaInitial, cacheRemoteFiles: false }); const imageObject: IApDocument = { type: 'Document', @@ -396,7 +399,7 @@ describe('ActivityPub', () => { }); test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { - meta = { ...metaInitial, cacheRemoteSensitiveFiles: false }; + updateMeta({ ...metaInitial, cacheRemoteSensitiveFiles: false }); const imageObject: IApDocument = { type: 'Document', @@ -437,7 +440,7 @@ describe('ActivityPub', () => { }); }); - describe('JSON-LD', () =>{ + describe('JSON-LD', () => { test('Compaction', async () => { const jsonLd = jsonLdService.use(); diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts index ee16d421c4c9caebf82b1105c880e0254b5f7eaf..e4f42809f8ec486827740ac9f69ca9dc64cb7194 100644 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -4,10 +4,10 @@ */ import { Test, TestingModule } from '@nestjs/testing'; +import type { MiUser } from '@/models/User.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; -import type { MiUser } from '@/models/User.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { genAidx } from '@/misc/id/aidx.js'; import { @@ -49,6 +49,7 @@ import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { ReactionService } from '@/core/ReactionService.js'; import { NotificationService } from '@/core/NotificationService.js'; +import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; process.env.NODE_ENV = 'test'; @@ -169,6 +170,7 @@ describe('UserEntityService', () => { ApLoggerService, AccountMoveService, ReactionService, + ReactionsBufferingService, NotificationService, ]; diff --git a/packages/backend/test/unit/misc/from-tuple.ts b/packages/backend/test/unit/misc/from-tuple.ts new file mode 100644 index 0000000000000000000000000000000000000000..b523cb5782979a03e09d80f7d4205896617470ab --- /dev/null +++ b/packages/backend/test/unit/misc/from-tuple.ts @@ -0,0 +1,13 @@ +import { fromTuple } from '@/misc/from-tuple.js'; + +describe(fromTuple, () => { + it('should return value when value is not an array', () => { + const value = fromTuple('abc'); + expect(value).toBe('abc'); + }); + + it('should return first element when value is an array', () => { + const value = fromTuple(['abc']); + expect(value).toBe('abc'); + }); +}); diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts index 080271e404164b1a74b047ee8a573b6e9aa13b04..1baa995f592b747999624ef091908cf489eddd4d 100644 --- a/packages/backend/test/unit/misc/is-renote.ts +++ b/packages/backend/test/unit/misc/is-renote.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { isQuote, isRenote } from '@/misc/is-renote.js'; +import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js'; import { MiNote } from '@/models/Note.js'; const base: MiNote = { @@ -86,4 +86,24 @@ describe('misc:is-renote', () => { expect(isRenote(note)).toBe(true); expect(isQuote(note as any)).toBe(true); }); + + describe('isPureRenote', () => { + it('should return true when note is pure renote', () => { + const note = new MiNote({ renoteId: 'abc123', fileIds: [] }); + const result = isPureRenote(note); + expect(result).toBeTruthy(); + }); + + it('should return false when note is quote', () => { + const note = new MiNote({ renoteId: 'abc123', text: 'text', fileIds: [] }); + const result = isPureRenote(note); + expect(result).toBeFalsy(); + }); + + it('should return false when note is not renote', () => { + const note = new MiNote({ renoteId: null, fileIds: [] }); + const result = isPureRenote(note); + expect(result).toBeFalsy(); + }); + }); }); diff --git a/packages/backend/test/unit/misc/is-system-account.ts b/packages/backend/test/unit/misc/is-system-account.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0ab1526309d3b474518a8cf821aaf71da0bc6e6 --- /dev/null +++ b/packages/backend/test/unit/misc/is-system-account.ts @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { isSystemAccount } from '@/misc/is-system-account.js'; + +describe(isSystemAccount, () => { + it('should return true for instance.actor', () => { + expect(isSystemAccount({ username: 'instance.actor', host: null })).toBeTruthy(); + }); + + it('should return true for relay.actor', () => { + expect(isSystemAccount({ username: 'relay.actor', host: null })).toBeTruthy(); + }); + + it('should return true for any username with a dot', () => { + expect(isSystemAccount({ username: 'some.user', host: null })).toBeTruthy(); + expect(isSystemAccount({ username: 'some.', host: null })).toBeTruthy(); + expect(isSystemAccount({ username: '.user', host: null })).toBeTruthy(); + expect(isSystemAccount({ username: '.', host: null })).toBeTruthy(); + }); + + it('should return true for usernames with multiple dots', () => { + expect(isSystemAccount({ username: 'some.user.account', host: null })).toBeTruthy(); + expect(isSystemAccount({ username: '..', host: null })).toBeTruthy(); + }); + + it('should return false for usernames without a dot', () => { + expect(isSystemAccount({ username: 'instance_actor', host: null })).toBeFalsy(); + expect(isSystemAccount({ username: 'instanceactor', host: null })).toBeFalsy(); + expect(isSystemAccount({ username: 'relay_actor', host: null })).toBeFalsy(); + expect(isSystemAccount({ username: 'relayactor', host: null })).toBeFalsy(); + expect(isSystemAccount({ username: '', host: null })).toBeFalsy(); + }); + + it('should return false for users from another instance', () => { + expect(isSystemAccount({ username: 'instance.actor', host: 'example.com' })).toBeFalsy(); + expect(isSystemAccount({ username: 'relay.actor', host: 'example.com' })).toBeFalsy(); + expect(isSystemAccount({ username: 'some.user', host: 'example.com' })).toBeFalsy(); + expect(isSystemAccount({ username: 'some.', host: 'example.com' })).toBeFalsy(); + expect(isSystemAccount({ username: '.user', host: 'example.com' })).toBeFalsy(); + expect(isSystemAccount({ username: '.', host: 'example.com' })).toBeFalsy(); + expect(isSystemAccount({ username: 'some.user.account', host: 'example.com' })).toBeFalsy(); + expect(isSystemAccount({ username: '..', host: 'example.com' })).toBeFalsy(); + }); +}); diff --git a/packages/backend/test/unit/models/LatestNote.ts b/packages/backend/test/unit/models/LatestNote.ts new file mode 100644 index 0000000000000000000000000000000000000000..8785cfc8c5dd2170d09080ef7de504ee59cd9e3c --- /dev/null +++ b/packages/backend/test/unit/models/LatestNote.ts @@ -0,0 +1,149 @@ +import { SkLatestNote } from '@/models/LatestNote.js'; +import { MiNote } from '@/models/Note.js'; + +describe(SkLatestNote, () => { + describe('keyFor', () => { + it('should include userId', () => { + const note = new MiNote({ userId: 'abc123', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.userId).toBe(note.userId); + }); + + it('should include isPublic when is public', () => { + const note = new MiNote({ visibility: 'public', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isPublic).toBeTruthy(); + }); + + it('should include isPublic when is home-only', () => { + const note = new MiNote({ visibility: 'home', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isPublic).toBeFalsy(); + }); + + it('should include isPublic when is followers-only', () => { + const note = new MiNote({ visibility: 'followers', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isPublic).toBeFalsy(); + }); + + it('should include isPublic when is specified', () => { + const note = new MiNote({ visibility: 'specified', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isPublic).toBeFalsy(); + }); + + it('should include isReply when is reply', () => { + const note = new MiNote({ replyId: 'abc123', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isReply).toBeTruthy(); + }); + + it('should include isReply when is not reply', () => { + const note = new MiNote({ replyId: null, fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isReply).toBeFalsy(); + }); + + it('should include isQuote when is quote', () => { + const note = new MiNote({ renoteId: 'abc123', text: 'text', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isQuote).toBeTruthy(); + }); + + it('should include isQuote when is reblog', () => { + const note = new MiNote({ renoteId: 'abc123', fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isQuote).toBeFalsy(); + }); + + it('should include isQuote when is neither quote nor reblog', () => { + const note = new MiNote({ renoteId: null, fileIds: [] }); + const key = SkLatestNote.keyFor(note); + expect(key.isQuote).toBeFalsy(); + }); + }); + + describe('areEquivalent', () => { + it('should return true when keys match', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: [] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: [] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeTruthy(); + }); + + it('should return true when keys match with different reply IDs', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: '3', renoteId: null, fileIds: [] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'public', replyId: '4', renoteId: null, fileIds: [] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeTruthy(); + }); + + it('should return true when keys match with different renote IDs', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: null, renoteId: '3', fileIds: ['1'] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'public', replyId: null, renoteId: '4', fileIds: ['1'] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeTruthy(); + }); + + it('should return true when keys match with different file counts', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: ['1'] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: ['1', '2'] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeTruthy(); + }); + + it('should return true when keys match with different private visibilities', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'home', replyId: null, renoteId: null, fileIds: [] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'followers', replyId: null, renoteId: null, fileIds: [] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeTruthy(); + }); + + it('should return false when user ID differs', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: [] }); + const second = new MiNote({ id: '2', userId: 'def456', visibility: 'public', replyId: null, renoteId: null, fileIds: [] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeFalsy(); + }); + + it('should return false when visibility differs', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: [] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'home', replyId: null, renoteId: null, fileIds: [] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeFalsy(); + }); + + it('should return false when reply differs', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: '1', renoteId: null, fileIds: [] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: [] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeFalsy(); + }); + + it('should return false when quote differs', () => { + const first = new MiNote({ id: '1', userId: 'abc123', visibility: 'public', replyId: null, renoteId: '3', fileIds: ['1'] }); + const second = new MiNote({ id: '2', userId: 'abc123', visibility: 'public', replyId: null, renoteId: null, fileIds: [] }); + + const result = SkLatestNote.areEquivalent(first, second); + + expect(result).toBeFalsy(); + }); + }); +}); diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json index 2b15a5cc7a3fc8cd7ccda890116010cec8ad6d7d..392da169adfae8d9791b4b8545fdc812f6f4ba28 100644 --- a/packages/backend/tsconfig.json +++ b/packages/backend/tsconfig.json @@ -46,6 +46,7 @@ "./src/**/*.ts" ], "exclude": [ + "node_modules", "./src/**/*.test.ts" ] } diff --git a/packages/frontend-embed/.gitignore b/packages/frontend-embed/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1aa0ac14e8288adbba89868e9b9b1f231342679e --- /dev/null +++ b/packages/frontend-embed/.gitignore @@ -0,0 +1 @@ +/storybook-static diff --git a/packages/frontend-embed/@types/global.d.ts b/packages/frontend-embed/@types/global.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..15373cbd2dd35eeb7f41f066f1bd25e165f16e35 --- /dev/null +++ b/packages/frontend-embed/@types/global.d.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +type FIXME = any; + +declare const _LANGS_: string[][]; +declare const _LANGS_VERSION_: string; +declare const _VERSION_: string; +declare const _ENV_: string; +declare const _DEV_: boolean; +declare const _PERF_PREFIX_: string; +declare const _DATA_TRANSFER_DRIVE_FILE_: string; +declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; +declare const _DATA_TRANSFER_DECK_COLUMN_: string; + +// for dev-mode +declare const _LANGS_FULL_: string[][]; + +// TagCanvas +interface Window { + TagCanvas: any; +} diff --git a/packages/frontend-embed/@types/theme.d.ts b/packages/frontend-embed/@types/theme.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ac1037493e672d5f38e593fcfb5e51b0cd6ef69 --- /dev/null +++ b/packages/frontend-embed/@types/theme.d.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +declare module '@@/themes/*.json5' { + import { Theme } from '@/theme.js'; + + const theme: Theme; + + export default theme; +} diff --git a/packages/frontend-embed/assets/dummy.png b/packages/frontend-embed/assets/dummy.png new file mode 100644 index 0000000000000000000000000000000000000000..39332b0c1beeda1edb90d78d25c16e7372aff030 Binary files /dev/null and b/packages/frontend-embed/assets/dummy.png differ diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..e686da9cc3d2aa5d34469885decec1f0614dbd5a --- /dev/null +++ b/packages/frontend-embed/eslint.config.js @@ -0,0 +1,109 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import parser from 'vue-eslint-parser'; +import pluginVue from 'eslint-plugin-vue'; +import pluginMisskey from '@misskey-dev/eslint-plugin'; +import sharedConfig from '../shared/eslint.config.js'; +import localeRule from '../../eslint/locale.js'; +import { build as buildLocales } from '../../locales/index.js'; + +export default [ + ...sharedConfig, + { + files: ['{src,test,js,@types}/**/*.vue'], + ...pluginMisskey.configs.typescript, + }, + ...pluginVue.configs['flat/recommended'], + { + files: ['{src,test,js,@types}/**/*.{ts,vue}'], + plugins: { sharkey: { rules: { locale: localeRule } } }, + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), + ...globals.browser, + + // Node.js + module: false, + require: false, + __dirname: false, + + // Misskey + _DEV_: false, + _LANGS_: false, + _VERSION_: false, + _ENV_: false, + _PERF_PREFIX_: false, + _DATA_TRANSFER_DRIVE_FILE_: false, + _DATA_TRANSFER_DRIVE_FOLDER_: false, + _DATA_TRANSFER_DECK_COLUMN_: false, + }, + parser, + parserOptions: { + extraFileExtensions: ['.vue'], + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'sharkey/locale': ['error', buildLocales()['en-US']], + + '@typescript-eslint/no-empty-interface': ['error', { + allowSingleExtends: true, + }], + // window ã®ç¦æ¢ç†ç”±: ã‚°ãƒãƒ¼ãƒãƒ«ã‚¹ã‚³ãƒ¼ãƒ—ã¨è¡çªã—ã€äºˆæœŸã›ã¬çµæžœã‚’æ‹›ããŸã‚ + // e ã®ç¦æ¢ç†ç”±: error ã‚„ event ãªã©ã€è¤‡æ•°ã®ã‚ーワードã®é æ–‡å—ã§ã‚り分ã‹ã‚Šã«ãã„ãŸã‚ + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + alphabetical: false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + allowUsingIterationVar: false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + ignoreProperties: false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + attribute: 1, + baseIndent: 0, + closeBracket: 0, + alignAttributesVertically: true, + ignores: [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + startTag: 'never', + endTag: 'never', + selfClosingTag: 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-reactivity-loss': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/v-on-event-hyphenation': ['error', 'never', { + autofix: true, + }], + 'vue/attribute-hyphenation': ['error', 'never'], + }, + }, + { + ignores: [ + "**/lib/", + "**/temp/", + "**/built/", + "**/coverage/", + "**/node_modules/", + ] + }, +]; diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json new file mode 100644 index 0000000000000000000000000000000000000000..021a63068a12c40739196f70dbe7b4c1d81046f1 --- /dev/null +++ b/packages/frontend-embed/package.json @@ -0,0 +1,72 @@ +{ + "name": "frontend-embed", + "private": true, + "type": "module", + "scripts": { + "watch": "vite", + "dev": "vite --config vite.config.local-dev.ts --debug hmr", + "build": "vite build", + "typecheck": "vue-tsc --noEmit", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", + "lint": "pnpm typecheck && pnpm eslint" + }, + "dependencies": { + "@discordapp/twemoji": "15.1.0", + "@phosphor-icons/web": "^2.0.3", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-replace": "5.0.7", + "@rollup/pluginutils": "5.1.2", + "@transfem-org/sfm-js": "0.24.5", + "@twemoji/parser": "15.1.1", + "@vitejs/plugin-vue": "5.1.4", + "@vue/compiler-sfc": "3.5.10", + "astring": "1.9.0", + "buraha": "0.0.1", + "estree-walker": "3.0.3", + "misskey-js": "workspace:*", + "frontend-shared": "workspace:*", + "punycode": "2.3.1", + "rollup": "4.22.5", + "sass": "1.79.3", + "shiki": "1.12.0", + "tinycolor2": "1.6.0", + "tsc-alias": "1.8.10", + "tsconfig-paths": "4.2.0", + "typescript": "5.6.2", + "uuid": "10.0.0", + "json5": "2.2.3", + "vite": "5.4.8", + "vue": "3.5.10" + }, + "devDependencies": { + "@misskey-dev/summaly": "5.1.0", + "@testing-library/vue": "8.1.0", + "@types/estree": "1.0.6", + "@types/micromatch": "4.0.9", + "@types/node": "20.14.12", + "@types/punycode": "2.1.4", + "@types/tinycolor2": "1.4.6", + "@types/uuid": "10.0.0", + "@types/ws": "8.5.12", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", + "@vitest/coverage-v8": "1.6.0", + "@vue/runtime-core": "3.5.10", + "acorn": "8.12.1", + "cross-env": "7.0.3", + "eslint-plugin-import": "2.30.0", + "eslint-plugin-vue": "9.28.0", + "fast-glob": "3.3.2", + "happy-dom": "10.0.3", + "intersection-observer": "0.12.2", + "micromatch": "4.0.8", + "msw": "2.3.4", + "nodemon": "3.1.7", + "prettier": "3.3.3", + "start-server-and-test": "2.0.8", + "vite-plugin-turbosnap": "1.0.3", + "vue-component-type-helpers": "2.1.6", + "vue-eslint-parser": "9.4.3", + "vue-tsc": "2.1.6" + } +} diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a16efe4ab7eabfe7195abded5a0641844b48b50 --- /dev/null +++ b/packages/frontend-embed/src/boot.ts @@ -0,0 +1,139 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// https://vitejs.dev/config/build-options.html#build-modulepreload +import 'vite/modulepreload-polyfill'; + +import '@/style.scss'; +import { createApp, defineAsyncComponent } from 'vue'; +import defaultLightTheme from '@@/themes/l-light.json5'; +import defaultDarkTheme from '@@/themes/d-dark.json5'; +import { MediaProxy } from '@@/js/media-proxy.js'; +import { applyTheme, assertIsTheme } from '@/theme.js'; +import { fetchCustomEmojis } from '@/custom-emojis.js'; +import { DI } from '@/di.js'; +import { serverMetadata } from '@/server-metadata.js'; +import { url } from '@@/js/config.js'; +import { parseEmbedParams } from '@@/js/embed-page.js'; +import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; +import { serverContext } from '@/server-context.js'; + +import type { Theme } from '@/theme.js'; + +console.log('Sharkey Embed'); + +//#region Embedパラメータã®å–得・パース +const params = new URLSearchParams(location.search); +const embedParams = parseEmbedParams(params); +if (_DEV_) console.log(embedParams); +//#endregion + +//#region テーマ +function parseThemeOrNull(theme: string | null): Theme | null { + if (theme == null) return null; + try { + const parsed = JSON.parse(theme); + if (assertIsTheme(parsed)) { + return parsed; + } else { + return null; + } + } catch (err) { + return null; + } +} + +const lightTheme = parseThemeOrNull(serverMetadata.defaultLightTheme) ?? defaultLightTheme; +const darkTheme = parseThemeOrNull(serverMetadata.defaultDarkTheme) ?? defaultDarkTheme; + +if (embedParams.colorMode === 'dark') { + applyTheme(darkTheme); +} else if (embedParams.colorMode === 'light') { + applyTheme(lightTheme); +} else { + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + applyTheme(darkTheme); + } else { + applyTheme(lightTheme); + } + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { + if (mql.matches) { + applyTheme(darkTheme); + } else { + applyTheme(lightTheme); + } + }); +} +//#endregion + +// サイズã®åˆ¶é™ +document.documentElement.style.maxWidth = '500px'; + +// iframeIdã®è¨å®š +function setIframeIdHandler(event: MessageEvent) { + if (event.data?.type === 'misskey:embedParent:registerIframeId' && event.data.payload?.iframeId != null) { + setIframeId(event.data.payload.iframeId); + window.removeEventListener('message', setIframeIdHandler); + } +} + +window.addEventListener('message', setIframeIdHandler); + +try { + await fetchCustomEmojis(); +} catch (err) { /* empty */ } + +const app = createApp( + defineAsyncComponent(() => import('@/ui.vue')), +); + +app.provide(DI.mediaProxy, new MediaProxy(serverMetadata, url)); + +app.provide(DI.serverMetadata, serverMetadata); + +app.provide(DI.serverContext, serverContext); + +app.provide(DI.embedParams, embedParams); + +// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 +// ãªãœã‹2回実行ã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹ãŸã‚ã€mountã™ã‚‹divã‚’1ã¤ã«åˆ¶é™ã™ã‚‹ +const rootEl = ((): HTMLElement => { + const MISSKEY_MOUNT_DIV_ID = 'sharkey_app'; + + const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); + + if (currentRoot) { + console.warn('multiple import detected'); + return currentRoot; + } + + const root = document.createElement('div'); + root.id = MISSKEY_MOUNT_DIV_ID; + document.body.appendChild(root); + return root; +})(); + +postMessageToParentWindow('misskey:embed:ready'); + +app.mount(rootEl); + +// boot.jsã®ã‚„ã¤ã‚’解除 +window.onerror = null; +window.onunhandledrejection = null; + +removeSplash(); + +function removeSplash() { + const splash = document.getElementById('splash'); + if (splash) { + splash.style.opacity = '0'; + splash.style.pointerEvents = 'none'; + + // transitionendイベントãŒç™ºç«ã—ãªã„å ´åˆãŒã‚ã‚‹ãŸã‚ + window.setTimeout(() => { + splash.remove(); + }, 1000); + } +} diff --git a/packages/frontend-embed/src/components/EmA.vue b/packages/frontend-embed/src/components/EmA.vue new file mode 100644 index 0000000000000000000000000000000000000000..1c236b9a3592e32e189366dec590aa72ab9ecb4f --- /dev/null +++ b/packages/frontend-embed/src/components/EmA.vue @@ -0,0 +1,21 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<a :href="to" target="_blank" rel="noopener"> + <slot></slot> +</a> +</template> + +<script lang="ts" setup> +import { } from 'vue'; + +const props = withDefaults(defineProps<{ + to: string; + activeClass?: null | string; +}>(), { + activeClass: null, +}); +</script> diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue new file mode 100644 index 0000000000000000000000000000000000000000..6856b8272e86dd6693e0b528b5452395767f022c --- /dev/null +++ b/packages/frontend-embed/src/components/EmAcct.vue @@ -0,0 +1,24 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<span> + <span>@{{ user.username }}</span> + <span v-if="user.host || detail" style="opacity: 0.5;">@{{ user.host || host }}</span> +</span> +</template> + +<script lang="ts" setup> +import * as Misskey from 'misskey-js'; +import { toUnicode } from 'punycode/'; +import { host as hostRaw } from '@@/js/config.js'; + +defineProps<{ + user: Misskey.entities.UserLite; + detail?: boolean; +}>(); + +const host = toUnicode(hostRaw); +</script> diff --git a/packages/frontend-embed/src/components/EmAvatar.vue b/packages/frontend-embed/src/components/EmAvatar.vue new file mode 100644 index 0000000000000000000000000000000000000000..58c35c8ef0cd96b18fe3ac53aef8defecd46dad3 --- /dev/null +++ b/packages/frontend-embed/src/components/EmAvatar.vue @@ -0,0 +1,250 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<component :is="link ? EmA : 'span'" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat }]"> + <EmImgWithBlurhash :class="$style.inner" :src="url" :hash="user.avatarBlurhash" :cover="true" :onlyAvgColor="true"/> + <div v-if="user.isCat" :class="[$style.ears]"> + <div :class="$style.earLeft"> + <div v-if="false" :class="$style.layer"> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + </div> + </div> + <div :class="$style.earRight"> + <div v-if="false" :class="$style.layer"> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + <div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/> + </div> + </div> + </div> + <img + v-for="decoration in user.avatarDecorations" + :class="[$style.decoration]" + :src="getDecorationUrl(decoration)" + :style="{ + rotate: getDecorationAngle(decoration), + scale: getDecorationScale(decoration), + translate: getDecorationOffset(decoration), + }" + alt="" + > +</component> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; +import EmImgWithBlurhash from './EmImgWithBlurhash.vue'; +import EmA from './EmA.vue'; +import { userPage } from '@/utils.js'; + +const props = withDefaults(defineProps<{ + user: Misskey.entities.User; + link?: boolean; + preview?: boolean; + indicator?: boolean; +}>(), { + link: false, + preview: false, + indicator: false, +}); + +const emit = defineEmits<{ + (ev: 'click', v: MouseEvent): void; +}>(); + +const bound = computed(() => props.link + ? { to: userPage(props.user) } + : {}); + +const url = computed(() => { + if (props.user.avatarUrl == null) return null; + return props.user.avatarUrl; +}); + +function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + return decoration.url; +} + +function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + const angle = decoration.angle ?? 0; + return angle === 0 ? undefined : `${angle * 360}deg`; +} + +function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + const scaleX = decoration.flipH ? -1 : 1; + return scaleX === 1 ? undefined : `${scaleX} 1`; +} + +function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + const offsetX = decoration.offsetX ?? 0; + const offsetY = decoration.offsetY ?? 0; + return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`; +} +</script> + +<style lang="scss" module> +.root { + position: relative; + display: inline-block; + vertical-align: bottom; + flex-shrink: 0; + border-radius: 100%; + line-height: 16px; +} + +.inner { + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: 0; + border-radius: 100%; + z-index: 1; + overflow: clip; + object-fit: cover; + width: 100%; + height: 100%; +} + +.indicator { + position: absolute; + z-index: 2; + bottom: 0; + left: 0; + width: 20%; + height: 20%; +} + +.cat { + > .ears { + contain: strict; + position: absolute; + top: -50%; + left: -50%; + width: 100%; + height: 100%; + padding: 50%; + pointer-events: none; + + > .earLeft, + > .earRight { + contain: strict; + display: inline-block; + height: 50%; + width: 50%; + background: currentColor; + + &::after { + contain: strict; + content: ''; + display: block; + width: 60%; + height: 60%; + margin: 20%; + background: #df548f; + } + + > .layer { + contain: strict; + position: absolute; + top: 0; + width: 280%; + height: 280%; + + > .plot { + contain: strict; + position: absolute; + width: 100%; + height: 100%; + clip-path: path('M0 0H1V1H0z'); + transform: scale(32767); + transform-origin: 0 0; + opacity: 0.5; + + &:first-child { + opacity: 1; + } + + &:last-child { + opacity: calc(1 / 3); + } + } + } + } + + > .earLeft { + transform: rotate(37.5deg) skew(30deg); + + &, &::after { + border-radius: 25% 75% 75%; + } + + > .layer { + left: 0; + transform: + skew(-30deg) + rotate(-37.5deg) + translate(-2.82842712475%, /* -2 * sqrt(2) */ + -38.5857864376%); /* 40 - 2 * sqrt(2) */ + + > .plot { + background-position: 20% 10%; /* ~= 37.5deg */ + + &:first-child { + background-position-x: 21%; + } + + &:last-child { + background-position-y: 11%; + } + } + } + } + + > .earRight { + transform: rotate(-37.5deg) skew(-30deg); + + &, &::after { + border-radius: 75% 25% 75% 75%; + } + + > .layer { + right: 0; + transform: + skew(30deg) + rotate(37.5deg) + translate(2.82842712475%, /* 2 * sqrt(2) */ + -38.5857864376%); /* 40 - 2 * sqrt(2) */ + + > .plot { + position: absolute; + background-position: 80% 10%; /* ~= 37.5deg */ + + &:first-child { + background-position-x: 79%; + } + + &:last-child { + background-position-y: 11%; + } + } + } + } + } +} + +.decoration { + position: absolute; + z-index: 1; + top: -50%; + left: -50%; + width: 200%; + pointer-events: none; +} +</style> diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue new file mode 100644 index 0000000000000000000000000000000000000000..e4149cf363f33082203633eafb9d143b40603980 --- /dev/null +++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue @@ -0,0 +1,101 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<img + v-if="errored && fallbackToImage" + :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" + src="/client-assets/dummy.png" + :title="alt" +/> +<span v-else-if="errored">:{{ customEmojiName }}:</span> +<img + v-else + :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" + :src="url" + :alt="alt" + :title="alt" + decoding="async" + @error="errored = true" + @load="errored = false" +/> +</template> + +<script lang="ts" setup> +import { computed, inject, ref } from 'vue'; +import { customEmojisMap } from '@/custom-emojis.js'; + +import { DI } from '@/di.js'; + +const mediaProxy = inject(DI.mediaProxy)!; + +const props = defineProps<{ + name: string; + normal?: boolean; + noStyle?: boolean; + host?: string | null; + url?: string; + useOriginalSize?: boolean; + menu?: boolean; + menuReaction?: boolean; + fallbackToImage?: boolean; +}>(); + +const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', '')); +const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@'))); + +const rawUrl = computed(() => { + if (props.url) { + return props.url; + } + if (isLocal.value) { + return customEmojisMap.get(customEmojiName.value)?.url ?? null; + } + return props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`; +}); + +const url = computed(() => { + if (rawUrl.value == null) return undefined; + + const proxied = + (rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value)) + ? rawUrl.value + : mediaProxy.getProxiedImageUrl( + rawUrl.value, + props.useOriginalSize ? undefined : 'emoji', + false, + true, + ); + return proxied; +}); + +const alt = computed(() => `:${customEmojiName.value}:`); +const errored = ref(url.value == null); +</script> + +<style lang="scss" module> +.root { + height: 2em; + vertical-align: middle; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.2); + } +} + +.normal { + height: 1.25em; + vertical-align: -0.25em; + + &:hover { + transform: none; + } +} + +.noStyle { + height: auto !important; +} +</style> diff --git a/packages/frontend-embed/src/components/EmEmoji.vue b/packages/frontend-embed/src/components/EmEmoji.vue new file mode 100644 index 0000000000000000000000000000000000000000..224979707b971c0a5e84c87a7619944946f9c358 --- /dev/null +++ b/packages/frontend-embed/src/components/EmEmoji.vue @@ -0,0 +1,26 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<img :class="$style.root" :src="url" :alt="props.emoji" decoding="async"/> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import { char2twemojiFilePath } from '@@/js/emoji-base.js'; + +const props = defineProps<{ + emoji: string; +}>(); + +const url = computed(() => char2twemojiFilePath(props.emoji)); +</script> + +<style lang="scss" module> +.root { + height: 1.25em; + vertical-align: -0.25em; +} +</style> diff --git a/packages/frontend-embed/src/components/EmError.vue b/packages/frontend-embed/src/components/EmError.vue new file mode 100644 index 0000000000000000000000000000000000000000..d376b29a7f85cc3926cc02db18ee603cde187181 --- /dev/null +++ b/packages/frontend-embed/src/components/EmError.vue @@ -0,0 +1,43 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p> + <button class="_buttonGray _buttonRounded" :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</button> +</div> +</template> + +<script lang="ts" setup> +import { i18n } from '@/i18n.js'; + +const emit = defineEmits<{ + (ev: 'retry'): void; +}>(); +</script> + +<style lang="scss" module> +.root { + padding: 32px; + text-align: center; + align-items: center; +} + +.text { + margin: 0 0 8px 0; +} + +.button { + margin: 0 auto; +} + +.img { + vertical-align: bottom; + width: 128px; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; +} +</style> diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue new file mode 100644 index 0000000000000000000000000000000000000000..bf976c71ae21260e38643760212ea5e1390d34a2 --- /dev/null +++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue @@ -0,0 +1,240 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''"> + <canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/> + <img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/> +</div> +</template> + +<script lang="ts"> +import DrawBlurhash from '@/workers/draw-blurhash?worker'; +import TestWebGL2 from '@/workers/test-webgl2?worker'; +import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js'; +import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; + +const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => { + // テスト環境㧠Web Worker インスタンスã¯ä½œæˆã§ããªã„ + if (import.meta.env.MODE === 'test') { + const canvas = document.createElement('canvas'); + canvas.width = 64; + canvas.height = 64; + resolve(canvas); + return; + } + const testWorker = new TestWebGL2(); + testWorker.addEventListener('message', event => { + if (event.data.result) { + const workers = new WorkerMultiDispatch( + () => new DrawBlurhash(), + Math.min(navigator.hardwareConcurrency - 1, 4), + ); + resolve(workers); + if (_DEV_) console.log('WebGL2 in worker is supported!'); + } else { + const canvas = document.createElement('canvas'); + canvas.width = 64; + canvas.height = 64; + resolve(canvas); + if (_DEV_) console.log('WebGL2 in worker is not supported...'); + } + testWorker.terminate(); + }); +}); +</script> + +<script lang="ts" setup> +import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue'; +import { v4 as uuid } from 'uuid'; +import { render } from 'buraha'; + +const props = withDefaults(defineProps<{ + src?: string | null; + hash?: string | null; + alt?: string | null; + title?: string | null; + height?: number; + width?: number; + cover?: boolean; + forceBlurhash?: boolean; + onlyAvgColor?: boolean; // 軽é‡åŒ–ã®ãŸã‚ã«Blurhashを使ã‚ãšã«å¹³å‡è‰²ã ã‘ã‚’æç”» +}>(), { + src: null, + alt: '', + title: null, + height: 64, + width: 64, + cover: true, + forceBlurhash: false, + onlyAvgColor: false, +}); + +const viewId = uuid(); +const canvas = shallowRef<HTMLCanvasElement>(); +const root = shallowRef<HTMLDivElement>(); +const img = shallowRef<HTMLImageElement>(); +const loaded = ref(false); +const canvasWidth = ref(64); +const canvasHeight = ref(64); +const imgWidth = ref(props.width); +const imgHeight = ref(props.height); +const bitmapTmp = ref<CanvasImageSource | undefined>(); +const hide = computed(() => !loaded.value || props.forceBlurhash); + +function waitForDecode() { + if (props.src != null && props.src !== '') { + nextTick() + .then(() => img.value?.decode()) + .then(() => { + loaded.value = true; + }, error => { + console.log('Error occurred during decoding image', img.value, error); + }); + } else { + loaded.value = false; + } +} + +watch([() => props.width, () => props.height, root], () => { + const ratio = props.width / props.height; + if (ratio > 1) { + canvasWidth.value = Math.round(64 * ratio); + canvasHeight.value = 64; + } else { + canvasWidth.value = 64; + canvasHeight.value = Math.round(64 / ratio); + } + + const clientWidth = root.value?.clientWidth ?? 300; + imgWidth.value = clientWidth; + imgHeight.value = Math.round(clientWidth / ratio); +}, { + immediate: true, +}); + +function drawImage(bitmap: CanvasImageSource) { + // canvasãŒãªã„(mountedã•ã‚Œã¦ã„ãªã„ï¼‰å ´åˆã¯Tmpã«ä¿å˜ã—ã¦ãŠã + if (!canvas.value) { + bitmapTmp.value = bitmap; + return; + } + + // canvasãŒã‚ã‚Œã°æç”»ã™ã‚‹ + bitmapTmp.value = undefined; + const ctx = canvas.value.getContext('2d'); + if (!ctx) return; + ctx.drawImage(bitmap, 0, 0, canvasWidth.value, canvasHeight.value); +} + +function drawAvg() { + if (!canvas.value) return; + + const color = (props.hash != null && extractAvgColorFromBlurhash(props.hash)) || '#888'; + + const ctx = canvas.value.getContext('2d'); + if (!ctx) return; + + // avgColorã§ãŠèŒ¶ã‚’ã«ã”ã™ + ctx.beginPath(); + ctx.fillStyle = color; + ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value); +} + +async function draw() { + if (import.meta.env.MODE === 'test' && props.hash == null) return; + + drawAvg(); + + if (props.hash == null) return; + + if (props.onlyAvgColor) return; + + const work = await canvasPromise; + if (work instanceof WorkerMultiDispatch) { + work.postMessage( + { + id: viewId, + hash: props.hash, + }, + undefined, + ); + } else { + try { + render(props.hash, work); + drawImage(work); + } catch (error) { + console.error('Error occurred during drawing blurhash', error); + } + } +} + +function workerOnMessage(event: MessageEvent) { + if (event.data.id !== viewId) return; + drawImage(event.data.bitmap as ImageBitmap); +} + +canvasPromise.then(work => { + if (work instanceof WorkerMultiDispatch) { + work.addListener(workerOnMessage); + } + + draw(); +}); + +watch(() => props.src, () => { + waitForDecode(); +}); + +watch(() => props.hash, () => { + draw(); +}); + +onMounted(() => { + // drawImageãŒmountedより先ã«å‘¼ã°ã‚Œã¦ã„ã‚‹å ´åˆã¯ã“ã“ã§æç”»ã™ã‚‹ + if (bitmapTmp.value) { + drawImage(bitmapTmp.value); + } + waitForDecode(); +}); + +onUnmounted(() => { + canvasPromise.then(work => { + if (work instanceof WorkerMultiDispatch) { + work.removeListener(workerOnMessage); + } + }); +}); +</script> + +<style lang="scss" module> +.root { + position: relative; + width: 100%; + height: 100%; + + &.cover { + > .canvas, + > .img { + object-fit: cover; + } + } +} + +.canvas, +.img { + display: block; + width: 100%; + height: 100%; +} + +.canvas { + object-fit: contain; +} + +.img { + object-fit: contain; +} +</style> diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue new file mode 100644 index 0000000000000000000000000000000000000000..4a116e317acab3a1785f81a44496bc99dddd1172 --- /dev/null +++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue @@ -0,0 +1,87 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root" :style="bg"> + <img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/> + <div :class="$style.name">{{ instance.name }}</div> +</div> +</template> + +<script lang="ts" setup> +import { computed, inject } from 'vue'; + +import { DI } from '@/di.js'; + +const serverMetadata = inject(DI.serverMetadata)!; +const mediaProxy = inject(DI.mediaProxy)!; + +const props = defineProps<{ + instance?: { + faviconUrl?: string | null + name?: string | null + themeColor?: string | null + } +}>(); + +// if no instance data is given, this is for the local instance +const instance = props.instance ?? { + name: serverMetadata.name, + themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content, +}; + +const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico'); + +const themeColor = props.instance?.themeColor ?? serverMetadata.themeColor ?? '#777777'; + +const bg = { + background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`, +}; +</script> + +<style lang="scss" module> +$height: 2ex; + +.root { + display: flex; + align-items: center; + height: $height; + border-radius: 4px 0 0 4px; + overflow: clip; + color: #fff; + text-shadow: /* .866 ≈ sin(60deg) */ + 1px 0 1px #000, + .866px .5px 1px #000, + .5px .866px 1px #000, + 0 1px 1px #000, + -.5px .866px 1px #000, + -.866px .5px 1px #000, + -1px 0 1px #000, + -.866px -.5px 1px #000, + -.5px -.866px 1px #000, + 0 -1px 1px #000, + .5px -.866px 1px #000, + .866px -.5px 1px #000; + mask-image: linear-gradient(90deg, + rgb(0,0,0), + rgb(0,0,0) calc(100% - 16px), + rgba(0,0,0,0) 100% + ); +} + +.icon { + height: $height; + flex-shrink: 0; +} + +.name { + margin-left: 4px; + line-height: 1; + font-size: 0.9em; + font-weight: bold; + white-space: nowrap; + overflow: visible; +} +</style> diff --git a/packages/frontend-embed/src/components/EmLink.vue b/packages/frontend-embed/src/components/EmLink.vue new file mode 100644 index 0000000000000000000000000000000000000000..aec9b330720aac1a837eb8d4fe5627e65f8a9efa --- /dev/null +++ b/packages/frontend-embed/src/components/EmLink.vue @@ -0,0 +1,40 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<component + :is="self ? EmA : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target" + :title="url" +> + <slot></slot> + <i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i> +</component> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import EmA from './EmA.vue'; +import { url as local } from '@@/js/config.js'; + +const props = withDefaults(defineProps<{ + url: string; + rel?: null | string; +}>(), { +}); + +const self = props.url.startsWith(local); +const attr = self ? 'to' : 'href'; +const target = self ? null : '_blank'; + +const el = ref<HTMLElement | { $el: HTMLElement }>(); + +</script> + +<style lang="scss" module> +.icon { + padding-left: 2px; + font-size: .9em; +} +</style> diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue new file mode 100644 index 0000000000000000000000000000000000000000..49d8ace37bee32d60c09783c03bfd738c12bbc03 --- /dev/null +++ b/packages/frontend-embed/src/components/EmLoading.vue @@ -0,0 +1,112 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[$style.root, { [$style.inline]: inline, [$style.colored]: colored, [$style.mini]: mini, [$style.em]: em }]"> + <div :class="$style.container"> + <svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1.125,0,0,1.125,12,12)"> + <circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/> + </g> + </svg> + <svg :class="[$style.spinner, $style.fg, { [$style.static]: static }]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg"> + <g transform="matrix(1.125,0,0,1.125,12,12)"> + <path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/> + </g> + </svg> + </div> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; + +const props = withDefaults(defineProps<{ + static?: boolean; + inline?: boolean; + colored?: boolean; + mini?: boolean; + em?: boolean; +}>(), { + static: false, + inline: false, + colored: true, + mini: false, + em: false, +}); +</script> + +<style lang="scss" module> +@keyframes spinner { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.root { + padding: 32px; + text-align: center; + cursor: wait; + + --size: 38px; + + &.colored { + color: var(--accent); + } + + &.inline { + display: inline; + padding: 0; + --size: 32px; + } + + &.mini { + padding: 16px; + --size: 32px; + } + + &.em { + display: inline-block; + vertical-align: middle; + padding: 0; + --size: 1em; + } +} + +.container { + position: relative; + width: var(--size); + height: var(--size); + margin: 0 auto; +} + +.spinner { + position: absolute; + top: 0; + left: 0; + width: var(--size); + height: var(--size); + fill-rule: evenodd; + clip-rule: evenodd; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 1.5; +} + +.bg { + opacity: 0.275; +} + +.fg { + animation: spinner 0.5s linear infinite; + + &.static { + animation-play-state: paused; + } +} +</style> diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue new file mode 100644 index 0000000000000000000000000000000000000000..435da238a4e689ae7ebf51fceee3869013472957 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaBanner.vue @@ -0,0 +1,55 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<a :href="href" target="_blank" :class="$style.root"> + <div :class="$style.label"> + <template v-if="media.type.startsWith('audio')"><i class="ti ti-music"></i> {{ i18n.ts.audio }}</template> + <template v-else><i class="ti ti-file"></i> {{ i18n.ts.file }}</template> + </div> + <div :class="$style.go"> + <i class="ti ti-chevron-right"></i> + </div> +</a> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { i18n } from '@/i18n.js'; + +defineProps<{ + media: Misskey.entities.DriveFile; + href: string; +}>(); +</script> + +<style lang="scss" module> +.root { + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + padding: var(--margin); + margin-top: 4px; + border: 1px solid var(--inputBorder); + border-radius: var(--radius); + background-color: var(--panel); + transition: background-color .1s, border-color .1s; + + &:hover { + text-decoration: none; + border-color: var(--inputBorderHover); + background-color: var(--buttonHoverBg); + } +} + +.label { + font-size: .9em; +} + +.go { + margin-left: auto; +} +</style> diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue new file mode 100644 index 0000000000000000000000000000000000000000..076386e87675af4a4cf6364aa99ff5eb693098f5 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaImage.vue @@ -0,0 +1,162 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[hide ? $style.hidden : $style.visible]" @click="onclick"> + <a + :title="image.comment || image.name" + :class="$style.imageContainer" + :href="href ?? image.url" + target="_blank" + rel="noopener" + > + <ImgWithBlurhash + :hash="image.blurhash" + :src="hide ? null : url" + :forceBlurhash="hide" + :cover="hide || cover" + :alt="image.comment || image.name" + :title="image.comment || image.name" + :width="image.properties.width" + :height="image.properties.height" + :style="hide ? 'filter: brightness(0.7);' : null" + /> + </a> + <template v-if="hide"> + <div :class="$style.hiddenText"> + <div :class="$style.hiddenTextWrapper"> + <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</b> + <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ i18n.ts.image }}</b> + <span style="display: block;">{{ i18n.ts.clickToShow }}</span> + </div> + </div> + </template> + <div :class="$style.indicators"> + <div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div> + <div v-if="image.comment" :class="$style.indicator">ALT</div> + <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> + </div> + <i v-if="!hide" class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i> +</div> +</template> + +<script lang="ts" setup> +import { ref, computed } from 'vue'; +import * as Misskey from 'misskey-js'; +import ImgWithBlurhash from '@/components/EmImgWithBlurhash.vue'; +import { i18n } from '@/i18n.js'; + +const props = withDefaults(defineProps<{ + image: Misskey.entities.DriveFile; + href?: string; + raw?: boolean; + cover?: boolean; +}>(), { + cover: false, +}); + +const hide = ref(props.image.isSensitive); + +const url = computed(() => (props.raw) + ? props.image.url + : props.image.thumbnailUrl, +); + +async function onclick(ev: MouseEvent) { + if (hide.value) { + ev.stopPropagation(); + hide.value = false; + } +} +</script> + +<style lang="scss" module> +.hidden { + position: relative; +} + +.hiddenText { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 1; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.hide { + display: block; + position: absolute; + border-radius: 6px; + background-color: var(--fg); + color: var(--accentLighten); + font-size: 12px; + opacity: .5; + padding: 5px 8px; + text-align: center; + cursor: pointer; + top: 12px; + right: 12px; +} + +.hiddenTextWrapper { + display: table-cell; + text-align: center; + font-size: 0.8em; + color: #fff; +} + +.visible { + position: relative; + //box-shadow: 0 0 0 1px var(--divider) inset; + background: var(--bg); + background-size: 16px 16px; +} + +html[data-color-scheme=dark] .visible { + --c: rgb(255 255 255 / 2%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); +} + +html[data-color-scheme=light] .visible { + --c: rgb(0 0 0 / 2%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); +} + +.imageContainer { + display: block; + overflow: hidden; + width: 100%; + height: 100%; + background-position: center; + background-size: contain; + background-repeat: no-repeat; +} + +.indicators { + display: inline-flex; + position: absolute; + top: 10px; + left: 10px; + pointer-events: none; + opacity: .5; + gap: 6px; +} + +.indicator { + /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */ + background-color: black; + border-radius: 6px; + color: var(--accentLighten); + display: inline-block; + font-weight: bold; + font-size: 0.8em; + padding: 2px 5px; +} +</style> diff --git a/packages/frontend-embed/src/components/EmMediaList.vue b/packages/frontend-embed/src/components/EmMediaList.vue new file mode 100644 index 0000000000000000000000000000000000000000..0b2d835abe9ea2885ef623e2bb0f895ee32d183e --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaList.vue @@ -0,0 +1,146 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <div v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :class="$style.banner"> + <XBanner :media="media" :href="originalEntityUrl"/> + </div> + <div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container"> + <div + :class="[ + $style.medias, + count === 1 ? [$style.n1] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany, + ]" + > + <div v-for="media in mediaList.filter(media => previewable(media))" :class="$style.media"> + <XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :class="$style.mediaInner" :video="media" :href="originalEntityUrl"/> + <XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.mediaInner" class="image" :image="media" :raw="raw" :href="originalEntityUrl"/> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; +import XBanner from './EmMediaBanner.vue'; +import XImage from './EmMediaImage.vue'; +import XVideo from './EmMediaVideo.vue'; +import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js'; + +const props = defineProps<{ + mediaList: Misskey.entities.DriveFile[]; + raw?: boolean; + + /** 埋ã‚è¾¼ã¿ãƒšãƒ¼ã‚¸ç”¨ 親è¦ç´ ã®æ£è¦URL */ + originalEntityUrl: string; +}>(); + +const count = computed(() => props.mediaList.filter(media => previewable(media)).length); + +const previewable = (file: Misskey.entities.DriveFile): boolean => { + if (file.type === 'image/svg+xml') return true; // svgã®webpublic/thumbnailã¯pngãªã®ã§true + // FILE_TYPE_BROWSERSAFEã«é©åˆã—ãªã„ã‚‚ã®ã¯ãƒ–ラウザã§è¡¨ç¤ºã™ã‚‹ã®ã«ä¸é©åˆ‡ + return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); +}; +</script> + +<style lang="scss" module> +.container { + position: relative; + width: 100%; + margin-top: 4px; +} + +.medias { + display: grid; + grid-gap: 8px; + + height: 100%; + width: 100%; + + &.n1 { + grid-template-rows: 1fr; + + // default but fallback (expand) + min-height: 64px; + max-height: clamp( + 64px, + 50cqh, + min(360px, 50vh) + ); + + &.n116_9 { + min-height: initial; + max-height: initial; + aspect-ratio: 16 / 9; // fallback + } + + &.n11_1{ + min-height: initial; + max-height: initial; + aspect-ratio: 1 / 1; // fallback + } + + &.n12_3 { + min-height: initial; + max-height: initial; + aspect-ratio: 2 / 3; // fallback + } + } + + &.n2 { + aspect-ratio: 16/9; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + } + + &.n3 { + aspect-ratio: 16/9; + grid-template-columns: 1fr 0.5fr; + grid-template-rows: 1fr 1fr; + + > .media:nth-child(1) { + grid-row: 1 / 3; + } + + > .media:nth-child(3) { + grid-column: 2 / 3; + grid-row: 2 / 3; + } + } + + &.n4 { + aspect-ratio: 16/9; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + } + + &.nMany { + grid-template-columns: 1fr 1fr; + + > .media { + aspect-ratio: 16/9; + } + } +} + +.media { + overflow: hidden; // clipã«ã™ã‚‹ã¨ãƒã‚°ã‚‹ + border-radius: 8px; + position: relative; + + >.mediaInner { + width: 100%; + height: 100%; + } +} + +.banner { + position: relative; +} +</style> diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue new file mode 100644 index 0000000000000000000000000000000000000000..ce751f9acdf83fd49c5d61a4ffcd97ff46acba25 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaVideo.vue @@ -0,0 +1,64 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<a :href="href" target="_blank" :class="$style.root"> + <img v-if="!video.isSensitive && video.thumbnailUrl" :class="$style.thumbnail" :src="video.thumbnailUrl"> + <div :class="$style.videoOverlayPlayButton"><i class="ti ti-player-play-filled"></i></div> +</a> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; + +defineProps<{ + video: Misskey.entities.DriveFile; + href: string; +}>(); +</script> + +<style lang="scss" module> +.root { + position: relative; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: auto; + aspect-ratio: 16 / 9; + padding: var(--margin); + border: 1px solid var(--divider); + border-radius: var(--radius); + background-color: #000; + + &:hover { + text-decoration: none; + } +} + +.thumbnail { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.videoOverlayPlayButton { + background: var(--accent); + color: #fff; + padding: 1rem; + border-radius: 99rem; + + font-size: 1rem; + line-height: 1rem; + + &:focus-visible { + outline: none; + } +} +</style> diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue new file mode 100644 index 0000000000000000000000000000000000000000..a631783507a9440753a83477f47662995239bb1a --- /dev/null +++ b/packages/frontend-embed/src/components/EmMention.vue @@ -0,0 +1,46 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkA :class="[$style.root]" :to="url" :style="{ background: bgCss }"> + <span> + <span>@{{ username }}</span> + <span v-if="(host != localHost)" :class="$style.host">@{{ toUnicode(host) }}</span> + </span> +</MkA> +</template> + +<script lang="ts" setup> +import { toUnicode } from 'punycode'; +import { } from 'vue'; +import tinycolor from 'tinycolor2'; +import { host as localHost } from '@@/js/config.js'; + +const props = defineProps<{ + username: string; + host: string; +}>(); + +const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`; + +const url = `/${canonical}`; + +const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--mention')); +bg.setAlpha(0.1); +const bgCss = bg.toRgbString(); +</script> + +<style lang="scss" module> +.root { + display: inline-block; + padding: 4px 8px 4px 4px; + border-radius: 999px; + color: var(--mention); +} + +.host { + opacity: 0.5; +} +</style> diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts new file mode 100644 index 0000000000000000000000000000000000000000..40a09df939fc6c19b8b62cf5433ef01669c2e42b --- /dev/null +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -0,0 +1,492 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { VNode, h, SetupContext, provide } from 'vue'; +import * as mfm from '@transfem-org/sfm-js'; +import * as Misskey from 'misskey-js'; +import EmUrl from '@/components/EmUrl.vue'; +import EmTime from '@/components/EmTime.vue'; +import EmLink from '@/components/EmLink.vue'; +import EmMention from '@/components/EmMention.vue'; +import EmEmoji from '@/components/EmEmoji.vue'; +import EmCustomEmoji from '@/components/EmCustomEmoji.vue'; +import EmA from '@/components/EmA.vue'; +import { host } from '@@/js/config.js'; + +function safeParseFloat(str: unknown): number | null { + if (typeof str !== 'string' || str === '') return null; + const num = parseFloat(str); + if (isNaN(num)) return null; + return num; +} + +const QUOTE_STYLE = ` +display: block; +margin: 8px; +padding: 6px 0 6px 12px; +color: var(--fg); +border-left: solid 3px var(--fg); +opacity: 0.7; +`.split('\n').join(' '); + +type MfmProps = { + text: string; + plain?: boolean; + nowrap?: boolean; + author?: Misskey.entities.UserLite; + isNote?: boolean; + emojiUrls?: Record<string, string>; + rootScale?: number; + nyaize?: boolean | 'respect'; + parsedNodes?: mfm.MfmNode[] | null; + enableEmojiMenu?: boolean; + enableEmojiMenuReaction?: boolean; + linkNavigationBehavior?: string; +}; + +type MfmEvents = { + clickEv(id: string): void; +}; + +// eslint-disable-next-line import/no-default-export +export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) { + provide('linkNavigationBehavior', props.linkNavigationBehavior); + + const isNote = props.isNote ?? true; + const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.text == null || props.text === '') return; + + const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text); + + const validTime = (t: string | boolean | null | undefined) => { + if (t == null) return null; + if (typeof t === 'boolean') return null; + return t.match(/^\-?[0-9.]+s$/) ? t : null; + }; + + const validColor = (c: unknown): string | null => { + if (typeof c !== 'string') return null; + return c.match(/^[0-9a-f]{3,6}$/i) ? c : null; + }; + + const useAnim = true; + + const isBlock = props.isBlock ?? false; + + /** + * Gen Vue Elements from MFM AST + * @param ast MFM AST + * @param scale How times large the text is + * @param disableNyaize Whether nyaize is disabled or not + */ + const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => { + switch (token.type) { + case 'text': { + let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + + if (!props.plain) { + const res: (VNode | string)[] = []; + for (const t of text.split('\n')) { + res.push(h('br')); + res.push(t); + } + res.shift(); + return res; + } else { + return [text.replace(/\n/g, ' ')]; + } + } + + case 'bold': { + return [h('b', genEl(token.children, scale))]; + } + + case 'strike': { + return [h('del', genEl(token.children, scale))]; + } + + case 'italic': { + return h('i', { + style: 'font-style: oblique;', + }, genEl(token.children, scale)); + } + + case 'fn': { + // TODO: CSSã‚’æ–‡å—列ã§çµ„ã¿ç«‹ã¦ã¦ã„ã㨠token.props.args.~~~ 経由ã§CSSインジェクションã§ãã‚‹ã®ã§ã‚ˆã—ãªã«ã‚„ã‚‹ + let style: string | undefined; + switch (token.props.name) { + case 'tada': { + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : ''); + break; + } + case 'jelly': { + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : ''); + break; + } + case 'twitch': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : ''; + break; + } + case 'shake': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : ''; + break; + } + case 'spin': { + const direction = + token.props.args.left ? 'reverse' : + token.props.args.alternate ? 'alternate' : + 'normal'; + const anime = + token.props.args.x ? 'mfm-spinX' : + token.props.args.y ? 'mfm-spinY' : + 'mfm-spin'; + const speed = validTime(token.props.args.speed) ?? '1.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : ''; + break; + } + case 'jump': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : ''; + break; + } + case 'bounce': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : ''; + break; + } + case 'flip': { + const transform = + (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : + token.props.args.v ? 'scaleY(-1)' : + 'scaleX(-1)'; + style = `transform: ${transform};`; + break; + } + case 'x2': { + return h('span', { + class: 'mfm-x2', + }, genEl(token.children, scale * 2)); + } + case 'x3': { + return h('span', { + class: 'mfm-x3', + }, genEl(token.children, scale * 3)); + } + case 'x4': { + return h('span', { + class: 'mfm-x4', + }, genEl(token.children, scale * 4)); + } + case 'font': { + const family = + token.props.args.serif ? 'serif' : + token.props.args.monospace ? 'monospace' : + token.props.args.cursive ? 'cursive' : + token.props.args.fantasy ? 'fantasy' : + token.props.args.emoji ? 'emoji' : + token.props.args.math ? 'math' : + null; + if (family) style = `font-family: ${family};`; + break; + } + case 'blur': { + return h('span', { + class: '_mfm_blur_', + }, genEl(token.children, scale)); + } + case 'rainbow': { + if (!useAnim) { + return h('span', { + class: '_mfm_rainbow_fallback_', + }, genEl(token.children, scale)); + } + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`; + break; + } + case 'sparkle': { + return genEl(token.children, scale); + } + case 'fade': { + const direction = token.props.args.out + ? 'alternate-reverse' + : 'alternate'; + const speed = validTime(token.props.args.speed) ?? '1.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + const loop = safeParseFloat(token.props.args.loop) ?? 'infinite'; + style = `animation: mfm-fade ${speed} ${delay} linear ${loop}; animation-direction: ${direction};`; + break; + } + case 'rotate': { + const degrees = safeParseFloat(token.props.args.deg) ?? 90; + style = `transform: rotate(${degrees}deg); transform-origin: center center;`; + break; + } + case 'followmouse': { + return genEl(token.children, scale); + } + case 'position': { + const x = safeParseFloat(token.props.args.x) ?? 0; + const y = safeParseFloat(token.props.args.y) ?? 0; + style = `transform: translateX(${x}em) translateY(${y}em);`; + break; + } + case 'crop': { + const top = Number.parseFloat( + (token.props.args.top ?? '0').toString(), + ); + const right = Number.parseFloat( + (token.props.args.right ?? '0').toString(), + ); + const bottom = Number.parseFloat( + (token.props.args.bottom ?? '0').toString(), + ); + const left = Number.parseFloat( + (token.props.args.left ?? '0').toString(), + ); + style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`; + break; + } + case 'scale': { + const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5); + const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5); + style = `transform: scale(${x}, ${y});`; + scale = scale * Math.max(x, y); + break; + } + case 'fg': { + let color = validColor(token.props.args.color); + color = color ?? 'f00'; + style = `color: #${color}; overflow-wrap: anywhere;`; + break; + } + case 'bg': { + let color = validColor(token.props.args.color); + color = color ?? 'f00'; + style = `background-color: #${color}; overflow-wrap: anywhere;`; + break; + } + case 'border': { + let color = validColor(token.props.args.color); + color = color ? `#${color}` : 'var(--accent)'; + let b_style = token.props.args.style; + if ( + typeof b_style !== 'string' || + !['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] + .includes(b_style) + ) b_style = 'solid'; + const width = safeParseFloat(token.props.args.width) ?? 1; + const radius = safeParseFloat(token.props.args.radius) ?? 0; + style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`; + break; + } + case 'ruby': { + if (token.children.length === 1) { + const child = token.children[0]; + let text = child.type === 'text' ? child.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); + } else { + const rt = token.children.at(-1)!; + let text = rt.type === 'text' ? rt.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); + } + } + case 'unixtime': { + const child = token.children[0]; + const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); + return h('span', { + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;', + }, [ + h('i', { + class: 'ti ti-clock', + style: 'margin-right: 0.25em;', + }), + h(EmTime, { + key: Math.random(), + time: unixtime * 1000, + mode: 'detail', + }), + ]); + } + case 'clickable': { + return h('span', { onClick(ev: MouseEvent): void { + ev.stopPropagation(); + ev.preventDefault(); + const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : ''; + emit('clickEv', clickEv); + } }, genEl(token.children, scale)); + } + } + if (style === undefined) { + return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); + } else { + return h('span', { + style: 'display: inline-block; ' + style, + }, genEl(token.children, scale)); + } + } + + case 'small': { + return [h('small', { + style: 'opacity: 0.7;', + }, genEl(token.children, scale))]; + } + + case 'center': { + return [h('div', { + style: 'text-align:center;', + }, h('bdi', genEl(token.children, scale)))]; + } + + case 'url': { + return [h('bdi', h(EmUrl, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + }))]; + } + + case 'link': { + return [h('bdi', h(EmLink, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + }, genEl(token.children, scale, true)))]; + } + + case 'mention': { + return [h('bdi', h(EmMention, { + key: Math.random(), + host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, + username: token.props.username, + }))]; + } + + case 'hashtag': { + return [h('bdi', h(EmA, { + key: Math.random(), + to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, + style: 'color:var(--hashtag);', + }, `#${token.props.hashtag}`))]; + } + + case 'blockCode': { + return [h('bdi', { class: 'block' }, h('code', { + key: Math.random(), + lang: token.props.lang ?? undefined, + }, token.props.code))]; + } + + case 'inlineCode': { + return [h('bdi', h('code', { + key: Math.random(), + }, token.props.code))]; + } + + case 'quote': { + if (!props.nowrap) { + return [h('bdi', { class: 'block' }, h('div', { + style: QUOTE_STYLE, + }, h('bdi', genEl(token.children, scale, true))))]; + } else { + return [h('span', { + style: QUOTE_STYLE, + }, h('bdi', genEl(token.children, scale, true)))]; + } + } + + case 'emojiCode': { + if (props.author?.host == null) { + return [h(EmCustomEmoji, { + key: Math.random(), + name: token.props.name, + normal: props.plain, + host: null, + useOriginalSize: scale >= 2.5, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, + fallbackToImage: false, + })]; + } else { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) { + return [h('span', `:${token.props.name}:`)]; + } else { + return [h(EmCustomEmoji, { + key: Math.random(), + name: token.props.name, + url: props.emojiUrls && props.emojiUrls[token.props.name], + normal: props.plain, + host: props.author.host, + useOriginalSize: scale >= 2.5, + })]; + } + } + } + + case 'unicodeEmoji': { + return [h(EmEmoji, { + key: Math.random(), + emoji: token.props.emoji, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, + })]; + } + + case 'mathInline': { + return [h('bdi', h('code', token.props.formula))]; + } + + case 'mathBlock': { + return [h('bdi', h('code', token.props.formula))]; + } + + case 'search': { + return [h('bdi', h('div', { + key: Math.random(), + }, token.props.query))]; + } + + case 'plain': { + return [h('bdi', h('span', genEl(token.children, scale, true)))]; + } + + default: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + console.error('unrecognized ast type:', (token as any).type); + + return []; + } + } + }).flat(Infinity) as (VNode | string)[]; + + return h('bdi', { ...( isBlock ? { class: 'block' } : {}) }, h('span', { + // https://codeday.me/jp/qa/20190424/690106.html + style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', + }, genEl(rootAst, props.rootScale ?? 1))); +} diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue new file mode 100644 index 0000000000000000000000000000000000000000..4677284747b18e9c80c37220828c105fe940f759 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNote.vue @@ -0,0 +1,612 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + v-show="!isDeleted" + ref="rootEl" + :class="[$style.root]" + :tabindex="isDeleted ? '-1' : '0'" +> + <EmNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/> + <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div> + <!--<div v-if="appearNote._prId_" class="tip"><i class="ti ti-speakerphone"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>--> + <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>--> + <div v-if="isRenote" :class="$style.renote"> + <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> + <EmAvatar :class="$style.renoteAvatar" :user="note.user" link/> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> + <I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText"> + <template #user> + <EmA :class="$style.renoteUserName" :to="userPage(note.user)"> + <EmUserName :user="note.user"/> + </EmA> + </template> + </I18n> + <div :class="$style.renoteInfo"> + <button ref="renoteTime" :class="$style.renoteTime" class="_button"> + <i class="ti ti-dots" :class="$style.renoteMenu"></i> + <EmTime :time="note.createdAt"/> + </button> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> + <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> + </div> + </div> + <article :class="$style.article"> + <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> + <EmAvatar :class="$style.avatar" :user="appearNote.user" link/> + <div :class="$style.main"> + <EmNoteHeader :note="appearNote" :mini="true"/> + <EmInstanceTicker v-if="appearNote.user.instance != null" :instance="appearNote.user.instance"/> + <div style="container-type: inline-size;"> + <p v-if="appearNote.cw != null" :class="$style.cw"> + <EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/> + <button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> + </p> + <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> + <div :class="$style.text"> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> + <EmA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA> + <EmMfm + v-if="appearNote.text" + :parsedNodes="parsed" + :text="appearNote.text" + :author="appearNote.user" + :nyaize="'respect'" + :emojiUrls="appearNote.emojis" + :enableEmojiMenu="!true" + :enableEmojiMenuReaction="true" + :isBlock="true" + /> + </div> + <div v-if="appearNote.files && appearNote.files.length > 0"> + <EmMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/> + </div> + <EmPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :readOnly="true" :class="$style.poll"/> + <div v-if="appearNote.renote" :class="$style.quote"><EmNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> + <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false"> + <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> + </button> + <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true"> + <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> + </button> + </div> + <EmA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</EmA> + </div> + <EmReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16"> + <template #more> + <EmA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</EmA> + </template> + </EmReactionsViewer> + <footer :class="$style.footer"> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-arrow-back-up"></i> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-repeat"></i> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button"> + <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.footerButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-dots"></i> + </a> + </footer> + </div> + </article> +</div> +</template> + +<script lang="ts" setup> +import { computed, inject, ref, shallowRef } from 'vue'; +import * as mfm from '@transfem-org/sfm-js'; +import * as Misskey from 'misskey-js'; +import I18n from '@/components/I18n.vue'; +import EmNoteSub from '@/components/EmNoteSub.vue'; +import EmNoteHeader from '@/components/EmNoteHeader.vue'; +import EmNoteSimple from '@/components/EmNoteSimple.vue'; +import EmInstanceTicker from '@/components/EmInstanceTicker.vue'; +import EmReactionsViewer from '@/components/EmReactionsViewer.vue'; +import EmMediaList from '@/components/EmMediaList.vue'; +import EmPoll from '@/components/EmPoll.vue'; +import EmMfm from '@/components/EmMfm.js'; +import EmA from '@/components/EmA.vue'; +import EmAvatar from '@/components/EmAvatar.vue'; +import EmUserName from '@/components/EmUserName.vue'; +import EmTime from '@/components/EmTime.vue'; +import { userPage } from '@/utils.js'; +import { i18n } from '@/i18n.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import { url } from '@@/js/config.js'; + +function getAppearNote(note: Misskey.entities.Note) { + return Misskey.note.isPureRenote(note) ? note.renote : note; +} + +const props = withDefaults(defineProps<{ + note: Misskey.entities.Note; + pinned?: boolean; +}>(), { +}); + +const emit = defineEmits<{ + (ev: 'reaction', emoji: string): void; + (ev: 'removeReaction', emoji: string): void; +}>(); + +const inChannel = inject('inChannel', null); + +const note = ref((props.note)); + +const isRenote = Misskey.note.isPureRenote(note.value); + +const rootEl = shallowRef<HTMLElement>(); +const renoteTime = shallowRef<HTMLElement>(); +const appearNote = computed(() => getAppearNote(note.value)); +const showContent = ref(false); +const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); +const isLong = shouldCollapsed(appearNote.value, []); +const collapsed = ref(appearNote.value.cw == null && isLong); +const isDeleted = ref(false); +</script> + +<style lang="scss" module> +.root { + position: relative; + transition: box-shadow 0.1s ease; + font-size: 1.05em; + overflow: clip; + contain: content; + + // ã“れらã®æŒ‡å®šã¯ãƒ‘フォーマンスå‘上ã«ã¯æœ‰åŠ¹ã ãŒã€ãƒŽãƒ¼ãƒˆã®é«˜ã•ã¯ä¸€å®šã§ãªã„ãŸã‚〠+ // 下ã®æ–¹ã¾ã§ã‚¹ã‚¯ãƒãƒ¼ãƒ«ã™ã‚‹ã¨ä¸Šã®ãƒŽãƒ¼ãƒˆã®é«˜ã•ãŒã“ã“ã§æ±ºã‚打ã¡ã•ã‚ŒãŸã‚‚ã®ã«å¤‰åŒ–ã—ã€è¡¨ç¤ºã—ã¦ã„るノートã®ä½ç½®ãŒå¤‰ã‚ã£ã¦ã—ã¾ã† + // ノートãŒãƒžã‚¦ãƒ³ãƒˆã•ã‚ŒãŸã¨ãã«è‡ªèº«ã®é«˜ã•ã‚’å–å¾—ã— contain-intrinsic-size ã‚’è¨å®šã—ãªãŠã›ã°ã»ã¼è§£æ±ºã§ããã†ã ãŒã€ + // 今度ã¯ãã®å‡¦ç†è‡ªä½“ãŒãƒ‘フォーマンス低下ã®åŽŸå› ã«ãªã‚‰ãªã„ã‹æ‡¸å¿µã•ã‚Œã‚‹ã€‚ã¾ãŸã€è¢«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ã‚‚高ã•ã¯å¤‰åŒ–ã™ã‚‹ãŸã‚ã€ã‚„ã¯ã‚Šå¤šå°‘ã®ã‚ºãƒ¬ã¯ç”Ÿã˜ã‚‹ + // 一度レンダリングã•ã‚ŒãŸè¦ç´ ã¯ãƒ–ラウザãŒã‚ˆã—ãªã«ã‚µã‚¤ã‚ºã‚’覚ãˆã¦ãŠã„ã¦ãれるよã†ãªå®Ÿè£…ã«ãªã‚‹ã¾ã§å¾…ã£ãŸæ–¹ãŒè‰¯ã•ãã†(ãªã‚‹ã®ã‹ï¼Ÿ) + //content-visibility: auto; + //contain-intrinsic-size: 0 128px; + + &:focus-visible { + outline: none; + + &::after { + content: ""; + pointer-events: none; + display: block; + position: absolute; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: calc(100% - 8px); + height: calc(100% - 8px); + border: dashed 2px var(--focus); + border-radius: var(--radius); + box-sizing: border-box; + } + } + + .footer { + position: relative; + z-index: 1; + } + + &:hover > .article > .main > .footer > .footerButton { + opacity: 1; + } + + &.showActionsOnlyHover { + .footer { + visibility: hidden; + position: absolute; + top: 12px; + right: 12px; + padding: 0 4px; + margin-bottom: 0 !important; + background: var(--popup); + border-radius: 8px; + box-shadow: 0px 4px 32px var(--shadow); + } + + .footerButton { + font-size: 90%; + + &:not(:last-child) { + margin-right: 0; + } + } + } + + &.showActionsOnlyHover:hover { + .footer { + visibility: visible; + } + } +} + +.tip { + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 24px; + font-size: 90%; + white-space: pre; + color: #d28a3f; +} + +.tip + .article { + padding-top: 8px; +} + +.replyTo { + opacity: 0.7; + padding-bottom: 0; +} + +.renote { + position: relative; + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 28px; + white-space: pre; + color: var(--renote); + + & + .article { + padding-top: 8px; + } + + > .colorBar { + height: calc(100% - 6px); + } +} + +.renoteAvatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; +} + +.renoteText { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.renoteUserName { + font-weight: bold; +} + +.renoteInfo { + margin-left: auto; + font-size: 0.9em; +} + +.renoteTime { + flex-shrink: 0; + color: inherit; +} + +.renoteMenu { + margin-right: 4px; +} + +.collapsedRenoteTarget { + display: flex; + align-items: center; + line-height: 28px; + white-space: pre; + padding: 0 32px 18px; +} + +.collapsedRenoteTargetAvatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; +} + +.collapsedRenoteTargetText { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 90%; + opacity: 0.7; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + +.article { + position: relative; + display: flex; + padding: 28px 32px; +} + +.colorBar { + position: absolute; + top: 8px; + left: 8px; + width: 5px; + height: calc(100% - 16px); + border-radius: 999px; + pointer-events: none; +} + +.avatar { + flex-shrink: 0; + display: block !important; + margin: 0 14px 0 0; + width: 58px; + height: 58px; + position: sticky !important; + top: calc(22px + var(--stickyTop, 0px)); + left: 0; +} + +.main { + flex: 1; + min-width: 0; +} + +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + +.showLess { + width: 100%; + margin-top: 14px; + position: sticky; + bottom: calc(var(--stickyBottom, 0px) + 14px); +} + +.showLessLabel { + display: inline-block; + background: var(--popup); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.contentCollapsed { + position: relative; + max-height: 9em; + overflow: clip; +} + +.collapsed { + display: block; + position: absolute; + bottom: 0; + left: 0; + z-index: 2; + width: 100%; + height: 64px; + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + + &:hover > .collapsedLabel { + background: var(--panelHighlight); + } +} + +.collapsedLabel { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.text { + overflow-wrap: break-word; +} + +.replyIcon { + color: var(--accent); + margin-right: 0.5em; +} + +.translation { + border: solid 0.5px var(--divider); + border-radius: var(--radius); + padding: 12px; + margin-top: 8px; +} + +.urlPreview { + margin-top: 8px; +} + +.poll { + font-size: 80%; +} + +.quote { + padding: 8px 0; +} + +.quoteNote { + padding: 16px; + border: dashed 1px var(--renote); + border-radius: 8px; + overflow: clip; +} + +.channel { + opacity: 0.7; + font-size: 80%; +} + +.footer { + margin-bottom: -14px; +} + +.footerButton { + margin: 0; + padding: 8px; + opacity: 0.7; + + &:not(:last-child) { + margin-right: 28px; + } + + &:hover { + color: var(--fgHighlighted); + } +} + +.footerButtonLink:hover, +.footerButtonLink:focus, +.footerButtonLink:active { + text-decoration: none; +} + +.footerButtonCount { + display: inline; + margin: 0 0 0 8px; + opacity: 0.7; +} + +@container (max-width: 580px) { + .root { + font-size: 0.95em; + } + + .renote { + padding: 12px 26px 0 26px; + } + + .article { + padding: 24px 26px; + } + + .avatar { + width: 50px; + height: 50px; + } +} + +@container (max-width: 500px) { + .root { + font-size: 0.9em; + } + + .renote { + padding: 10px 22px 0 22px; + } + + .article { + padding: 20px 22px; + } + + .footer { + margin-bottom: -8px; + } +} + +@container (max-width: 480px) { + .renote { + padding: 8px 16px 0 16px; + } + + .tip { + padding: 8px 16px 0 16px; + } + + .collapsedRenoteTarget { + padding: 0 16px 9px; + margin-top: 4px; + } + + .article { + padding: 14px 16px; + } +} + +@container (max-width: 450px) { + .avatar { + margin: 0 10px 0 0; + width: 46px; + height: 46px; + top: calc(14px + var(--stickyTop, 0px)); + } +} + +@container (max-width: 400px) { + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 18px; + } + } + } +} + +@container (max-width: 350px) { + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 12px; + } + } + } + + .colorBar { + top: 6px; + left: 6px; + width: 4px; + height: calc(100% - 12px); + } +} + +@container (max-width: 300px) { + .avatar { + width: 44px; + height: 44px; + } + + .root:not(.showActionsOnlyHover) { + .footerButton { + &:not(:last-child) { + margin-right: 8px; + } + } + } +} + +@container (max-width: 250px) { + .quoteNote { + padding: 12px; + } +} + +.reactionOmitted { + display: inline-block; + margin-left: 8px; + opacity: .8; + font-size: 95%; +} +</style> diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue new file mode 100644 index 0000000000000000000000000000000000000000..0068e05bc4c3284fa3511ccd037c1f9a6013fc27 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -0,0 +1,491 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + v-show="!isDeleted" + ref="rootEl" + :class="$style.root" +> + <EmNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/> + <div v-if="isRenote" :class="$style.renote"> + <EmAvatar :class="$style.renoteAvatar" :user="note.user" link/> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> + <span :class="$style.renoteText"> + <I18n :src="i18n.ts.renotedBy" tag="span"> + <template #user> + <EmA :class="$style.renoteName" :to="userPage(note.user)"> + <EmUserName :user="note.user"/> + </EmA> + </template> + </I18n> + </span> + <div :class="$style.renoteInfo"> + <div class="$style.renoteTime"> + <EmTime :time="note.createdAt"/> + </div> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> + </div> + </div> + <article :class="$style.note"> + <header :class="$style.noteHeader"> + <EmAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link/> + <div :class="$style.noteHeaderBody"> + <div :class="$style.noteHeaderBodyUpper"> + <div style="min-width: 0;"> + <div class="_nowrap"> + <EmA :class="$style.noteHeaderName" :to="userPage(appearNote.user)"> + <EmUserName :nowrap="true" :user="appearNote.user"/> + </EmA> + <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span> + </div> + <div :class="$style.noteHeaderUsername"><EmAcct :user="appearNote.user"/></div> + </div> + <div :class="$style.noteHeaderInfo"> + <a :href="url" :class="$style.noteHeaderInstanceIconLink" target="_blank" rel="noopener noreferrer"> + <img :src="serverMetadata.iconUrl || '/favicon.ico'" alt="" :class="$style.noteHeaderInstanceIcon"/> + </a> + </div> + </div> + <EmInstanceTicker v-if="appearNote.user.instance != null" :instance="appearNote.user.instance"/> + </div> + </header> + <div :class="[$style.noteContent, { [$style.contentCollapsed]: collapsed }]"> + <p v-if="appearNote.cw != null" :class="$style.cw"> + <EmMfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'" :isBlock="true"/> + <button style="display: block; width: 100%; margin: 4px 0;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> + </p> + <div v-show="appearNote.cw == null || showContent"> + <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> + <EmA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA> + <EmMfm + v-if="appearNote.text" + :parsedNodes="parsed" + :text="appearNote.text" + :author="appearNote.user" + :nyaize="'respect'" + :emojiUrls="appearNote.emojis" + :isBlock="true" + /> + <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> + <div v-if="appearNote.files && appearNote.files.length > 0"> + <EmMediaList :mediaList="appearNote.files" :originalEntityUrl="`${url}/notes/${appearNote.id}`"/> + </div> + <EmPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :readOnly="true" :class="$style.poll"/> + <div v-if="appearNote.renote" :class="$style.quote"><EmNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> + <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false"> + <span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span> + </button> + <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true"> + <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> + </button> + </div> + <EmA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</EmA> + </div> + <footer> + <div :class="$style.noteFooterInfo"> + <span v-if="appearNote.visibility !== 'public'" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> + <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="appearNote.localOnly" style="display: inline-block; margin-right: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> + <EmA :to="notePage(appearNote)"> + <EmTime :time="appearNote.createdAt" mode="detail" colored/> + </EmA> + </div> + <EmReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :maxNumber="16" :note="appearNote"> + <template #more> + <EmA :to="`/notes/${appearNote.id}`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</EmA> + </template> + </EmReactionsViewer> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-arrow-back-up"></i> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-repeat"></i> + <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ (appearNote.renoteCount) }}</p> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> + <i v-else class="ti ti-plus"></i> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly') && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ (appearNote.reactionCount) }}</p> + </a> + <a :href="`/notes/${appearNote.id}`" target="_blank" rel="noopener" :class="[$style.noteFooterButton, $style.footerButtonLink]" class="_button"> + <i class="ti ti-dots"></i> + </a> + </footer> + </article> +</div> +</template> + +<script lang="ts" setup> +import { computed, inject, ref } from 'vue'; +import * as mfm from '@transfem-org/sfm-js'; +import * as Misskey from 'misskey-js'; +import I18n from '@/components/I18n.vue'; +import EmMediaList from '@/components/EmMediaList.vue'; +import EmNoteSub from '@/components/EmNoteSub.vue'; +import EmNoteSimple from '@/components/EmNoteSimple.vue'; +import EmInstanceTicker from '@/components/EmInstanceTicker.vue'; +import EmReactionsViewer from '@/components/EmReactionsViewer.vue'; +import EmPoll from '@/components/EmPoll.vue'; +import EmA from '@/components/EmA.vue'; +import EmAvatar from '@/components/EmAvatar.vue'; +import EmTime from '@/components/EmTime.vue'; +import EmUserName from '@/components/EmUserName.vue'; +import EmAcct from '@/components/EmAcct.vue'; +import { userPage } from '@/utils.js'; +import { notePage } from '@/utils.js'; +import { i18n } from '@/i18n.js'; +import { DI } from '@/di.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import { url } from '@@/js/config.js'; +import EmMfm from '@/components/EmMfm.js'; + +const props = defineProps<{ + note: Misskey.entities.Note; +}>(); + +const serverMetadata = inject(DI.serverMetadata)!; + +const inChannel = inject('inChannel', null); + +const note = ref(props.note); + +const isRenote = ( + note.value.renote != null && + note.value.reply == null && + note.value.text == null && + note.value.cw == null && + note.value.fileIds && note.value.fileIds.length === 0 && + note.value.poll == null +); + +const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const showContent = ref(false); +const isDeleted = ref(false); +const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; +const isLong = shouldCollapsed(appearNote.value, []); +const collapsed = ref(appearNote.value.cw == null && isLong); +</script> + +<style lang="scss" module> +.root { + position: relative; + transition: box-shadow 0.1s ease; + overflow: clip; + contain: content; +} + +.replyTo { + opacity: 0.7; + padding-bottom: 0; +} + +.renote { + display: flex; + align-items: center; + padding: 16px 32px 8px 32px; + line-height: 28px; + white-space: pre; + color: var(--renote); +} + +.renoteAvatar { + flex-shrink: 0; + display: inline-block; + width: 28px; + height: 28px; + margin: 0 8px 0 0; + border-radius: 6px; +} + +.renoteText { + overflow: hidden; + flex-shrink: 1; + text-overflow: ellipsis; + white-space: nowrap; +} + +.renoteName { + font-weight: bold; +} + +.renoteInfo { + margin-left: auto; + font-size: 0.9em; +} + +.renoteTime { + flex-shrink: 0; + color: inherit; +} + +.renote + .note { + padding-top: 8px; +} + +.note { + padding: 24px 32px 16px; + font-size: 1.2em; + + &:hover > .main > .footer > .button { + opacity: 1; + } +} + +.noteHeader { + display: flex; + position: relative; + margin-bottom: 16px; + align-items: center; +} + +.noteHeaderAvatar { + display: block; + flex-shrink: 0; + width: 50px; + height: 50px; +} + +.noteHeaderBody { + flex: 1; + display: flex; + min-width: 0; + flex-direction: column; + justify-content: center; + padding-left: 16px; + font-size: 0.95em; +} + +.noteHeaderBodyUpper { + display: flex; + min-width: 0; +} + +.noteHeaderName { + font-weight: bold; + line-height: 1.3; +} + +.isBot { + display: inline-block; + margin: 0 0.5em; + padding: 4px 6px; + font-size: 80%; + line-height: 1; + border: solid 0.5px var(--divider); + border-radius: 4px; +} + +.noteHeaderInfo { + margin-left: auto; + display: flex; + gap: 0.5em; + align-items: center; +} + +.noteHeaderInstanceIconLink { + display: inline-block; + margin-left: 4px; +} + +.noteHeaderInstanceIcon { + width: 32px; + height: 32px; + border-radius: 4px; +} + +.noteHeaderUsername { + margin-bottom: 2px; + line-height: 1.3; + word-wrap: anywhere; +} + +.noteContent { + container-type: inline-size; + overflow-wrap: break-word; +} + +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + +.noteReplyTarget { + color: var(--accent); + margin-right: 0.5em; +} + +.rn { + margin-left: 4px; + font-style: oblique; + color: var(--renote); +} + +.reactionOmitted { + display: inline-block; + margin-left: 8px; + opacity: .8; + font-size: 95%; +} + +.poll { + font-size: 80%; +} + +.quote { + padding: 8px 0; +} + +.quoteNote { + padding: 16px; + border: dashed 1px var(--renote); + border-radius: 8px; + overflow: clip; +} + +.channel { + opacity: 0.7; + font-size: 80%; +} + +.showLess { + width: 100%; + margin-top: 14px; + position: sticky; + bottom: calc(var(--stickyBottom, 0px) + 14px); +} + +.showLessLabel { + display: inline-block; + background: var(--popup); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.contentCollapsed { + position: relative; + max-height: 9em; + overflow: clip; +} + +.collapsed { + display: block; + position: absolute; + bottom: 0; + left: 0; + z-index: 2; + width: 100%; + height: 64px; + background: linear-gradient(0deg, var(--panel), var(--X15)); + + &:hover > .collapsedLabel { + background: var(--panelHighlight); + } +} + +.collapsedLabel { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} + +.noteFooterInfo { + margin: 16px 0; + opacity: 0.7; + font-size: 0.9em; +} + +.noteFooterButton { + margin: 0; + padding: 8px; + opacity: 0.7; + + &:not(:last-child) { + margin-right: 28px; + } + + &:hover { + color: var(--fgHighlighted); + } +} + +.footerButtonLink:hover, +.footerButtonLink:focus, +.footerButtonLink:active { + text-decoration: none; +} + +.noteFooterButtonCount { + display: inline; + margin: 0 0 0 8px; + opacity: 0.7; + + &.reacted { + color: var(--accent); + } +} + +@container (max-width: 500px) { + .root { + font-size: 0.9em; + } +} + +@container (max-width: 450px) { + .renote { + padding: 8px 16px 0 16px; + } + + .note { + padding: 16px; + } + + .noteHeaderAvatar { + width: 50px; + height: 50px; + } +} + +@container (max-width: 350px) { + .noteFooterButton { + &:not(:last-child) { + margin-right: 18px; + } + } +} + +@container (max-width: 300px) { + .root { + font-size: 0.825em; + } + + .noteHeaderAvatar { + width: 50px; + height: 50px; + } + + .noteFooterButton { + &:not(:last-child) { + margin-right: 12px; + } + } +} +</style> diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue new file mode 100644 index 0000000000000000000000000000000000000000..7d0b9bacadc65d3414ed420f80ca80698136e30a --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteHeader.vue @@ -0,0 +1,104 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<header :class="$style.root"> + <EmA :class="$style.name" :to="userPage(note.user)"> + <EmUserName :user="note.user"/> + </EmA> + <div v-if="note.user.isBot" :class="$style.isBot">bot</div> + <div :class="$style.username"><EmAcct :user="note.user"/></div> + <div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> + <img v-for="(role, i) in note.user.badgeRoles" :key="i" :class="$style.badgeRole" :src="role.iconUrl!"/> + </div> + <div :class="$style.info"> + <EmA :to="notePage(note)"> + <EmTime :time="note.createdAt" colored/> + </EmA> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="margin-left: 0.5em;"><i class="ti ti-rocket-off"></i></span> + <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span> + </div> +</header> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import * as Misskey from 'misskey-js'; +import { notePage } from '@/utils.js'; +import { userPage } from '@/utils.js'; +import EmA from '@/components/EmA.vue'; +import EmUserName from '@/components/EmUserName.vue'; +import EmAcct from '@/components/EmAcct.vue'; +import EmTime from '@/components/EmTime.vue'; + +defineProps<{ + note: Misskey.entities.Note; +}>(); +</script> + +<style lang="scss" module> +.root { + display: flex; + align-items: baseline; + white-space: nowrap; +} + +.name { + flex-shrink: 1; + display: block; + margin: 0 .5em 0 0; + padding: 0; + overflow: hidden; + font-size: 1em; + font-weight: bold; + text-decoration: none; + text-overflow: ellipsis; + + &:hover { + text-decoration: underline; + } +} + +.isBot { + flex-shrink: 0; + align-self: center; + margin: 0 .5em 0 0; + padding: 1px 6px; + font-size: 80%; + border: solid 0.5px var(--divider); + border-radius: 3px; +} + +.username { + flex-shrink: 9999999; + margin: 0 .5em 0 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.info { + flex-shrink: 0; + margin-left: auto; + font-size: 0.9em; +} + +.badgeRoles { + margin: 0 .5em 0 0; +} + +.badgeRole { + height: 1.3em; + vertical-align: -20%; + + & + .badgeRole { + margin-left: 0.2em; + } +} +</style> diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue new file mode 100644 index 0000000000000000000000000000000000000000..d4afc0b4d7f83cf110fcca30e9f112737415483d --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteSimple.vue @@ -0,0 +1,106 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <EmAvatar :class="$style.avatar" :user="note.user" link preview/> + <div :class="$style.main"> + <EmNoteHeader :class="$style.header" :note="note" :mini="true"/> + <div> + <p v-if="note.cw != null" :class="$style.cw"> + <EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis" :isBlock="true"/> + <button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> + </p> + <div v-show="note.cw == null || showContent"> + <EmSubNoteContent :class="$style.text" :note="note"/> + </div> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import { i18n } from '@/i18n.js'; +import EmAvatar from '@/components/EmAvatar.vue'; +import EmNoteHeader from '@/components/EmNoteHeader.vue'; +import EmSubNoteContent from '@/components/EmSubNoteContent.vue'; +import EmMfm from '@/components/EmMfm.js'; + +const props = defineProps<{ + note: Misskey.entities.Note; +}>(); + +const showContent = ref(false); +</script> + +<style lang="scss" module> +.root { + display: flex; + margin: 0; + padding: 0; + font-size: 0.95em; +} + +.avatar { + flex-shrink: 0; + display: block; + margin: 0 10px 0 0; + width: 34px; + height: 34px; + border-radius: 8px; + position: sticky !important; + top: calc(16px + var(--stickyTop, 0px)); + left: 0; +} + +.main { + flex: 1; + min-width: 0; +} + +.header { + margin-bottom: 2px; +} + +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + +.text { + cursor: default; + margin: 0; + padding: 0; +} + +@container (min-width: 250px) { + .avatar { + margin: 0 10px 0 0; + width: 40px; + height: 40px; + } +} + +@container (min-width: 350px) { + .avatar { + margin: 0 10px 0 0; + width: 44px; + height: 44px; + } +} + +@container (min-width: 500px) { + .avatar { + margin: 0 12px 0 0; + width: 48px; + height: 48px; + } +} +</style> diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue new file mode 100644 index 0000000000000000000000000000000000000000..9fbd1eae35f29f6781b1e3f07e2a02a115fe2b7a --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteSub.vue @@ -0,0 +1,151 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[$style.root, { [$style.children]: depth > 1 }]"> + <div :class="$style.main"> + <div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div> + <EmAvatar :class="$style.avatar" :user="note.user" link preview/> + <div :class="$style.body"> + <EmNoteHeader :class="$style.header" :note="note" :mini="true"/> + <div> + <p v-if="note.cw != null" :class="$style.cw"> + <EmMfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :isBlock="true"/> + <button style="display: block; width: 100%;" class="_buttonGray _buttonRounded" @click="showContent = !showContent">{{ showContent ? i18n.ts._cw.hide : i18n.ts._cw.show }}</button> + </p> + <div v-show="note.cw == null || showContent"> + <EmSubNoteContent :class="$style.text" :note="note"/> + </div> + </div> + </div> + </div> + <template v-if="depth < 5"> + <EmNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1"/> + </template> + <div v-else :class="$style.more"> + <EmA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></EmA> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import EmA from '@/components/EmA.vue'; +import EmAvatar from '@/components/EmAvatar.vue'; +import EmNoteHeader from '@/components/EmNoteHeader.vue'; +import EmSubNoteContent from '@/components/EmSubNoteContent.vue'; +import { notePage } from '@/utils.js'; +import { misskeyApi } from '@/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import EmMfm from '@/components/EmMfm.js'; + +const props = withDefaults(defineProps<{ + note: Misskey.entities.Note; + detail?: boolean; + + // how many notes are in between this one and the note being viewed in detail + depth?: number; +}>(), { + depth: 1, +}); + +const showContent = ref(false); +const replies = ref<Misskey.entities.Note[]>([]); + +if (props.detail) { + misskeyApi('notes/children', { + noteId: props.note.id, + limit: 5, + }).then(res => { + replies.value = res; + }); +} +</script> + +<style lang="scss" module> +.root { + padding: 16px 32px; + font-size: 0.9em; + position: relative; + + &.children { + padding: 10px 0 0 16px; + font-size: 1em; + } +} + +.main { + display: flex; +} + +.colorBar { + position: absolute; + top: 8px; + left: 8px; + width: 5px; + height: calc(100% - 8px); + border-radius: 999px; + pointer-events: none; +} + +.avatar { + flex-shrink: 0; + display: block; + margin: 0 8px 0 0; + width: 38px; + height: 38px; + border-radius: 8px; +} + +.body { + flex: 1; + min-width: 0; +} + +.header { + margin-bottom: 2px; +} + +.cw { + cursor: default; + display: block; + margin: 0; + padding: 0; + overflow-wrap: break-word; +} + +.text { + margin: 0; + padding: 0; +} + +.reply, .more { + border-left: solid 0.5px var(--divider); + margin-top: 10px; +} + +.more { + padding: 10px 0 0 16px; +} + +@container (max-width: 450px) { + .root { + padding: 14px 16px; + + &.children { + padding: 10px 0 0 8px; + } + } +} + +.muted { + text-align: center; + padding: 8px !important; + border: 1px solid var(--divider); + margin: 8px 8px 0 8px; + border-radius: 8px; +} +</style> diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue new file mode 100644 index 0000000000000000000000000000000000000000..3418d97f77e486d61a623de4fc7618478657125b --- /dev/null +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -0,0 +1,52 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<EmPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad"> + <template #empty> + <div class="_fullinfo"> + <div>{{ i18n.ts.noNotes }}</div> + </div> + </template> + + <template #default="{ items: notes }"> + <div :class="[$style.root]"> + <EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/> + </div> + </template> +</EmPagination> +</template> + +<script lang="ts" setup> +import { useTemplateRef } from 'vue'; +import EmNote from '@/components/EmNote.vue'; +import EmPagination, { Paging } from '@/components/EmPagination.vue'; +import { i18n } from '@/i18n.js'; + +withDefaults(defineProps<{ + pagination: Paging; + noGap?: boolean; + disableAutoLoad?: boolean; + ad?: boolean; +}>(), { + ad: true, +}); + +const pagingComponent = useTemplateRef('pagingComponent'); + +defineExpose({ + pagingComponent, +}); +</script> + +<style lang="scss" module> +.root { + background: var(--panel); +} + +.note { + border-bottom: 0.5px solid var(--divider); +} +</style> diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue new file mode 100644 index 0000000000000000000000000000000000000000..5d5317a91281ac62afbf56f119ca86b5d27cb6bd --- /dev/null +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -0,0 +1,504 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<EmLoading v-if="fetching"/> + +<EmError v-else-if="error" @retry="init()"/> + +<div v-else-if="empty" key="_empty_" class="empty"> + <slot name="empty"> + <div class="_fullinfo"> + <div>{{ i18n.ts.nothing }}</div> + </div> + </slot> +</div> + +<div v-else ref="rootEl"> + <div v-show="pagination.reversed && more" key="_more_" class="_margin"> + <button v-if="!moreFetching" class="_buttonPrimary" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMoreAhead"> + {{ i18n.ts.loadMore }} + </button> + <EmLoading v-else class="loading"/> + </div> + <slot :items="Array.from(items.values())" :fetching="fetching || moreFetching"></slot> + <div v-show="!pagination.reversed && more" key="_more_" class="_margin"> + <button v-if="!moreFetching" class="_buttonRounded _buttonPrimary" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore"> + {{ i18n.ts.loadMore }} + </button> + <EmLoading v-else class="loading"/> + </div> +</div> +</template> + +<script lang="ts"> +import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; +import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js'; +import { misskeyApi } from '@/misskey-api.js'; +import { i18n } from '@/i18n.js'; + +const SECOND_FETCH_LIMIT = 30; +const TOLERANCE = 16; +const APPEAR_MINIMUM_INTERVAL = 600; + +export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> = { + endpoint: E; + limit: number; + params?: Misskey.Endpoints[E]['req'] | ComputedRef<Misskey.Endpoints[E]['req']>; + + /** + * 検索APIã®ã‚ˆã†ãªã€ãƒšãƒ¼ã‚¸ãƒ³ã‚°ä¸å¯ãªã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã‚’利用ã™ã‚‹å ´åˆ + * (ãã®ã‚ˆã†ãªAPIã‚’ã“ã®é–¢æ•°ã§ä½¿ã†ã®ã¯è‹¥å¹²çŸ›ç›¾ã—ã¦ã‚‹ã‘ã©) + */ + noPaging?: boolean; + + /** + * items é…列ã®ä¸èº«ã‚’é€†é †ã«ã™ã‚‹(æ–°ã—ã„æ–¹ãŒæœ€å¾Œ) + */ + reversed?: boolean; + + offsetMode?: boolean; + + pageEl?: HTMLElement; +}; + +type MisskeyEntity = { + id: string; + createdAt: string; + _shouldInsertAd_?: boolean; + [x: string]: any; +}; + +type MisskeyEntityMap = Map<string, MisskeyEntity>; + +function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] { + return entities.map(en => [en.id, en]); +} + +function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): MisskeyEntityMap { + return new Map([...map, ...arrayToEntries(entities)]); +} + +</script> +<script lang="ts" setup> +import EmError from '@/components/EmError.vue'; +import EmLoading from '@/components/EmLoading.vue'; + +const props = withDefaults(defineProps<{ + pagination: Paging; + disableAutoLoad?: boolean; + displayLimit?: number; +}>(), { + displayLimit: 20, +}); + +const emit = defineEmits<{ + (ev: 'queue', count: number): void; + (ev: 'status', error: boolean): void; +}>(); + +const rootEl = shallowRef<HTMLElement>(); + +// é¡ã‚Šä¸ã‹ã©ã†ã‹ +const backed = ref(false); + +const scrollRemove = ref<(() => void) | null>(null); + +/** + * 表示ã™ã‚‹ã‚¢ã‚¤ãƒ†ãƒ ã®ã‚½ãƒ¼ã‚¹ + * 最新ãŒ0番目 + */ +const items = ref<MisskeyEntityMap>(new Map()); + +/** + * タブãŒéžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã©ã®å ´åˆã«æ›´æ–°ã‚’貯ã‚ã¦ãŠã + * 最新ãŒ0番目 + */ +const queue = ref<MisskeyEntityMap>(new Map()); + +const offset = ref(0); + +/** + * åˆæœŸåŒ–ä¸ã‹ã©ã†ã‹ï¼ˆtrueãªã‚‰EmLoadingã§å…¨ã¦éš ã™ï¼‰ + */ +const fetching = ref(true); + +const moreFetching = ref(false); +const more = ref(false); +const preventAppearFetchMore = ref(false); +const preventAppearFetchMoreTimer = ref<number | null>(null); +const isBackTop = ref(false); +const empty = computed(() => items.value.size === 0); +const error = ref(false); + +const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value); +const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body); + +const visibility = useDocumentVisibility(); + +let isPausingUpdate = false; +let timerForSetPause: number | null = null; +const BACKGROUND_PAUSE_WAIT_SEC = 10; + +// å…ˆé ãŒè¡¨ç¤ºã•ã‚Œã¦ã„ã‚‹ã‹ã©ã†ã‹ã‚’検出 +// https://qiita.com/mkataigi/items/0154aefd2223ce23398e +const scrollObserver = ref<IntersectionObserver>(); + +watch([() => props.pagination.reversed, scrollableElement], () => { + if (scrollObserver.value) scrollObserver.value.disconnect(); + + scrollObserver.value = new IntersectionObserver(entries => { + backed.value = entries[0].isIntersecting; + }, { + root: scrollableElement.value, + rootMargin: props.pagination.reversed ? '-100% 0px 100% 0px' : '100% 0px -100% 0px', + threshold: 0.01, + }); +}, { immediate: true }); + +watch(rootEl, () => { + scrollObserver.value?.disconnect(); + nextTick(() => { + if (rootEl.value) scrollObserver.value?.observe(rootEl.value); + }); +}); + +watch([backed, contentEl], () => { + if (!backed.value) { + if (!contentEl.value) return; + + scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE); + } else { + if (scrollRemove.value) scrollRemove.value(); + scrollRemove.value = null; + } +}); + +// パラメータã«ä½•ã‚‰ã‹ã®å¤‰æ›´ãŒã‚ã£ãŸéš›ã€å†èªè¾¼ã—ãŸã„(ãƒãƒ£ãƒ³ãƒãƒ«ç‰ã®IDãŒå¤‰ã‚ã£ãŸãªã©ï¼‰ +watch(() => [props.pagination.endpoint, props.pagination.params], init, { deep: true }); + +watch(queue, (a, b) => { + if (a.size === 0 && b.size === 0) return; + emit('queue', queue.value.size); +}, { deep: true }); + +watch(error, (n, o) => { + if (n === o) return; + emit('status', n); +}); + +async function init(): Promise<void> { + items.value = new Map(); + queue.value = new Map(); + fetching.value = true; + const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; + await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { + ...params, + limit: props.pagination.limit ?? 10, + allowPartial: true, + }).then(res => { + for (let i = 0; i < res.length; i++) { + const item = res[i]; + if (i === 3) item._shouldInsertAd_ = true; + } + + if (res.length === 0 || props.pagination.noPaging) { + concatItems(res); + more.value = false; + } else { + if (props.pagination.reversed) moreFetching.value = true; + concatItems(res); + more.value = true; + } + + offset.value = res.length; + error.value = false; + fetching.value = false; + }, err => { + error.value = true; + fetching.value = false; + }); +} + +const reload = (): Promise<void> => { + return init(); +}; + +const fetchMore = async (): Promise<void> => { + if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; + moreFetching.value = true; + const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; + await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { + ...params, + limit: SECOND_FETCH_LIMIT, + ...(props.pagination.offsetMode ? { + offset: offset.value, + } : { + untilId: Array.from(items.value.keys()).at(-1), + }), + }).then(res => { + for (let i = 0; i < res.length; i++) { + const item = res[i]; + if (i === 10) item._shouldInsertAd_ = true; + } + + const reverseConcat = _res => { + const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight(); + const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY; + + items.value = concatMapWithArray(items.value, _res); + + return nextTick(() => { + if (scrollableElement.value) { + scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' }); + } else { + window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' }); + } + + return nextTick(); + }); + }; + + if (res.length === 0) { + if (props.pagination.reversed) { + reverseConcat(res).then(() => { + more.value = false; + moreFetching.value = false; + }); + } else { + items.value = concatMapWithArray(items.value, res); + more.value = false; + moreFetching.value = false; + } + } else { + if (props.pagination.reversed) { + reverseConcat(res).then(() => { + more.value = true; + moreFetching.value = false; + }); + } else { + items.value = concatMapWithArray(items.value, res); + more.value = true; + moreFetching.value = false; + } + } + offset.value += res.length; + }, err => { + moreFetching.value = false; + }); +}; + +const fetchMoreAhead = async (): Promise<void> => { + if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; + moreFetching.value = true; + const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; + await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { + ...params, + limit: SECOND_FETCH_LIMIT, + ...(props.pagination.offsetMode ? { + offset: offset.value, + } : { + sinceId: Array.from(items.value.keys()).at(-1), + }), + }).then(res => { + if (res.length === 0) { + items.value = concatMapWithArray(items.value, res); + more.value = false; + } else { + items.value = concatMapWithArray(items.value, res); + more.value = true; + } + offset.value += res.length; + moreFetching.value = false; + }, err => { + moreFetching.value = false; + }); +}; + +/** + * Appear(IntersectionObserver)ã«ã‚ˆã£ã¦fetchMoreãŒå‘¼ã°ã‚Œã‚‹å ´åˆã€ + * APPEAR_MINIMUM_INTERVALミリ秒以内ã«2回fetchMoreãŒå‘¼ã°ã‚Œã‚‹ã®ã‚’防ã + */ +const fetchMoreApperTimeoutFn = (): void => { + preventAppearFetchMore.value = false; + preventAppearFetchMoreTimer.value = null; +}; +const fetchMoreAppearTimeout = (): void => { + preventAppearFetchMore.value = true; + preventAppearFetchMoreTimer.value = window.setTimeout(fetchMoreApperTimeoutFn, APPEAR_MINIMUM_INTERVAL); +}; + +const appearFetchMore = async (): Promise<void> => { + if (preventAppearFetchMore.value) return; + await fetchMore(); + fetchMoreAppearTimeout(); +}; + +const appearFetchMoreAhead = async (): Promise<void> => { + if (preventAppearFetchMore.value) return; + await fetchMoreAhead(); + fetchMoreAppearTimeout(); +}; + +const isTop = (): boolean => isBackTop.value || (props.pagination.reversed ? isBottomVisible : isTopVisible)(contentEl.value!, TOLERANCE); + +watch(visibility, () => { + if (visibility.value === 'hidden') { + timerForSetPause = window.setTimeout(() => { + isPausingUpdate = true; + timerForSetPause = null; + }, + BACKGROUND_PAUSE_WAIT_SEC * 1000); + } else { // 'visible' + if (timerForSetPause) { + clearTimeout(timerForSetPause); + timerForSetPause = null; + } else { + isPausingUpdate = false; + if (isTop()) { + executeQueue(); + } + } + } +}); + +/** + * 最新ã®ã‚‚ã®ã¨ã—ã¦1ã¤ã ã‘ã‚¢ã‚¤ãƒ†ãƒ ã‚’è¿½åŠ ã™ã‚‹ + * ストリーミングã‹ã‚‰é™ã£ã¦ããŸã‚¢ã‚¤ãƒ†ãƒ ã¯ã“ã‚Œã§è¿½åŠ ã™ã‚‹ + * @param item アイテム+ */ +const prepend = (item: MisskeyEntity): void => { + if (items.value.size === 0) { + items.value.set(item.id, item); + fetching.value = false; + return; + } + + if (isTop() && !isPausingUpdate) unshiftItems([item]); + else prependQueue(item); +}; + +/** + * æ–°ç€ã‚¢ã‚¤ãƒ†ãƒ ã‚’itemsã®å…ˆé ã«è¿½åŠ ã—ã€displayLimitã‚’é©ç”¨ã™ã‚‹ + * @param newItems æ–°ã—ã„アイテムã®é…列 + */ +function unshiftItems(newItems: MisskeyEntity[]) { + const length = newItems.length + items.value.size; + items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit)); + + if (length >= props.displayLimit) more.value = true; +} + +/** + * å¤ã„アイテムをitemsã®æœ«å°¾ã«è¿½åŠ ã—ã€displayLimitã‚’é©ç”¨ã™ã‚‹ + * @param oldItems å¤ã„アイテムã®é…列 + */ +function concatItems(oldItems: MisskeyEntity[]) { + const length = oldItems.length + items.value.size; + items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit)); + + if (length >= props.displayLimit) more.value = true; +} + +function executeQueue() { + unshiftItems(Array.from(queue.value.values())); + queue.value = new Map(); +} + +function prependQueue(newItem: MisskeyEntity) { + queue.value = new Map([[newItem.id, newItem], ...queue.value].slice(0, props.displayLimit) as [string, MisskeyEntity][]); +} + +/* + * アイテムを末尾ã«è¿½åŠ ã™ã‚‹ï¼ˆä½¿ã†ã®ï¼Ÿï¼‰ + */ +const appendItem = (item: MisskeyEntity): void => { + items.value.set(item.id, item); +}; + +const removeItem = (id: string) => { + items.value.delete(id); + queue.value.delete(id); +}; + +const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => MisskeyEntity): void => { + const item = items.value.get(id); + if (item) items.value.set(id, replacer(item)); + + const queueItem = queue.value.get(id); + if (queueItem) queue.value.set(id, replacer(queueItem)); +}; + +onActivated(() => { + isBackTop.value = false; +}); + +onDeactivated(() => { + isBackTop.value = props.pagination.reversed ? window.scrollY >= (rootEl.value ? rootEl.value.scrollHeight - window.innerHeight : 0) : window.scrollY === 0; +}); + +function toBottom() { + scrollToBottom(contentEl.value!); +} + +onBeforeMount(() => { + init().then(() => { + if (props.pagination.reversed) { + nextTick(() => { + setTimeout(toBottom, 800); + + // scrollToBottomã§moreFetchingボタンãŒç”»é¢å¤–ã¾ã§å‡ºã‚‹ã¾ã§ + // more = trueã‚’é…らã›ã‚‹ + setTimeout(() => { + moreFetching.value = false; + }, 2000); + }); + } + }); +}); + +onBeforeUnmount(() => { + if (timerForSetPause) { + clearTimeout(timerForSetPause); + timerForSetPause = null; + } + if (preventAppearFetchMoreTimer.value) { + clearTimeout(preventAppearFetchMoreTimer.value); + preventAppearFetchMoreTimer.value = null; + } + scrollObserver.value?.disconnect(); +}); + +defineExpose({ + items, + queue, + backed: backed.value, + more, + reload, + prepend, + append: appendItem, + removeItem, + updateItem, +}); +</script> + +<style lang="scss" module> +.transition_fade_enterActive, +.transition_fade_leaveActive { + transition: opacity 0.125s ease; +} +.transition_fade_enterFrom, +.transition_fade_leaveTo { + opacity: 0; +} + +.more { + display: block; + margin-left: auto; + margin-right: auto; +} +</style> diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue new file mode 100644 index 0000000000000000000000000000000000000000..a2b1203449403116027b4cd0b17725392269d68f --- /dev/null +++ b/packages/frontend-embed/src/components/EmPoll.vue @@ -0,0 +1,82 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <ul :class="$style.choices"> + <li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice"> + <div :class="$style.bg" :style="{ 'width': `${choice.votes / total * 100}%` }"></div> + <span :class="$style.fg"> + <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> + <EmMfm :text="choice.text" :plain="true"/> + <span style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span> + </span> + </li> + </ul> + <p :class="$style.info"> + <span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span> + </p> +</div> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; +import { i18n } from '@/i18n.js'; +import EmMfm from '@/components/EmMfm.js'; + +function sum(xs: number[]): number { + return xs.reduce((a, b) => a + b, 0); +} + +const props = defineProps<{ + noteId: string; + poll: NonNullable<Misskey.entities.Note['poll']>; +}>(); + +const total = computed(() => sum(props.poll.choices.map(x => x.votes))); +</script> + +<style lang="scss" module> +.choices { + display: block; + margin: 0; + padding: 0; + list-style: none; +} + +.choice { + display: block; + position: relative; + margin: 4px 0; + padding: 4px; + //border: solid 0.5px var(--divider); + background: var(--accentedBg); + border-radius: 4px; + overflow: clip; +} + +.bg { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: var(--accent); + background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB)); + transition: width 1s ease; +} + +.fg { + position: relative; + display: inline-block; + padding: 3px 5px; + background: var(--panel); + border-radius: 3px; +} + +.info { + color: var(--fg); +} +</style> diff --git a/packages/frontend-embed/src/components/EmReactionIcon.vue b/packages/frontend-embed/src/components/EmReactionIcon.vue new file mode 100644 index 0000000000000000000000000000000000000000..5c38ecb0ed3b4a192afb15da3e1ebe8a72c3a40a --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionIcon.vue @@ -0,0 +1,23 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<EmCustomEmoji v-if="reaction[0] === ':'" ref="elRef" :name="reaction" :normal="true" :noStyle="noStyle" :url="emojiUrl" :fallbackToImage="true"/> +<EmEmoji v-else ref="elRef" :emoji="reaction" :normal="true" :noStyle="noStyle"/> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import EmCustomEmoji from './EmCustomEmoji.vue'; +import EmEmoji from './EmEmoji.vue'; + +const props = defineProps<{ + reaction: string; + noStyle?: boolean; + emojiUrl?: string; + withTooltip?: boolean; +}>(); + +</script> diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue new file mode 100644 index 0000000000000000000000000000000000000000..2e43eb8d170be6df285048ec15483e2b3c0d3b4a --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue @@ -0,0 +1,99 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<button + class="_button" + :class="[$style.root, { [$style.reacted]: note.myReaction == reaction }]" +> + <EmReactionIcon :class="$style.limitWidth" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/> + <span :class="$style.count">{{ count }}</span> +</button> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import * as Misskey from 'misskey-js'; +import EmReactionIcon from '@/components/EmReactionIcon.vue'; + +const props = defineProps<{ + reaction: string; + count: number; + isInitial: boolean; + note: Misskey.entities.Note; +}>(); +</script> + +<style lang="scss" module> +.root { + display: inline-flex; + height: 42px; + margin: 2px; + padding: 0 6px; + font-size: 1.5em; + border-radius: 6px; + align-items: center; + justify-content: center; + + &.canToggle { + background: var(--buttonBg); + + &:hover { + background: rgba(0, 0, 0, 0.1); + } + } + + &:not(.canToggle) { + cursor: default; + } + + &.small { + height: 32px; + font-size: 1em; + border-radius: 4px; + + > .count { + font-size: 0.9em; + line-height: 32px; + } + } + + &.large { + height: 52px; + font-size: 2em; + border-radius: 8px; + + > .count { + font-size: 0.6em; + line-height: 52px; + } + } + + &.reacted, &.reacted:hover { + background: var(--accentedBg); + color: var(--accent); + box-shadow: 0 0 0 1px var(--accent) inset; + + > .count { + color: var(--accent); + } + + > .icon { + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5)); + } + } +} + +.limitWidth { + max-width: 70px; + object-fit: contain; +} + +.count { + font-size: 0.7em; + line-height: 42px; + margin: 0 0 0 4px; +} +</style> diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.vue b/packages/frontend-embed/src/components/EmReactionsViewer.vue new file mode 100644 index 0000000000000000000000000000000000000000..014dd1c935f8bc594aca398806dd751b2949204b --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionsViewer.vue @@ -0,0 +1,104 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/> + <slot v-if="hasMoreReactions" name="more"></slot> +</div> +</template> + +<script lang="ts" setup> +import * as Misskey from 'misskey-js'; +import { inject, watch, ref } from 'vue'; +import XReaction from '@/components/EmReactionsViewer.reaction.vue'; + +const props = withDefaults(defineProps<{ + note: Misskey.entities.Note; + maxNumber?: number; +}>(), { + maxNumber: Infinity, +}); + +const mock = inject<boolean>('mock', false); + +const emit = defineEmits<{ + (ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; +}>(); + +const initialReactions = new Set(Object.keys(props.note.reactions)); + +const reactions = ref<[string, number][]>([]); +const hasMoreReactions = ref(false); + +if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) { + reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction]; +} + +function onMockToggleReaction(emoji: string, count: number) { + if (!mock) return; + + const i = reactions.value.findIndex((item) => item[0] === emoji); + if (i < 0) return; + + emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1])); +} + +watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { + let newReactions: [string, number][] = []; + hasMoreReactions.value = Object.keys(newSource).length > maxNumber; + + for (let i = 0; i < reactions.value.length; i++) { + const reaction = reactions.value[i][0]; + if (reaction in newSource && newSource[reaction] !== 0) { + reactions.value[i][1] = newSource[reaction]; + newReactions.push(reactions.value[i]); + } + } + + const newReactionsNames = newReactions.map(([x]) => x); + newReactions = [ + ...newReactions, + ...Object.entries(newSource) + .sort(([, a], [, b]) => b - a) + .filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)), + ]; + + newReactions = newReactions.slice(0, props.maxNumber); + + if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) { + newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); + } + + reactions.value = newReactions; +}, { immediate: true, deep: true }); +</script> + +<style lang="scss" module> +.transition_x_move, +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important; +} +.transition_x_enterFrom, +.transition_x_leaveTo { + opacity: 0; + transform: scale(0.7); +} +.transition_x_leaveActive { + position: absolute; +} + +.root { + display: flex; + flex-wrap: wrap; + align-items: center; + margin: 4px -2px 0 -2px; + + &:empty { + display: none; + } +} +</style> diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue new file mode 100644 index 0000000000000000000000000000000000000000..f433573df5074913d8c0dc4335119516fbfe685f --- /dev/null +++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue @@ -0,0 +1,114 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="[$style.root, { [$style.collapsed]: collapsed }]"> + <div> + <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> + <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span> + <EmA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></EmA> + <EmMfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis" :isBlock="true"/> + <EmA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</EmA> + </div> + <details v-if="note.files && note.files.length > 0"> + <summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary> + <EmMediaList :mediaList="note.files" :originalEntityUrl="`${url}/notes/${note.id}`"/> + </details> + <details v-if="note.poll"> + <summary>{{ i18n.ts.poll }}</summary> + <EmPoll :noteId="note.id" :poll="note.poll"/> + </details> + <button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false"> + <span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span> + </button> + <button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true"> + <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> + </button> +</div> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import EmMediaList from '@/components/EmMediaList.vue'; +import EmPoll from '@/components/EmPoll.vue'; +import { i18n } from '@/i18n.js'; +import { url } from '@@/js/config.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import EmA from '@/components/EmA.vue'; +import EmMfm from '@/components/EmMfm.js'; + +const props = defineProps<{ + note: Misskey.entities.Note; +}>(); + +const isLong = shouldCollapsed(props.note, []); + +const collapsed = ref(isLong); +</script> + +<style lang="scss" module> +.root { + overflow-wrap: break-word; + + &.collapsed { + position: relative; + max-height: 9em; + overflow: clip; + + > .fade { + display: block; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 64px; + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + + > .fadeLabel { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); + } + + &:hover { + > .fadeLabel { + background: var(--panelHighlight); + } + } + } + } +} + +.reply { + margin-right: 6px; + color: var(--accent); +} + +.rp { + margin-left: 4px; + font-style: oblique; + color: var(--renote); +} + +.showLess { + width: 100%; + margin-top: 14px; + position: sticky; + bottom: calc(var(--stickyBottom, 0px) + 14px); +} + +.showLessLabel { + display: inline-block; + background: var(--popup); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); +} +</style> diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue new file mode 100644 index 0000000000000000000000000000000000000000..c3986f7d7031956f541f43cf156ecfefefda912e --- /dev/null +++ b/packages/frontend-embed/src/components/EmTime.vue @@ -0,0 +1,107 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<time :title="absolute" :class="{ [$style.old1]: colored && (ago > 60 * 60 * 24 * 90), [$style.old2]: colored && (ago > 60 * 60 * 24 * 180) }"> + <template v-if="invalid">{{ i18n.ts._ago.invalid }}</template> + <template v-else-if="mode === 'relative'">{{ relative }}</template> + <template v-else-if="mode === 'absolute'">{{ absolute }}</template> + <template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template> +</time> +</template> + +<script lang="ts" setup> +import { onMounted, onUnmounted, ref, computed } from 'vue'; +import { i18n } from '@/i18n.js'; +import { dateTimeFormat } from '@@/js/intl-const.js'; + +const props = withDefaults(defineProps<{ + time: Date | string | number | null; + origin?: Date | null; + mode?: 'relative' | 'absolute' | 'detail'; + colored?: boolean; +}>(), { + origin: null, + mode: 'relative', +}); + +function getDateSafe(n: Date | string | number) { + try { + if (n instanceof Date) { + return n; + } + return new Date(n); + } catch (err) { + return { + getTime: () => NaN, + }; + } +} + +// eslint-disable-next-line vue/no-setup-props-reactivity-loss +const _time = props.time == null ? NaN : getDateSafe(props.time).getTime(); +const invalid = Number.isNaN(_time); +const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; + +// eslint-disable-next-line vue/no-setup-props-reactivity-loss +const now = ref(props.origin?.getTime() ?? Date.now()); +const ago = computed(() => (now.value - _time) / 1000/*ms*/); + +const relative = computed<string>(() => { + if (props.mode === 'absolute') return ''; // absoluteã§ã¯relativeを使ã‚ãªã„ã®ã§è¨ˆç®—ã—ãªã„ + if (invalid) return i18n.ts._ago.invalid; + + return ( + ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) : + ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) : + ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) : + ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) : + ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) : + ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) : + ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) : + ago.value >= -3 ? i18n.ts._ago.justNow : + ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) : + ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) : + ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) : + ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) : + ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) : + ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) : + i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() }) + ); +}); + +let tickId: number; +let currentInterval: number; + +function tick() { + now.value = Date.now(); + const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000; + + if (currentInterval !== nextInterval) { + if (tickId) window.clearInterval(tickId); + currentInterval = nextInterval; + tickId = window.setInterval(tick, nextInterval); + } +} + +if (!invalid && props.origin === null && (props.mode === 'relative' || props.mode === 'detail')) { + onMounted(() => { + tick(); + }); + onUnmounted(() => { + if (tickId) window.clearInterval(tickId); + }); +} +</script> + +<style lang="scss" module> +.old1 { + color: var(--warn); +} + +.old1.old2 { + color: var(--error); +} +</style> diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue new file mode 100644 index 0000000000000000000000000000000000000000..6c30b1102d9ed2decdb7bbd84390be9b8fe263f7 --- /dev/null +++ b/packages/frontend-embed/src/components/EmTimelineContainer.vue @@ -0,0 +1,39 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.timelineRoot"> + <div v-if="showHeader" :class="$style.header"><slot name="header"></slot></div> + <div :class="$style.body"><slot name="body"></slot></div> +</div> +</template> + +<script setup lang="ts"> +withDefaults(defineProps<{ + showHeader?: boolean; +}>(), { + showHeader: true, +}); +</script> + +<style module lang="scss"> +.timelineRoot { + background-color: var(--panel); + height: 100%; + max-height: var(--embedMaxHeight, none); + display: flex; + flex-direction: column; +} + +.header { + flex-shrink: 0; + border-bottom: 1px solid var(--divider); +} + +.body { + flex-grow: 1; + overflow-y: auto; +} +</style> diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue new file mode 100644 index 0000000000000000000000000000000000000000..94424cab28913417610e5012283909ea3c220266 --- /dev/null +++ b/packages/frontend-embed/src/components/EmUrl.vue @@ -0,0 +1,96 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<component + :is="self ? EmA : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target" + @contextmenu.stop="() => {}" +> + <template v-if="!self"> + <span :class="$style.schema">{{ schema }}//</span> + <span :class="$style.hostname">{{ hostname }}</span> + <span v-if="port != ''">:{{ port }}</span> + </template> + <template v-if="pathname === '/' && self"> + <span :class="$style.self">{{ hostname }}</span> + </template> + <span v-if="pathname != ''" :class="$style.pathname">{{ self ? pathname.substring(1) : pathname }}</span> + <span :class="$style.query">{{ query }}</span> + <span :class="$style.hash">{{ hash }}</span> + <i v-if="target === '_blank'" :class="$style.icon" class="ti ti-external-link"></i> +</component> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import { toUnicode as decodePunycode } from 'punycode/'; +import EmA from './EmA.vue'; +import { url as local } from '@@/js/config.js'; + +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} + +const props = withDefaults(defineProps<{ + url: string; + rel?: string; + showUrlPreview?: boolean; +}>(), { + showUrlPreview: true, +}); + +const self = props.url.startsWith(local); +const url = new URL(props.url); +if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url'); +const el = ref(); + +const schema = url.protocol; +const hostname = decodePunycode(url.hostname); +const port = url.port; +const pathname = safeURIDecode(url.pathname); +const query = safeURIDecode(url.search); +const hash = safeURIDecode(url.hash); +const attr = self ? 'to' : 'href'; +const target = self ? null : '_blank'; +</script> + +<style lang="scss" module> +.root { + word-break: break-all; +} + +.icon { + padding-left: 2px; + font-size: .9em; +} + +.self { + font-weight: bold; +} + +.schema { + opacity: 0.5; +} + +.hostname { + font-weight: bold; +} + +.pathname { + opacity: 0.8; +} + +.query { + opacity: 0.5; +} + +.hash { + font-style: italic; +} +</style> diff --git a/packages/frontend-embed/src/components/EmUserName.vue b/packages/frontend-embed/src/components/EmUserName.vue new file mode 100644 index 0000000000000000000000000000000000000000..c0c7c443caebb7045ad59c792aacb31dce2a17ca --- /dev/null +++ b/packages/frontend-embed/src/components/EmUserName.vue @@ -0,0 +1,21 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<EmMfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emojiUrls="user.emojis"/> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import * as Misskey from 'misskey-js'; +import EmMfm from './EmMfm.js'; + +const props = withDefaults(defineProps<{ + user: Misskey.entities.User; + nowrap?: boolean; +}>(), { + nowrap: true, +}); +</script> diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue new file mode 100644 index 0000000000000000000000000000000000000000..b621110ec9ede8f68dc5df979627d27036176dd2 --- /dev/null +++ b/packages/frontend-embed/src/components/I18n.vue @@ -0,0 +1,51 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<render/> +</template> + +<script setup lang="ts" generic="T extends string | ParameterizedString"> +import { computed, h } from 'vue'; +import type { ParameterizedString } from '../../../../locales/index.js'; + +const props = withDefaults(defineProps<{ + src: T; + tag?: string; + // eslint-disable-next-line vue/require-default-prop + textTag?: string; +}>(), { + tag: 'span', +}); + +const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: () => unknown } : NonNullable<unknown>>(); + +const parsed = computed(() => { + let str = props.src as string; + const value: (string | { arg: string; })[] = []; + for (;;) { + const nextBracketOpen = str.indexOf('{'); + const nextBracketClose = str.indexOf('}'); + + if (nextBracketOpen === -1) { + value.push(str); + break; + } else { + if (nextBracketOpen > 0) value.push(str.substring(0, nextBracketOpen)); + value.push({ + arg: str.substring(nextBracketOpen + 1, nextBracketClose), + }); + } + + str = str.substring(nextBracketClose + 1); + } + + return value; +}); + +const render = () => { + return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); +}; +</script> diff --git a/packages/frontend-embed/src/custom-emojis.ts b/packages/frontend-embed/src/custom-emojis.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5b40885c1d441064ea410859ce5af8b526a3f33 --- /dev/null +++ b/packages/frontend-embed/src/custom-emojis.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { shallowRef, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import { misskeyApi, misskeyApiGet } from '@/misskey-api.js'; + +function get(key: string) { + const value = localStorage.getItem(key); + if (value === null) return null; + return JSON.parse(value); +} + +function set(key: string, value: any) { + localStorage.setItem(key, JSON.stringify(value)); +} + +const storageCache = await get('emojis'); +export const customEmojis = shallowRef<Misskey.entities.EmojiSimple[]>(Array.isArray(storageCache) ? storageCache : []); + +export const customEmojisMap = new Map<string, Misskey.entities.EmojiSimple>(); +watch(customEmojis, emojis => { + customEmojisMap.clear(); + for (const emoji of emojis) { + customEmojisMap.set(emoji.name, emoji); + } +}, { immediate: true }); + +export async function fetchCustomEmojis(force = false) { + const now = Date.now(); + + let res; + if (force) { + res = await misskeyApi('emojis', {}); + } else { + const lastFetchedAt = await get('lastEmojisFetchedAt'); + if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return; + res = await misskeyApiGet('emojis', {}); + } + + customEmojis.value = res.emojis; + set('emojis', res.emojis); + set('lastEmojisFetchedAt', now); +} + +let cachedTags; +export function getCustomEmojiTags() { + if (cachedTags) return cachedTags; + + const tags = new Set(); + for (const emoji of customEmojis.value) { + for (const tag of emoji.aliases) { + tags.add(tag); + } + } + const res = Array.from(tags); + cachedTags = res; + return res; +} diff --git a/packages/frontend-embed/src/di.ts b/packages/frontend-embed/src/di.ts new file mode 100644 index 0000000000000000000000000000000000000000..22f62766309a93ec7394fd3f177ad2bf6657c634 --- /dev/null +++ b/packages/frontend-embed/src/di.ts @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { InjectionKey } from 'vue'; +import * as Misskey from 'misskey-js'; +import { MediaProxy } from '@@/js/media-proxy.js'; +import type { ParsedEmbedParams } from '@@/js/embed-page.js'; +import type { ServerContext } from '@/server-context.js'; + +export const DI = { + serverMetadata: Symbol() as InjectionKey<Misskey.entities.MetaDetailed>, + embedParams: Symbol() as InjectionKey<ParsedEmbedParams>, + serverContext: Symbol() as InjectionKey<ServerContext>, + mediaProxy: Symbol() as InjectionKey<MediaProxy>, +}; diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ad503b0898a53004660609444b91ddaba9ea160 --- /dev/null +++ b/packages/frontend-embed/src/i18n.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { markRaw } from 'vue'; +import { I18n } from '@@/js/i18n.js'; +import type { Locale } from '../../../locales/index.js'; +import { locale } from '@@/js/config.js'; + +export const i18n = markRaw(new I18n<Locale>(locale, _DEV_)); + +export function updateI18n(newLocale: Locale) { + i18n.locale = newLocale; +} diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html new file mode 100644 index 0000000000000000000000000000000000000000..55be2f4ec14c49a0ba10081d2b14c82adb825564 --- /dev/null +++ b/packages/frontend-embed/src/index.html @@ -0,0 +1,36 @@ +<!-- + SPDX-FileCopyrightText: syuilo and misskey-project + SPDX-License-Identifier: AGPL-3.0-only +--> + +<!-- + 開発モードã®viteã¯ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’起点ã«ã‚µãƒ¼ãƒãƒ¼ã‚’èµ·å‹•ã—ã¾ã™ã€‚ + ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã«æ›¸ã‹ã‚ŒãŸ [t]js ã®ãƒªãƒ³ã‚¯ã¨ (s)cssã®ãƒªãƒ³ã‚¯ã¨ã€ãã®ä¾å˜é–¢ä¿‚ã«ã‚るファイルã¯ãƒ“ルドã•ã‚Œã¾ã™ +--> + +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8" /> + <title>[DEV] Loading...</title> + <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> + <meta + http-equiv="Content-Security-Policy" + content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/; + worker-src 'self'; + script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh https://cdn.jsdelivr.net; + style-src 'self' 'unsafe-inline'; + img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; + media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; + connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com; + frame-src *;" + /> + <meta property="og:site_name" content="[DEV BUILD] Sharkey" /> + <meta name="viewport" content="width=device-width, initial-scale=1"> +</head> + +<body> +<div id="sharkey_app"></div> +<script type="module" src="./boot.ts"></script> +</body> +</html> diff --git a/packages/frontend-embed/src/misskey-api.ts b/packages/frontend-embed/src/misskey-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..0d3c679359741f7ba0fabc40dce62aa62db0faaf --- /dev/null +++ b/packages/frontend-embed/src/misskey-api.ts @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { ref } from 'vue'; +import { apiUrl } from '@@/js/config.js'; + +export const pendingApiRequestsCount = ref(0); + +// Implements Misskey.api.ApiClient.request +export function misskeyApi< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( + endpoint: E, + data: P = {} as any, + signal?: AbortSignal, +): Promise<_ResT> { + if (endpoint.includes('://')) throw new Error('invalid endpoint'); + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const promise = new Promise<_ResT>((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: 'omit', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + signal, + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(undefined as _ResT); // void -> undefined + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} + +// Implements Misskey.api.ApiClient.request +export function misskeyApiGet< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( + endpoint: E, + data: P = {} as any, +): Promise<_ResT> { + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const query = new URLSearchParams(data as any); + + const promise = new Promise<_ResT>((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}?${query}`, { + method: 'GET', + credentials: 'omit', + cache: 'default', + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(undefined as _ResT); // void -> undefined + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue new file mode 100644 index 0000000000000000000000000000000000000000..2528dc4b807f9f2f4d4c1c67638d0697cb6c8a5b --- /dev/null +++ b/packages/frontend-embed/src/pages/clip.vue @@ -0,0 +1,143 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <EmTimelineContainer v-if="clip" :showHeader="embedParams.header"> + <template #header> + <div :class="$style.clipHeader"> + <div :class="$style.headerClipIconRoot"> + <i class="ti ti-paperclip"></i> + </div> + <div :class="$style.headerTitle" @click="top"> + <div class="_nowrap"><a :href="`/clips/${clip.id}`" target="_blank" rel="noopener">{{ clip.name }}</a></div> + <div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div> + </div> + <a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer"> + <img + :class="$style.instanceIcon" + :src="serverMetadata.iconUrl || '/favicon.ico'" + /> + </a> + </div> + </template> + <template #body> + <EmNotes + ref="notesEl" + :pagination="pagination" + :disableAutoLoad="!embedParams.autoload" + :noGap="true" + :ad="false" + /> + </template> + </EmTimelineContainer> + <XNotFound v-else/> +</div> +</template> + +<script setup lang="ts"> +import { ref, computed, inject, useTemplateRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import { scrollToTop } from '@@/js/scroll.js'; +import { url, instanceName } from '@@/js/config.js'; +import { isLink } from '@@/js/is-link.js'; +import { defaultEmbedParams } from '@@/js/embed-page.js'; +import type { Paging } from '@/components/EmPagination.vue'; +import EmNotes from '@/components/EmNotes.vue'; +import XNotFound from '@/pages/not-found.vue'; +import EmTimelineContainer from '@/components/EmTimelineContainer.vue'; +import { misskeyApi } from '@/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { assertServerContext } from '@/server-context.js'; +import { DI } from '@/di.js'; + +const props = defineProps<{ + clipId: string; +}>(); + +const embedParams = inject(DI.embedParams, defaultEmbedParams); + +const serverMetadata = inject(DI.serverMetadata)!; + +const serverContext = inject(DI.serverContext)!; + +const clip = ref<Misskey.entities.Clip | null>(); + +if (assertServerContext(serverContext, 'clip')) { + clip.value = serverContext.clip; +} else { + clip.value = await misskeyApi('clips/show', { + clipId: props.clipId, + }).catch(() => { + return null; + }); +} + +const pagination = computed(() => ({ + endpoint: 'clips/notes', + params: { + clipId: props.clipId, + }, +} as Paging)); + +const notesEl = useTemplateRef('notesEl'); + +function top(ev: MouseEvent) { + const target = ev.target as HTMLElement | null; + if (target && isLink(target)) return; + + if (notesEl.value) { + scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' }); + } +} +</script> + +<style lang="scss" module> +.clipHeader { + padding: 8px 16px; + display: flex; + min-width: 0; + align-items: center; + gap: var(--margin); + overflow: hidden; + + .headerClipIconRoot { + flex-shrink: 0; + width: 32px; + height: 32px; + line-height: 32px; + font-size: 14px; + text-align: center; + background-color: var(--accentedBg); + color: var(--accent); + border-radius: 50%; + } + + .headerTitle { + flex-grow: 1; + font-weight: 700; + line-height: 1.1; + min-width: 0; + + .sub { + font-size: 0.8em; + font-weight: 400; + opacity: 0.7; + } + } + + .instanceIconLink { + flex-shrink: 0; + display: block; + margin-left: auto; + height: 24px; + } + + .instanceIcon { + height: 24px; + border-radius: 3px; + } +} +</style> diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue new file mode 100644 index 0000000000000000000000000000000000000000..bbb03b4e642d25bf820adc01aa7d43c66ba46bc3 --- /dev/null +++ b/packages/frontend-embed/src/pages/not-found.vue @@ -0,0 +1,24 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <div class="_fullinfo"> + <img :src="notFoundImageUrl" class="_ghost"/> + <div>{{ i18n.ts.notFoundDescription }}</div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { inject, computed } from 'vue'; +import { DEFAULT_NOT_FOUND_IMAGE_URL } from '@@/js/const.js'; +import { DI } from '@/di.js'; +import { i18n } from '@/i18n.js'; + +const serverMetadata = inject(DI.serverMetadata)!; + +const notFoundImageUrl = computed(() => serverMetadata?.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL); +</script> diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue new file mode 100644 index 0000000000000000000000000000000000000000..6f6c8c0f639b41f9a436c6b962239bb5e33c63bf --- /dev/null +++ b/packages/frontend-embed/src/pages/note.vue @@ -0,0 +1,52 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.noteEmbedRoot"> + <EmNoteDetailed v-if="note && !prohibited" :note="note"/> + <XNotFound v-else/> +</div> +</template> + +<script setup lang="ts"> +import { inject, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import EmNoteDetailed from '@/components/EmNoteDetailed.vue'; +import XNotFound from '@/pages/not-found.vue'; +import { DI } from '@/di.js'; +import { misskeyApi } from '@/misskey-api.js'; +import { assertServerContext } from '@/server-context'; + +const props = defineProps<{ + noteId: string; +}>(); + +const serverContext = inject(DI.serverContext)!; + +const note = ref<Misskey.entities.Note | null>(null); + +const prohibited = ref(false); + +if (assertServerContext(serverContext, 'note')) { + note.value = serverContext.note; +} else { + note.value = await misskeyApi('notes/show', { + noteId: props.noteId, + }).catch(() => { + return null; + }); +} + +if (note.value?.url != null || note.value?.uri != null) { + // リモートサーãƒãƒ¼ã®ãƒŽãƒ¼ãƒˆã¯å¼¾ã + prohibited.value = true; +} +</script> + +<style lang="scss" module> +.noteEmbedRoot { + background-color: var(--panel); +} +</style> diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue new file mode 100644 index 0000000000000000000000000000000000000000..b481b3ebe5527c6c3b45eba2c5d52a3974d89746 --- /dev/null +++ b/packages/frontend-embed/src/pages/tag.vue @@ -0,0 +1,126 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <EmTimelineContainer v-if="tag" :showHeader="embedParams.header"> + <template #header> + <div :class="$style.clipHeader"> + <div :class="$style.headerClipIconRoot"> + <i class="ti ti-hash"></i> + </div> + <div :class="$style.headerTitle" @click="top"> + <div class="_nowrap"><a :href="`/tags/${tag}`" target="_blank" rel="noopener">#{{ tag }}</a></div> + <div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div> + </div> + <a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer"> + <img + :class="$style.instanceIcon" + :src="serverMetadata.iconUrl || '/favicon.ico'" + /> + </a> + </div> + </template> + <template #body> + <EmNotes + ref="notesEl" + :pagination="pagination" + :disableAutoLoad="!embedParams.autoload" + :noGap="true" + :ad="false" + /> + </template> + </EmTimelineContainer> + <XNotFound v-else/> +</div> +</template> + +<script setup lang="ts"> +import { computed, inject, useTemplateRef } from 'vue'; +import { scrollToTop } from '@@/js/scroll.js'; +import type { Paging } from '@/components/EmPagination.vue'; +import EmNotes from '@/components/EmNotes.vue'; +import XNotFound from '@/pages/not-found.vue'; +import EmTimelineContainer from '@/components/EmTimelineContainer.vue'; +import { i18n } from '@/i18n.js'; +import { url, instanceName } from '@@/js/config.js'; +import { isLink } from '@@/js/is-link.js'; +import { DI } from '@/di.js'; +import { defaultEmbedParams } from '@@/js/embed-page.js'; + +const props = defineProps<{ + tag: string; +}>(); + +const serverMetadata = inject(DI.serverMetadata)!; + +const embedParams = inject(DI.embedParams, defaultEmbedParams); + +const pagination = computed(() => ({ + endpoint: 'notes/search-by-tag', + params: { + tag: props.tag, + }, +} as Paging)); + +const notesEl = useTemplateRef('notesEl'); + +function top(ev: MouseEvent) { + const target = ev.target as HTMLElement | null; + if (target && isLink(target)) return; + + if (notesEl.value) { + scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' }); + } +} +</script> + +<style lang="scss" module> +.clipHeader { + padding: 8px 16px; + display: flex; + min-width: 0; + align-items: center; + gap: var(--margin); + overflow: hidden; + + .headerClipIconRoot { + flex-shrink: 0; + width: 32px; + height: 32px; + line-height: 32px; + font-size: 14px; + text-align: center; + background-color: var(--accentedBg); + color: var(--accent); + border-radius: 50%; + } + + .headerTitle { + flex-grow: 1; + font-weight: 700; + line-height: 1.1; + min-width: 0; + + .sub { + font-size: 0.8em; + font-weight: 400; + opacity: 0.7; + } + } + + .instanceIconLink { + flex-shrink: 0; + display: block; + margin-left: auto; + height: 24px; + } + + .instanceIcon { + height: 24px; + border-radius: 3px; + } +} +</style> diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue new file mode 100644 index 0000000000000000000000000000000000000000..85e6f52d5079384c202b2b925490b0af9671f76a --- /dev/null +++ b/packages/frontend-embed/src/pages/user-timeline.vue @@ -0,0 +1,158 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <EmTimelineContainer v-if="user && !prohibited" :showHeader="embedParams.header"> + <template #header> + <div :class="$style.userHeader"> + <a :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer" :class="$style.avatarLink"> + <EmAvatar :class="$style.avatar" :user="user"/> + </a> + <div :class="$style.headerTitle" @click="top"> + <I18n :src="i18n.ts.noteOf" tag="div" class="_nowrap"> + <template #user> + <a v-if="user != null" :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer"> + <EmUserName :user="user"/> + </a> + <span v-else>{{ i18n.ts.user }}</span> + </template> + </I18n> + <div :class="$style.sub">{{ i18n.tsx.fromX({ x: instanceName }) }}</div> + </div> + <a :href="url" :class="$style.instanceIconLink" target="_blank" rel="noopener noreferrer"> + <img + :class="$style.instanceIcon" + :src="serverMetadata.iconUrl || '/favicon.ico'" + /> + </a> + </div> + </template> + <template #body> + <EmNotes + ref="notesEl" + :pagination="pagination" + :disableAutoLoad="!embedParams.autoload" + :noGap="true" + :ad="false" + /> + </template> + </EmTimelineContainer> + <XNotFound v-else/> +</div> +</template> + +<script setup lang="ts"> +import { ref, computed, inject, useTemplateRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import { url, instanceName } from '@@/js/config.js'; +import { defaultEmbedParams } from '@@/js/embed-page.js'; +import type { Paging } from '@/components/EmPagination.vue'; +import EmNotes from '@/components/EmNotes.vue'; +import EmAvatar from '@/components/EmAvatar.vue'; +import EmUserName from '@/components/EmUserName.vue'; +import I18n from '@/components/I18n.vue'; +import XNotFound from '@/pages/not-found.vue'; +import EmTimelineContainer from '@/components/EmTimelineContainer.vue'; +import { scrollToTop } from '@@/js/scroll.js'; +import { isLink } from '@@/js/is-link.js'; +import { misskeyApi } from '@/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { assertServerContext } from '@/server-context.js'; +import { DI } from '@/di.js'; + +const props = defineProps<{ + userId: string; +}>(); + +const embedParams = inject(DI.embedParams, defaultEmbedParams); + +const serverMetadata = inject(DI.serverMetadata)!; + +const serverContext = inject(DI.serverContext)!; + +const user = ref<Misskey.entities.UserLite | null>(); + +const prohibited = ref(false); + +if (assertServerContext(serverContext, 'user')) { + user.value = serverContext.user; +} else { + user.value = await misskeyApi('users/show', { + userId: props.userId, + }).catch(() => { + return null; + }); +} + +if (user.value?.host != null) { + // リモートサーãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯å¼¾ã + prohibited.value = true; +} + +const pagination = computed(() => ({ + endpoint: 'users/notes', + params: { + userId: user.value?.id, + }, +} as Paging)); + +const notesEl = useTemplateRef('notesEl'); + +function top(ev: MouseEvent) { + const target = ev.target as HTMLElement | null; + if (target && isLink(target)) return; + + if (notesEl.value) { + scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' }); + } +} +</script> + +<style lang="scss" module> +.userHeader { + padding: 8px 16px; + display: flex; + min-width: 0; + align-items: center; + gap: var(--margin); + overflow: hidden; + + .avatarLink { + display: block; + } + + .avatar { + display: inline-block; + width: 32px; + height: 32px; + } + + .headerTitle { + flex-grow: 1; + font-weight: 700; + line-height: 1.1; + min-width: 0; + + .sub { + font-size: 0.8em; + font-weight: 400; + opacity: 0.7; + } + } + + .instanceIconLink { + flex-shrink: 0; + display: block; + margin-left: auto; + height: 24px; + } + + .instanceIcon { + height: 24px; + border-radius: 3px; + } +} +</style> diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts new file mode 100644 index 0000000000000000000000000000000000000000..fd8eb8a5d2a094b3c76187db48299b44e56aa41f --- /dev/null +++ b/packages/frontend-embed/src/post-message.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const postMessageEventTypes = [ + 'misskey:embed:ready', + 'misskey:embed:changeHeight', +] as const; + +export type PostMessageEventType = typeof postMessageEventTypes[number]; + +export interface PostMessageEventPayload extends Record<PostMessageEventType, any> { + 'misskey:embed:ready': undefined; + 'misskey:embed:changeHeight': { + height: number; + }; +} + +export type MiPostMessageEvent<T extends PostMessageEventType = PostMessageEventType> = { + type: T; + iframeId?: string; + payload?: PostMessageEventPayload[T]; +} + +let defaultIframeId: string | null = null; + +export function setIframeId(id: string): void { + if (defaultIframeId != null) return; + + if (_DEV_) console.log('setIframeId', id); + defaultIframeId = id; +} + +/** + * 親フレームã«ã‚¤ãƒ™ãƒ³ãƒˆã‚’é€ä¿¡ + */ +export function postMessageToParentWindow<T extends PostMessageEventType = PostMessageEventType>(type: T, payload?: PostMessageEventPayload[T], iframeId: string | null = null): void { + let _iframeId = iframeId; + if (_iframeId == null) { + _iframeId = defaultIframeId; + } + if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload); + window.parent.postMessage({ + type, + iframeId: _iframeId, + payload, + }, '*'); +} diff --git a/packages/frontend-embed/src/server-context.ts b/packages/frontend-embed/src/server-context.ts new file mode 100644 index 0000000000000000000000000000000000000000..a84a1a726a03c92cba081891d20e4f4a6879c1fd --- /dev/null +++ b/packages/frontend-embed/src/server-context.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import * as Misskey from 'misskey-js'; + +const providedContextEl = document.getElementById('misskey_embedCtx'); + +export type ServerContext = { + clip?: Misskey.entities.Clip; + note?: Misskey.entities.Note; + user?: Misskey.entities.UserLite; +} | null; + +// NOTE: devモードã®ã¨ãã—ã‹ embedCtx ㌠null ã«ãªã‚‹ã“ã¨ã¯ç„¡ã„ +export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null; + +export function assertServerContext<K extends keyof NonNullable<ServerContext>>(ctx: ServerContext, entity: K): ctx is Required<Pick<NonNullable<ServerContext>, K>> { + if (ctx == null) return false; + return entity in ctx; +} diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c94aacd4840ef0fd732f72d757ce4af986e2705 --- /dev/null +++ b/packages/frontend-embed/src/server-metadata.ts @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { misskeyApi } from '@/misskey-api.js'; + +const providedMetaEl = document.getElementById('misskey_meta'); + +const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null; + +// NOTE: devモードã®ã¨ãã—ã‹ _serverMetadata ㌠null ã«ãªã‚‹ã“ã¨ã¯ç„¡ã„ +export const serverMetadata: Misskey.entities.MetaDetailed = _serverMetadata ?? await misskeyApi('meta', { + detail: true, +}); diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss new file mode 100644 index 0000000000000000000000000000000000000000..979e959f52c60d9a57274281e1602315f6b95222 --- /dev/null +++ b/packages/frontend-embed/src/style.scss @@ -0,0 +1,477 @@ +@charset "utf-8"; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +:root { + --radius: 12px; + --marginFull: 14px; + --marginHalf: 10px; + + --margin: var(--marginFull); +} + +html { + background-color: transparent; + color-scheme: light dark; + color: var(--fg); + accent-color: var(--accent); + overflow: clip; + overflow-wrap: break-word; + font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; + font-size: 14px; + line-height: 1.35; + text-size-adjust: 100%; + tab-size: 2; + -webkit-text-size-adjust: 100%; + + &, * { + scrollbar-color: var(--scrollbarHandle) transparent; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-track { + background: inherit; + } + + &::-webkit-scrollbar-thumb { + background: var(--scrollbarHandle); + + &:hover { + background: var(--scrollbarHandleHover); + } + + &:active { + background: var(--accent); + } + } + } +} + +html, body { + height: 100%; + touch-action: manipulation; + margin: 0; + padding: 0; + scroll-behavior: smooth; +} + +#sharkey_app { + height: 100%; +} + +a { + text-decoration: none; + cursor: pointer; + color: inherit; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + + &:focus-visible { + outline-offset: 2px; + } + + &:hover { + text-decoration: underline; + } + + &[target="_blank"] { + -webkit-touch-callout: default; + } +} + +rt { + white-space: initial; +} + +:focus-visible { + outline: var(--focus) solid 2px; + outline-offset: -2px; + + &:hover { + text-decoration: none; + } +} + +.ti { + width: 1.28em; + vertical-align: -12%; + line-height: 1em; + + &::before { + font-size: 128%; + } +} + +.ti-fw { + display: inline-block; + text-align: center; +} + +._nowrap { + white-space: pre !important; + word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html + overflow: hidden; + text-overflow: ellipsis; +} + +._button { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + appearance: none; + display: inline-block; + padding: 0; + margin: 0; // for Safari + background: none; + border: none; + cursor: pointer; + color: inherit; + touch-action: manipulation; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + font-size: 1em; + font-family: inherit; + line-height: inherit; + max-width: 100%; + + &:disabled { + opacity: 0.5; + cursor: default; + } +} + +._buttonGray { + @extend ._button; + background: var(--buttonBg); + + &:not(:disabled):hover { + background: var(--buttonHoverBg); + } +} + +._buttonPrimary { + @extend ._button; + color: var(--fgOnAccent); + background: var(--accent); + + &:not(:disabled):hover { + background: hsl(from var(--accent) h s calc(l + 5)); + } + + &:not(:disabled):active { + background: hsl(from var(--accent) h s calc(l - 5)); + } +} + +._buttonGradate { + @extend ._buttonPrimary; + color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + + &:not(:disabled):hover { + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + } + + &:not(:disabled):active { + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + } +} + +._buttonRounded { + font-size: 0.95em; + padding: 0.5em 1em; + min-width: 100px; + border-radius: 99rem; + + &._buttonPrimary, + &._buttonGradate { + font-weight: 700; + } +} + +._help { + color: var(--accent); + cursor: help; +} + +._textButton { + @extend ._button; + color: var(--accent); + + &:focus-visible { + outline-offset: 2px; + } + + &:not(:disabled):hover { + text-decoration: underline; + } +} + +._panel { + background: var(--panel); + border-radius: var(--radius); + overflow: clip; +} + +._margin { + margin: var(--margin) 0; +} + +._gaps_m { + display: flex; + flex-direction: column; + gap: 1.5em; +} + +._gaps_s { + display: flex; + flex-direction: column; + gap: 0.75em; +} + +._gaps { + display: flex; + flex-direction: column; + gap: var(--margin); +} + +._buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +._buttonsCenter { + @extend ._buttons; + + justify-content: center; +} + +._borderButton { + @extend ._button; + display: block; + width: 100%; + padding: 10px; + box-sizing: border-box; + text-align: center; + border: solid 0.5px var(--divider); + border-radius: var(--radius); + + &:active { + border-color: var(--accent); + } +} + +._popup { + background: var(--popup); + border-radius: var(--radius); + contain: content; +} + +._acrylic { + background: var(--acrylicPanel); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} + +._fullinfo { + padding: 64px 32px; + text-align: center; + + > img { + vertical-align: bottom; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; + } +} + +._link { + color: var(--link); +} + +._caption { + font-size: 0.8em; + opacity: 0.7; +} + +._monospace { + font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; +} + +// MFM ----------------------------- + +bdi.block { display: block } + +._mfm_blur_ { + filter: blur(6px); + transition: filter 0.3s; + + &:hover { + filter: blur(0px); + } +} + +.mfm-x2 { + --mfm-zoom-size: 200%; +} + +.mfm-x3 { + --mfm-zoom-size: 400%; +} + +.mfm-x4 { + --mfm-zoom-size: 600%; +} + +.mfm-x2, .mfm-x3, .mfm-x4 { + font-size: var(--mfm-zoom-size); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* only half effective */ + font-size: calc(var(--mfm-zoom-size) / 2 + 50%); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* disabled */ + font-size: 100%; + } + } +} + +._mfm_rainbow_fallback_ { + background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +@keyframes mfm-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes mfm-spinX { + 0% { transform: perspective(128px) rotateX(0deg); } + 100% { transform: perspective(128px) rotateX(360deg); } +} + +@keyframes mfm-spinY { + 0% { transform: perspective(128px) rotateY(0deg); } + 100% { transform: perspective(128px) rotateY(360deg); } +} + +@keyframes mfm-jump { + 0% { transform: translateY(0); } + 25% { transform: translateY(-16px); } + 50% { transform: translateY(0); } + 75% { transform: translateY(-8px); } + 100% { transform: translateY(0); } +} + +@keyframes mfm-bounce { + 0% { transform: translateY(0) scale(1, 1); } + 25% { transform: translateY(-16px) scale(1, 1); } + 50% { transform: translateY(0) scale(1, 1); } + 75% { transform: translateY(0) scale(1.5, 0.75); } + 100% { transform: translateY(0) scale(1, 1); } +} + +// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-twitch { + 0% { transform: translate(7px, -2px) } + 5% { transform: translate(-3px, 1px) } + 10% { transform: translate(-7px, -1px) } + 15% { transform: translate(0px, -1px) } + 20% { transform: translate(-8px, 6px) } + 25% { transform: translate(-4px, -3px) } + 30% { transform: translate(-4px, -6px) } + 35% { transform: translate(-8px, -8px) } + 40% { transform: translate(4px, 6px) } + 45% { transform: translate(-3px, 1px) } + 50% { transform: translate(2px, -10px) } + 55% { transform: translate(-7px, 0px) } + 60% { transform: translate(-2px, 4px) } + 65% { transform: translate(3px, -8px) } + 70% { transform: translate(6px, 7px) } + 75% { transform: translate(-7px, -2px) } + 80% { transform: translate(-7px, -8px) } + 85% { transform: translate(9px, 3px) } + 90% { transform: translate(-3px, -2px) } + 95% { transform: translate(-10px, 2px) } + 100% { transform: translate(-2px, -6px) } +} + +// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-shake { + 0% { transform: translate(-3px, -1px) rotate(-8deg) } + 5% { transform: translate(0px, -1px) rotate(-10deg) } + 10% { transform: translate(1px, -3px) rotate(0deg) } + 15% { transform: translate(1px, 1px) rotate(11deg) } + 20% { transform: translate(-2px, 1px) rotate(1deg) } + 25% { transform: translate(-1px, -2px) rotate(-2deg) } + 30% { transform: translate(-1px, 2px) rotate(-3deg) } + 35% { transform: translate(2px, 1px) rotate(6deg) } + 40% { transform: translate(-2px, -3px) rotate(-9deg) } + 45% { transform: translate(0px, -1px) rotate(-12deg) } + 50% { transform: translate(1px, 2px) rotate(10deg) } + 55% { transform: translate(0px, -3px) rotate(8deg) } + 60% { transform: translate(1px, -1px) rotate(8deg) } + 65% { transform: translate(0px, -1px) rotate(-7deg) } + 70% { transform: translate(-1px, -3px) rotate(6deg) } + 75% { transform: translate(0px, -2px) rotate(4deg) } + 80% { transform: translate(-2px, -1px) rotate(3deg) } + 85% { transform: translate(1px, -3px) rotate(-10deg) } + 90% { transform: translate(1px, 0px) rotate(3deg) } + 95% { transform: translate(-2px, 0px) rotate(-3deg) } + 100% { transform: translate(2px, 1px) rotate(2deg) } +} + +@keyframes mfm-rubberBand { + from { transform: scale3d(1, 1, 1); } + 30% { transform: scale3d(1.25, 0.75, 1); } + 40% { transform: scale3d(0.75, 1.25, 1); } + 50% { transform: scale3d(1.15, 0.85, 1); } + 65% { transform: scale3d(0.95, 1.05, 1); } + 75% { transform: scale3d(1.05, 0.95, 1); } + to { transform: scale3d(1, 1, 1); } +} + +@keyframes mfm-rainbow { + 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); } + 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); } +} + +@keyframes mfm-fade { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@media (prefers-reduced-motion) { + @keyframes mfm-spin { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-spinX { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-spinY { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-jump { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-bounce { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-twitch { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-shake { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-rubberBand { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-rainbow { 0% { transform: none; filter: none; opacity: 1 } } + @keyframes mfm-fade { 0% { transform: none; filter: none; opacity: 1 } } +} diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..23e70cd0d375c5246141399c8f800592e9b9cd8e --- /dev/null +++ b/packages/frontend-embed/src/theme.ts @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; +import type { BundledTheme } from 'shiki/themes'; + +export type Theme = { + id: string; + name: string; + author: string; + desc?: string; + base?: 'dark' | 'light'; + props: Record<string, string>; + codeHighlighter?: { + base: BundledTheme; + overrides?: Record<string, any>; + } | { + base: '_none_'; + overrides: Record<string, any>; + }; +}; + +let timeout: number | null = null; + +export function assertIsTheme(theme: Record<string, unknown>): theme is Theme { + return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme; +} + +export function applyTheme(theme: Theme, persist = true) { + if (timeout) window.clearTimeout(timeout); + + document.documentElement.classList.add('_themeChanging_'); + + timeout = window.setTimeout(() => { + document.documentElement.classList.remove('_themeChanging_'); + }, 1000); + + const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; + + document.documentElement.dataset.colorScheme = colorScheme; + + // Deep copy + const _theme = JSON.parse(JSON.stringify(theme)); + + if (_theme.base) { + const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); + if (base) _theme.props = Object.assign({}, base.props, _theme.props); + } + + const props = compile(_theme); + + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', props['htmlThemeColor']); + break; + } + } + + for (const [k, v] of Object.entries(props)) { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + } + + // iframeã‚’æ£å¸¸ã«é€éŽã•ã›ã‚‹ãŸã‚ã«ã€cssã®color-scheme㯠`light dark;` 固定ã«ã—ã¦ã‚る。style.scsså‚ç…§ +} + +function compile(theme: Theme): Record<string, string> { + function getColor(val: string): tinycolor.Instance { + if (val[0] === '@') { // ref (prop) + return getColor(theme.props[val.substring(1)]); + } else if (val[0] === '$') { // ref (const) + return getColor(theme.props[val]); + } else if (val[0] === ':') { // func + const parts = val.split('<'); + const func = parts.shift().substring(1); + const arg = parseFloat(parts.shift()); + const color = getColor(parts.join('<')); + + switch (func) { + case 'darken': return color.darken(arg); + case 'lighten': return color.lighten(arg); + case 'alpha': return color.setAlpha(arg); + case 'hue': return color.spin(arg); + case 'saturate': return color.saturate(arg); + } + } + + // other case + return tinycolor(val); + } + + const props = {}; + + for (const [k, v] of Object.entries(theme.props)) { + if (k.startsWith('$')) continue; // ignore const + + props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); + } + + return props; +} + +function genValue(c: tinycolor.Instance): string { + return c.toRgbString(); +} diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue new file mode 100644 index 0000000000000000000000000000000000000000..8da5f46a96e80069e3777b568e448ade3984cf2a --- /dev/null +++ b/packages/frontend-embed/src/ui.vue @@ -0,0 +1,110 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + ref="rootEl" + :class="[ + $style.rootForEmbedPage, + { + [$style.rounded]: embedRounded, + [$style.noBorder]: embedNoBorder, + } + ]" + :style="maxHeight > 0 ? { maxHeight: `${maxHeight}px`, '--embedMaxHeight': `${maxHeight}px` } : {}" +> + <div + :class="$style.routerViewContainer" + > + <Suspense :timeout="0"> + <EmNotePage v-if="page === 'notes'" :noteId="contentId"/> + <EmUserTimelinePage v-else-if="page === 'user-timeline'" :userId="contentId"/> + <EmClipPage v-else-if="page === 'clips'" :clipId="contentId"/> + <EmTagPage v-else-if="page === 'tags'" :tag="contentId"/> + <XNotFound v-else/> + <template #fallback> + <EmLoading/> + </template> + </Suspense> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref, shallowRef, onMounted, onUnmounted, inject } from 'vue'; +import { postMessageToParentWindow } from '@/post-message.js'; +import { DI } from '@/di.js'; +import { defaultEmbedParams } from '@@/js/embed-page.js'; +import EmNotePage from '@/pages/note.vue'; +import EmUserTimelinePage from '@/pages/user-timeline.vue'; +import EmClipPage from '@/pages/clip.vue'; +import EmTagPage from '@/pages/tag.vue'; +import XNotFound from '@/pages/not-found.vue'; +import EmLoading from '@/components/EmLoading.vue'; + +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} + +const page = location.pathname.split('/')[2]; +const contentId = safeURIDecode(location.pathname.split('/')[3]); +if (_DEV_) console.log(page, contentId); + +const embedParams = inject(DI.embedParams, defaultEmbedParams); + +//#region Embed Style +const embedRounded = ref(embedParams.rounded); +const embedNoBorder = ref(!embedParams.border); +const maxHeight = ref(embedParams.maxHeight ?? 0); +//#endregion + +//#region Embed Resizer +const rootEl = shallowRef<HTMLElement | null>(null); + +let previousHeight = 0; +const resizeObserver = new ResizeObserver(async () => { + const height = rootEl.value!.scrollHeight + (embedNoBorder.value ? 0 : 2); // border 上下1px + if (Math.abs(previousHeight - height) < 1) return; // 1px未満ã®å¤‰åŒ–ã¯ç„¡è¦– + postMessageToParentWindow('misskey:embed:changeHeight', { + height: (maxHeight.value > 0 && height > maxHeight.value) ? maxHeight.value : height, + }); + previousHeight = height; +}); +onMounted(() => { + resizeObserver.observe(rootEl.value!); +}); +onUnmounted(() => { + resizeObserver.disconnect(); +}); +//#endregion +</script> + +<style lang="scss" module> +.rootForEmbedPage { + box-sizing: border-box; + border: 1px solid var(--divider); + background-color: var(--bg); + overflow: hidden; + position: relative; + height: auto; + + &.rounded { + border-radius: var(--radius); + } + + &.noBorder { + border: none; + } +} + +.routerViewContainer { + container-type: inline-size; + max-height: var(--embedMaxHeight, none); +} +</style> diff --git a/packages/frontend-embed/src/utils.ts b/packages/frontend-embed/src/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..939648aa383d0fd89662b5c94324745474350aae --- /dev/null +++ b/packages/frontend-embed/src/utils.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { url } from '@@/js/config.js'; + +export const acct = (user: Misskey.Acct) => { + return Misskey.acct.toString(user); +}; + +export const userName = (user: Misskey.entities.User) => { + return user.name || user.username; +}; + +export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => { + return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; +}; + +export const notePage = (note: Misskey.entities.Note) => { + return `/notes/${note.id}`; +}; diff --git a/packages/frontend-embed/src/workers/draw-blurhash.ts b/packages/frontend-embed/src/workers/draw-blurhash.ts new file mode 100644 index 0000000000000000000000000000000000000000..22de6cd3a8c5aaa227949ce755437ee70441cd6e --- /dev/null +++ b/packages/frontend-embed/src/workers/draw-blurhash.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { render } from 'buraha'; + +const canvas = new OffscreenCanvas(64, 64); + +onmessage = (event) => { + // console.log(event.data); + if (!('id' in event.data && typeof event.data.id === 'string')) { + return; + } + if (!('hash' in event.data && typeof event.data.hash === 'string')) { + return; + } + + render(event.data.hash, canvas); + const bitmap = canvas.transferToImageBitmap(); + postMessage({ id: event.data.id, bitmap }); +}; diff --git a/packages/frontend-embed/src/workers/test-webgl2.ts b/packages/frontend-embed/src/workers/test-webgl2.ts new file mode 100644 index 0000000000000000000000000000000000000000..b203ebe666b8ab1718738c6cb5fb11b2e3d5985a --- /dev/null +++ b/packages/frontend-embed/src/workers/test-webgl2.ts @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const canvas = globalThis.OffscreenCanvas && new OffscreenCanvas(1, 1); +// 環境ã«ã‚ˆã£ã¦ã¯OffscreenCanvasãŒå˜åœ¨ã—ãªã„ãŸã‚ +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +const gl = canvas?.getContext('webgl2'); +if (gl) { + postMessage({ result: true }); +} else { + postMessage({ result: false }); +} diff --git a/packages/frontend-embed/src/workers/tsconfig.json b/packages/frontend-embed/src/workers/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..8ee893046590f2c9aba8e4cbb8a51a722bdfa6b6 --- /dev/null +++ b/packages/frontend-embed/src/workers/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["esnext", "webworker"], + } +} diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..1af34f378c7505005d4549bb19e01f73b403b6a2 --- /dev/null +++ b/packages/frontend-embed/tsconfig.json @@ -0,0 +1,56 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": false, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": false, + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@@/*": ["../frontend-shared/*"] + }, + "typeRoots": [ + "./@types", + "./node_modules/@types", + "./node_modules/@vue-macros", + "./node_modules" + ], + "types": [ + "vite/client" + ], + "lib": [ + "esnext", + "dom", + "dom.iterable" + ], + "jsx": "preserve" + }, + "compileOnSave": false, + "include": [ + "./src/**/*.ts", + "./src/**/*.vue", + "./@types/**/*.ts" + ], + "exclude": [ + "node_modules", + ".storybook/**/*" + ] +} diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf2f478887d6e9a8501aaed6e0d0fbad0808c227 --- /dev/null +++ b/packages/frontend-embed/vite.config.local-dev.ts @@ -0,0 +1,96 @@ +import dns from 'dns'; +import { readFile } from 'node:fs/promises'; +import type { IncomingMessage } from 'node:http'; +import { defineConfig } from 'vite'; +import type { UserConfig } from 'vite'; +import * as yaml from 'js-yaml'; +import locales from '../../locales/index.js'; +import { getConfig } from './vite.config.js'; + +dns.setDefaultResultOrder('ipv4first'); + +const defaultConfig = getConfig(); + +const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')); + +const httpUrl = `http://localhost:${port}/`; +const websocketUrl = `ws://localhost:${port}/`; + +// activitypubリクエストã¯Proxyを通ã—ã€ãれ以外ã¯Viteã®é–‹ç™ºã‚µãƒ¼ãƒãƒ¼ã‚’返㙠+function varyHandler(req: IncomingMessage) { + if (req.headers.accept?.includes('application/activity+json')) { + return null; + } + return '/index.html'; +} + +const devConfig: UserConfig = { + // 基本ã®è¨å®šã¯ vite.config.js ã‹ã‚‰å¼•ã継ã + ...defaultConfig, + root: 'src', + publicDir: '../assets', + base: '/embed', + server: { + host: 'localhost', + port: 5174, + proxy: { + '/api': { + changeOrigin: true, + target: httpUrl, + }, + '/assets': httpUrl, + '/static-assets': httpUrl, + '/client-assets': httpUrl, + '/files': httpUrl, + '/twemoji': httpUrl, + '/fluent-emoji': httpUrl, + '/sw.js': httpUrl, + '/streaming': { + target: websocketUrl, + ws: true, + }, + '/favicon.ico': httpUrl, + '/robots.txt': httpUrl, + '/embed.js': httpUrl, + '/identicon': { + target: httpUrl, + rewrite(path) { + return path.replace('@localhost:5173', ''); + }, + }, + '/url': httpUrl, + '/proxy': httpUrl, + '/_info_card_': httpUrl, + '/bios': httpUrl, + '/cli': httpUrl, + '/inbox': httpUrl, + '/emoji/': httpUrl, + '/notes': { + target: httpUrl, + bypass: varyHandler, + }, + '/users': { + target: httpUrl, + bypass: varyHandler, + }, + '/.well-known': { + target: httpUrl, + }, + }, + }, + build: { + ...defaultConfig.build, + rollupOptions: { + ...defaultConfig.build?.rollupOptions, + input: 'index.html', + }, + }, + + define: { + ...defaultConfig.define, + _LANGS_FULL_: JSON.stringify(Object.entries(locales)), + }, +}; + +export default defineConfig(({ command, mode }) => devConfig); + diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..b95533c2cdfead7f005948497c485734b2d70216 --- /dev/null +++ b/packages/frontend-embed/vite.config.ts @@ -0,0 +1,156 @@ +import path from 'path'; +import pluginVue from '@vitejs/plugin-vue'; +import { type UserConfig, defineConfig } from 'vite'; +import { localesVersion } from '../../locales/version.js'; +import locales from '../../locales/index.js'; +import meta from '../../package.json'; +import packageInfo from './package.json' with { type: 'json' }; +import pluginJson5 from './vite.json5.js'; +import { pluginReplaceIcons } from '../frontend/vite.replaceIcons.js'; + +const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; + +/** + * Misskeyã®ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰ã«ãƒãƒ³ãƒ‰ãƒ«ã›ãšã€CDNãªã©ã‹ã‚‰åˆ¥é€”èªã¿è¾¼ã‚€ãƒªã‚½ãƒ¼ã‚¹ã‚’記述ã™ã‚‹ã€‚ + * CDNを使ã‚ãšã«ãƒãƒ³ãƒ‰ãƒ«ã—ãŸã„å ´åˆã€ä»¥ä¸‹ã®é…列ã‹ã‚‰è©²å½“è¦ç´ を削除orコメントアウトã™ã‚Œã°OK + */ +const externalPackages = [ + // shiki(コードブãƒãƒƒã‚¯ã®ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã§ä½¿ç”¨ä¸ï¼‰ã¯ãƒ†ãƒ¼ãƒžãƒ»è¨€èªžã®å®šç¾©ã®å®¹é‡ãŒå¤§ãã„ãŸã‚ã€ãれらã¯CDNã‹ã‚‰èªã¿è¾¼ã‚€ + { + name: 'shiki', + match: /^shiki\/(?<subPkg>(langs|themes))$/, + path(id: string, pattern: RegExp): string { + const match = pattern.exec(id)?.groups; + return match + ? `https://esm.sh/shiki@${packageInfo.dependencies.shiki}/${match['subPkg']}` + : id; + }, + }, +]; + +const hash = (str: string, seed = 0): number => { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}; + +const BASE62_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +function toBase62(n: number): string { + if (n === 0) { + return '0'; + } + let result = ''; + while (n > 0) { + result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result; + n = Math.floor(n / BASE62_DIGITS.length); + } + + return result; +} + +export function getConfig(): UserConfig { + return { + base: '/embed_vite/', + + server: { + port: 5174, + }, + + plugins: [ + pluginVue(), + pluginJson5(), + ...pluginReplaceIcons(), + ], + + resolve: { + extensions, + alias: { + '@/': __dirname + '/src/', + '@@/': __dirname + '/../frontend-shared/', + '/client-assets/': __dirname + '/assets/', + '/static-assets/': __dirname + '/../backend/assets/' + }, + }, + + css: { + modules: { + generateScopedName(name, filename, _css): string { + const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, ''); + const shortId = id.replace(/^(components(-global)?|widgets|ui(-_common_)?)-/, ''); + return shortId + '-' + toBase62(hash(id)).substring(0, 4); + }, + }, + }, + + define: { + _VERSION_: JSON.stringify(meta.version), + _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _LANGS_VERSION_: JSON.stringify(localesVersion), + _ENV_: JSON.stringify(process.env.NODE_ENV), + _DEV_: process.env.NODE_ENV !== 'production', + _PERF_PREFIX_: JSON.stringify('Misskey:'), + __VUE_OPTIONS_API__: false, + __VUE_PROD_DEVTOOLS__: false, + }, + + build: { + target: [ + 'chrome116', + 'firefox116', + 'safari16', + ], + manifest: 'manifest.json', + rollupOptions: { + input: { + app: './src/boot.ts', + }, + external: externalPackages.map(p => p.match), + output: { + manualChunks: { + vue: ['vue'], + }, + chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js', + assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]', + paths(id) { + for (const p of externalPackages) { + if (p.match.test(id)) { + return p.path(id, p.match); + } + } + + return id; + }, + }, + }, + cssCodeSplit: true, + outDir: __dirname + '/../../built/_frontend_embed_vite_', + assetsDir: '.', + emptyOutDir: false, + sourcemap: process.env.NODE_ENV === 'development', + reportCompressedSize: false, + + // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies + commonjsOptions: { + include: [/misskey-js/, /node_modules/], + }, + }, + + worker: { + format: 'es', + }, + }; +} + +const config = defineConfig(({ command, mode }) => getConfig()); + +export default config; diff --git a/packages/frontend-embed/vite.json5.ts b/packages/frontend-embed/vite.json5.ts new file mode 100644 index 0000000000000000000000000000000000000000..87b67c2142414a94763808ebd83e06cdab3d588c --- /dev/null +++ b/packages/frontend-embed/vite.json5.ts @@ -0,0 +1,48 @@ +// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json + +import JSON5 from 'json5'; +import { Plugin } from 'rollup'; +import { createFilter, dataToEsm } from '@rollup/pluginutils'; +import { RollupJsonOptions } from '@rollup/plugin-json'; + +// json5 extends SyntaxError with additional fields (without subclassing) +// https://github.com/json5/json5/blob/de344f0619bda1465a6e25c76f1c0c3dda8108d9/lib/parse.js#L1111-L1112 +interface Json5SyntaxError extends SyntaxError { + lineNumber: number; + columnNumber: number; +} + +export default function json5(options: RollupJsonOptions = {}): Plugin { + const filter = createFilter(options.include, options.exclude); + const indent = 'indent' in options ? options.indent : '\t'; + + return { + name: 'json5', + + // eslint-disable-next-line no-shadow + transform(json, id) { + if (id.slice(-6) !== '.json5' || !filter(id)) return null; + + try { + const parsed = JSON5.parse(json); + return { + code: dataToEsm(parsed, { + preferConst: options.preferConst, + compact: options.compact, + namedExports: options.namedExports, + indent, + }), + map: { mappings: '' }, + }; + } catch (err) { + if (!(err instanceof SyntaxError)) { + throw err; + } + const message = 'Could not parse JSON5 file'; + const { lineNumber, columnNumber } = err as Json5SyntaxError; + this.warn({ message, id, loc: { line: lineNumber, column: columnNumber } }); + return null; + } + }, + }; +} diff --git a/packages/frontend-embed/vue-shims.d.ts b/packages/frontend-embed/vue-shims.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..eba994772dd4680f58296de2c82d714a8f044ba3 --- /dev/null +++ b/packages/frontend-embed/vue-shims.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +declare module "*.vue" { + import { defineComponent } from "vue"; + const component: ReturnType<typeof defineComponent>; + export default component; +} diff --git a/packages/frontend-shared/.gitignore b/packages/frontend-shared/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5f6be09d7c01e1fa28715890bf8a17338cc73bc7 --- /dev/null +++ b/packages/frontend-shared/.gitignore @@ -0,0 +1,2 @@ +/storybook-static +js-built diff --git a/packages/frontend-shared/@types/global.d.ts b/packages/frontend-shared/@types/global.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..308515564b26b97049450671d2c2fa46dd8315f9 --- /dev/null +++ b/packages/frontend-shared/@types/global.d.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FIXME = any; + +declare const _LANGS_: string[][]; +declare const _LANGS_VERSION_: string; +declare const _VERSION_: string; +declare const _ENV_: string; +declare const _DEV_: boolean; +declare const _PERF_PREFIX_: string; +declare const _DATA_TRANSFER_DRIVE_FILE_: string; +declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; +declare const _DATA_TRANSFER_DECK_COLUMN_: string; + +// for dev-mode +declare const _LANGS_FULL_: string[][]; + +// TagCanvas +interface Window { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TagCanvas: any; +} diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js new file mode 100644 index 0000000000000000000000000000000000000000..17b6da8d30a2ab847aef0181ffcf82db83057f27 --- /dev/null +++ b/packages/frontend-shared/build.js @@ -0,0 +1,106 @@ +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as esbuild from 'esbuild'; +import { build } from 'esbuild'; +import { globSync } from 'glob'; +import { execa } from 'execa'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); +const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); + +const entryPoints = globSync('./js/**/**.{ts,tsx}'); + +/** @type {import('esbuild').BuildOptions} */ +const options = { + entryPoints, + minify: process.env.NODE_ENV === 'production', + outdir: './js-built', + target: 'es2022', + platform: 'browser', + format: 'esm', + sourcemap: 'linked', +}; + +// js-builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ +fs.rmSync('./js-built', { recursive: true, force: true }); + +if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { + await watchSrc(); +} else { + await buildSrc(); +} + +async function buildSrc() { + console.log(`[${_package.name}] start building...`); + + await build(options) + .then(() => { + console.log(`[${_package.name}] build succeeded.`); + }) + .catch((err) => { + process.stderr.write(err.stderr); + process.exit(1); + }); + + if (process.env.NODE_ENV === 'production') { + console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`); + } else { + await buildDts(); + } + + fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json'); + + console.log(`[${_package.name}] finish building.`); +} + +function buildDts() { + return execa( + 'tsc', + [ + '--project', 'tsconfig.json', + '--outDir', 'js-built', + '--declaration', 'true', + '--emitDeclarationOnly', 'true', + ], + { + stdout: process.stdout, + stderr: process.stderr, + }, + ); +} + +async function watchSrc() { + const plugins = [{ + name: 'gen-dts', + setup(build) { + build.onStart(() => { + console.log(`[${_package.name}] detect changed...`); + }); + build.onEnd(async result => { + if (result.errors.length > 0) { + console.error(`[${_package.name}] watch build failed:`, result); + return; + } + await buildDts(); + }); + }, + }]; + + console.log(`[${_package.name}] start watching...`); + + const context = await esbuild.context({ ...options, plugins }); + await context.watch(); + + await new Promise((resolve, reject) => { + process.on('SIGHUP', resolve); + process.on('SIGINT', resolve); + process.on('SIGTERM', resolve); + process.on('uncaughtException', reject); + process.on('exit', resolve); + }).finally(async () => { + await context.dispose(); + console.log(`[${_package.name}] finish watching.`); + }); +} diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..689f7870c0e4e785c582d0f7cef8fdff104d121e --- /dev/null +++ b/packages/frontend-shared/eslint.config.js @@ -0,0 +1,109 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import parser from 'vue-eslint-parser'; +import pluginVue from 'eslint-plugin-vue'; +import pluginMisskey from '@misskey-dev/eslint-plugin'; +import sharedConfig from '../shared/eslint.config.js'; + +// eslint-disable-next-line import/no-default-export +export default [ + ...sharedConfig, + { + files: ['**/*.vue'], + ...pluginMisskey.configs.typescript, + }, + ...pluginVue.configs['flat/recommended'], + { + files: [ + '@types/**/*.ts', + 'js/**/*.ts', + '**/*.vue', + ], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), + ...globals.browser, + + // Node.js + module: false, + require: false, + __dirname: false, + + // Misskey + _DEV_: false, + _LANGS_: false, + _VERSION_: false, + _ENV_: false, + _PERF_PREFIX_: false, + _DATA_TRANSFER_DRIVE_FILE_: false, + _DATA_TRANSFER_DRIVE_FOLDER_: false, + _DATA_TRANSFER_DECK_COLUMN_: false, + }, + parser, + parserOptions: { + extraFileExtensions: ['.vue'], + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-empty-interface': ['error', { + allowSingleExtends: true, + }], + // window ã®ç¦æ¢ç†ç”±: ã‚°ãƒãƒ¼ãƒãƒ«ã‚¹ã‚³ãƒ¼ãƒ—ã¨è¡çªã—ã€äºˆæœŸã›ã¬çµæžœã‚’æ‹›ããŸã‚ + // e ã®ç¦æ¢ç†ç”±: error ã‚„ event ãªã©ã€è¤‡æ•°ã®ã‚ーワードã®é æ–‡å—ã§ã‚り分ã‹ã‚Šã«ãã„ãŸã‚ + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + alphabetical: false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + allowUsingIterationVar: false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + ignoreProperties: false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + attribute: 1, + baseIndent: 0, + closeBracket: 0, + alignAttributesVertically: true, + ignores: [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + startTag: 'never', + endTag: 'never', + selfClosingTag: 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-reactivity-loss': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/v-on-event-hyphenation': ['error', 'never', { + autofix: true, + }], + 'vue/attribute-hyphenation': ['error', 'never'], + }, + }, + { + ignores: [ + "**/lib/", + "**/temp/", + "**/built/", + "**/coverage/", + "**/node_modules/", + ] + }, +]; diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend-shared/js/collapsed.ts similarity index 86% rename from packages/frontend/src/scripts/collapsed.ts rename to packages/frontend-shared/js/collapsed.ts index 4ec88a3c657329bc4533be4b7db2e060a38bdb18..af1f88cb73b1566e376a24e6ac854f3e348c22de 100644 --- a/packages/frontend/src/scripts/collapsed.ts +++ b/packages/frontend-shared/js/collapsed.ts @@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js'; export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { const collapsed = note.cw == null && ( - note.text != null && ( + (note.text != null && ( (note.text.includes('$[x2')) || (note.text.includes('$[x3')) || (note.text.includes('$[x4')) || @@ -15,7 +15,7 @@ export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): bo (note.text.split('\n').length > 9) || (note.text.length > 500) || (urls.length >= 4) - ) || note.files.length >= 5 + )) || (note.files != null && note.files.length >= 5) ); return collapsed; diff --git a/packages/frontend-shared/js/config.ts b/packages/frontend-shared/js/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..9a23e0e7f3fa7ccdf3e97077c4ba37ee2edcbb34 --- /dev/null +++ b/packages/frontend-shared/js/config.ts @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Locale } from '../../../locales/index.js'; + +// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing +const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href); +const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content; + +export const host = address.host; +export const hostname = address.hostname; +export const url = address.origin; +export const apiUrl = location.origin + '/api'; +export const wsOrigin = location.origin; +export const lang = localStorage.getItem('lang') ?? 'en-US'; +export const langs = _LANGS_; +export const langsVersion = _LANGS_VERSION_; +const preParseLocale = localStorage.getItem('locale'); +export let locale: Locale = preParseLocale ? JSON.parse(preParseLocale) : null; +export const version = _VERSION_; +export const instanceName = (siteName === 'Sharkey' || siteName == null) ? host : siteName; +export const ui = localStorage.getItem('ui'); +export const debug = localStorage.getItem('debug') === 'true'; + +export function updateLocale(newLocale: Locale): void { + locale = newLocale; +} diff --git a/packages/frontend/src/const.ts b/packages/frontend-shared/js/const.ts similarity index 90% rename from packages/frontend/src/const.ts rename to packages/frontend-shared/js/const.ts index c94c0d440882f10581d3c0365fab3b7ed9596474..8236cc9a4db5e9465bfd2fe1fcbd6f6c88ae212e 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -118,8 +118,10 @@ export const notificationTypes = [ 'followRequestAccepted', 'roleAssigned', 'achievementEarned', + 'exportCompleted', + 'test', 'app', - 'edited' + 'edited', ] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; @@ -152,6 +154,11 @@ export const ROLE_POLICIES = [ 'userEachUserListsLimit', 'rateLimitFactor', 'avatarDecorationLimit', + 'canImportAntennas', + 'canImportBlocking', + 'canImportFollowing', + 'canImportMuting', + 'canImportUserLists', ] as const; // ãªã‚“ã‹å‹•ã‹ãªã„ @@ -160,9 +167,9 @@ export const ROLE_POLICIES = [ export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP'; export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM'; -export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://launcher.moe/error.png'; -export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://launcher.moe/missingpage.webp'; -export const DEFAULT_INFO_IMAGE_URL = 'https://launcher.moe/nothinghere.png'; +export const DEFAULT_SERVER_ERROR_IMAGE_URL = '/client-assets/status/error.png'; +export const DEFAULT_NOT_FOUND_IMAGE_URL = '/client-assets/status/missingpage.webp'; +export const DEFAULT_INFO_IMAGE_URL = '/client-assets/status/nothinghere.png'; export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime', 'crop', 'fade', 'followmouse']; export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = { diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5555a98c3b0fae39ba44479cdcd5fa999d87e00 --- /dev/null +++ b/packages/frontend-shared/js/embed-page.ts @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//#region Embed関連ã®å®šç¾© + +/** 埋ã‚è¾¼ã¿ã®å¯¾è±¡ã¨ãªã‚‹ã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ï¼ˆ/embed/xxx ã® xxx ã®éƒ¨åˆ†ã¨å¯¾å¿œã•ã›ã‚‹ï¼‰ */ +const embeddableEntities = [ + 'notes', + 'user-timeline', + 'clips', + 'tags', +] as const; + +/** 埋ã‚è¾¼ã¿ã®å¯¾è±¡ã¨ãªã‚‹ã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ */ +export type EmbeddableEntity = typeof embeddableEntities[number]; + +/** 内部ã§ã‚¹ã‚¯ãƒãƒ¼ãƒ«ãŒã‚るページ */ +export const embedRouteWithScrollbar: EmbeddableEntity[] = [ + 'clips', + 'tags', + 'user-timeline', +]; + +/** 埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã®ãƒ‘ラメータ */ +export type EmbedParams = { + maxHeight?: number; + colorMode?: 'light' | 'dark'; + rounded?: boolean; + border?: boolean; + autoload?: boolean; + header?: boolean; +}; + +/** æ£è¦åŒ–ã•ã‚ŒãŸãƒ‘ラメータ */ +export type ParsedEmbedParams = Required<Omit<EmbedParams, 'maxHeight' | 'colorMode'>> & Pick<EmbedParams, 'maxHeight' | 'colorMode'>; + +/** パラメータã®ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å€¤ */ +export const defaultEmbedParams = { + maxHeight: undefined, + colorMode: undefined, + rounded: true, + border: true, + autoload: false, + header: true, +} as const satisfies EmbedParams; + +//#endregion + +/** + * パラメータをæ£è¦åŒ–ã™ã‚‹ï¼ˆåŸ‹ã‚è¾¼ã¿ãƒšãƒ¼ã‚¸åˆæœŸåŒ–用) + * @param searchParams URLSearchParamsã‚‚ã—ãã¯ã‚¯ã‚¨ãƒªæ–‡å—列 + * @returns æ£è¦åŒ–ã•ã‚ŒãŸãƒ‘ラメータ + */ +export function parseEmbedParams(searchParams: URLSearchParams | string): ParsedEmbedParams { + let _searchParams: URLSearchParams; + if (typeof searchParams === 'string') { + _searchParams = new URLSearchParams(searchParams); + } else if (searchParams instanceof URLSearchParams) { + _searchParams = searchParams; + } else { + throw new Error('searchParams must be URLSearchParams or string'); + } + + function convertBoolean(value: string | null): boolean | undefined { + if (value === 'true') { + return true; + } else if (value === 'false') { + return false; + } + return undefined; + } + + function convertNumber(value: string | null): number | undefined { + if (value != null && !isNaN(Number(value))) { + return Number(value); + } + return undefined; + } + + function convertColorMode(value: string | null): 'light' | 'dark' | undefined { + if (value != null && ['light', 'dark'].includes(value)) { + return value as 'light' | 'dark'; + } + return undefined; + } + + return { + maxHeight: convertNumber(_searchParams.get('maxHeight')) ?? defaultEmbedParams.maxHeight, + colorMode: convertColorMode(_searchParams.get('colorMode')) ?? defaultEmbedParams.colorMode, + rounded: convertBoolean(_searchParams.get('rounded')) ?? defaultEmbedParams.rounded, + border: convertBoolean(_searchParams.get('border')) ?? defaultEmbedParams.border, + autoload: convertBoolean(_searchParams.get('autoload')) ?? defaultEmbedParams.autoload, + header: convertBoolean(_searchParams.get('header')) ?? defaultEmbedParams.header, + }; +} diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend-shared/js/emoji-base.ts similarity index 91% rename from packages/frontend/src/scripts/emoji-base.ts rename to packages/frontend-shared/js/emoji-base.ts index 16a5a6aa5b3fa6739f1f4c4ff55fea3853495c71..858cd801de7f7bd964e567464570c8656631848d 100644 --- a/packages/frontend/src/scripts/emoji-base.ts +++ b/packages/frontend-shared/js/emoji-base.ts @@ -20,8 +20,8 @@ export function char2fluentEmojiFilePath(char: string): string { // Fluent Emojiã¯å›½æ——éžå¯¾å¿œ https://github.com/microsoft/fluentui-emoji/issues/25 if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); + codes = codes.filter(x => x != null && x.length > 0); + const fileName = (codes as string[]).map(x => x.padStart(4, '0')).join('-'); return `${fluentEmojiPngBase}/${fileName}.png`; } diff --git a/packages/frontend/src/emojilist.json b/packages/frontend-shared/js/emojilist.json similarity index 100% rename from packages/frontend/src/emojilist.json rename to packages/frontend-shared/js/emojilist.json diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend-shared/js/emojilist.ts similarity index 96% rename from packages/frontend/src/scripts/emojilist.ts rename to packages/frontend-shared/js/emojilist.ts index 6565feba97d2d1e1b3831cd5fabc882b9d49d149..bde30a864fd0259531399c4e0f8ca7ae6a4acfd0 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend-shared/js/emojilist.ts @@ -12,12 +12,12 @@ export type UnicodeEmojiDef = { } // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; +import _emojilist from './emojilist.json'; export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ name: x[1] as string, char: x[0] as string, - category: unicodeEmojiCategories[x[2]], + category: unicodeEmojiCategories[x[2] as number], })); const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>( diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts similarity index 100% rename from packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts rename to packages/frontend-shared/js/extract-avg-color-from-blurhash.ts diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend-shared/js/i18n.ts similarity index 78% rename from packages/frontend/src/scripts/i18n.ts rename to packages/frontend-shared/js/i18n.ts index c2f44a33cc4713c2d33cad610b7eca85cd9df57c..18232691fa7e6069df874782e93e8070a795cc97 100644 --- a/packages/frontend/src/scripts/i18n.ts +++ b/packages/frontend-shared/js/i18n.ts @@ -2,7 +2,10 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; +import type { ILocale, ParameterizedString } from '../../../locales/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TODO = any; type FlattenKeys<T extends ILocale, TPrediction> = keyof { [K in keyof T as T[K] extends ILocale @@ -32,15 +35,18 @@ type Tsx<T extends ILocale> = { export class I18n<T extends ILocale> { private tsxCache?: Tsx<T>; + private devMode: boolean; + + constructor(public locale: T, devMode = false) { + this.devMode = devMode; - constructor(public locale: T) { //#region BIND this.t = this.t.bind(this); //#endregion } public get ts(): T { - if (_DEV_) { + if (this.devMode) { class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { get(target: TTarget, p: string | symbol): unknown { const value = target[p as keyof TTarget]; @@ -72,7 +78,7 @@ export class I18n<T extends ILocale> { } public get tsx(): Tsx<T> { - if (_DEV_) { + if (this.devMode) { if (this.tsxCache) { return this.tsxCache; } @@ -113,7 +119,7 @@ export class I18n<T extends ILocale> { return () => value; } - return (arg) => { + return (arg: TODO) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -137,7 +143,6 @@ export class I18n<T extends ILocale> { return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>; } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.tsxCache) { return this.tsxCache; } @@ -153,7 +158,7 @@ export class I18n<T extends ILocale> { const value = target[k as keyof typeof target]; if (typeof value === 'object') { - result[k] = build(value as ILocale); + (result as TODO)[k] = build(value as ILocale); } else if (typeof value === 'string') { const quasis: string[] = []; const expressions: string[] = []; @@ -180,7 +185,7 @@ export class I18n<T extends ILocale> { continue; } - result[k] = (arg) => { + (result as TODO)[k] = (arg: TODO) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -209,9 +214,9 @@ export class I18n<T extends ILocale> { let str: string | ParameterizedString | ILocale = this.locale; for (const k of key.split('.')) { - str = str[k]; + str = (str as TODO)[k]; - if (_DEV_) { + if (this.devMode) { if (typeof str === 'undefined') { console.error(`Unexpected locale key: ${key}`); return key; @@ -220,7 +225,7 @@ export class I18n<T extends ILocale> { } if (args) { - if (_DEV_) { + if (this.devMode) { const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); if (missing.length) { @@ -231,7 +236,7 @@ export class I18n<T extends ILocale> { for (const [k, v] of Object.entries(args)) { const search = `{${k}}`; - if (_DEV_) { + if (this.devMode) { if (!(str as string).includes(search)) { console.error(`Unexpected locale parameter: ${k} at ${key}`); } @@ -244,51 +249,3 @@ export class I18n<T extends ILocale> { return str; } } - -if (import.meta.vitest) { - const { describe, expect, it } = import.meta.vitest; - - describe('i18n', () => { - it('t', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.t('foo')).toBe('foo'); - expect(i18n.t('bar.baz')).toBe('baz'); - expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); - expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); - }); - it('ts', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.ts.foo).toBe('foo'); - expect(i18n.ts.bar.baz).toBe('baz'); - }); - it('tsx', () => { - const i18n = new I18n({ - foo: 'foo', - bar: { - baz: 'baz', - qux: 'qux {0}' as unknown as ParameterizedString<'0'>, - quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, - }, - }); - - expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); - expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); - }); - }); -} diff --git a/packages/frontend-shared/js/intl-const.ts b/packages/frontend-shared/js/intl-const.ts new file mode 100644 index 0000000000000000000000000000000000000000..33b65b6e9b782bca8d0eca7840d237ed74cf77e9 --- /dev/null +++ b/packages/frontend-shared/js/intl-const.ts @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { lang } from '@@/js/config.js'; + +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); + +let _dateTimeFormat: Intl.DateTimeFormat; +try { + _dateTimeFormat = new Intl.DateTimeFormat(versatileLang, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }); +} catch (err) { + console.warn(err); + if (_DEV_) console.log('[Intl] Fallback to en-US'); + + // Fallback to en-US + _dateTimeFormat = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }); +} +export const dateTimeFormat = _dateTimeFormat; + +export const timeZone = dateTimeFormat.resolvedOptions().timeZone; + +export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N'; + +let _numberFormat: Intl.NumberFormat; +try { + _numberFormat = new Intl.NumberFormat(versatileLang); +} catch (err) { + console.warn(err); + if (_DEV_) console.log('[Intl] Fallback to en-US'); + + // Fallback to en-US + _numberFormat = new Intl.NumberFormat('en-US'); +} +export const numberFormat = _numberFormat; diff --git a/packages/frontend-shared/js/is-link.ts b/packages/frontend-shared/js/is-link.ts new file mode 100644 index 0000000000000000000000000000000000000000..946f86400e16e75b4f4e73b460b98006363be0ec --- /dev/null +++ b/packages/frontend-shared/js/is-link.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isLink(el: HTMLElement) { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; +} diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts new file mode 100644 index 0000000000000000000000000000000000000000..2837870c9a639d7cda6934c1ea805f87e6837bda --- /dev/null +++ b/packages/frontend-shared/js/media-proxy.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { query } from './url.js'; + +export class MediaProxy { + private serverMetadata: Misskey.entities.MetaDetailed; + private url: string; + + constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) { + this.serverMetadata = serverMetadata; + this.url = url; + } + + public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { + const localProxy = `${this.url}/proxy`; + let _imageUrl = imageUrl; + + if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { + // ã‚‚ã†æ—¢ã«proxyã£ã½ãã†ã ã£ãŸã‚‰urlã‚’å–り出㙠+ _imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; + } + + return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${ + type === 'preview' ? 'preview.webp' + : 'image.webp' + }?${query({ + url: _imageUrl, + ...(!noFallback ? { 'fallback': '1' } : {}), + ...(type ? { [type]: '1' } : {}), + ...(mustOrigin ? { origin: '1' } : {}), + })}`; + } + + public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { + if (imageUrl == null) return null; + return this.getProxiedImageUrl(imageUrl, type); + } + + public getStaticImageUrl(baseUrl: string): string { + const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url); + + if (u.href.startsWith(`${this.url}/emoji/`)) { + // ã‚‚ã†æ—¢ã«emojiã£ã½ãã†ã ã£ãŸã‚‰searchParams付ã‘ã‚‹ã ã‘ + u.searchParams.set('static', '1'); + return u.href; + } + + if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) { + // ã‚‚ã†æ—¢ã«proxyã£ã½ãã†ã ã£ãŸã‚‰searchParams付ã‘ã‚‹ã ã‘ + u.searchParams.set('static', '1'); + return u.href; + } + + return `${this.serverMetadata.mediaProxy}/static.webp?${query({ + url: u.href, + static: '1', + })}`; + } +} diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend-shared/js/scroll.ts similarity index 82% rename from packages/frontend/src/scripts/scroll.ts rename to packages/frontend-shared/js/scroll.ts index f0274034b5b3d027824c9041236386d240b577f3..4f2e9105c3404720caed3e6bc31d2efc482ccc53 100644 --- a/packages/frontend/src/scripts/scroll.ts +++ b/packages/frontend-shared/js/scroll.ts @@ -36,19 +36,27 @@ export function getScrollPosition(el: HTMLElement | null): number { return container == null ? window.scrollY : container.scrollTop; } -export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { +export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknown, tolerance = 1, once = false) { // ã¨ã‚Šã‚ãˆãšè©•ä¾¡ã—ã¦ã¿ã‚‹ - if (el.isConnected && isTopVisible(el)) { - cb(); + const firstTopVisible = isTopVisible(el); + if (el.isConnected && firstTopVisible) { + cb(firstTopVisible); if (once) return null; } const container = getScrollContainer(el) ?? window; - const onScroll = ev => { + // 以下ã®ã‚±ãƒ¼ã‚¹ã«ãŠã„ã¦ã€cbãŒä½•åº¦ã‚‚呼ã³å‡ºã•ã‚Œã¦ã—ã¾ã£ã¦å…·åˆãŒæ‚ªã„ã®ã§1回呼んã ら以é™ã¯ç„¡è¦–ã™ã‚‹ã‚ˆã†ã«ã™ã‚‹ + // - スクãƒãƒ¼ãƒ«ã‚¤ãƒ™ãƒ³ãƒˆã¯1回ã®ã‚¹ã‚¯ãƒãƒ¼ãƒ«ã§è¤‡æ•°å›žç™ºç”Ÿã™ã‚‹ã“ã¨ãŒã‚ã‚‹ + // - toleranceã®ç¯„囲内ã«åŽã¾ã‚‹ç¨‹åº¦ã®å¾®é‡ãªã‚¹ã‚¯ãƒãƒ¼ãƒ«ãŒç™ºç”Ÿã—㟠+ let prevTopVisible = firstTopVisible; + const onScroll = () => { if (!document.body.contains(el)) return; - if (isTopVisible(el, tolerance)) { - cb(); + + const topVisible = isTopVisible(el, tolerance); + if (topVisible !== prevTopVisible) { + prevTopVisible = topVisible; + cb(topVisible); if (once) removeListener(); } }; @@ -69,7 +77,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 } const containerOrWindow = container ?? window; - const onScroll = ev => { + const onScroll = () => { if (!document.body.contains(el)) return; if (isBottomVisible(el, 1, container)) { cb(); @@ -126,6 +134,7 @@ export function scrollToBottom( export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { const scrollTop = getScrollPosition(el); + if (_DEV_) console.log(scrollTop, tolerance, scrollTop <= tolerance); return scrollTop <= tolerance; } diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend-shared/js/url.ts similarity index 70% rename from packages/frontend/src/scripts/url.ts rename to packages/frontend-shared/js/url.ts index 5a8265af9e1050d0c7e6ac260828d26f92b2f499..eb830b1eeaec7ac22ab64d01b2fb8ba5058ae246 100644 --- a/packages/frontend/src/scripts/url.ts +++ b/packages/frontend-shared/js/url.ts @@ -8,18 +8,18 @@ * 2. プãƒãƒ‘ティãŒundefinedã®æ™‚ã¯ã‚¯ã‚¨ãƒªã‚’付ã‘ãªã„ * (new URLSearchParams(obj)ã§ã¯ãã“ã¾ã§ä¸å¯§ãªã“ã¨ã‚’ã—ã¦ãã‚Œãªã„) */ -export function query(obj: Record<string, any>): string { +export function query(obj: Record<string, string | number | boolean>): string { const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); + .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition + .reduce<Record<string, string | number | boolean>>((a, [k, v]) => (a[k] = v, a), {}); return Object.entries(params) .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) .join('&'); } -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; +export function appendQuery(url: string, queryString: string): string { + return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`; } export function extractDomain(url: string) { diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts similarity index 85% rename from packages/frontend/src/scripts/use-document-visibility.ts rename to packages/frontend-shared/js/use-document-visibility.ts index a8f4d5e03ae2158d609ca78dcc1a8f85f6a4064b..b1197e68dae2bcdedeefb09de5cf7b3202d289b3 100644 --- a/packages/frontend/src/scripts/use-document-visibility.ts +++ b/packages/frontend-shared/js/use-document-visibility.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { onMounted, onUnmounted, ref, Ref } from 'vue'; +import { onMounted, onUnmounted, ref } from 'vue'; +import type { Ref } from 'vue'; export function useDocumentVisibility(): Ref<DocumentVisibilityState> { const visibility = ref(document.visibilityState); diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend-shared/js/use-interval.ts similarity index 100% rename from packages/frontend/src/scripts/use-interval.ts rename to packages/frontend-shared/js/use-interval.ts diff --git a/packages/frontend/src/scripts/worker-multi-dispatch.ts b/packages/frontend-shared/js/worker-multi-dispatch.ts similarity index 84% rename from packages/frontend/src/scripts/worker-multi-dispatch.ts rename to packages/frontend-shared/js/worker-multi-dispatch.ts index 6b3fcd938334d03595e57508d2dfe6aeada60085..5d393ed1edef9138bff1a57d753fad2a7b0044d5 100644 --- a/packages/frontend/src/scripts/worker-multi-dispatch.ts +++ b/packages/frontend-shared/js/worker-multi-dispatch.ts @@ -3,16 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -function defaultUseWorkerNumber(prev: number, totalWorkers: number) { +function defaultUseWorkerNumber(prev: number) { return prev + 1; } -export class WorkerMultiDispatch<POST = any, RETURN = any> { +type WorkerNumberGetter = (prev: number, totalWorkers: number) => number; + +export class WorkerMultiDispatch<POST = unknown, RETURN = unknown> { private symbol = Symbol('WorkerMultiDispatch'); private workers: Worker[] = []; private terminated = false; private prevWorkerNumber = 0; - private getUseWorkerNumber = defaultUseWorkerNumber; + private getUseWorkerNumber: WorkerNumberGetter; private finalizationRegistry: FinalizationRegistry<symbol>; constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { @@ -29,7 +31,7 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> { if (_DEV_) console.log('WorkerMultiDispatch: Created', this); } - public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) { + public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) { let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); @@ -46,12 +48,14 @@ export class WorkerMultiDispatch<POST = any, RETURN = any> { return workerNumber; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) { this.workers.forEach(worker => { worker.addEventListener('message', callback, options); }); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) { this.workers.forEach(worker => { worker.removeEventListener('message', callback, options); diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json new file mode 100644 index 0000000000000000000000000000000000000000..82fa62cb322b2ae83ba300bdc113b73b686b8940 --- /dev/null +++ b/packages/frontend-shared/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend-shared", + "type": "module", + "main": "./js-built/index.js", + "types": "./js-built/index.d.ts", + "exports": { + ".": { + "import": "./js-built/index.js", + "types": "./js-built/index.d.ts" + }, + "./*": { + "import": "./js-built/*", + "types": "./js-built/*" + } + }, + "scripts": { + "build": "node ./build.js", + "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", + "typecheck": "tsc --noEmit", + "lint": "pnpm typecheck && pnpm eslint" + }, + "devDependencies": { + "@types/node": "20.14.12", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", + "esbuild": "0.23.0", + "eslint-plugin-vue": "9.27.0", + "typescript": "5.5.4", + "vue-eslint-parser": "9.4.3" + }, + "files": [ + "js-built" + ], + "dependencies": { + "misskey-js": "workspace:*", + "vue": "3.4.37" + } +} diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 similarity index 89% rename from packages/frontend/src/themes/_dark.json5 rename to packages/frontend-shared/themes/_dark.json5 index 7b70aa1e0941cb7e5184e7b8f7a6c142c55f14ea..e4649311c375ec58369fe9f5200fe5e7ba9bfc12 100644 --- a/packages/frontend/src/themes/_dark.json5 +++ b/packages/frontend-shared/themes/_dark.json5 @@ -13,6 +13,7 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', + love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#000', acrylicBg: ':alpha<0.5<@bg', @@ -54,11 +55,13 @@ infoFg: '#fff', infoWarnBg: '#42321c', infoWarnFg: '#ffbd3e', - switchBg: 'rgba(255, 255, 255, 0.15)', - buttonBg: 'rgba(255, 255, 255, 0.05)', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', + folderHeaderBg: 'rgba(255, 255, 255, 0.05)', + folderHeaderHoverBg: 'rgba(255, 255, 255, 0.1)', + buttonBg: ':lighten<5<@panel', + buttonHoverBg: ':lighten<10<@panel', buttonGradateA: '@accent', buttonGradateB: ':hue<20<@accent', + switchBg: 'rgba(255, 255, 255, 0.15)', switchOffBg: 'rgba(255, 255, 255, 0.1)', switchOffFg: ':alpha<0.8<@fg', switchOnBg: '@accentedBg', @@ -78,22 +81,14 @@ codeBoolean: '#c59eff', deckBg: '#000', htmlThemeColor: '@bg', - X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.3)', X12: 'rgba(255, 255, 255, 0.1)', X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', }, codeHighlighter: { diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5 similarity index 89% rename from packages/frontend/src/themes/_light.json5 rename to packages/frontend-shared/themes/_light.json5 index d797aec734954d47773f8f782ca1b9169064e15b..b6218a5f1d6ee942a1eb4c1ae2868d47a75708dc 100644 --- a/packages/frontend/src/themes/_light.json5 +++ b/packages/frontend-shared/themes/_light.json5 @@ -13,6 +13,7 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', + love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#fff', acrylicBg: ':alpha<0.5<@bg', @@ -54,11 +55,13 @@ infoFg: '#72818a', infoWarnBg: '#fff0db', infoWarnFg: '#8f6e31', - switchBg: 'rgba(0, 0, 0, 0.15)', - buttonBg: 'rgba(0, 0, 0, 0.05)', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', + folderHeaderBg: 'rgba(0, 0, 0, 0.05)', + folderHeaderHoverBg: 'rgba(0, 0, 0, 0.1)', + buttonBg: ':darken<5<@panel', + buttonHoverBg: ':darken<10<@panel', buttonGradateA: '@accent', buttonGradateB: ':hue<20<@accent', + switchBg: 'rgba(0, 0, 0, 0.15)', switchOffBg: 'rgba(0, 0, 0, 0.1)', switchOffFg: '@panel', switchOnBg: '@accent', @@ -78,22 +81,14 @@ codeBoolean: '#62b70c', deckBg: ':darken<3<@bg', htmlThemeColor: '@bg', - X2: ':darken<2<@panel', X3: 'rgba(0, 0, 0, 0.05)', X4: 'rgba(0, 0, 0, 0.1)', X5: 'rgba(0, 0, 0, 0.05)', X6: 'rgba(0, 0, 0, 0.25)', X7: 'rgba(0, 0, 0, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.1)', X12: 'rgba(0, 0, 0, 0.1)', X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', }, codeHighlighter: { diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5 similarity index 87% rename from packages/frontend/src/themes/d-astro.json5 rename to packages/frontend-shared/themes/d-astro.json5 index fee25cc4a4205d5b2c40b827650b8c1cadbaa720..a674a5c5c9ce73a8b7097e2426b315af5bd2ba8a 100644 --- a/packages/frontend/src/themes/d-astro.json5 +++ b/packages/frontend-shared/themes/d-astro.json5 @@ -25,7 +25,6 @@ mention: '#ffd152', modalBg: 'rgba(0, 0, 0, 0.5)', success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', mentionMe: '#fb5d38', @@ -42,7 +41,6 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@accent', accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<-20<@accent', driveFolderBg: ':alpha<0.3<@accent', @@ -57,20 +55,13 @@ wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', panelHeaderDivider: 'rgba(0, 0, 0, 0)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.3)', X12: 'rgba(255, 255, 255, 0.1)', X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', }, } diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend-shared/themes/d-botanical.json5 similarity index 100% rename from packages/frontend/src/themes/d-botanical.json5 rename to packages/frontend-shared/themes/d-botanical.json5 diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend-shared/themes/d-cherry.json5 similarity index 100% rename from packages/frontend/src/themes/d-cherry.json5 rename to packages/frontend-shared/themes/d-cherry.json5 diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend-shared/themes/d-dark.json5 similarity index 100% rename from packages/frontend/src/themes/d-dark.json5 rename to packages/frontend-shared/themes/d-dark.json5 diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend-shared/themes/d-future.json5 similarity index 100% rename from packages/frontend/src/themes/d-future.json5 rename to packages/frontend-shared/themes/d-future.json5 diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend-shared/themes/d-green-lime.json5 similarity index 100% rename from packages/frontend/src/themes/d-green-lime.json5 rename to packages/frontend-shared/themes/d-green-lime.json5 diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend-shared/themes/d-green-orange.json5 similarity index 100% rename from packages/frontend/src/themes/d-green-orange.json5 rename to packages/frontend-shared/themes/d-green-orange.json5 diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend-shared/themes/d-ice.json5 similarity index 100% rename from packages/frontend/src/themes/d-ice.json5 rename to packages/frontend-shared/themes/d-ice.json5 diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend-shared/themes/d-persimmon.json5 similarity index 100% rename from packages/frontend/src/themes/d-persimmon.json5 rename to packages/frontend-shared/themes/d-persimmon.json5 diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5 similarity index 93% rename from packages/frontend/src/themes/d-u0.json5 rename to packages/frontend-shared/themes/d-u0.json5 index 3bd0b9483c3df1aee7ed7f1004539acde9062e65..32ac9ec5cfff2c920ef0a14edb62a60436d125e9 100644 --- a/packages/frontend/src/themes/d-u0.json5 +++ b/packages/frontend-shared/themes/d-u0.json5 @@ -3,14 +3,11 @@ base: 'dark', name: 'Mi U0 Dark', props: { - X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', bg: '#172426', fg: '#dadada', X10: ':alpha<0.4<@accent', @@ -41,7 +38,6 @@ mention: '@accent', modalBg: 'rgba(0, 0, 0, 0.5)', success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', switchBg: 'rgba(255, 255, 255, 0.15)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', @@ -64,7 +60,6 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@indicator', accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', fgTransparent: ':alpha<0.5<@fg', diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend-shared/themes/l-apricot.json5 similarity index 100% rename from packages/frontend/src/themes/l-apricot.json5 rename to packages/frontend-shared/themes/l-apricot.json5 diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend-shared/themes/l-botanical.json5 similarity index 100% rename from packages/frontend/src/themes/l-botanical.json5 rename to packages/frontend-shared/themes/l-botanical.json5 diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend-shared/themes/l-cherry.json5 similarity index 100% rename from packages/frontend/src/themes/l-cherry.json5 rename to packages/frontend-shared/themes/l-cherry.json5 diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend-shared/themes/l-coffee.json5 similarity index 100% rename from packages/frontend/src/themes/l-coffee.json5 rename to packages/frontend-shared/themes/l-coffee.json5 diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend-shared/themes/l-light.json5 similarity index 100% rename from packages/frontend/src/themes/l-light.json5 rename to packages/frontend-shared/themes/l-light.json5 diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend-shared/themes/l-rainy.json5 similarity index 100% rename from packages/frontend/src/themes/l-rainy.json5 rename to packages/frontend-shared/themes/l-rainy.json5 diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend-shared/themes/l-sushi.json5 similarity index 100% rename from packages/frontend/src/themes/l-sushi.json5 rename to packages/frontend-shared/themes/l-sushi.json5 diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5 similarity index 96% rename from packages/frontend/src/themes/l-u0.json5 rename to packages/frontend-shared/themes/l-u0.json5 index dbc777d49368107c638f60e7368e1c3b00563c8c..0b952b003ae52f0e8c1ea9af06f4fadaf93cf034 100644 --- a/packages/frontend/src/themes/l-u0.json5 +++ b/packages/frontend-shared/themes/l-u0.json5 @@ -3,14 +3,11 @@ base: 'light', name: 'Mi U0 Light', props: { - X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', bg: '#e7e7eb', fg: '#5f5f5f', X10: ':alpha<0.4<@accent', diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5 similarity index 86% rename from packages/frontend/src/themes/l-vivid.json5 rename to packages/frontend-shared/themes/l-vivid.json5 index 3368855b5e96896449a95c6227cc3e87862551d4..f1c63dde6eaf618408068f10d87618b5bfbac029 100644 --- a/packages/frontend/src/themes/l-vivid.json5 +++ b/packages/frontend-shared/themes/l-vivid.json5 @@ -28,7 +28,6 @@ mention: '@accent', modalBg: 'rgba(0, 0, 0, 0.3)', success: '#86b300', - buttonBg: 'rgba(0, 0, 0, 0.05)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', mentionMe: '@mention', @@ -45,7 +44,6 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@accent', accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':darken<3<@fg', fgTransparent: ':alpha<0.5<@fg', @@ -60,21 +58,13 @@ fgTransparentWeak: ':alpha<0.75<@fg', panelHeaderDivider: '@divider', scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - X2: ':darken<2<@panel', X3: 'rgba(0, 0, 0, 0.05)', X4: 'rgba(0, 0, 0, 0.1)', X5: 'rgba(0, 0, 0, 0.05)', X6: 'rgba(0, 0, 0, 0.25)', X7: 'rgba(0, 0, 0, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.1)', X12: 'rgba(0, 0, 0, 0.1)', X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', }, } diff --git a/packages/frontend/src/themes/rosepine-dawn.json5 b/packages/frontend-shared/themes/rosepine-dawn.json5 similarity index 100% rename from packages/frontend/src/themes/rosepine-dawn.json5 rename to packages/frontend-shared/themes/rosepine-dawn.json5 diff --git a/packages/frontend/src/themes/rosepine.json5 b/packages/frontend-shared/themes/rosepine.json5 similarity index 100% rename from packages/frontend/src/themes/rosepine.json5 rename to packages/frontend-shared/themes/rosepine.json5 diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..48228d4e4833a5cf0700e949506a1904cc853722 --- /dev/null +++ b/packages/frontend-shared/tsconfig.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "declarationMap": true, + "sourceMap": false, + "outDir": "./js-built/", + "removeComments": true, + "resolveJsonModule": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + "@@/*": ["./*"] + }, + "typeRoots": [ + "./@types", + "./node_modules/@types" + ], + "lib": [ + "esnext", + "dom" + ] + }, + "include": [ + "@types/**/*.ts", + "js/**/*" + ], + "exclude": [ + "node_modules", + "test/**/*" + ] +} diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 490a441b70553924ede8753a8f3e7873de5c1ff7..42d1a10f0ad093c106a699d4348e87d5ebff7c7c 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -405,8 +405,9 @@ function toStories(component: string): Promise<string> { glob('src/components/MkUserSetupDialog.*.vue'), glob('src/components/MkInstanceCardMini.vue'), glob('src/components/MkInviteCode.vue'), - glob('src/pages/search.vue'), + glob('src/pages/admin/overview.ap-requests.vue'), glob('src/pages/user/home.vue'), + glob('src/pages/search.vue'), ]); const components = globs.flat(); await Promise.all(components.map(async (component) => { diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index e174c72b4856ebf030fc76e4daee4b1e57b27237..1b6a605a6e2765ff1870c4fb15ac222e7ca56d54 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -32,7 +32,7 @@ const keys = [ 'rosepine-dawn', ] -await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { +await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { writeFile( new URL('./themes.ts', import.meta.url), `export default ${JSON.stringify( diff --git a/packages/frontend/@types/global.d.ts b/packages/frontend/@types/global.d.ts index 1025d1bedbb8a6f69fef263191c43bbd677548be..15373cbd2dd35eeb7f41f066f1bd25e165f16e35 100644 --- a/packages/frontend/@types/global.d.ts +++ b/packages/frontend/@types/global.d.ts @@ -6,6 +6,7 @@ type FIXME = any; declare const _LANGS_: string[][]; +declare const _LANGS_VERSION_: string; declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts index 0a7281898d9896a1fc82f38c2d61bcb23102c0f2..70afc356c19d9d281a640f686f1974dc1b541805 100644 --- a/packages/frontend/@types/theme.d.ts +++ b/packages/frontend/@types/theme.d.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -declare module '@/themes/*.json5' { +declare module '@@/themes/*.json5' { import { Theme } from '@/scripts/theme.js'; const theme: Theme; diff --git a/packages/frontend/assets/sharkey.svg b/packages/frontend/assets/sharkey.svg index 82f8c8cfa82e27105e89ffbe9b5c32a93a6edd5f..ae49863e5b3946517316ac2650eb71b83a30aad9 100644 Binary files a/packages/frontend/assets/sharkey.svg and b/packages/frontend/assets/sharkey.svg differ diff --git a/packages/frontend/assets/status/error.png b/packages/frontend/assets/status/error.png new file mode 100644 index 0000000000000000000000000000000000000000..9f21236c39379d34cad8cb6a195ab793fcd9f65f Binary files /dev/null and b/packages/frontend/assets/status/error.png differ diff --git a/packages/frontend/assets/status/missingpage.webp b/packages/frontend/assets/status/missingpage.webp new file mode 100644 index 0000000000000000000000000000000000000000..3ac83b31101ee6d7bf21a456a28e80d809f5e419 Binary files /dev/null and b/packages/frontend/assets/status/missingpage.webp differ diff --git a/packages/frontend/assets/status/nothinghere.png b/packages/frontend/assets/status/nothinghere.png new file mode 100644 index 0000000000000000000000000000000000000000..5ebe210acdaa5008678a64f3595af606755f5e57 Binary files /dev/null and b/packages/frontend/assets/status/nothinghere.png differ diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index dd8f03dac5102ecb648e0beb6516a40c5421a621..565b3d6ae7b69d00c969586d359457d875cb7b19 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -4,16 +4,19 @@ import parser from 'vue-eslint-parser'; import pluginVue from 'eslint-plugin-vue'; import pluginMisskey from '@misskey-dev/eslint-plugin'; import sharedConfig from '../shared/eslint.config.js'; +import localeRule from '../../eslint/locale.js'; +import { build as buildLocales } from '../../locales/index.js'; export default [ ...sharedConfig, { - files: ['src/**/*.vue'], + files: ['{src,test,js,@types}/**/*.vue'], ...pluginMisskey.configs.typescript, }, ...pluginVue.configs['flat/recommended'], { - files: ['src/**/*.{ts,vue}'], + files: ['{src,test,js,@types}/**/*.{ts,vue}'], + plugins: { sharkey: { rules: { locale: localeRule } } }, languageOptions: { globals: { ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), @@ -44,6 +47,8 @@ export default [ }, }, rules: { + 'sharkey/locale': ['error', buildLocales()['en-US']], + '@typescript-eslint/no-empty-interface': ['error', { allowSingleExtends: true, }], @@ -92,4 +97,15 @@ export default [ 'vue/attribute-hyphenation': ['error', 'never'], }, }, + { + ignores: [ + "**/lib/", + "**/temp/", + "**/built/", + "**/coverage/", + "**/node_modules/", + "**/libopenmpt/", + "**/storybook-static/" + ] + }, ]; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 5da671b9b61986d6b0177085f5dcd1121543edf1..1b4b5d5bd1c2c66df41f037aa4018ace7544d6fa 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -13,130 +13,130 @@ "test": "vitest --run --globals", "test-and-coverage": "vitest --run --coverage --globals", "typecheck": "vue-tsc --noEmit", - "eslint": "eslint --quiet \"src/**/*.{ts,vue}\" --cache", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { - "@discordapp/twemoji": "15.0.3", + "@discordapp/twemoji": "15.1.0", "@github/webauthn-json": "2.1.1", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@misskey-dev/browser-image-resizer": "2024.1.0", "@phosphor-icons/web": "^2.0.3", "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.0", + "@rollup/pluginutils": "5.1.2", "@transfem-org/sfm-js": "0.24.5", "@syuilo/aiscript": "0.19.0", "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.1.0", - "@vue/compiler-sfc": "3.4.37", + "@vitejs/plugin-vue": "5.1.4", + "@vue/compiler-sfc": "3.5.10", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11", - "astring": "1.8.6", + "astring": "1.9.0", "broadcast-channel": "7.0.0", "buraha": "0.0.1", "canvas-confetti": "1.9.3", - "chart.js": "4.4.3", + "chart.js": "4.4.4", "chartjs-adapter-date-fns": "3.0.0", "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "11.5.6", + "chromatic": "11.10.4", "compare-versions": "6.1.1", - "cropperjs": "2.0.0-rc.1", + "cropperjs": "2.0.0-rc.2", "date-fns": "2.30.0", - "escape-regexp": "0.0.1", "estree-walker": "3.0.3", "eventemitter3": "5.0.1", "idb-keyval": "6.2.1", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", - "katex": "0.16.9", + "katex": "0.16.10", "matter-js": "0.19.0", "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", + "frontend-shared": "workspace:*", "photoswipe": "5.4.4", "punycode": "2.3.1", - "rollup": "4.19.1", + "rollup": "4.22.5", "sanitize-html": "2.13.0", - "sass": "1.77.8", + "sass": "1.79.3", "shiki": "1.12.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.167.0", + "three": "0.169.0", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", - "typescript": "5.5.4", + "typescript": "5.6.2", "uuid": "10.0.0", - "v-code-diff": "1.12.0", - "vite": "5.3.5", - "vue": "3.4.37", + "v-code-diff": "1.13.1", + "vite": "5.4.8", + "vue": "3.5.10", "vuedraggable": "next" }, "devDependencies": { "@misskey-dev/summaly": "5.1.0", - "@storybook/addon-actions": "8.2.6", - "@storybook/addon-essentials": "8.2.6", - "@storybook/addon-interactions": "8.2.6", - "@storybook/addon-links": "8.2.6", - "@storybook/addon-mdx-gfm": "8.2.6", - "@storybook/addon-storysource": "8.2.6", - "@storybook/blocks": "8.2.6", - "@storybook/components": "8.2.6", - "@storybook/core-events": "8.2.6", - "@storybook/manager-api": "8.2.6", - "@storybook/preview-api": "8.2.6", - "@storybook/react": "8.2.6", - "@storybook/react-vite": "8.2.6", - "@storybook/test": "8.2.6", - "@storybook/theming": "8.2.6", - "@storybook/types": "8.2.6", - "@storybook/vue3": "8.2.6", - "@storybook/vue3-vite": "8.1.11", + "@storybook/addon-actions": "8.3.3", + "@storybook/addon-essentials": "8.3.3", + "@storybook/addon-interactions": "8.3.3", + "@storybook/addon-links": "8.3.3", + "@storybook/addon-mdx-gfm": "8.3.3", + "@storybook/addon-storysource": "8.3.3", + "@storybook/blocks": "8.3.3", + "@storybook/components": "8.3.3", + "@storybook/core-events": "8.3.3", + "@storybook/manager-api": "8.3.3", + "@storybook/preview-api": "8.3.3", + "@storybook/react": "8.3.3", + "@storybook/react-vite": "8.3.3", + "@storybook/test": "8.3.3", + "@storybook/theming": "8.3.3", + "@storybook/types": "8.3.3", + "@storybook/vue3": "8.3.3", + "@storybook/vue3-vite": "8.3.3", "@testing-library/vue": "8.1.0", - "@types/escape-regexp": "0.0.3", - "@types/estree": "1.0.5", + "@types/estree": "1.0.6", + "@types/katex": "^0.16.7", "@types/matter-js": "0.19.7", "@types/micromatch": "4.0.9", "@types/node": "20.14.12", "@types/punycode": "2.1.4", - "@types/sanitize-html": "2.11.0", + "@types/sanitize-html": "2.13.0", "@types/seedrandom": "3.0.8", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", - "@types/ws": "8.5.11", + "@types/ws": "8.5.12", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "@vitest/coverage-v8": "1.6.0", - "@vue/runtime-core": "3.4.37", + "@vue/runtime-core": "3.5.10", "acorn": "8.12.1", "cross-env": "7.0.3", - "cypress": "13.13.1", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-vue": "9.27.0", + "cypress": "13.15.0", + "eslint-plugin-import": "2.30.0", + "eslint-plugin-vue": "9.28.0", "fast-glob": "3.3.2", "happy-dom": "10.0.3", "intersection-observer": "0.12.2", - "micromatch": "4.0.7", - "msw": "2.3.4", + "micromatch": "4.0.8", + "msw": "2.4.9", "msw-storybook-addon": "2.0.3", - "nodemon": "3.1.4", + "nodemon": "3.1.7", "prettier": "3.3.3", "react": "18.3.1", "react-dom": "18.3.1", "seedrandom": "3.0.5", - "start-server-and-test": "2.0.4", - "storybook": "8.2.6", + "start-server-and-test": "2.0.8", + "storybook": "8.3.3", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", "vitest": "1.6.0", "vitest-fetch-mock": "0.2.2", - "vue-component-type-helpers": "2.0.29", + "vue-component-type-helpers": "2.1.6", "vue-eslint-parser": "9.4.3", - "vue-tsc": "2.0.29" + "vue-tsc": "2.1.6" } } diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts index 09495dece4c45eddc926b6da3856a03766183324..1601f247d759084bc061ac0b6b8f6c906e04bf13 100644 --- a/packages/frontend/src/_dev_boot_.ts +++ b/packages/frontend/src/_dev_boot_.ts @@ -3,11 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -// devモードã§èµ·å‹•ã•ã‚Œã‚‹éš›ï¼ˆindex.htmlを使ã†ã¨ã)ã¯routerãŒæš´ç™ºã—ã¦ã—ã¾ã£ã¦ã†ã¾ãèªã¿è¾¼ã‚ãªã„。 -// よã£ã¦ã€devモードã¨ã—ã¦èµ·å‹•ã•ã‚Œã‚‹ã¨ãã¯ãƒ“ルド時ã«çµ„ã¿è¾¼ã‚€å½¢ã¨ã—ã¦ãŠã。 -// (pnpm start時ã¯pugファイルã®ä¸ã§é™çš„リソースã¨ã—ã¦èªã¿è¾¼ã‚€ã‚ˆã†ã«ãªã£ã¦ãŠã‚Šã€ã“ã®å•é¡Œã¯èµ·ã“ã£ã¦ã„ãªã„) -import '@phosphor-icons/web/bold'; - await main(); import('@/_boot_.js'); diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 4fdd51c33bbd35eaaea7299baf0a455014ebc40a..e3416f2c296f2042f9fea67d429cc79a409fbfe6 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -8,9 +8,9 @@ import * as Misskey from 'misskey-js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; -import { MenuButton } from '@/types/menu.js'; +import type { MenuItem, MenuButton } from '@/types/menu.js'; import { del, get, set } from '@/scripts/idb-proxy.js'; -import { apiUrl } from '@/config.js'; +import { apiUrl } from '@@/js/config.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; @@ -289,14 +289,26 @@ export async function openAccountMenu(opts: { }); })); + const menuItems: MenuItem[] = []; + if (opts.withExtraOperation) { - popupMenu([...[{ - type: 'link' as const, + menuItems.push({ + type: 'link', text: i18n.ts.profile, - to: `/@${ $i.username }`, + to: `/@${$i.username}`, avatar: $i, - }, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { - type: 'parent' as const, + }, { + type: 'divider', + }); + + if (opts.includeCurrentAccount) { + menuItems.push(createItem($i)); + } + + menuItems.push(...accountItemPromises); + + menuItems.push({ + type: 'parent', icon: 'ti ti-plus', text: i18n.ts.addAccount, children: [{ @@ -307,7 +319,7 @@ export async function openAccountMenu(opts: { action: () => { createAccount(); }, }], }, { - type: 'link' as const, + type: 'link', icon: 'ti ti-users', text: i18n.ts.manageAccounts, to: '/settings/accounts', @@ -316,14 +328,18 @@ export async function openAccountMenu(opts: { icon: 'ph-power ph-bold ph-lg', text: i18n.ts.logout, action: () => { signout(); }, - }]], ev.currentTarget ?? ev.target, { - align: 'left', }); } else { - popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { - align: 'left', - }); + if (opts.includeCurrentAccount) { + menuItems.push(createItem($i)); + } + + menuItems.push(...accountItemPromises); } + + popupMenu(menuItems, ev.currentTarget ?? ev.target, { + align: 'left', + }); } if (_DEV_) { diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 94040c64132f69d676b7413198a35375f9a89ebf..af8bbf57d28761e08bc7336b70190ecd24451977 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -5,10 +5,10 @@ import { computed, watch, version as vueVersion, App } from 'vue'; import { compareVersions } from 'compare-versions'; +import { version, lang, langsVersion, updateLocale, locale } from '@@/js/config.js'; import widgets from '@/widgets/index.js'; import directives from '@/directives/index.js'; import components from '@/components/index.js'; -import { version, lang, updateLocale, locale } from '@/config.js'; import { applyTheme } from '@/scripts/theme.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; import { updateI18n } from '@/i18n.js'; @@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { setupRouter } from '@/router/definition.js'; +import { setupRouter } from '@/router/main.js'; +import { createMainRouter } from '@/router/definition.js'; export async function common(createVue: () => App<Element>) { console.info(`Sharkey v${version}`); @@ -80,14 +81,15 @@ export async function common(createVue: () => App<Element>) { //#region Detect language & fetch translations const localeVersion = miLocalStorage.getItem('localeVersion'); - const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null); + const localeOutdated = (localeVersion == null || localeVersion !== langsVersion || locale == null); if (localeOutdated) { - const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); + console.info(`Updating locales from version ${localeVersion ?? 'N/A'} to ${langsVersion}`); + const res = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`); if (res.status === 200) { const newLocale = await res.text(); const parsedNewLocale = JSON.parse(newLocale); miLocalStorage.setItem('locale', newLocale); - miLocalStorage.setItem('localeVersion', version); + miLocalStorage.setItem('localeVersion', langsVersion); updateLocale(parsedNewLocale); updateI18n(parsedNewLocale); } @@ -145,10 +147,9 @@ export async function common(createVue: () => App<Element>) { // NOTE: ã“ã®å‡¦ç†ã¯å¿…ãšã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆæ›´æ–°ãƒã‚§ãƒƒã‚¯å‡¦ç†ã‚ˆã‚Šå¾Œã«æ¥ã‚‹ã“ã¨(テーマå†æ§‹ç¯‰ã®ãŸã‚) watch(defaultStore.reactiveState.darkMode, (darkMode) => { applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); - document.documentElement.dataset.colorMode = darkMode ? 'dark' : 'light'; }, { immediate: miLocalStorage.getItem('theme') == null }); - document.documentElement.dataset.colorMode = defaultStore.state.darkMode ? 'dark' : 'light'; + document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light'; const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); @@ -243,7 +244,7 @@ export async function common(createVue: () => App<Element>) { const app = createVue(); - setupRouter(app); + setupRouter(app, createMainRouter); if (_DEV_) { app.config.performance = true; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index c10930a03820bc7a20e52a1e28bbdfefc224ae10..395d7d9ad1cbf2262c57d948c3202eb03b5f8636 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -6,7 +6,7 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue'; import { common } from './common.js'; import type * as Misskey from 'misskey-js'; -import { ui } from '@/config.js'; +import { ui } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { alert, confirm, popup, post, toast } from '@/os.js'; import { useStream } from '@/stream.js'; @@ -23,6 +23,7 @@ import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; import { setFavIconDot } from '@/scripts/favicon-dot.js'; import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; +import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -61,6 +62,18 @@ export async function mainBoot() { } }); + stream.on('emojiAdded', emojiData => { + addCustomEmoji(emojiData.emoji); + }); + + stream.on('emojiUpdated', emojiData => { + updateCustomEmojis(emojiData.emojis); + }); + + stream.on('emojiDeleted', emojiData => { + removeCustomEmojis(emojiData.emojis); + }); + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { import('@/plugin.js').then(async ({ install }) => { // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 @@ -216,19 +229,25 @@ export async function mainBoot() { claimAchievement('collectAchievements30'); } - window.setInterval(() => { - if (Math.floor(Math.random() * 20000) === 0) { - claimAchievement('justPlainLucky'); - } - }, 1000 * 10); + if (!claimedAchievements.includes('justPlainLucky')) { + window.setInterval(() => { + if (Math.floor(Math.random() * 20000) === 0) { + claimAchievement('justPlainLucky'); + } + }, 1000 * 10); + } - window.setTimeout(() => { - claimAchievement('client30min'); - }, 1000 * 60 * 30); + if (!claimedAchievements.includes('client30min')) { + window.setTimeout(() => { + claimAchievement('client30min'); + }, 1000 * 60 * 30); + } - window.setTimeout(() => { - claimAchievement('client60min'); - }, 1000 * 60 * 60); + if (!claimedAchievements.includes('client60min')) { + window.setTimeout(() => { + claimAchievement('client60min'); + }, 1000 * 60 * 60); + } // é‚ªé” //const lastUsed = miLocalStorage.getItem('lastUsed'); diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue index 7c8c7dbd304b877c7fd0c822dbcf00e934baac24..d13eedaade4380ea7ad90b51a6de216b73c505a4 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -4,40 +4,49 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="bcekxzvu _margin _panel"> - <div class="target"> - <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'"> - <MkAvatar class="avatar" :user="report.targetUser" indicator/> - <div class="names"> - <MkUserName class="name" :user="report.targetUser"/> - <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> + <div class="bcekxzvu _margin _panel"> + <div class="target"> + <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'"> + <MkAvatar class="avatar" :user="report.targetUser" indicator/> + <div class="names"> + <MkUserName class="name" :user="report.targetUser"/> + <MkAcct class="acct" :user="report.targetUser" style="display: block;"/> + </div> + </MkA> + <div class="keyvalCtn"> + <MkKeyValue> + <template #key>{{ i18n.ts.registeredDate }}</template> + <template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.reporter }}</template> + <template #value><MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.createdAt }}</template> + <template #value><MkTime :time="report.createdAt" mode="absolute"/> (<MkTime :time="report.createdAt" mode="relative"/>)</template> + </MkKeyValue> </div> - </MkA> - <MkKeyValue> - <template #key>{{ i18n.ts.registeredDate }}</template> - <template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template> - </MkKeyValue> - </div> - <div class="detail"> - <div> - <Mfm :text="report.comment" :isBlock="true" :linkNavigationBehavior="'window'"/> - </div> - <hr/> - <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div> - <div v-if="report.assignee"> - {{ i18n.ts.moderator }}: - <MkAcct :user="report.assignee"/> + <hr> </div> - <div><MkTime :time="report.createdAt"/></div> - <div class="action"> - <MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved"> - {{ i18n.ts.forwardReport }} - <template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template> - </MkSwitch> - <MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton> + <div class="detail"> + <div> + <Mfm :text="report.comment" :isBlock="true" :linkNavigationBehavior="'window'"/> + </div> + <hr/> + <div v-if="report.assignee" class="assignee"> + {{ i18n.ts.moderator }}: + <MkA :to="`/admin/user/${report.assignee.id}`" class="_link" :behavior="'window'">@{{ report.assignee.username }}</MkA> + </div> + <div class="action"> + <MkSwitch v-model="forward" c:disabled="report.targetUser.host == null || report.resolved"> + {{ i18n.ts.forwardReport }} + <template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template> + </MkSwitch> + <MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton> + </div> </div> </div> -</div> </template> <script lang="ts" setup> @@ -72,13 +81,13 @@ function resolve() { <style lang="scss" scoped> .bcekxzvu { display: flex; + flex-direction: column; + transition: .1s; > .target { - width: 35%; box-sizing: border-box; text-align: left; - padding: 24px; - border-right: solid 1px var(--divider); + padding: 24px 24px 0px 24px; > .info { display: flex; @@ -100,16 +109,36 @@ function resolve() { padding: 0 8px; flex: 1; + white-space: pre; + overflow: hidden; + > .name { font-weight: bold; } } } + + > .keyvalCtn { + display: inline-flex; + gap: 15px; + margin-top: 15px; + } } > .detail { - flex: 1; - padding: 24px; + display: flex; + flex-direction: column; + padding: 0px 24px 24px 24px; + + .assignee { + margin-bottom: 15px; + } + + .action { + display: flex; + flex-direction: column; + gap: 15px; + } } } </style> diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 6c0774b63426239e3c75edec379226a2fbc32651..796524fce949cff704971fd24066fe120df05ae2 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -16,7 +16,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkMention from './MkMention.vue'; import { i18n } from '@/i18n.js'; -import { host as localHost } from '@/config.js'; +import { host as localHost } from '@@/js/config.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; const user = ref<Misskey.entities.UserLite>(); diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index 7e150f7dd59de038f9224632b1082b19b2ec1d39..e2af4f034e439d83502a81213053f9fb29a596ca 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> </template> </MkFolder> - <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> + <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="containerStyle"> <template v-for="child in c.children" :key="child"> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/> </template> @@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { Ref, ref } from 'vue'; +import { Ref, ref, computed } from 'vue'; import * as os from '@/os.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -97,6 +97,29 @@ function g(id) { } as AsUiRoot; } +const containerStyle = computed(() => { + if (c.type !== 'container') return undefined; + + // width, color, styleã®ã†ã¡ä¸€ã¤ã§ã‚‚指定ãŒã‚ã‚Œã°ã€æž ç·šãŒã¡ã‚ƒã‚“ã¨è¡¨ç¤ºã•ã‚Œã‚‹ã‚ˆã†ã«widthã¨styleã®ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆå€¤ã‚’è¨å®š + // radiusã¯å˜ã«è§’を丸ã‚る用途もã‚ã‚‹ãŸã‚除外 + const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle; + + const border = isBordered ? { + borderWidth: c.borderWidth ?? '1px', + borderColor: c.borderColor ?? 'var(--divider)', + borderStyle: c.borderStyle ?? 'solid', + } : undefined; + + return { + textAlign: c.align, + backgroundColor: c.bgColor, + color: c.fgColor, + padding: c.padding ? `${c.padding}px` : 0, + borderRadius: (c.borderRadius ?? (c.rounded ? 8 : 0)) + 'px', + ...border, + }; +}); + const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false); function onSwitchUpdate(v) { diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index b04a1c92e7cf55a455ebd1207dfcc043e4603090..de5207f3504e5575099e94fa272d770dec84ab71 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import sanitizeHtml from 'sanitize-html'; +import { emojilist, getEmojiName } from '@@/js/emojilist.js'; import contains from '@/scripts/contains.js'; -import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js'; +import { char2twemojiFilePath, char2fluentEmojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; -import { emojilist, getEmojiName } from '@/scripts/emojilist.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { MFM_TAGS, MFM_PARAMS } from '@/const.js'; +import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js'; import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js'; const lib = emojilist.filter(x => x.category !== 'flags'); diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index f968fc586109aaf2228a52e22a2e459b6231299a..e30f74460db3fc978aa05eac651d580ec9bbdd35 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -171,11 +171,11 @@ function onMousedown(evt: MouseEvent): void { background: var(--accent); &:not(:disabled):hover { - background: var(--X8); + background: hsl(from var(--accent) h s calc(l + 5)); } &:not(:disabled):active { - background: var(--X8); + background: hsl(from var(--accent) h s calc(l + 5)); } } @@ -220,15 +220,16 @@ function onMousedown(evt: MouseEvent): void { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } &:not(:disabled):active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } &.danger { + font-weight: bold; color: #ff2a2a; &.primary { @@ -246,7 +247,7 @@ function onMousedown(evt: MouseEvent): void { } &:disabled { - opacity: 0.7; + opacity: 0.5; } &:focus-visible { diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index c5b6e0caede7f712c6408f54c35e9357377524b7..ab00ea9930ac4a737e98fccc1dbe9833695624ac 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -27,9 +27,12 @@ export type Captcha = { execute(id: string): void; reset(id?: string): void; getResponse(id: string): string; + WidgetInstance(container: string | Node, options: { + readonly [_ in 'sitekey' | 'doneCallback' | 'errorCallback' | 'puzzleEndpoint']?: unknown; + }): void; }; -export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; @@ -60,6 +63,7 @@ const variable = computed(() => { case 'recaptcha': return 'grecaptcha'; case 'turnstile': return 'turnstile'; case 'mcaptcha': return 'mcaptcha'; + case 'fc': return 'friendlyChallenge'; } }); @@ -70,6 +74,7 @@ const src = computed(() => { case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off'; case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit'; case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + case 'fc': return 'https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.18/widget.min.js'; case 'mcaptcha': return null; } }); @@ -110,6 +115,14 @@ async function requestRender() { key: props.sitekey, }, }); + } else if (variable.value === 'friendlyChallenge' && captchaEl.value instanceof Element) { + new captcha.value.WidgetInstance(captchaEl.value, { + sitekey: props.sitekey, + doneCallback: callback, + errorCallback: callback, + }); + // The following line is needed so that the design gets applied without it the captcha will look broken + captchaEl.value.className = 'frc-captcha'; } else { window.setTimeout(requestRender, 1); } diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index a50416befce239dfc9a413aa84b55420e0d855e4..2f7ec34d44ebbc3f827e854241b70c58c4e37d2a 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -117,7 +117,7 @@ const bannerStyle = computed(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); } > .name { diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 4b2456224991c22f3ce57c912c56c9e65570f82b..57d325b11ad38e16f7213c45f5a81264f4f74083 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> -<script lang="ts" setup> -/* eslint-disable id-denylist -- - Chart.js has a `data` attribute in most chart definitions, which triggers the - id-denylist violation when setting it. This is causing about 60+ lint issues. - As this is part of Chart.js's API it makes sense to disable the check here. -*/ -import { onMounted, ref, shallowRef, watch } from 'vue'; -import { Chart } from 'chart.js'; -import * as Misskey from 'misskey-js'; -import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { defaultStore } from '@/store.js'; -import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; -import { chartVLine } from '@/scripts/chart-vline.js'; -import { alpha } from '@/scripts/color.js'; -import date from '@/filters/date.js'; -import bytes from '@/filters/bytes.js'; -import { initChart } from '@/scripts/init-chart.js'; -import { chartLegend } from '@/scripts/chart-legend.js'; -import MkChartLegend from '@/components/MkChartLegend.vue'; - -initChart(); - -type ChartSrc = +<script lang="ts"> +export type ChartSrc = | 'federation' | 'ap-request' | 'users' @@ -62,7 +41,30 @@ type ChartSrc = | 'per-user-pv' | 'per-user-following' | 'per-user-followers' - | 'per-user-drive' + | 'per-user-drive'; +</script> + +<script lang="ts" setup> +/* eslint-disable id-denylist -- + Chart.js has a `data` attribute in most chart definitions, which triggers the + id-denylist violation when setting it. This is causing about 60+ lint issues. + As this is part of Chart.js's API it makes sense to disable the check here. +*/ +import { onMounted, ref, shallowRef, watch } from 'vue'; +import { Chart } from 'chart.js'; +import * as Misskey from 'misskey-js'; +import { misskeyApiGet } from '@/scripts/misskey-api.js'; +import { defaultStore } from '@/store.js'; +import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; +import { chartVLine } from '@/scripts/chart-vline.js'; +import { alpha } from '@/scripts/color.js'; +import date from '@/filters/date.js'; +import bytes from '@/filters/bytes.js'; +import { initChart } from '@/scripts/init-chart.js'; +import { chartLegend } from '@/scripts/chart-legend.js'; +import MkChartLegend from '@/components/MkChartLegend.vue'; + +initChart(); const props = withDefaults(defineProps<{ src: ChartSrc; diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 00506fb73513d6e3dba787706f6d409e94da9ad4..9a0a9fba0515297755b5d60f4687a825f9c11290 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onMounted, onUnmounted, ref } from 'vue'; import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import * as game from '@/scripts/clicker-game.js'; import number from '@/filters/number.js'; import { claimAchievement } from '@/scripts/achievements.js'; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index 2475e3dc89c75a72c9b69a3af10a10b0fc490068..05cde89dd973a7cd689181ebd35d140707cbca26 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.codeBlockRoot"> - <button :class="$style.codeBlockCopyButton" class="_button" @click="copy"> + <button v-if="copyButton" :class="$style.codeBlockCopyButton" class="_button" @click="copy"> <i class="ti ti-copy"></i> </button> <Suspense> @@ -32,12 +32,17 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ code: string; + forceShow?: boolean; + copyButton?: boolean; lang?: string; -}>(); +}>(), { + copyButton: true, + forceShow: false, +}); -const show = ref(!defaultStore.state.dataSaver.code); +const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code); const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue')); diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index d1fe5dbdcf267e9ee8f83432f3e873aa37669a9c..5f71e289b80bc5a246c5a0c4ae2e8a355faf16a8 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -216,7 +216,7 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index 8ea8fa6cf3a3a97af6be81c68104bdf4d200aafe..f51fefa0c0776b1fdd5a2d7349babbbee68e01a4 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue'; import MkMenu from './MkMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import contains from '@/scripts/contains.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index 54f6f39c9dc50679b16cd9f1a6273483db656bd7..2e1e92cbdfee4b3c2f04e0abb620d97dea1f883b 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -39,7 +39,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue'; import * as os from '@/os.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; -import { apiUrl } from '@/config.js'; +import { apiUrl } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrl } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 9976cd00c9f498f2062b72e2bf0768c1521c33bb..98bf5191f724ba749c5c95983177b895d54f1c50 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -43,9 +43,9 @@ export default defineComponent({ setup(props, { slots, expose }) { const $style = useCssModule(); // カスタムレンダラãªã®ã§ä½¿ã£ã¦ã‚‚大丈夫 - function getDateText(time: string) { - const date = new Date(time).getDate(); - const month = new Date(time).getMonth() + 1; + function getDateText(dateInstance: Date) { + const date = dateInstance.getDate(); + const month = dateInstance.getMonth() + 1; return i18n.tsx.monthAndDay({ month: month.toString(), day: date.toString(), @@ -62,9 +62,16 @@ export default defineComponent({ })[0]; if (el.key == null && item.id) el.key = item.id; + const date = new Date(item.createdAt); + const nextDate = props.items[i + 1] ? new Date(props.items[i + 1].createdAt) : null; + if ( i !== props.items.length - 1 && - new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate() + nextDate != null && ( + date.getFullYear() !== nextDate.getFullYear() || + date.getMonth() !== nextDate.getMonth() || + date.getDate() !== nextDate.getDate() + ) ) { const separator = h('div', { class: $style['separator'], @@ -78,12 +85,12 @@ export default defineComponent({ h('i', { class: `ti ti-chevron-up ${$style['date-1-icon']}`, }), - getDateText(item.createdAt), + getDateText(date), ]), h('span', { class: $style['date-2'], }, [ - getDateText(props.items[i + 1].createdAt), + getDateText(nextDate), h('i', { class: `ti ti-chevron-down ${$style['date-2-icon']}`, }), diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 825c1d0513662e7bf972014752d6ecbca3fd26d8..7dc381b662a246ba583cffa110c1c2b69ef1ccc6 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/> </div> <header v-if="title" :class="$style.title"><Mfm :text="title"/></header> - <div v-if="text" :class="$style.text"><Mfm :text="text" :isBlock="true" /></div> + <div v-if="text" :class="$style.text"><Mfm :text="text" :isBlock="true" :plain="plain" /></div> <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> <template #caption> @@ -105,6 +105,7 @@ const props = withDefaults(defineProps<{ cancelableByBgClick?: boolean; okText?: string; cancelText?: string; + plain?: boolean; }>(), { type: 'info', showOkButton: true, diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index 930f0f54cc29f94dc84ba06b3262b944e2e43327..1dfdebf0d4f3bc77972f882c705c6a3d99d1c19a 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkButton from '@/components/MkButton.vue'; import MkLink from '@/components/MkLink.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { miLocalStorage } from '@/local-storage.js'; diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 3990d0b861691fe5f7b01e47d27095e05523777c..44788a6ffb9ab7bf4a12f37f6dab1a9f3dcb2968 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 2d1c7c95f0d16cce887b1ed17daef997ba0dfced..a471457b449ad902a012f27543724403f1be7eec 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -634,7 +634,9 @@ function fetchMoreFiles() { } function getMenu() { - const menu: MenuItem[] = [{ + const menu: MenuItem[] = []; + + menu.push({ type: 'switch', text: i18n.ts.keepOriginalUploading, ref: keepOriginal, @@ -652,19 +654,25 @@ function getMenu() { }, { type: 'divider' }, { text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label', - }, folder.value ? { - text: i18n.ts.renameFolder, - icon: 'ti ti-forms', - action: () => { if (folder.value) renameFolder(folder.value); }, - } : undefined, folder.value ? { - text: i18n.ts.deleteFolder, - icon: 'ti ti-trash', - action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, - } : undefined, { + }); + + if (folder.value) { + menu.push({ + text: i18n.ts.renameFolder, + icon: 'ti ti-forms', + action: () => { if (folder.value) renameFolder(folder.value); }, + }, { + text: i18n.ts.deleteFolder, + icon: 'ti ti-trash', + action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, + }); + } + + menu.push({ text: i18n.ts.createFolder, icon: 'ti ti-folder-plus', action: () => { createFolder(); }, - }]; + }); return menu; } diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 9a6d272113c258d060e46e6e045e41ca11677999..543cf24022c5edfb5cc30f5cd08b6eb5acfc24b4 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div ref="thumbnail" :class="$style.root"> +<div + ref="thumbnail" + :class="[ + $style.root, + { [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive }, + ]" +> <ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/> <i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i> <i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i> @@ -27,6 +33,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; const props = defineProps<{ file: Misskey.entities.DriveFile; fit: 'cover' | 'contain'; + highlightWhenSensitive?: boolean; }>(); const is = computed(() => { @@ -67,6 +74,18 @@ const isThumbnailAvailable = computed(() => { overflow: clip; } +.sensitiveHighlight::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + border-radius: inherit; + box-shadow: inset 0 0 0 4px var(--warn); +} + .iconSub { position: absolute; width: 30%; diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..c060c3a659735e23f0008308d91e3ce1e8d0b440 --- /dev/null +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -0,0 +1,414 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialogEl" + :width="1000" + :height="600" + :scroll="false" + :withOkButton="false" + @close="cancel()" + @closed="$emit('closed')" +> + <template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template> + + <div :class="$style.embedCodeGenRoot"> + <Transition + mode="out-in" + :enterActiveClass="$style.transition_x_enterActive" + :leaveActiveClass="$style.transition_x_leaveActive" + :enterFromClass="$style.transition_x_enterFrom" + :leaveToClass="$style.transition_x_leaveTo" + > + <div v-if="phase === 'input'" key="input" :class="$style.embedCodeGenInputRoot"> + <div + :class="$style.embedCodeGenPreviewRoot" + > + <MkLoading v-if="iframeLoading" :class="$style.embedCodeGenPreviewSpinner"/> + <div :class="$style.embedCodeGenPreviewWrapper"> + <div class="_acrylic" :class="$style.embedCodeGenPreviewTitle">{{ i18n.ts.preview }}</div> + <div ref="resizerRootEl" :class="$style.embedCodeGenPreviewResizerRoot" inert> + <div + :class="$style.embedCodeGenPreviewResizer" + :style="{ transform: iframeStyle }" + > + <iframe + ref="iframeEl" + :src="embedPreviewUrl" + :class="$style.embedCodeGenPreviewIframe" + :style="{ height: `${iframeHeight}px` }" + @load="iframeOnLoad" + ></iframe> + </div> + </div> + </div> + </div> + <div :class="$style.embedCodeGenSettings" class="_gaps"> + <MkInput v-if="isEmbedWithScrollbar" v-model="maxHeight" type="number" :min="0"> + <template #label>{{ i18n.ts._embedCodeGen.maxHeight }}</template> + <template #suffix>px</template> + <template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template> + </MkInput> + <MkSelect v-model="colorMode"> + <template #label>{{ i18n.ts.theme }}</template> + <option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option> + <option value="light">{{ i18n.ts.light }}</option> + <option value="dark">{{ i18n.ts.dark }}</option> + </MkSelect> + <MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch> + <MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch> + <MkSwitch v-model="border">{{ i18n.ts._embedCodeGen.border }}</MkSwitch> + <MkInfo v-if="isEmbedWithScrollbar && (!maxHeight || maxHeight <= 0)" warn>{{ i18n.ts._embedCodeGen.maxHeightWarn }}</MkInfo> + <MkInfo v-if="typeof maxHeight === 'number' && (maxHeight <= 0 || maxHeight > 700)">{{ i18n.ts._embedCodeGen.previewIsNotActual }}</MkInfo> + <div class="_buttons"> + <MkButton :disabled="iframeLoading" @click="applyToPreview">{{ i18n.ts._embedCodeGen.applyToPreview }}</MkButton> + <MkButton :disabled="iframeLoading" primary @click="generate">{{ i18n.ts._embedCodeGen.generateCode }} <i class="ti ti-arrow-right"></i></MkButton> + </div> + </div> + </div> + <div v-else-if="phase === 'result'" key="result" :class="$style.embedCodeGenResultRoot"> + <div :class="$style.embedCodeGenResultWrapper" class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.embedCodeGenResultHeadingIcon"><i class="ti ti-check"></i></div> + <div :class="$style.embedCodeGenResultHeading">{{ i18n.ts._embedCodeGen.codeGenerated }}</div> + <div :class="$style.embedCodeGenResultDescription">{{ i18n.ts._embedCodeGen.codeGeneratedDescription }}</div> + </div> + <div class="_gaps_s"> + <MkCode :code="result" lang="html" :forceShow="true" :copyButton="false" :class="$style.embedCodeGenResultCode"/> + <MkButton :class="$style.embedCodeGenResultButtons" rounded primary @click="doCopy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> + </div> + <MkButton :class="$style.embedCodeGenResultButtons" rounded transparent @click="close">{{ i18n.ts.close }}</MkButton> + </div> + </div> + </Transition> + </div> +</MkModalWindow> +</template> + +<script setup lang="ts"> +import { shallowRef, ref, computed, nextTick, onMounted, onDeactivated, onUnmounted } from 'vue'; +import { url } from '@@/js/config.js'; +import { embedRouteWithScrollbar } from '@@/js/embed-page.js'; +import type { EmbeddableEntity, EmbedParams } from '@@/js/embed-page.js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; + +import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkButton from '@/components/MkButton.vue'; + +import MkCode from '@/components/MkCode.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js'; + +const emit = defineEmits<{ + (ev: 'ok'): void; + (ev: 'cancel'): void; + (ev: 'closed'): void; +}>(); + +const props = defineProps<{ + entity: EmbeddableEntity; + id: string; + params?: EmbedParams; +}>(); + +//#region Modalã®åˆ¶å¾¡ +const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); + +function cancel() { + emit('cancel'); + dialogEl.value?.close(); +} + +function close() { + dialogEl.value?.close(); +} + +const phase = ref<'input' | 'result'>('input'); +//#endregion + +//#region 埋ã‚è¾¼ã¿URL生æˆãƒ»ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚º + +// 本URL生æˆç”¨params +const paramsForUrl = computed<EmbedParams>(() => ({ + header: header.value, + maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined, + colorMode: colorMode.value === 'auto' ? undefined : colorMode.value, + rounded: rounded.value, + border: border.value, +})); + +// プレビュー用params(手動ã§æ›´æ–°ã‚’掛ã‘ã‚‹ã®ã§ref) +const paramsForPreview = ref<EmbedParams>(props.params ?? {}); + +const embedPreviewUrl = computed(() => { + const paramClass = new URLSearchParams(normalizeEmbedParams(paramsForPreview.value)); + if (paramClass.has('maxHeight')) { + const maxHeight = parseInt(paramClass.get('maxHeight')!); + paramClass.set('maxHeight', maxHeight === 0 ? '500' : Math.min(maxHeight, 700).toString()); // プレビューã§ã‚ã¾ã‚Šã«ã‚‚縮å°ã•ã‚Œã‚‹ã¨è¦‹ã¥ã‚‰ã„ãŸã‚ã€700pxã¾ã§ã«åˆ¶é™ + } + return `${url}/embed/${props.entity}/${props.id}${paramClass.toString() ? '?' + paramClass.toString() : ''}`; +}); + +const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity)); +const header = ref(props.params?.header ?? true); +const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500); + +const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto'); +const rounded = ref(props.params?.rounded ?? true); +const border = ref(props.params?.border ?? true); + +function applyToPreview() { + const currentPreviewUrl = embedPreviewUrl.value; + + paramsForPreview.value = { + header: header.value, + maxHeight: typeof maxHeight.value === 'number' ? Math.max(0, maxHeight.value) : undefined, + colorMode: colorMode.value === 'auto' ? undefined : colorMode.value, + rounded: rounded.value, + border: border.value, + }; + + nextTick(() => { + if (currentPreviewUrl === embedPreviewUrl.value) { + // URLãŒå¤‰ã‚らãªãã¦ã‚‚リãƒãƒ¼ãƒ‰ + iframeEl.value?.contentWindow?.location.reload(); + } + }); +} + +const result = ref(''); + +function generate() { + result.value = getEmbedCode(`/embed/${props.entity}/${props.id}`, paramsForUrl.value); + phase.value = 'result'; +} + +function doCopy() { + copyToClipboard(result.value); + os.success(); +} +//#endregion + +//#region プレビューã®ãƒªã‚µã‚¤ã‚º +const resizerRootEl = shallowRef<HTMLDivElement>(); +const iframeLoading = ref(true); +const iframeEl = shallowRef<HTMLIFrameElement>(); +const iframeHeight = ref(0); +const iframeScale = ref(1); +const iframeStyle = computed(() => { + return `translate(-50%, -50%) scale(${iframeScale.value})`; +}); +const resizeObserver = new ResizeObserver(() => { + calcScale(); +}); + +function iframeOnLoad() { + iframeEl.value?.contentWindow?.addEventListener('beforeunload', () => { + iframeLoading.value = true; + nextTick(() => { + iframeHeight.value = 0; + iframeScale.value = 1; + }); + }); +} + +function windowEventHandler(event: MessageEvent) { + if (event.source !== iframeEl.value?.contentWindow) { + return; + } + if (event.data.type === 'misskey:embed:ready') { + iframeEl.value!.contentWindow?.postMessage({ + type: 'misskey:embedParent:registerIframeId', + payload: { + iframeId: 'embedCodeGen', // åŒã˜ã‚¿ã‚¤ãƒŸãƒ³ã‚°ã§è¤‡æ•°ã®embed iframeãŒã‚ã‚‹éš›ã®åŒºåˆ¥ç”¨ãªã®ã§ã“ã“ã§ã¯ãªã‚“ã§ã‚‚ã„ã„ + }, + }); + } + if (event.data.type === 'misskey:embed:changeHeight') { + iframeHeight.value = event.data.payload.height; + nextTick(() => { + calcScale(); + iframeLoading.value = false; // åˆå›žã®é«˜ã•å¤‰æ›´ã¾ã§å¾…㤠+ }); + } +} + +function calcScale() { + if (!resizerRootEl.value) return; + const previewWidth = resizerRootEl.value.clientWidth - 40; // å·¦å³ã®ä½™ç™½ 20pxãšã¤ + const previewHeight = resizerRootEl.value.clientHeight - 40; // 上下ã®ä½™ç™½ 20pxãšã¤ + const iframeWidth = 500; + const scale = Math.min(previewWidth / iframeWidth, previewHeight / iframeHeight.value, 1); // 拡大ã¯ã—ãªã„ã®ã§1を上é™ã« + iframeScale.value = scale; +} + +onMounted(() => { + window.addEventListener('message', windowEventHandler); + if (!resizerRootEl.value) return; + resizeObserver.observe(resizerRootEl.value); +}); + +function reset() { + window.removeEventListener('message', windowEventHandler); + resizeObserver.disconnect(); + + // プレビューã®ãƒªã‚»ãƒƒãƒˆ + iframeHeight.value = 0; + iframeScale.value = 1; + iframeLoading.value = true; + result.value = ''; + phase.value = 'input'; +} + +onDeactivated(() => { + reset(); +}); + +onUnmounted(() => { + reset(); +}); +//#endregion +</script> + +<style module> +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.embedCodeGenRoot { + container-type: inline-size; + height: 100%; +} + +.embedCodeGenInputRoot { + height: 100%; + display: grid; + grid-template-columns: 1fr 400px; +} + +.embedCodeGenPreviewRoot { + position: relative; + background-color: var(--bg); + background-size: auto auto; + background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px); + cursor: not-allowed; +} + +.embedCodeGenPreviewWrapper { + display: flex; + flex-direction: column; + height: 100%; + pointer-events: none; + user-select: none; + -webkit-user-drag: none; +} + +.embedCodeGenPreviewTitle { + position: absolute; + z-index: 100; + top: 8px; + left: 8px; + padding: 6px 10px; + border-radius: 6px; + font-size: 85%; +} + +.embedCodeGenPreviewSpinner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + user-select: none; + -webkit-user-drag: none; +} + +.embedCodeGenPreviewResizerRoot { + position: relative; + flex: 1 0; +} + +.embedCodeGenPreviewResizer { + position: absolute; + top: 50%; + left: 50%; +} + +.embedCodeGenPreviewIframe { + display: block; + border: none; + width: 500px; + color-scheme: light dark; +} + +.embedCodeGenSettings { + padding: 24px; + overflow-y: scroll; +} + +.embedCodeGenResultRoot { + box-sizing: border-box; + padding: 24px; + height: 100%; + max-width: 700px; + margin: 0 auto; + display: flex; + align-items: center; +} + +.embedCodeGenResultHeading { + text-align: center; + font-size: 1.2em; +} + +.embedCodeGenResultHeadingIcon { + margin: 0 auto; + background-color: var(--accentedBg); + color: var(--accent); + text-align: center; + height: 64px; + width: 64px; + font-size: 24px; + line-height: 64px; + border-radius: 50%; +} + +.embedCodeGenResultDescription { + text-align: center; + white-space: pre-wrap; +} + +.embedCodeGenResultWrapper, +.embedCodeGenResultCode { + width: 100%; +} + +.embedCodeGenResultButtons { + margin: 0 auto; +} + +@container (max-width: 800px) { + .embedCodeGenInputRoot { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } +} +</style> diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index 008613c27e74aa215afea0c84d880db8a2af42fa..151843b18ce660982ac911a393782c71d918e78a 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed, Ref } from 'vue'; -import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js'; +import { CustomEmojiFolderTree, getEmojiName } from '@@/js/emojilist.js'; import { i18n } from '@/i18n.js'; import { customEmojis } from '@/custom-emojis.js'; import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 297d5f899eba549fb11b74932e36a2e3d3755cb3..949ed4db91382640195d2818468c2c1ed22b9b9c 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -117,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, shallowRef, computed, watch, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; -import XSection from '@/components/MkEmojiPicker.section.vue'; import { emojilist, emojiCharByCategory, @@ -126,7 +125,8 @@ import { getEmojiName, CustomEmojiFolderTree, getUnicodeEmoji, -} from '@/scripts/emojilist.js'; +} from '@@/js/emojilist.js'; +import XSection from '@/components/MkEmojiPicker.section.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; @@ -233,7 +233,7 @@ watch(q, () => { // åå‰ã«ã‚ーワードãŒå«ã¾ã‚Œã¦ã„ã‚‹ for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword))) { + if (keywords.every(keyword => emoji.name.toLowerCase().includes(keyword))) { matches.add(emoji); if (matches.size >= max) break; } @@ -242,7 +242,7 @@ watch(q, () => { // åå‰ã¾ãŸã¯ã‚¨ã‚¤ãƒªã‚¢ã‚¹ã«ã‚ーワードãŒå«ã¾ã‚Œã¦ã„ã‚‹ for (const emoji of emojis) { - if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) { + if (keywords.every(keyword => emoji.name.toLowerCase().includes(keyword) || emoji.aliases.some(alias => alias.toLowerCase().includes(keyword)))) { matches.add(emoji); if (matches.size >= max) break; } @@ -254,7 +254,7 @@ watch(q, () => { if (matches.size >= max) return matches; for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias === newQ)) { + if (emoji.aliases.some(alias => alias.toLowerCase() === newQ)) { matches.add(emoji); if (matches.size >= max) break; } @@ -262,7 +262,7 @@ watch(q, () => { if (matches.size >= max) return matches; for (const emoji of emojis) { - if (emoji.name.startsWith(newQ)) { + if (emoji.name.toLowerCase().startsWith(newQ)) { matches.add(emoji); if (matches.size >= max) break; } @@ -270,7 +270,7 @@ watch(q, () => { if (matches.size >= max) return matches; for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.startsWith(newQ))) { + if (emoji.aliases.some(alias => alias.toLowerCase().startsWith(newQ))) { matches.add(emoji); if (matches.size >= max) break; } @@ -278,7 +278,7 @@ watch(q, () => { if (matches.size >= max) return matches; for (const emoji of emojis) { - if (emoji.name.includes(newQ)) { + if (emoji.name.toLowerCase().includes(newQ)) { matches.add(emoji); if (matches.size >= max) break; } @@ -286,7 +286,7 @@ watch(q, () => { if (matches.size >= max) return matches; for (const emoji of emojis) { - if (emoji.aliases.some(alias => alias.includes(newQ))) { + if (emoji.aliases.some(alias => alias.toLowerCase().includes(newQ))) { matches.add(emoji); if (matches.size >= max) break; } @@ -611,6 +611,7 @@ defineExpose({ width: auto; height: auto; min-width: 0; + padding: 0; &:disabled { cursor: not-allowed; @@ -717,7 +718,7 @@ defineExpose({ > .item { position: relative; - padding: 0; + padding: 0 3px; width: var(--eachSize); height: var(--eachSize); contain: strict; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index 0ec0679393faded61d150a1a3a82bf359c202dcd..18e80d84458031eac725dcc0f18f92449b7c7711 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="modal" v-slot="{ type, maxHeight }" :zPriority="'middle'" - :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'" + :preferType="defaultStore.state.emojiPickerStyle" :hasInteractionWithOtherFocusTrappedEls="true" :transparentBg="true" :manualShowing="manualShowing" diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue index bab844c27f83a40519eca363112cc67395f9b5d6..7af68a32ba681e2c0d439e698927a07b444196fb 100644 --- a/packages/frontend/src/components/MkFileListForAdmin.vue +++ b/packages/frontend/src/components/MkFileListForAdmin.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only class="file _button" > <div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div> - <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> + <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/> <div v-if="viewMode === 'list'" class="body"> <div> <small style="opacity: 0.7;">{{ file.name }}</small> diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 229cd59056778eb83d7d5d0623119d238a1b80f1..392963fdb94d9607caeb6f96da75ca4d2b998d03 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :marginMin="14" :marginMax="22"> <slot></slot> </MkSpacer> + <div v-if="$slots.footer" :class="$style.footer"> + <slot name="footer"></slot> + </div> </div> </KeepAlive> </Transition> @@ -136,7 +139,7 @@ onMounted(() => { width: 100%; box-sizing: border-box; padding: 9px 12px 9px 12px; - background: var(--buttonBg); + background: var(--folderHeaderBg); -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); border-radius: var(--radius-sm); @@ -144,7 +147,7 @@ onMounted(() => { &:hover { text-decoration: none; - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } &:focus-within { @@ -153,7 +156,7 @@ onMounted(() => { &.active { color: var(--accent); - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } &.opened { @@ -224,4 +227,18 @@ onMounted(() => { background: var(--bg); } } + +.footer { + position: sticky !important; + z-index: 1; + bottom: var(--stickyBottom, 0px); + left: 0; + padding: 12px; + background: var(--acrylicBg); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + background-size: auto auto; + background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px); + border-radius: 0 0 6px 6px; +} </style> diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index 3fdf673eb3667253d143656a8b4fa6f853d97054..52497a29948340f5fe1aa3c8c79975b0246756f7 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/> </template> <template v-else-if="isFollowing"> - <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i> </template> <template v-else-if="!isFollowing && user.isLocked"> <span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> @@ -43,7 +43,7 @@ import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { pleaseLogin } from '@/scripts/please-login.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue new file mode 100644 index 0000000000000000000000000000000000000000..1e88d59d8edeefbf5ae2581015d73dfb5fb17b81 --- /dev/null +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -0,0 +1,49 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div> + <div style="margin-left: auto;" class="_buttons"> + <MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton> + <MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </div> +</div> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import MkButton from './MkButton.vue'; +import { i18n } from '@/i18n.js'; + +const props = defineProps<{ + form: { + modifiedCount: { + value: number; + }; + discard: () => void; + save: () => void; + }; +}>(); +</script> + +<style lang="scss" module> +.root { + display: flex; + align-items: center; +} + +.text { + color: var(--warn); + font-size: 90%; + animation: modified-blink 2s infinite; +} + +@keyframes modified-blink { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} +</style> diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 8d301f16bd4e4e279d46af23b6cc802350f9d93b..c04d0864fb1e8a27877be449fefd3ddd7df3554c 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import DrawBlurhash from '@/workers/draw-blurhash?worker'; import TestWebGL2 from '@/workers/test-webgl2?worker'; -import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js'; -import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js'; +import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js'; +import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => { // テスト環境㧠Web Worker インスタンスã¯ä½œæˆã§ããªã„ diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 06389b40130b3f6baeb6dd1f631f95c4e00598b7..42e1146e270691319443533f2587c2932368f536 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { debounce } from 'throttle-debounce'; import MkButton from '@/components/MkButton.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js'; diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue index 094d2f177f3ba1b9edd0d9558f3f9974a05f6ec7..46d42248d3b0c8c70325c958bd4c3159f721161a 100644 --- a/packages/frontend/src/components/MkInstanceTicker.vue +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed } from 'vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { instance as Instance } from '@/instance.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index de51a98789dcda264cb29d19407a9f0f84473093..4aee64f78e52cf4280bbeca873d73675c6c0c19f 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -11,8 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span> <span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span> </template> + <template #footer> + <div class="_buttons"> + <MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> + <MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </template> - <div class="_gaps_s" :class="$style.root"> + <div :class="$style.root"> <div :class="$style.items"> <div> <div :class="$style.label">{{ i18n.ts.invitationCode }}</div> @@ -49,10 +55,6 @@ SPDX-License-Identifier: AGPL-3.0-only <div><MkTime :time="invite.createdAt" mode="absolute"/></div> </div> </div> - <div :class="$style.buttons"> - <MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> - <MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> </div> </MkFolder> </template> @@ -121,9 +123,4 @@ function copyInviteCode() { width: var(--height); height: var(--height); } - -.buttons { - display: flex; - gap: 8px; -} </style> diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 07cf9e0c37e0caa82a38c533ac170ed48d2780f1..263cd95eb10580b89ef2f39486a8b7f985cd41c4 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only :is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substring(local.length) : url" :rel="rel ?? 'nofollow noopener'" :target="target" :behavior="props.navigationBehavior" :title="url" + @click.prevent="self ? true : warningExternalWebsite(url)" @click.stop > <slot></slot> @@ -17,11 +18,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; -import { url as local } from '@/config.js'; +import { url as local } from '@@/js/config.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import * as os from '@/os.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { MkABehavior } from '@/components/global/MkA.vue'; +import { warningExternalWebsite } from '@/scripts/warning-external-website.js'; const props = withDefaults(defineProps<{ url: string; diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 93affd930f106f740eca65bf9acd39f1dbfc4d6c..2d7cde1af283c42bfd3ba97cca5504a90ec2926b 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -175,9 +175,7 @@ async function show() { const menuShowing = ref(false); function showMenu(ev: MouseEvent) { - let menu: MenuItem[] = []; - - menu = [ + const menu: MenuItem[] = [ // TODO: å†ç”Ÿã‚ューã«è¿½åŠ { type: 'switch', @@ -225,7 +223,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider', }, { - type: 'link' as const, + type: 'link', text: i18n.ts._fileViewer.title, icon: 'ti ti-info-circle', to: `/my/drive/file/${props.audio.id}`, diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index ed8d43273fbcb94545a97382c1fb7e66d1855dfe..77a86ff2fb5d887eeea63972ae0b85b7564f73e0 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> - <div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="show"> + <MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/> + <div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show"> <span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span> <b>{{ i18n.ts.sensitive }}</b> <span>{{ i18n.ts.clickToShow }}</span> </div> - <MkMediaAudio v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/> <a v-else :class="$style.download" :href="media.url" diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index cac419d42bd0298b3c058d0a3f7fea9febf07fb5..02c054956cf44098a771a7f6d9271be56fa82b42 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick"> +<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> <component :is="disableImageLink ? 'div' : 'a'" v-bind="disableImageLink ? { @@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; +import type { MenuItem } from '@/types/menu.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import bytes from '@/filters/bytes.js'; import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; @@ -75,7 +76,6 @@ const props = withDefaults(defineProps<{ }); const hide = ref(true); -const darkMode = ref<boolean>(defaultStore.state.darkMode); const url = computed(() => (props.raw || defaultStore.state.loadRawImages) ? props.image.url @@ -112,27 +112,39 @@ watch(() => props.image, () => { }); function showMenu(ev: MouseEvent) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ text: i18n.ts.hide, icon: 'ti ti-eye-off', action: () => { hide.value = true; }, - }, ...(iAmModerator ? [{ - text: i18n.ts.markAsSensitive, - icon: 'ti ti-eye-exclamation', - danger: true, - action: () => { - os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); - }, - }] : []), ...($i?.id === props.image.userId ? [{ - type: 'divider' as const, - }, { - type: 'link' as const, - text: i18n.ts._fileViewer.title, - icon: 'ti ti-info-circle', - to: `/my/drive/file/${props.image.id}`, - }] : [])], ev.currentTarget ?? ev.target); + }); + + if (iAmModerator) { + menuItems.push({ + text: i18n.ts.markAsSensitive, + icon: 'ti ti-eye-exclamation', + danger: true, + action: () => { + os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); + }, + }); + } + + if ($i?.id === props.image.userId) { + menuItems.push({ + type: 'divider', + }, { + type: 'link', + text: i18n.ts._fileViewer.title, + icon: 'ti ti-info-circle', + to: `/my/drive/file/${props.image.id}`, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } </script> @@ -197,10 +209,19 @@ function showMenu(ev: MouseEvent) { position: relative; //box-shadow: 0 0 0 1px var(--divider) inset; background: var(--bg); - background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); background-size: 16px 16px; } +html[data-color-scheme=dark] .visible { + --c: rgb(255 255 255 / 2%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); +} + +html[data-color-scheme=light] .visible { + --c: rgb(0 0 0 / 2%); + background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%); +} + .menu { display: block; position: absolute; diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 4bc2b9fba7df8ed506dde94b321d78a8a08fa739..39fa6ff012275596b35ef669392365edae77d11e 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -39,7 +39,7 @@ import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; import XModPlayer from '@/components/SkModPlayer.vue'; import * as os from '@/os.js'; -import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js'; +import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@@/js/const.js'; import { defaultStore } from '@/store.js'; import { focusParent } from '@/scripts/focus.js'; diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 1c3c9a312b0aa05f7fdfe11b1b849a4125f7f97a..0502bdd40112cb7f966217f758cd9d9694a6fe0d 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -195,9 +195,7 @@ async function show() { const menuShowing = ref(false); function showMenu(ev: MouseEvent) { - let menu: MenuItem[] = []; - - menu = [ + const menu: MenuItem[] = [ // TODO: å†ç”Ÿã‚ューã«è¿½åŠ { type: 'switch', @@ -250,7 +248,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider', }, { - type: 'link' as const, + type: 'link', text: i18n.ts._fileViewer.title, icon: 'ti ti-info-circle', to: `/my/drive/file/${props.video.id}`, diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 80a1b684598c1bedc9b02f9ed3e4cef9e136f462..de2048b6f210c5396dc42b50e79ad3120b653ea6 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { toUnicode } from 'punycode'; import { computed } from 'vue'; import tinycolor from 'tinycolor2'; -import { host as localHost } from '@/config.js'; +import { host as localHost } from '@@/js/config.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue index 235790556c5bb667f8ab92b3f53c21b06603e24f..086573ba6dd61ad8f27d37666b1adf82439ac18f 100644 --- a/packages/frontend/src/components/MkMenu.child.vue +++ b/packages/frontend/src/components/MkMenu.child.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue'; import MkMenu from './MkMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ items: MenuItem[]; diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 0537f4f98873b8c1e66bf9ec97b7025836bf2f95..fe6df7090cbc399a930d26ccea542ac6ac3b3461 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -4,17 +4,22 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div role="menu" @focusin.passive.stop="() => {}"> +<div + role="menu" + :class="{ + [$style.root]: true, + [$style.center]: align === 'center', + [$style.big]: big, + [$style.asDrawer]: asDrawer, + }" + @focusin.passive.stop="() => {}" +> <div ref="itemsEl" v-hotkey="keymap" tabindex="0" class="_popup _shadow" - :class="{ - [$style.root]: true, - [$style.center]: align === 'center', - [$style.asDrawer]: asDrawer, - }" + :class="$style.menu" :style="{ width: (width && !asDrawer) ? `${width}px` : '', maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)', @@ -200,6 +205,8 @@ const emit = defineEmits<{ (ev: 'hide'): void; }>(); +const big = isTouchUsing; + const isNestingMenu = inject<boolean>('isNestingMenu', false); const itemsEl = shallowRef<HTMLElement>(); @@ -297,6 +304,8 @@ async function showRadioOptions(item: MenuRadio, ev: Event) { } async function showChildren(item: MenuParent, ev: Event) { + ev.stopPropagation(); + const children: MenuItem[] = await (async () => { if (childrenCache.has(item)) { return childrenCache.get(item)!; @@ -418,48 +427,67 @@ onBeforeUnmount(() => { <style lang="scss" module> .root { - padding: 8px 0; - box-sizing: border-box; - max-width: 100vw; - min-width: 200px; - overflow: auto; - overscroll-behavior: contain; - - &:focus-visible { - outline: none; + &.center { + > .menu { + > .item { + text-align: center; + } + } } - &.center { - > .item { - text-align: center; + &.big:not(.asDrawer) { + > .menu { + > .item { + padding: 6px 20px; + font-size: 1em; + line-height: 24px; + } } } &.asDrawer { - padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; - width: 100%; - border-radius: var(--radius-lg); - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - - > .item { - font-size: 1em; - padding: 12px 24px; + max-width: 600px; + margin: auto; - &::before { - width: calc(100% - 24px); - border-radius: var(--radius); + > .menu { + padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; + width: 100%; + border-radius: var(--radius-lg); + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + + > .item { + font-size: 1em; + padding: 12px 24px; + + &::before { + width: calc(100% - 24px); + border-radius: var(--radius); + } + + > .icon { + margin-right: 14px; + width: 24px; + } } - > .icon { - margin-right: 14px; - width: 24px; + > .divider { + margin: 12px 0; } } + } +} - > .divider { - margin: 12px 0; - } +.menu { + padding: 8px 0; + box-sizing: border-box; + max-width: 100vw; + min-width: 200px; + overflow: auto; + overscroll-behavior: contain; + + &:focus-visible { + outline: none; } } diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue index f2f2bf47a8b59a1bf0d26be87ea8f10c7dd34b70..1b6f6cef31e45a25b60a37d063eda62d04ca7a27 100644 --- a/packages/frontend/src/components/MkMiniChart.vue +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { watch, ref } from 'vue'; import { v4 as uuid } from 'uuid'; import tinycolor from 'tinycolor2'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const props = defineProps<{ src: number[]; diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index f8032f9b431bb547ce7249fb2fff2b518700d5ef..c766a338239abdeb5fb38eb97ef6a7dcaca425b8 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority); const useSendAnime = ref(false); const type = computed<ModalTypes>(() => { if (props.preferType === 'auto') { - if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { + if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) { return 'drawer'; } else { return props.src != null ? 'popup' : 'dialog'; diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index c3c78120362194363f7cee980503fda2c3017223..f26959888b428a57792f3f68d5283bffc46c971e 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -94,12 +94,12 @@ defineExpose({ --root-margin: 24px; + --headerHeight: 46px; + --headerHeightNarrow: 42px; + @media (max-width: 500px) { --root-margin: 16px; } - - --headerHeight: 46px; - --headerHeightNarrow: 42px; } .header { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index e2f0a4e492351ccd486424aaf16ad9f20ed26b1d..7ba4f0b9d9135cca7699bca449fbac2d995d0a91 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -62,7 +62,15 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="container-type: inline-size;"> <bdi> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/> + <Mfm + v-if="appearNote.cw != ''" + :text="appearNote.cw" + :author="appearNote.user" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + :isBlock="true" + /> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> @@ -94,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.files && appearNote.files.length > 0"> <MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/> </div> - <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" @click.stop/> + <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" @click.stop/> <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/> </div> @@ -148,7 +156,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> @@ -192,6 +200,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; +import { isLink } from '@@/js/is-link.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; @@ -224,13 +233,13 @@ import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; -import { shouldCollapsed } from '@/scripts/collapsed.js'; import { useRouter } from '@/router/supplier.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; -import { host } from '@/config.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import { host } from '@@/js/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; @@ -565,7 +574,8 @@ function quote() { os.post({ renote: appearNote.value, channel: appearNote.value.channel, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -589,7 +599,8 @@ function quote() { } else { os.post({ renote: appearNote.value, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -742,16 +753,6 @@ function onContextmenu(ev: MouseEvent): void { return; } - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - // å†ç”Ÿé€Ÿåº¦ã®é¸æŠžãªã©ã®ãŸã‚ã«ã€Audioè¦ç´ ã®ã‚³ãƒ³ãƒ†ã‚ストメニューã¯ãƒ–ラウザデフォルトã¨ã™ã‚‹ã€‚ - if (el.tagName === 'AUDIO') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; - if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; @@ -882,7 +883,7 @@ function emitUpdReaction(emoji: string, delta: number) { // 今度ã¯ãã®å‡¦ç†è‡ªä½“ãŒãƒ‘フォーマンス低下ã®åŽŸå› ã«ãªã‚‰ãªã„ã‹æ‡¸å¿µã•ã‚Œã‚‹ã€‚ã¾ãŸã€è¢«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ã‚‚高ã•ã¯å¤‰åŒ–ã™ã‚‹ãŸã‚ã€ã‚„ã¯ã‚Šå¤šå°‘ã®ã‚ºãƒ¬ã¯ç”Ÿã˜ã‚‹ // 一度レンダリングã•ã‚ŒãŸè¦ç´ ã¯ãƒ–ラウザãŒã‚ˆã—ãªã«ã‚µã‚¤ã‚ºã‚’覚ãˆã¦ãŠã„ã¦ãれるよã†ãªå®Ÿè£…ã«ãªã‚‹ã¾ã§å¾…ã£ãŸæ–¹ãŒè‰¯ã•ãã†(ãªã‚‹ã®ã‹ï¼Ÿ) //content-visibility: auto; - //contain-intrinsic-size: 0 128px; + //contain-intrinsic-size: 0 128px; &:focus-visible { outline: none; @@ -1126,7 +1127,7 @@ function emitUpdReaction(emoji: string, delta: number) { z-index: 2; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), var(--X15)); + //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); &:hover > .collapsedLabel { background: var(--panelHighlight); diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 64559ef2655d8bc5e6d0ad19ba545a95fb6e9943..5acb18c871537032cd042fee3fdb47416663d4ce 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -69,7 +69,15 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/> + <Mfm + v-if="appearNote.cw != ''" + :text="appearNote.cw" + :author="appearNote.user" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + :isBlock="true" + /> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/> </p> <div v-show="appearNote.cw == null || showContent"> @@ -100,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.files && appearNote.files.length > 0"> <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/> </div> - <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/> + <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll"/> <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/> </div> @@ -149,7 +157,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> @@ -227,6 +235,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; +import { isLink } from '@@/js/is-link.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -250,9 +259,10 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; + import { useNoteCapture } from '@/scripts/use-note-capture.js'; import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; @@ -550,7 +560,8 @@ function quote() { os.post({ renote: appearNote.value, channel: appearNote.value.channel, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -574,7 +585,8 @@ function quote() { } else { os.post({ renote: appearNote.value, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -701,14 +713,6 @@ function toggleReact() { } function onContextmenu(ev: MouseEvent): void { - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; - if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index fd5da0d68784509a5048559fc8869184eef8a499..cd6fdf576ce476510a13afe340d78358408d7dc6 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,14 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <header :class="$style.root"> - <div v-if="mock" :class="$style.name"> - <MkUserName :user="note.user"/> - </div> - <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> - <MkUserName :user="note.user"/> - </MkA> - <div v-if="note.user.isBot" :class="$style.isBot">bot</div> - <div :class="$style.username"><MkAcct :user="note.user"/></div> + <component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;"> + <div style="display: flex; white-space: nowrap; align-items: baseline;"> + <div v-if="mock" :class="$style.name"> + <MkUserName :user="note.user"/> + </div> + <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + <div v-if="note.user.isBot" :class="$style.isBot">bot</div> + <div :class="$style.username"><MkAcct :user="note.user"/></div> + </div> + </component> <div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/> </div> @@ -43,6 +47,7 @@ import { notePage } from '@/filters/note.js'; import { userPage } from '@/filters/user.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { popupMenu } from '@/os.js'; +import { defaultStore } from '@/store.js'; const props = defineProps<{ note: Misskey.entities.Note; diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 9caed62ce2447a36d3004331c6ec42d7d466958a..45276839add436e63e9e5df0dc7d8fa3fd0be13b 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -339,7 +339,8 @@ function quote() { os.post({ renote: appearNote.value, channel: appearNote.value.channel, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, @@ -363,7 +364,8 @@ function quote() { } else { os.post({ renote: appearNote.value, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 99486761983206e493ec450ad0c3c3edccf452dd..7bec9bdc65022b133a0f4cfba0fb9dd87efb20dd 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/> - <img v-else-if="'icon' in notification" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> + <MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/> + <img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> <div :class="[$style.subIcon, { [$style.t_follow]: notification.type === 'follow', @@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_quote]: notification.type === 'quote', [$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_achievementEarned]: notification.type === 'achievementEarned', + [$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_pollEnded]: notification.type === 'edited', }]" @@ -38,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> + <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> <template v-else-if="notification.type === 'roleAssigned'"> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <i v-else class="ti ti-badges"></i> @@ -49,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withTooltip="true" :reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')" :noStyle="true" - style="width: 100%; height: 100%;" + style="width: 100%; height: 100% !important; object-fit: contain;" /> </div> </div> @@ -60,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> + <span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> @@ -102,10 +106,20 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> {{ i18n.ts._achievements._types['_' + notification.achievement].title }} </MkA> + <MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`"> + {{ i18n.ts.showFile }} + </MkA> <template v-else-if="notification.type === 'follow'"> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> </template> - <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> + <template v-else-if="notification.type === 'followRequestAccepted'"> + <div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div> + <div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;"> + <i class="ti ti-quote" :class="$style.quote"></i> + <span>{{ notification.message }}</span> + <i class="ti ti-quote" :class="$style.quote"></i> + </div> + </template> <template v-else-if="notification.type === 'receiveFollowRequest'"> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span> <div v-if="full && !followRequestDone" :class="$style.followRequestCommands"> @@ -126,7 +140,7 @@ SPDX-License-Identifier: AGPL-3.0-only :withTooltip="true" :reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')" :noStyle="true" - style="width: 100%; height: 100%;" + style="width: 100%; height: 100% !important; object-fit: contain;" /> </div> </div> @@ -171,6 +185,20 @@ const props = withDefaults(defineProps<{ full: false, }); +type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' }; + +const exportEntityName = { + antenna: i18n.ts.antennas, + blocking: i18n.ts.blockedUsers, + clip: i18n.ts.clips, + customEmoji: i18n.ts.customEmojis, + favorite: i18n.ts.favorites, + following: i18n.ts.following, + muting: i18n.ts.mutedUsers, + note: i18n.ts.notes, + userList: i18n.ts.lists, +} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>; + const followRequestDone = ref(false); const acceptFollowRequest = () => { @@ -200,6 +228,14 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) overflow-wrap: break-word; display: flex; contain: content; + + --eventFollow: #36aed2; + --eventRenote: #36d298; + --eventReply: #007aff; + --eventReactionHeart: var(--love); + --eventReaction: #e99a0b; + --eventAchievement: #cb9a11; + --eventOther: #88a6b7; } .head { @@ -308,6 +344,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_exportCompleted { + padding: 3px; + background: var(--eventOther); + pointer-events: none; +} + .t_roleAssigned { padding: 3px; background: var(--eventOther); diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue index 71b38d99ed9c92897842402b5ca269ce79634b8c..47a9c79e4551c2f72a97f51ea58a7d5f1ffa82a6 100644 --- a/packages/frontend/src/components/MkNotificationSelectWindow.vue +++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue @@ -35,7 +35,7 @@ import MkSwitch from './MkSwitch.vue'; import MkInfo from './MkInfo.vue'; import MkButton from './MkButton.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; import { i18n } from '@/i18n.js'; type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 2dd6c21ef6d383256cbb3ff78e6142234d6b0106..a395734add40dd589f832c136dfebae8b13f45d3 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -30,7 +30,7 @@ import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue index a0bc0c628e8753ba766506f0b692f45fc39ad38e..94cbaf5c917ad83378f701f466a15a9bacd068bb 100644 --- a/packages/frontend/src/components/MkOmit.vue +++ b/packages/frontend/src/components/MkOmit.vue @@ -62,7 +62,7 @@ onUnmounted(() => { left: 0; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), var(--X15)); + //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 8f6109ca04b782b4c5f9e508a1aee772a4bfbde0..f67a1e5b634566a6baff09be481420187bd599b1 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -34,13 +34,13 @@ import RouterView from '@/components/global/RouterView.vue'; import MkWindow from '@/components/MkWindow.vue'; import { popout as _popout } from '@/scripts/popout.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { useScrollPositionManager } from '@/nirax.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { openingWindowsCount } from '@/os.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { useRouterFactory } from '@/router/supplier.js'; import { mainRouter } from '@/router/main.js'; import MkUserName from './global/MkUserName.vue'; diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 4f2a4d270311f28a45029ea16a900045c8c5aafc..f37cb10f6dfd3b133cb0054e991353f3941ee64d 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch, type Ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useDocumentVisibility } from '@@/js/use-document-visibility.js'; +import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; -import { useDocumentVisibility } from '@/scripts/use-document-visibility.js'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list.js'; import { i18n } from '@/i18n.js'; @@ -104,6 +104,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'queue', count: number): void; (ev: 'status', error: boolean): void; + (ev: 'init'): void; }>(); const rootEl = shallowRef<HTMLElement>(); @@ -125,8 +126,6 @@ const items = ref<MisskeyEntityMap>(new Map()); */ const queue = ref<MisskeyEntityMap>(new Map()); -const offset = ref(0); - /** * åˆæœŸåŒ–ä¸ã‹ã©ã†ã‹ï¼ˆtrueãªã‚‰MkLoadingã§å…¨ã¦éš ã™ï¼‰ */ @@ -179,7 +178,9 @@ watch([backed, contentEl], () => { if (!backed.value) { if (!contentEl.value) return; - scrollRemove.value = (props.pagination.reversed ? onScrollBottom : onScrollTop)(contentEl.value, executeQueue, TOLERANCE); + scrollRemove.value = props.pagination.reversed + ? onScrollBottom(contentEl.value, executeQueue, TOLERANCE) + : onScrollTop(contentEl.value, (topVisible) => { if (topVisible) executeQueue(); }, TOLERANCE); } else { if (scrollRemove.value) scrollRemove.value(); scrollRemove.value = null; @@ -229,9 +230,10 @@ async function init(): Promise<void> { more.value = true; } - offset.value = res.length; error.value = false; fetching.value = false; + + emit('init'); }, err => { error.value = true; fetching.value = false; @@ -251,7 +253,7 @@ const fetchMore = async (): Promise<void> => { ...params, limit: SECOND_FETCH_LIMIT, ...(offsetMode ? { - offset: offset.value, + offset: items.value.size, } : { untilId: Array.from(items.value.keys()).at(-1), }), @@ -301,7 +303,6 @@ const fetchMore = async (): Promise<void> => { moreFetching.value = false; } } - offset.value += res.length; }, err => { moreFetching.value = false; }); @@ -316,7 +317,7 @@ const fetchMoreAhead = async (): Promise<void> => { ...params, limit: SECOND_FETCH_LIMIT, ...(offsetMode ? { - offset: offset.value, + offset: items.value.size, } : { sinceId: Array.from(items.value.keys()).at(-1), }), @@ -328,7 +329,6 @@ const fetchMoreAhead = async (): Promise<void> => { items.value = concatMapWithArray(items.value, res); more.value = true; } - offset.value += res.length; moreFetching.value = false; }, err => { moreFetching.value = false; diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 393ac4efbac0c10c323b252973dacbb47792c682..592a511fb012cbbe753fafd6ff40911afbd41306 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="{ [$style.done]: closed || isVoted }"> <ul :class="$style.choices"> - <li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)"> + <li v-for="(choice, i) in props.poll.choices" :key="i" :class="$style.choice" @click="vote(i)"> <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <span :class="$style.fg"> <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> @@ -24,6 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> <span v-else-if="closed">{{ i18n.ts._poll.closed }}</span> <span v-if="remaining > 0"> · {{ timer }}</span> + <span v-if="!closed && $i && !props.local"> · </span> + <a v-if="!closed && $i && !props.local" style="color: inherit;" @click="refreshVotes()">{{ i18n.ts.reload }}</a> </p> </div> </template> @@ -31,19 +33,21 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { sum } from '@/scripts/array.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { host } from '@/config.js'; -import { useInterval } from '@/scripts/use-interval.js'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; +import { host } from '@@/js/config.js'; +import { useInterval } from '@@/js/use-interval.js'; +import { $i } from '@/account.js'; const props = defineProps<{ noteId: string; poll: NonNullable<Misskey.entities.Note['poll']>; readOnly?: boolean; + local?: boolean; }>(); const remaining = ref(-1); @@ -85,9 +89,10 @@ if (props.poll.expiresAt) { } const vote = async (id) => { + if (props.readOnly || closed.value || isVoted.value) return; + pleaseLogin(undefined, pleaseLoginContext.value); - if (props.readOnly || closed.value || isVoted.value) return; if (!props.poll.multiple) { const { canceled } = await os.confirm({ type: 'question', @@ -108,6 +113,17 @@ const vote = async (id) => { }); if (!showResult.value) showResult.value = !props.poll.multiple; }; + +const refreshVotes = async () => { + pleaseLogin(undefined, pleaseLoginContext.value); + + if (props.readOnly || closed.value) return; + await misskeyApi('notes/polls/refresh', { + noteId: props.noteId, + // Sadly due to being in the same component and the poll being a prop we require to break Vue's recommendation of not mutating the prop to update it. + // eslint-disable-next-line vue/no-mutating-props + }).then((res: any) => res.poll ? props.poll.choices = res.poll.choices : null ); +}; </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index ff29b6619376dcc3d82c5329d7bf4499d94b278a..d14873008b5b0b1def74bad72aabbf81b449b038 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, shallowRef } from 'vue'; import MkModal from './MkModal.vue'; import MkMenu from './MkMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; defineProps<{ items: MenuItem[]; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 2bc607fbb68213309e1833fc7e913345a6d77c1b..4a29b27ac4dfb383cccd4f20a58bb79dc467c209 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.headerRight"> <template v-if="!(channel != null && fixed)"> - <button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" @click="setVisibility"> + <button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" :disabled="editId != null" @click="setVisibility"> <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.headerRightButtonText">{{ channel.name }}</span> </button> </template> - <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified'" @click="toggleLocalOnly"> + <button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified' || editId != null" @click="toggleLocalOnly"> <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> <span v-else><i class="ti ti-rocket-off"></i></span> </button> @@ -65,7 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> - <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> + <div v-show="useCw" :class="$style.cwFrame"> + <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> + <div v-if="maxCwLength - cwLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: cwLength > maxCwLength }]">{{ maxCwLength - cwLength }}</div> + </div> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> @@ -106,11 +109,11 @@ import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { toASCII } from 'punycode/'; +import { host, url } from '@@/js/config.js'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue'; -import { host, url } from '@/config.js'; import { erase, unique } from '@/scripts/array.js'; import { extractMentions } from '@/scripts/extract-mentions.js'; import { formatTimeString } from '@/scripts/format-time-string.js'; @@ -247,13 +250,16 @@ const submitText = computed((): string => { }); const textLength = computed((): number => { - return (text.value + imeText.value).trim().length; + return (text.value + imeText.value).length; }); const maxTextLength = computed((): number => { return instance ? instance.maxNoteTextLength : 1000; }); +const cwLength = computed(() => cw.value?.length ?? 0); +const maxCwLength = computed(() => instance.maxCwLength); + const canPost = computed((): boolean => { return !props.mock && !posting.value && !posted.value && ( @@ -264,6 +270,7 @@ const canPost = computed((): boolean => { quoteId.value != null ) && (textLength.value <= maxTextLength.value) && + (cwLength.value <= maxCwLength.value) && (!poll.value || poll.value.choices.length >= 2); }); @@ -630,11 +637,22 @@ async function onPaste(ev: ClipboardEvent) { if (paste.length > 1000) { ev.preventDefault(); - os.confirm({ - type: 'info', + os.actions({ + type: 'question', text: i18n.ts.attachAsFileQuestion, - }).then(({ canceled }) => { - if (canceled) { + actions: [ + { + value: 'yes', + text: i18n.ts.yes, + primary: true, + }, + { + value: 'no', + text: i18n.ts.no, + }, + ], + }).then(({ result }) => { + if (result !== 'yes') { insertTextAtCursor(textareaEl.value, paste); return; } @@ -1172,13 +1190,13 @@ defineExpose({ &:not(:disabled):hover { > .inner { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } &:not(:disabled):active { > .inner { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } } @@ -1245,6 +1263,7 @@ defineExpose({ min-height: 75px; max-height: 150px; overflow: auto; + background-size: auto auto; } .targetNote { @@ -1304,10 +1323,13 @@ defineExpose({ } } -.cw { +.cwFrame { z-index: 1; padding-bottom: 8px; border-bottom: solid 0.5px var(--divider); + + width: 100%; + position: relative; } .hashtags { diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index a3fb7c691fcf5989ae1dc86951a70e0887d2d2dc..f90fcfef33bdbc0124ae97d7a04a5a998065017e 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -7,7 +7,14 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-show="props.modelValue.length != 0" :class="$style.root"> <Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)"> <template #item="{element}"> - <div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)"> + <div + :class="$style.file" + role="button" + tabindex="0" + @click="showFileMenu(element, $event)" + @keydown.space.enter="showFileMenu(element, $event)" + @contextmenu.prevent="showFileMenu(element, $event)" + > <MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/> <div v-if="element.isSensitive" :class="$style.sensitive"> <i class="ti ti-eye-exclamation" style="margin: auto;"></i> @@ -26,6 +33,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import type { MenuItem } from '@/types/menu.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -63,7 +71,7 @@ async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('driveFileDeleteConfirm', { name: file.name }), + text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), }); if (canceled) return; @@ -132,11 +140,14 @@ async function crop(file: Misskey.entities.DriveFile): Promise<void> { emit('replaceFile', file, newFile); } -function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { +function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | KeyboardEvent): void { if (menuShowing) return; const isImage = file.type.startsWith('image/'); - os.popupMenu([{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ text: i18n.ts.renameFile, icon: 'ti ti-forms', action: () => { rename(file); }, @@ -148,11 +159,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => { describe(file); }, - }, ...isImage ? [{ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () : void => { crop(file); }, - }] : [], { + }); + + if (isImage) { + menuItems.push({ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () : void => { crop(file); }, + }); + } + + menuItems.push({ type: 'divider', }, { text: i18n.ts.attachCancel, @@ -163,7 +180,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { icon: 'ti ti-trash', danger: true, action: () => { detachAndDeleteMedia(file); }, - }], ev.currentTarget ?? ev.target).then(() => menuShowing = false); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false); menuShowing = true; } </script> @@ -187,6 +206,10 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { border-radius: var(--radius-xs); overflow: hidden; cursor: move; + + &:focus-visible { + outline-offset: 4px; + } } .thumbnail { diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index 947c0ee4d065d003b60e1f9a988e2e70510297ee..811a6378f2a0c4fdc94429178ec1bafa90646851 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()" @esc="modal?.close()"> - <MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/> + <MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="onCancel" @esc="onCancel"/> </MkModal> </template> @@ -37,7 +37,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'closed'): void; + (ev: 'closed', cancelled: boolean): void; }>(); const modal = shallowRef<InstanceType<typeof MkModal>>(); @@ -47,10 +47,18 @@ function onPosted() { modal.value?.close({ useSendAnimation: true, }); + emit('closed', false); +} + +function onCancel() { + // for some reason onModalClosed does not get called properly when closing the model through other functions. + modal.value?.close(); + // emit is required so that the dialog gets properly disposed otherwise it will float around as a "zombie" + emit('closed', true); } function onModalClosed() { - emit('closed'); + emit('closed', true); } </script> diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue index 649dee2fdb7146574b20906c6002b485d0aad4b3..6efd99d14bf5ba8f78d064d2ee37ef5bdd50a009 100644 --- a/packages/frontend/src/components/MkPreview.vue +++ b/packages/frontend/src/components/MkPreview.vue @@ -42,7 +42,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkRadio from '@/components/MkRadio.vue'; import * as os from '@/os.js'; -import * as config from '@/config.js'; +import * as config from '@@/js/config.js'; import { $i } from '@/account.js'; const text = ref(''); diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index e0d0b561beb08f961fca6b1bb4a15928c6e66a4f..4fb4c6fe56a305cb1718bdaec4077cdaab9b9e8e 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref, shallowRef } from 'vue'; import { i18n } from '@/i18n.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { isHorizontalSwipeSwiping } from '@/scripts/touch.js'; const SCROLL_STOP = 10; diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 244fcdceaeb391604537ab1ef954266d79a00307..22c187c3578a401fb55c6b0eca89a2dbfe56a660 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -5,7 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="timctyfi" :class="{ disabled, easing }"> - <div class="label"><slot name="label"></slot></div> + <div class="label"> + <slot name="label"></slot> + </div> <div v-adaptive-border class="body"> <div ref="containerEl" class="container"> <div class="track"> @@ -14,15 +16,25 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="steps && showTicks" class="ticks"> <div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div> </div> - <div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> + <div + ref="thumbEl" + class="thumb" + :style="{ left: thumbPosition + 'px' }" + @mouseenter.passive="onMouseenter" + @mousedown="onMousedown" + @touchstart="onMousedown" + ></div> </div> </div> - <div class="caption"><slot name="caption"></slot></div> + <div class="caption"> + <slot name="caption"></slot> + </div> </div> </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch, shallowRef } from 'vue'; +import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'; +import { isTouchUsing } from '@/scripts/touch.js'; import * as os from '@/os.js'; const props = withDefaults(defineProps<{ @@ -101,12 +113,36 @@ const steps = computed(() => { } }); +const tooltipForDragShowing = ref(false); +const tooltipForHoverShowing = ref(false); + +function onMouseenter() { + if (isTouchUsing) return; + + tooltipForHoverShowing.value = true; + + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { + showing: computed(() => tooltipForHoverShowing.value && !tooltipForDragShowing.value), + text: computed(() => { + return props.textConverter(finalValue.value); + }), + targetElement: thumbEl, + }, { + closed: () => dispose(), + }); + + thumbEl.value!.addEventListener('mouseleave', () => { + tooltipForHoverShowing.value = false; + }, { once: true, passive: true }); +} + function onMousedown(ev: MouseEvent | TouchEvent) { ev.preventDefault(); - const tooltipShowing = ref(true); + tooltipForDragShowing.value = true; + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { - showing: tooltipShowing, + showing: tooltipForDragShowing, text: computed(() => { return props.textConverter(finalValue.value); }), @@ -137,7 +173,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { const onMouseup = () => { document.head.removeChild(style); - tooltipShowing.value = false; + tooltipForDragShowing.value = false; window.removeEventListener('mousemove', onDrag); window.removeEventListener('touchmove', onDrag); window.removeEventListener('mouseup', onMouseup); @@ -261,12 +297,12 @@ function onMousedown(ev: MouseEvent | TouchEvent) { > .container { > .track { > .highlight { - transition: width 0.2s cubic-bezier(0,0,0,1); + transition: width 0.2s cubic-bezier(0, 0, 0, 1); } } > .thumb { - transition: left 0.2s cubic-bezier(0,0,0,1); + transition: left 0.2s cubic-bezier(0, 0, 0, 1); } } } diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue index 15409a216a300cb814b45f321ca7d4a7a4069201..77ca841ad0c03175386f832b5263d282bc54c978 100644 --- a/packages/frontend/src/components/MkReactionTooltip.vue +++ b/packages/frontend/src/components/MkReactionTooltip.vue @@ -36,6 +36,7 @@ const emit = defineEmits<{ .icon { display: block; width: 60px; + max-height: 60px; font-size: 60px; // unicodeãªçµµæ–‡å—ã«ã¤ã„ã¦ã¯widthãŒåŠ¹ã‹ãªã„ãŸã‚ margin: 0 auto; object-fit: contain; diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 60118fadd2a4f87c04f01afe2963d0cea949a664..6fdeb3a3abbe74f517bf3620f19f648ede1e409b 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.users"> <div v-for="u in users" :key="u.id" :class="$style.user"> <MkAvatar :class="$style.avatar" :user="u"/> - <MkUserName :user="u" :nowrap="true"/> + <MkUserName :user="u" :nowrap="true" :class="$style.username"/> </div> <div v-if="count > 10" :class="$style.more">+{{ count - 10 }}</div> </div> @@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; +import { getEmojiName } from '@@/js/emojilist.js'; import MkTooltip from './MkTooltip.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; -import { getEmojiName } from '@/scripts/emojilist.js'; defineProps<{ showing: boolean; @@ -63,6 +63,7 @@ function getReactionName(reaction: string): string { .reactionIcon { display: block; width: 60px; + max-height: 60px; font-size: 60px; // unicodeãªçµµæ–‡å—ã«ã¤ã„ã¦ã¯widthãŒåŠ¹ã‹ãªã„ãŸã‚ object-fit: contain; margin: 0 auto; @@ -98,4 +99,11 @@ function getReactionName(reaction: string): string { .more { padding-top: 4px; } + +.username { + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + overflow: hidden; +} </style> diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 6506035f8f8f2dbdf3acd41901308840f4697a0c..957ee0e76b37c981a6845bdf9d48c87c8b139c03 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject, onMounted, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { getUnicodeEmoji } from '@@/js/emojilist.js'; import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -34,7 +35,6 @@ import { i18n } from '@/i18n.js'; import * as sound from '@/scripts/sound.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; import { customEmojisMap } from '@/custom-emojis.js'; -import { getUnicodeEmoji } from '@/scripts/emojilist.js'; const props = defineProps<{ reaction: string; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 8254ac83cfe6a2be90d50e50dbd655da8764d8a9..150a5c6d542749dcdb578f9ee36229571dd3aa96 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -44,9 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ modelValue: string | null; diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 42fa2bf4a78dbe4239b231d0d5a59a37c9f8a105..82e0df8a016f57fa28918ccd3155fad5f1afb38f 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #prefix>@</template> <template #suffix>@{{ host }}</template> </MkInput> - <MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password> + <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password> <template #prefix><i class="ti ti-lock"></i></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> </MkInput> @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="user && user.securityKeys" class="twofa-group tap-group"> <p>{{ i18n.ts.useSecurityKey }}</p> - <MkButton v-if="!queryingKey" @click="queryKey"> + <MkButton v-if="!queryingKey" @click="query2FaKey"> {{ i18n.ts.retry }} </MkButton> </div> @@ -45,10 +45,6 @@ SPDX-License-Identifier: AGPL-3.0-only <p :class="$style.orMsg">{{ i18n.ts.or }}</p> </div> <div class="twofa-group totp-group _gaps"> - <MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" autocomplete="current-password" :withPasswordToggle="true" required> - <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ti ti-lock"></i></template> - </MkInput> <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'"> <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template> <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> @@ -57,6 +53,16 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> </div> + <div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + <div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group"> + <MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin"> + <i class="ti ti-device-usb" style="font-size: medium;"></i> + {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }} + </MkButton> + <p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p> + </div> </div> </form> </template> @@ -66,20 +72,24 @@ import { defineAsyncComponent, ref } from 'vue'; import { toUnicode } from 'punycode/'; import * as Misskey from 'misskey-js'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; +import { SigninWithPasskeyResponse } from 'misskey-js/entities.js'; +import { query, extractDomain } from '@@/js/url.js'; +import { host as configHost } from '@@/js/config.js'; +import MkDivider from './MkDivider.vue'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { host as configHost } from '@/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { query, extractDomain } from '@/scripts/url.js'; import { login } from '@/account.js'; import { i18n } from '@/i18n.js'; +import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js'; const signing = ref(false); const user = ref<Misskey.entities.UserDetailed | null>(null); +const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true); const username = ref(''); const password = ref(''); const token = ref(''); @@ -88,6 +98,7 @@ const totpLogin = ref(false); const isBackupCode = ref(false); const queryingKey = ref(false); let credentialRequest: CredentialRequestOptions | null = null; +const passkey_context = ref(''); const emit = defineEmits<{ (ev: 'login', v: any): void; @@ -106,12 +117,19 @@ const props = withDefaults(defineProps<{ }); function onUsernameChange(): void { + const usernameRequested = username.value; misskeyApi('users/show', { - username: username.value, + username: usernameRequested, }).then(userResponse => { - user.value = userResponse; + if (userResponse.username === username.value) { + user.value = userResponse; + usePasswordLessLogin.value = userResponse.usePasswordLessLogin; + } }, () => { - user.value = null; + if (usernameRequested === username.value) { + user.value = null; + usePasswordLessLogin.value = true; + } }); } @@ -121,7 +139,7 @@ function onLogin(res: any): Promise<void> | void { } } -async function queryKey(): Promise<void> { +async function query2FaKey(): Promise<void> { if (credentialRequest == null) return; queryingKey.value = true; await webAuthnRequest(credentialRequest) @@ -150,6 +168,47 @@ async function queryKey(): Promise<void> { }); } +function onPasskeyLogin(): void { + signing.value = true; + if (webAuthnSupported()) { + misskeyApi('signin-with-passkey', {}) + .then((res: SigninWithPasskeyResponse) => { + totpLogin.value = false; + signing.value = false; + queryingKey.value = true; + passkey_context.value = res.context ?? ''; + credentialRequest = parseRequestOptionsFromJSON({ + publicKey: res.option, + }); + }) + .then(() => queryPasskey()) + .catch(loginFailed); + } +} + +async function queryPasskey(): Promise<void> { + if (credentialRequest == null) return; + queryingKey.value = true; + console.log('Waiting passkey auth...'); + await webAuthnRequest(credentialRequest) + .catch((err) => { + console.warn('Passkey Auth fail!: ', err); + queryingKey.value = false; + return Promise.reject(null); + }).then(credential => { + credentialRequest = null; + queryingKey.value = false; + signing.value = true; + return misskeyApi('signin-with-passkey', { + credential: credential.toJSON(), + context: passkey_context.value, + }); + }).then((res: SigninWithPasskeyResponse) => { + emit('login', res.signinResponse); + return onLogin(res.signinResponse); + }); +} + function onSubmit(): void { signing.value = true; if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { @@ -164,7 +223,7 @@ function onSubmit(): void { publicKey: res, }); }) - .then(() => queryKey()) + .then(() => query2FaKey()) .catch(loginFailed); } else { totpLogin.value = true; @@ -204,6 +263,10 @@ function loginFailed(err: any): void { showSuspendedDialog(); break; } + case 's8dhsj9s-a93j-493j-ja9k-kas9sj20aml2': { + showSystemAccountDialog(); + break; + } case '22d05606-fbcf-421a-a2db-b32610dcfd1b': { os.alert({ type: 'error', @@ -212,6 +275,30 @@ function loginFailed(err: any): void { }); break; } + case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.unknownWebAuthnKey, + }); + break; + } + case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.passkeyVerificationFailed, + }); + break; + } + case '2d84773e-f7b7-4d0b-8f72-bb69b584c912': { + os.alert({ + type: 'error', + title: i18n.ts.loginFailed, + text: i18n.ts.passkeyVerificationSucceededButPasswordlessLoginDisabled, + }); + break; + } default: { console.error(err); os.alert({ diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 524c62b4d3aaee8c469895dbd8d0e79c29716ea3..d48780e9de6def3bf9797395c802afb6a124e305 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkModalWindow ref="dialog" :width="400" - :height="430" + :height="450" @close="onClose" @closed="emit('closed')" > diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index e673b6d5308a52949f5c379bc73ddfc3d39c664a..4c55831a3a522dea706813055cd1ebeaafbaa019 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -70,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/> <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> + <MkCaptcha v-if="instance.enableFC" ref="fc" v-model="fcResponse" :class="$style.captcha" provider="fc" :sitekey="instance.fcSiteKey"/> <MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;"> <template v-if="submitting"> <MkLoading :em="true" :colored="false"/> @@ -88,7 +89,7 @@ import * as Misskey from 'misskey-js'; import MkButton from './MkButton.vue'; import MkInput from './MkInput.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; -import * as config from '@/config.js'; +import * as config from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; @@ -112,6 +113,7 @@ const host = toUnicode(config.host); const hcaptcha = ref<Captcha | undefined>(); const recaptcha = ref<Captcha | undefined>(); const turnstile = ref<Captcha | undefined>(); +const fc = ref<Captcha | undefined>(); const username = ref<string>(''); const password = ref<string>(''); @@ -128,6 +130,7 @@ const hCaptchaResponse = ref<string | null>(null); const mCaptchaResponse = ref<string | null>(null); const reCaptchaResponse = ref<string | null>(null); const turnstileResponse = ref<string | null>(null); +const fcResponse = ref<string | null>(null); const usernameAbortController = ref<null | AbortController>(null); const emailAbortController = ref<null | AbortController>(null); @@ -137,6 +140,7 @@ const shouldDisableSubmitting = computed((): boolean => { instance.enableMcaptcha && !mCaptchaResponse.value || instance.enableRecaptcha && !reCaptchaResponse.value || instance.enableTurnstile && !turnstileResponse.value || + instance.enableFC && !fcResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || usernameState.value !== 'ok' || passwordRetypeState.value !== 'match'; @@ -266,6 +270,7 @@ async function onSubmit(): Promise<void> { 'm-captcha-response': mCaptchaResponse.value, 'g-recaptcha-response': reCaptchaResponse.value, 'turnstile-response': turnstileResponse.value, + 'frc-captcha-solution': fcResponse.value, }); if (instance.emailRequiredForSignup) { os.alert({ @@ -297,6 +302,7 @@ async function onSubmit(): Promise<void> { hcaptcha.value?.reset?.(); recaptcha.value?.reset?.(); turnstile.value?.reset?.(); + fc.value?.reset?.(); os.alert({ type: 'error', diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue index cd884f0b19c22574c5ac8b2f98addfd4bed5bd13..7743a89242b7e180c799299c2317c867e4ad4104 100644 --- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue +++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkButton from '@/components/MkButton.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import { miLocalStorage } from '@/local-storage.js'; diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 8386a783fc8d4e05f1c265fc5da7033458cb7dc7..6bd00fcc2aa5879d3c565f4bf2508ac2f0f61990 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -46,7 +46,7 @@ import MkMediaList from '@/components/MkMediaList.vue'; import MkPoll from '@/components/MkPoll.vue'; import MkButton from '@/components/MkButton.vue'; import { i18n } from '@/i18n.js'; -import { shouldCollapsed } from '@/scripts/collapsed.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; import { defaultStore } from '@/store.js'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; @@ -110,7 +110,7 @@ watch(() => props.expandAllCws, (expandAllCws) => { left: 0; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), var(--X15)); + // background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); > .fadeLabel { display: inline-block; diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 041ae881095e4e90b8d6938e04787cf956c15422..430e3c79588be24a76ed64b169180735646d81d8 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -100,14 +100,14 @@ defineProps<{ &.grid { > .group { + margin-left: 0; + margin-right: 0; + & + .group { padding-top: 0; border-top: none; } - margin-left: 0; - margin-right: 0; - > .title { font-size: 1em; opacity: 0.7; diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts index 69b8edd85aec7f2729b9e8a02f75d6839a0783c1..19e4eea733cd9a2a4b618e3e3d41e06b21c68659 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts +++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts @@ -4,9 +4,10 @@ */ import { defineAsyncComponent } from 'vue'; +import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; -export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved'; +export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number]; export type MkSystemWebhookEditorProps = { mode: 'create' | 'edit'; diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue index f5c7a3160bb4d9df1b9dc7f74469d0aef48784d0..ec3b1c90caf98b3464fb10fe4213cd302e5db4fb 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.vue +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts._webhookSettings.trigger }}</template> - <div class="_gaps_s"> - <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport"> - <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template> - </MkSwitch> - <MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved"> - <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template> - </MkSwitch> - <MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated"> - <template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template> - </MkSwitch> + <div class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.abuseReport" :disabled="disabledEvents.abuseReport"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReport }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReport)" @click="test('abuseReport')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.abuseReportResolved" :disabled="disabledEvents.abuseReportResolved"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.abuseReportResolved)" @click="test('abuseReportResolved')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="events.userCreated" :disabled="disabledEvents.userCreated"> + <template #label>{{ i18n.ts._webhookSettings._systemEvents.userCreated }}</template> + </MkSwitch> + <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton> + </div> + </div> + + <div v-show="mode === 'edit'" :class="$style.description"> + {{ i18n.ts._webhookSettings.testRemarks }} + </div> </div> </MkFolder> @@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script setup lang="ts"> import { computed, onMounted, ref, shallowRef, toRefs } from 'vue'; +import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { @@ -180,6 +196,21 @@ async function loadingScope<T>(fn: () => Promise<T>): Promise<T> { } } +async function test(type: Misskey.entities.SystemWebhook['on'][number]): Promise<void> { + if (!id.value) { + return Promise.resolve(); + } + + await os.apiWithDialog('admin/system-webhook/test', { + webhookId: id.value, + type, + override: { + secret: secret.value, + url: url.value, + }, + }); +} + onMounted(async () => { await loadingScope(async () => { switch (mode.value) { @@ -235,4 +266,29 @@ onMounted(async () => { -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); } + +.switchBox { + display: flex; + align-items: center; + justify-content: start; + + .testButton { + $buttonSize: 28px; + padding: 0; + width: $buttonSize; + min-width: $buttonSize; + max-width: $buttonSize; + height: $buttonSize; + margin-left: auto; + line-height: normal; + font-size: 90%; + border-radius: 9999px; + } +} + +.description { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); +} </style> diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 9adc8d466c86f10befa98ce7310df9abee13450b..1f5a2b938167feef5ed643bc82f85bf3bebbf2aa 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -158,7 +158,7 @@ import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { claimAchievement } from '@/scripts/achievements.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue index 4fb0749931dcd88e47f82100cc6df6d41bfe1385..91f5b86c2db9110195806f187561b2cd44510f0b 100644 --- a/packages/frontend/src/components/MkUpdated.vue +++ b/packages/frontend/src/components/MkUpdated.vue @@ -19,7 +19,7 @@ import { onMounted, shallowRef } from 'vue'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkSparkle from '@/components/MkSparkle.vue'; -import { version } from '@/config.js'; +import { version } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { confetti } from '@/scripts/confetti.js'; diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index a51b8785802f69fdb6043f014d44055886a91009..04f5314463afd9e16264409408598c8ab58cafdf 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -85,12 +85,12 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue'; import type { summaly } from '@misskey-dev/summaly'; -import { url as local } from '@/config.js'; +import { url as local } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkButton from '@/components/MkButton.vue'; -import { versatileLang } from '@/scripts/intl-const.js'; +import { versatileLang } from '@@/js/intl-const.js'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/MkUrlWarningDialog.vue b/packages/frontend/src/components/MkUrlWarningDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..f2e9b11fd9020ed7c7b2faf033b3f3dffe9deb80 --- /dev/null +++ b/packages/frontend/src/components/MkUrlWarningDialog.vue @@ -0,0 +1,131 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')"> + <div :class="$style.root" class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.header"> + <div :class="$style.icon"> + <i class="ti ti-alert-triangle"></i> + </div> + <div :class="$style.title">{{ i18n.ts._externalNavigationWarning.title }}</div> + </div> + <div><Mfm :text="i18n.tsx._externalNavigationWarning.description({ host: instanceName })"/></div> + <div class="_monospace" :class="$style.urlAddress">{{ url }}</div> + <div> + <MkSwitch v-model="trustThisDomain">{{ i18n.ts._externalNavigationWarning.trustThisDomain }}</MkSwitch> + </div> + </div> + <div :class="$style.buttons"> + <MkButton data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton> + <MkButton data-cy-modal-dialog-ok inline primary rounded @click="ok"><i class="ti ti-external-link"></i> {{ i18n.ts.open }}</MkButton> + </div> + </div> +</MkModal> +</template> + +<script lang="ts" setup> +import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue'; +import { instanceName } from '@@/js/config.js'; +import MkModal from '@/components/MkModal.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { i18n } from '@/i18n.js'; +import { defaultStore } from '@/store.js'; + +type Result = string | number | true | null; + +const props = defineProps<{ + url: string; +}>(); + +const emit = defineEmits<{ + (ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void; + (ev: 'closed'): void; +}>(); + +const modal = shallowRef<InstanceType<typeof MkModal>>(); +const trustThisDomain = ref(false); + +const domain = computed(() => new URL(props.url).hostname); + +// overload function を使ã„ãŸã„ã®ã§ lint エラーを無視ã™ã‚‹ +function done(canceled: true): void; +function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare +function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare + emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result }); + modal.value?.close(); +} + +async function ok() { + const result = true; + if (!defaultStore.state.trustedDomains.includes(domain.value) && trustThisDomain.value) { + await defaultStore.set('trustedDomains', defaultStore.state.trustedDomains.concat(domain.value)); + } + done(false, result); +} + +function cancel() { + done(true); +} + +function onKeydown(evt: KeyboardEvent) { + if (evt.key === 'Escape') cancel(); +} + +onMounted(() => { + document.addEventListener('keydown', onKeydown); +}); + +onBeforeUnmount(() => { + document.removeEventListener('keydown', onKeydown); +}); +</script> + +<style lang="scss" module> +.root { + position: relative; + margin: auto; + padding: 32px; + width: 100%; + min-width: 320px; + max-width: 480px; + box-sizing: border-box; + background: var(--panel); + border-radius: 16px; +} + +.header { + display: flex; + align-items: center; + gap: 0.75em; +} + +.icon { + font-size: 18px; + color: var(--warn); +} + +.title { + font-weight: bold; + font-size: 1.1em; +} + +.urlAddress { + padding: 10px 14px; + border-radius: 8px; + border: 1px solid var(--divider); + overflow-x: auto; + white-space: nowrap; +} + +.buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: right; +} +</style> diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index e528f04dfc15200d0423206b4ceefbd6b5841564..73cdd9ce00014675f6742f3b36edbd39f0b295bc 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -11,7 +11,15 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> <p :class="$style.username"><MkAcct :user="user"/></p> </div> - <span v-if="$i && $i.id !== user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> + <ul v-if="$i && $i.id != user.id" :class="$style.infoBadges"> + <li v-if="user.isFollowed && user.isFollowing">{{ i18n.ts.mutuals }}</li> + <li v-else-if="user.isFollowing">{{ i18n.ts.following }}</li> + <li v-else-if="user.isFollowed">{{ i18n.ts.followsYou }}</li> + <li v-if="user.isMuted">{{ i18n.ts.muted }}</li> + <li v-if="user.isRenoteMuted">{{ i18n.ts.renoteMuted }}</li> + <li v-if="user.isBlocking">{{ i18n.ts.blocked }}</li> + <li v-if="user.isBlocked && $i.isModerator">{{ i18n.ts.blockingYou }}</li> + </ul> <div :class="$style.description"> <div v-if="user.description" :class="$style.mfm"> <Mfm :text="user.description" :isBlock="true" :author="user"/> @@ -144,4 +152,30 @@ defineProps<{ top: 8px; right: 8px; } + +.infoBadges { + position: absolute; + top: 12px; + left: 12px; + + display: flex; + flex-direction: row; + + padding: 0; + margin: 0; + + > * { + padding: 4px 8px; + color: #fff; + background: rgba(0, 0, 0, 0.7); + font-size: 0.7em; + border-radius: var(--radius-sm); + list-style-type: none; + margin-left: 0; + } + + > :not(:first-child) { + margin-left: 8px; + } +} </style> diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 7d210a4385cd49c8a8323e8821791911fb76dbaf..a5b48c8ce29249b99a40cda37447a2b6a9725596 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -70,7 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; -import { host as currentHost, hostname } from '@/config.js'; +import { host as currentHost, hostname } from '@@/js/config.js'; const emit = defineEmits<{ (ev: 'ok', selected: Misskey.entities.UserDetailed): void; diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 514350c930ff3085fea88e9acad00e6669362ac9..1fb1eda0398913ddc05e5643bcabfacdaede8203 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -137,7 +137,7 @@ import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue'; import MkAnimBg from '@/components/MkAnimBg.vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index 6fb33044680fd38c39591fbddbb8da6b10322bf9..874eff6c796779e8e2c692735b675c3be9817ad7 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div v-if="instance" :class="$style.root"> <div :class="[$style.main, $style.panel]"> - <img :src="instance.iconUrl || '/apple-touch-icon.png'" alt="" :class="$style.mainIcon"/> + <img :src="instance.sidebarLogoUrl || instance.iconUrl || '/apple-touch-icon.png'" alt="" :class="instance.sidebarLogoUrl ? $style.wideIcon : $style.mainIcon"/> <button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button> <div :class="$style.mainFg"> <h1 :class="$style.mainTitle"> @@ -62,7 +62,7 @@ import XSignupDialog from '@/components/MkSignupDialog.vue'; import MkButton from '@/components/MkButton.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -126,6 +126,14 @@ function showMenu(ev: MouseEvent) { filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.5)); } +.wideIcon { + min-width: 85px; + max-width: 60%; + margin-top: -47px; + vertical-align: bottom; + filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.5)); +} + .mainMenu { position: absolute; top: 16px; @@ -134,6 +142,7 @@ function showMenu(ev: MouseEvent) { height: 32px; border-radius: var(--radius-sm); font-size: 18px; + z-index: 50; } .mainFg { diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 06879f5917b6a1397f6aee7ff76142b040ecb402..99840bf8d7620615b6caefaac35ac52610c427e6 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -57,6 +57,7 @@ import MkButton from '@/components/MkButton.vue'; import { widgets as widgetDefs } from '@/widgets/index.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import { isLink } from '@@/js/is-link.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -65,13 +66,6 @@ const props = defineProps<{ edit: boolean; }>(); -// This will not be available for now as I don't think this is needed -// const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes)); -/* if (!notesSearchAvailable) { - const wid = widgetDefs.findIndex(widget => widget === 'search'); - widgetDefs.splice(wid, 1); -} */ - const emit = defineEmits<{ (ev: 'updateWidgets', widgets: Widget[]): void; (ev: 'addWidget', widget: Widget): void; @@ -105,13 +99,6 @@ const updateWidget = (id, data) => { function onContextmenu(widget: Widget, ev: MouseEvent) { const element = ev.target as HTMLElement | null; - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; if (element && isLink(element)) return; if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 303e49de00fa731dfc0604628239b80360e75f74..08906a1205f5349614b73abfff62a529b5049cff 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue'; import contains from '@/scripts/contains.js'; import * as os from '@/os.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -508,10 +508,6 @@ defineExpose({ .header { --height: 39px; - &.mini { - --height: 32px; - } - display: flex; position: relative; z-index: 1; @@ -524,6 +520,10 @@ defineExpose({ //border-bottom: solid 1px var(--divider); font-size: 90%; font-weight: bold; + + &.mini { + --height: 32px; + } } .headerButton { diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index e3711b3463f15465f516c62349f2a809625c3d87..1122976436e99c073cde6c42b4dce38c005384bc 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import MkWindow from '@/components/MkWindow.vue'; -import { versatileLang } from '@/scripts/intl-const.js'; +import { versatileLang } from '@@/js/intl-const.js'; import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/SkApprovalUser.vue b/packages/frontend/src/components/SkApprovalUser.vue index 20059f139d64e6c6f618d527ae9064ab3492c1ef..1ef0ac5b17dc86767b71668ce0ff9ea8623953b0 100644 --- a/packages/frontend/src/components/SkApprovalUser.vue +++ b/packages/frontend/src/components/SkApprovalUser.vue @@ -69,22 +69,10 @@ async function deleteAccount() { }); if (confirm.canceled) return; - const typed = await os.inputText({ - text: i18n.t('typeToConfirm', { x: props.user.username }), + await os.apiWithDialog('admin/decline-user', { + userId: props.user.id, }); - if (typed.canceled) return; - - if (typed.result === props.user.username) { - await os.apiWithDialog('admin/delete-account', { - userId: props.user.id, - }); - emits('deleted', props.user.id); - } else { - os.alert({ - type: 'error', - text: 'input not match', - }); - } + emits('deleted', props.user.id); } async function approveAccount() { diff --git a/packages/frontend/src/components/SkFollowingFeedEntry.vue b/packages/frontend/src/components/SkFollowingFeedEntry.vue new file mode 100644 index 0000000000000000000000000000000000000000..8fa5e014d8310688d750e2cbd9f577efdfd3721b --- /dev/null +++ b/packages/frontend/src/components/SkFollowingFeedEntry.vue @@ -0,0 +1,123 @@ +<!-- +SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root" @click="$emit('select', note.user)"> + <div :class="$style.avatar"> + <MkAvatar :class="$style.icon" :user="note.user" indictor/> + </div> + <div :class="$style.contents"> + <header :class="$style.header"> + <MkA v-user-preview="note.user.id" :class="$style.headerName" :to="userPage(note.user)"> + <MkUserName :user="note.user"/> + </MkA> + <MkA :to="notePage(note)"> + <MkTime :time="note.createdAt" :class="$style.headerTime" colored/> + </MkA> + </header> + <div> + <div v-if="isMuted" :class="[$style.text, $style.muted]">({{ i18n.ts.postFiltered }})</div> + <Mfm v-else :class="$style.text" :text="getNoteSummary(note)" :isBlock="true" :plain="true" :nowrap="false" :isNote="true" nyaize="respect" :author="note.user"/> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import * as Misskey from 'misskey-js'; +import { getNoteSummary } from '@/scripts/get-note-summary.js'; +import { userPage } from '@/filters/user.js'; +import { notePage } from '@/filters/note.js'; +import { i18n } from '@/i18n.js'; + +withDefaults(defineProps<{ + note: Misskey.entities.Note, + isMuted: boolean +}>(), { + isMuted: false, +}); + +defineEmits<{ + (event: 'select', user: Misskey.entities.UserLite): void +}>(); + +</script> + +<style lang="scss" module> +.root { + position: relative; + box-sizing: border-box; + padding: 12px 16px; + font-size: 0.9em; + overflow-wrap: break-word; + display: flex; + contain: content; +} + +.avatar { + align-self: center; + flex-shrink: 0; + width: 42px; + height: 42px; + margin-right: 8px; +} + +.contents { + flex: 1; + min-width: 0; +} + +.header { + display: flex; + align-items: baseline; + justify-content: space-between; + white-space: nowrap; +} + +.headerName { + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + overflow: hidden; + font-weight: bold; +} + +.headerTime { + margin-left: auto; + font-size: 0.9em; +} + +.icon { + display: block; + width: 100%; + height: 100%; +} + +.text { + display: flex; + width: 100%; + overflow: clip; + line-height: 1.25em; + height: 2.5em; +} + +.muted { + font-style: italic; +} + +@container (max-width: 600px) { + .root { + padding: 16px; + font-size: 0.9em; + } +} + +@container (max-width: 500px) { + .root { + padding: 12px; + font-size: 0.85em; + } +} +</style> diff --git a/packages/frontend/src/components/SkFormula.vue b/packages/frontend/src/components/SkFormula.vue index 039cef8da82b77b5c521b03f5a2f88d98a89b380..6faf36da55778c3fb4e723f8a75261b1f7e473e3 100644 --- a/packages/frontend/src/components/SkFormula.vue +++ b/packages/frontend/src/components/SkFormula.vue @@ -4,26 +4,26 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> - <div v-if="block" :class="$style.block" v-html="renderedFormula"></div> - <span v-else v-html="renderedFormula"></span> +<div v-if="block" :class="$style.block" v-html="renderedFormula"></div> +<span v-else v-html="renderedFormula"></span> </template> <script lang="ts" setup> - import { computed } from 'vue'; - import katex from 'katex'; - import 'katex/dist/katex.min.css'; +import { computed } from 'vue'; +import katex from 'katex'; +import 'katex/dist/katex.min.css'; - const props = defineProps<{ +const props = defineProps<{ formula: string; block: boolean; }>(); - const renderedFormula = computed(() => - katex.renderToString(props.formula, { - throwOnError: false, - trust: false, - displayMode: props.block, - } as any)); +const renderedFormula = computed(() => + katex.renderToString(props.formula, { + throwOnError: false, + trust: false, + displayMode: props.block, + })); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/SkInstanceTicker.vue b/packages/frontend/src/components/SkInstanceTicker.vue index 9cfc3326981fb58722b1f5c3bd99116b09a87c1f..eb987d9c779eeb8bedc6edee364dc9a47069703a 100644 --- a/packages/frontend/src/components/SkInstanceTicker.vue +++ b/packages/frontend/src/components/SkInstanceTicker.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed } from 'vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { instance as Instance } from '@/instance.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index b02d902482582ceec95b83ee93e0efd4acf764fc..bda528214425b6f1762ced800827d8f6a208fe90 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -65,7 +65,15 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="[{ [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click.stop="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined"> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/> + <Mfm + v-if="appearNote.cw != ''" + :text="appearNote.cw" + :author="appearNote.user" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + :isBlock="true" + /> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;" @click.stop/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> @@ -96,7 +104,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.files && appearNote.files.length > 0"> <MkMediaList ref="galleryEl" :mediaList="appearNote.files" @click.stop/> </div> - <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" @click.stop/> + <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" @click.stop/> <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/> </div> @@ -149,7 +157,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> @@ -193,6 +201,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; +import { isLink } from '@@/js/is-link.js'; import SkNoteSub from '@/components/SkNoteSub.vue'; import SkNoteHeader from '@/components/SkNoteHeader.vue'; import SkNoteSimple from '@/components/SkNoteSimple.vue'; @@ -224,13 +233,13 @@ import { deepClone } from '@/scripts/clone.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; -import { shouldCollapsed } from '@/scripts/collapsed.js'; import { useRouter } from '@/router/supplier.js'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; -import { host } from '@/config.js'; +import { shouldCollapsed } from '@@/js/collapsed.js'; +import { host } from '@@/js/config.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { type Keymap } from '@/scripts/hotkey.js'; import { focusPrev, focusNext } from '@/scripts/focus.js'; @@ -565,7 +574,8 @@ function quote() { os.post({ renote: appearNote.value, channel: appearNote.value.channel, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -589,7 +599,8 @@ function quote() { } else { os.post({ renote: appearNote.value, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -742,16 +753,6 @@ function onContextmenu(ev: MouseEvent): void { return; } - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - // å†ç”Ÿé€Ÿåº¦ã®é¸æŠžãªã©ã®ãŸã‚ã«ã€Audioè¦ç´ ã®ã‚³ãƒ³ãƒ†ã‚ストメニューã¯ãƒ–ラウザデフォルトã¨ã™ã‚‹ã€‚ - if (el.tagName === 'AUDIO') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; - if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; @@ -1153,7 +1154,7 @@ function emitUpdReaction(emoji: string, delta: number) { z-index: 2; width: 100%; height: 64px; - //background: linear-gradient(0deg, var(--panel), var(--X15)); + //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); &:hover > .collapsedLabel { background: var(--panelHighlight); diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index cca6c7a40c86e15157734e7c6c2089a6191988ae..7907da6c9416df148f24589f21c2fbcec274b16b 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -78,7 +78,15 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :isBlock="true" :author="appearNote.user" :nyaize="'respect'"/> + <Mfm + v-if="appearNote.cw != ''" + :text="appearNote.cw" + :author="appearNote.user" + :nyaize="'respect'" + :enableEmojiMenu="true" + :enableEmojiMenuReaction="true" + :isBlock="true" + /> <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/> </p> <div v-show="appearNote.cw == null || showContent"> @@ -108,7 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.files && appearNote.files.length > 0"> <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/> </div> - <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/> + <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll"/> <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/> </div> @@ -157,7 +165,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-heart ph-bold ph-lg"></i> </button> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> - <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i> <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else class="ph-smiley ph-bold ph-lg"></i> @@ -235,6 +243,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, onMounted, onUnmounted, onUpdated, provide, ref, shallowRef, watch } from 'vue'; import * as mfm from '@transfem-org/sfm-js'; import * as Misskey from 'misskey-js'; +import { isLink } from '@@/js/is-link.js'; import SkNoteSub from '@/components/SkNoteSub.vue'; import SkNoteSimple from '@/components/SkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -258,7 +267,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; @@ -559,7 +568,8 @@ function quote() { os.post({ renote: appearNote.value, channel: appearNote.value.channel, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -583,7 +593,8 @@ function quote() { } else { os.post({ renote: appearNote.value, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: appearNote.value.id, userId: $i?.id, @@ -710,14 +721,6 @@ function toggleReact() { } function onContextmenu(ev: MouseEvent): void { - const isLink = (el: HTMLElement): boolean => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - return false; - }; - if (ev.target && isLink(ev.target as HTMLElement)) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index c2986b252453efa63c9529b46c7d11ed471d15ea..fac35191b909012b34ce27451c6df1d6f993d214 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -353,7 +353,8 @@ function quote() { os.post({ renote: appearNote.value, channel: appearNote.value.channel, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, @@ -377,7 +378,8 @@ function quote() { } else { os.post({ renote: appearNote.value, - }).then(() => { + }).then((cancelled) => { + if (cancelled) return; misskeyApi('notes/renotes', { noteId: props.note.id, userId: $i.id, diff --git a/packages/frontend/src/components/SkUserRecentNotes.vue b/packages/frontend/src/components/SkUserRecentNotes.vue new file mode 100644 index 0000000000000000000000000000000000000000..2cdb4b6586cf41e2b5a7691d55cffada51ee34f1 --- /dev/null +++ b/packages/frontend/src/components/SkUserRecentNotes.vue @@ -0,0 +1,110 @@ +<!-- +SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkPullToRefresh :refresher="() => reload()"> + <div v-if="user" :class="$style.userInfo"> + <MkUserInfo :class="$style.userInfo" class="user" :user="user"/> + <MkNotes :noGap="true" :pagination="pagination"/> + </div> + <div v-else-if="loadError" :class="$style.panel">{{ loadError }}</div> + <MkLoading v-else-if="userId"/> +</MkPullToRefresh> +</template> + +<script setup lang="ts"> +import { computed, onMounted, ref, Ref, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkLoading from '@/components/global/MkLoading.vue'; +import MkNotes from '@/components/MkNotes.vue'; +import MkUserInfo from '@/components/MkUserInfo.vue'; +import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; +import { Paging } from '@/components/MkPagination.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; + +const props = defineProps<{ + userId: string; + withNonPublic: boolean; + withQuotes: boolean; + withReplies: boolean; + withBots: boolean; + onlyFiles: boolean; +}>(); + +const loadError: Ref<string | null> = ref(null); +const user: Ref<Misskey.entities.UserDetailed | null> = ref(null); + +const pagination: Paging<'users/notes'> = { + endpoint: 'users/notes' as const, + limit: 10, + params: computed(() => ({ + userId: props.userId, + withNonPublic: props.withNonPublic, + withRenotes: false, + withQuotes: props.withQuotes, + withReplies: props.withReplies, + withRepliesToSelf: props.withReplies, + withFiles: props.onlyFiles, + allowPartial: true, + })), +}; + +defineExpose({ + reload, + user, +}); + +async function reload(): Promise<void> { + loadError.value = null; + user.value = null; + + await Promise + .all([ + // We need a User entity, but the pagination returns only UserLite. + // An additional request is needed to "upgrade" the object. + misskeyApi('users/show', { userId: props.userId }), + + // Wait for 1 second to match the animation effects in MkHorizontalSwipe, MkPullToRefresh, and MkPagination. + // Otherwise, the page appears to load "backwards". + new Promise(resolve => setTimeout(resolve, 1000)), + ]) + .then(([u]) => user.value = u) + .catch(error => { + console.error('Error fetching user info', error); + + loadError.value = + typeof(error) === 'string' + ? String(error) + : JSON.stringify(error); + }); +} + +watch(() => props.userId, async () => { + await reload(); +}); + +onMounted(async () => { + await reload(); +}); + +</script> + +<style lang="scss" module> + +.panel { + background: var(--panel); +} + +.userInfo { + margin-bottom: 12px; +} + +@container (min-width: 451px) { + .userInfo { + margin-bottom: 24px; + } +} + +</style> diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index 164606e1f9495b59fe5bbb9dd8a82dca71f3fd62..f5546edf1e928396bb37ce8484c8d3a462ad00f5 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -60,18 +60,18 @@ const props = defineProps<{ width: 100%; box-sizing: border-box; padding: 10px 14px; - background: var(--buttonBg); + background: var(--folderHeaderBg); border-radius: var(--radius-sm); font-size: 0.9em; &:hover { text-decoration: none; - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } &.active { color: var(--accent); - background: var(--buttonHoverBg); + background: var(--folderHeaderHoverBg); } } diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index e0303dbb2734a456a7cf8bff75a58b56fe7bd1aa..23f049ebb43c3ab607f03500076987f50c49f0d6 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -17,7 +17,7 @@ export type MkABehavior = 'window' | 'browser' | null; import { computed, inject, shallowRef } from 'vue'; import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { useRouter } from '@/router/supplier.js'; diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue index bbcb070803850d7c734d8b8f907e2e348aa50173..9a1ac3aca2d7d79f7d88fc82c11cb47261cf0f03 100644 --- a/packages/frontend/src/components/global/MkAcct.vue +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -4,11 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3"> - <span>@{{ user.username }}</span> - <span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> -</MkCondensedLine> -<span v-else> +<span> <span>@{{ user.username }}</span> <span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span> </span> @@ -17,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; import { toUnicode } from 'punycode/'; -import { host as hostRaw } from '@/config.js'; +import { host as hostRaw } from '@@/js/config.js'; import { defaultStore } from '@/store.js'; defineProps<{ diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index bee1d9ca4c10037171ea8a9b36b6446a02024bdb..1238e6ef231047f8c470dbb352689e3455d38141 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { url as local, host } from '@/config.js'; +import { url as local, host } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue index de62fe12a97a4ce03c5013c52857327235e3f377..1f78b068a2ee986d2db3a5b8ded1fa4ee59a7d1e 100644 --- a/packages/frontend/src/components/global/MkAvatar.vue +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -26,12 +26,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="showDecoration"> <img v-for="decoration in decorations ?? user.avatarDecorations" - :class="[$style.decoration]" + :class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]" :src="getDecorationUrl(decoration)" :style="{ rotate: getDecorationAngle(decoration), scale: getDecorationScale(decoration), translate: getDecorationOffset(decoration), + zIndex: getDecorationZIndex(decoration), }" alt="" > @@ -42,10 +43,10 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; +import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; import MkImgWithBlurhash from '../MkImgWithBlurhash.vue'; import MkA from './MkA.vue'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js'; import { acct, userPage } from '@/filters/user.js'; import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue'; import { defaultStore } from '@/store.js'; @@ -60,7 +61,7 @@ const props = withDefaults(defineProps<{ link?: boolean; preview?: boolean; indicator?: boolean; - decorations?: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>[]; + decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[]; forceShowDecoration?: boolean; }>(), { target: null, @@ -113,6 +114,10 @@ function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['ava return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`; } +function getDecorationZIndex(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) { + return decoration.showBelow ? '-1' : undefined; +} + const color = ref<string | undefined>(); watch(() => props.user.avatarBlurhash, () => { @@ -159,6 +164,7 @@ watch(() => props.user.avatarBlurhash, () => { flex-shrink: 0; border-radius: 100%; // sharkey: controlled by square avatars setting! line-height: 16px; + z-index: 0; // sharkey: starts stacking context to help with showing decorations behind the avatar } .inner { @@ -330,4 +336,17 @@ watch(() => props.user.avatarBlurhash, () => { width: 200%; pointer-events: none; } + +.decorationBlink { + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 100% { + filter: brightness(2); + } + 50% { + filter: brightness(1); + } +} </style> diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue index 7c4957d77fac1c7261335b7eb7d087143b42ccc3..473d444c1606cfa492e775aa9b31d439224cfdba 100644 --- a/packages/frontend/src/components/global/MkCondensedLine.vue +++ b/packages/frontend/src/components/global/MkCondensedLine.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <span :class="$style.container"> - <span ref="content" :class="$style.content"> + <span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }"> <slot/> </span> </span> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 4cacc7b292eb8982e3f44333c10b8ec994eb7c76..fbc716016cdcd4aad0c072da9c3e646751ea3e83 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -35,6 +35,7 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ name: string; @@ -86,7 +87,10 @@ const errored = ref(url.value == null); function onClick(ev: MouseEvent) { if (props.menu) { ev.stopPropagation(); - os.popupMenu([{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'label', text: `:${props.name}:`, }, { @@ -96,14 +100,20 @@ function onClick(ev: MouseEvent) { copyToClipboard(`:${props.name}:`); os.success(); }, - }, ...(props.menuReaction && react ? [{ - text: i18n.ts.doReaction, - icon: 'ph-smiley ph-bold ph-lg', - action: () => { - react(`:${props.name}:`); - sound.playMisskeySfx('reaction'); - }, - }] : []), { + }); + + if (props.menuReaction && react) { + menuItems.push({ + text: i18n.ts.doReaction, + icon: 'ph-smiley ph-bold ph-lg', + action: () => { + react(`:${props.name}:`); + sound.playMisskeySfx('reaction'); + }, + }); + } + + menuItems.push({ text: i18n.ts.info, icon: 'ti ti-info-circle', action: async () => { @@ -115,7 +125,9 @@ function onClick(ev: MouseEvent) { closed: () => dispose(), }); }, - }], ev.currentTarget ?? ev.target); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } </script> diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index c485305376730932656cba26668f30ad21e5a1b4..bd9b1d665a9021161a74bfe7477096f87ff5c245 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -10,13 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, inject } from 'vue'; -import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } from '@/scripts/emoji-base.js'; +import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js'; +import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } from '@@/js/emoji-base.js'; import { defaultStore } from '@/store.js'; -import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js'; import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ emoji: string; @@ -40,7 +41,10 @@ function computeTitle(event: PointerEvent): void { function onClick(ev: MouseEvent) { if (props.menu) { ev.stopPropagation(); - os.popupMenu([{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'label', text: props.emoji, }, { @@ -50,14 +54,20 @@ function onClick(ev: MouseEvent) { copyToClipboard(props.emoji); os.success(); }, - }, ...(props.menuReaction && react ? [{ - text: i18n.ts.doReaction, - icon: 'ph-smiley ph-bold ph-lg', - action: () => { - react(props.emoji); - sound.playMisskeySfx('reaction'); - }, - }] : [])], ev.currentTarget ?? ev.target); + }); + + if (props.menuReaction && react) { + menuItems.push({ + text: i18n.ts.doReaction, + icon: 'ph-smiley ph-bold ph-lg', + action: () => { + react(props.emoji); + sound.playMisskeySfx('reaction'); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } </script> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMfm.stories.impl.ts similarity index 78% rename from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts rename to packages/frontend/src/components/global/MkMfm.stories.impl.ts index 730351f79534b9f2e5531b0e6ea461f7ad27c49a..1daf7a29cb6073a910343edec37e89b276b0fb7c 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts +++ b/packages/frontend/src/components/global/MkMfm.stories.impl.ts @@ -2,16 +2,15 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ - -/* eslint-disable @typescript-eslint/explicit-function-return-type */ + import { StoryObj } from '@storybook/vue3'; import { expect, within } from '@storybook/test'; -import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js'; +import MkMfm from './MkMfm.js'; export const Default = { render(args) { return { components: { - MkMisskeyFlavoredMarkdown, + MkMfm, }, setup() { return { @@ -25,7 +24,7 @@ export const Default = { }; }, }, - template: '<MkMisskeyFlavoredMarkdown v-bind="props" />', + template: '<MkMfm v-bind="props" />', }; }, async play({ canvasElement, args }) { @@ -54,25 +53,25 @@ export const Default = { parameters: { layout: 'centered', }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; export const Plain = { ...Default, args: { ...Default.args, plain: true, }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; export const Nowrap = { ...Default, args: { ...Default.args, nowrap: true, }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; export const IsNotNote = { ...Default, args: { ...Default.args, isNote: false, }, -} satisfies StoryObj<typeof MkMisskeyFlavoredMarkdown>; +} satisfies StoryObj<typeof MkMfm>; diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMfm.ts similarity index 97% rename from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts rename to packages/frontend/src/components/global/MkMfm.ts index a3a2b9f319ee7337e011c7b2a50228e67dc266f2..9bf9f4a87238e1388128333aee437b3356071c18 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -18,10 +18,15 @@ import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; import MkA, { MkABehavior } from '@/components/global/MkA.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { defaultStore } from '@/store.js'; -import { nyaize as doNyaize } from '@/scripts/nyaize.js'; -import { safeParseFloat } from '@/scripts/safe-parse.js'; + +function safeParseFloat(str: unknown): number | null { + if (typeof str !== 'string' || str === '') return null; + const num = parseFloat(str); + if (isNaN(num)) return null; + return num; +} const QUOTE_STYLE = ` display: block; @@ -58,8 +63,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven provide('linkNavigationBehavior', props.linkNavigationBehavior); const isNote = props.isNote ?? true; - const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat ? props.author.speakAsCat : false : false : false; - + const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat && !defaultStore.state.disableCatSpeak; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.text == null || props.text === '') return; @@ -93,7 +97,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven case 'text': { let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } if (!props.plain) { @@ -341,14 +345,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const child = token.children[0]; let text = child.type === 'text' ? child.props.text : ''; if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); } else { const rt = token.children.at(-1)!; let text = rt.type === 'text' ? rt.props.text : ''; if (!disableNyaize && shouldNyaize) { - text = doNyaize(text); + text = Misskey.nyaize(text); } return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); } @@ -460,7 +464,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'emojiCode': { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.author?.host == null) { return [h(MkCustomEmoji, { key: Math.random(), diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 95ac1020132afe4118cdf84fdd23ef18550eb744..bb20f54fa4cf8cb7b7a76d0f97a1a2c7d961a619 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue'; import tinycolor from 'tinycolor2'; import XTabs, { Tab } from './MkPageHeader.tabs.vue'; -import { scrollToTop } from '@/scripts/scroll.js'; +import { scrollToTop } from '@@/js/scroll.js'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index b12dc8cb31ad864b5249a694a5baf1bf00e6d0b9..72993991cef1135f33c579b03cd9ee598c368f02 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="bodyEl" :data-sticky-container-header-height="headerHeight" :data-sticky-container-footer-height="footerHeight" + style="position: relative; z-index: 0;" > <slot></slot> </div> @@ -24,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue'; -import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js'; const rootEl = shallowRef<HTMLElement>(); const headerEl = shallowRef<HTMLElement>(); @@ -83,14 +84,14 @@ onMounted(() => { if (headerEl.value != null) { headerEl.value.style.position = 'sticky'; headerEl.value.style.top = 'var(--stickyTop, 0)'; - headerEl.value.style.zIndex = '1000'; + headerEl.value.style.zIndex = '1'; observer.observe(headerEl.value); } if (footerEl.value != null) { footerEl.value.style.position = 'sticky'; footerEl.value.style.bottom = 'var(--stickyBottom, 0)'; - footerEl.value.style.zIndex = '1000'; + footerEl.value.style.zIndex = '1'; observer.observe(footerEl.value); } }); diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index ffd4a849a2ed86bf4b9f56015ea7c78e692bc1dc..ccf7f200b592a566fb4e44802bc1e9dedcb00dcd 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -8,7 +8,7 @@ import { expect } from '@storybook/test'; import { StoryObj } from '@storybook/vue3'; import MkTime from './MkTime.vue'; import { i18n } from '@/i18n.js'; -import { dateTimeFormat } from '@/scripts/intl-const.js'; +import { dateTimeFormat } from '@@/js/intl-const.js'; const now = new Date('2023-04-01T00:00:00.000Z'); const future = new Date('2024-04-01T00:00:00.000Z'); const oneHourAgo = new Date(now.getTime() - 3600000); diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 027b226f3fb0e8249f7965654463d1215be22a74..50bec990a1eee7beb940219aaecb9f8c8161c17d 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only import isChromatic from 'chromatic/isChromatic'; import { onMounted, onUnmounted, ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; -import { dateTimeFormat } from '@/scripts/intl-const.js'; +import { dateTimeFormat } from '@@/js/intl-const.js'; const props = withDefaults(defineProps<{ time: Date | string | number | null; diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 15595ba51576f01e0905f9d286761ddf26b30126..8cca47c1dbdc0db88a22c21e73b8bc40f2f02aa4 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target" :behavior="props.navigationBehavior" @contextmenu.stop="() => {}" + @click.prevent="self ? true : warningExternalWebsite(props.url)" @click.stop > <template v-if="!self"> @@ -28,12 +29,20 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; import { toUnicode as decodePunycode } from 'punycode/'; -import { url as local } from '@/config.js'; +import { url as local } from '@@/js/config.js'; import * as os from '@/os.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; -import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; import { isEnabledUrlPreview } from '@/instance.js'; import { MkABehavior } from '@/components/global/MkA.vue'; +import { warningExternalWebsite } from '@/scripts/warning-external-website.js'; + +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} const props = withDefaults(defineProps<{ url: string; diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 44d8d59941b3fac3379c28c2422f4e9cd747ee48..b36625ed1b47254dad6dec8995e9ba4305db0ec5 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -5,7 +5,7 @@ import { App } from 'vue'; -import Mfm from './global/MkMisskeyFlavoredMarkdown.js'; +import Mfm from './global/MkMfm.js'; import MkA from './global/MkA.vue'; import MkAcct from './global/MkAcct.vue'; import MkAvatar from './global/MkAvatar.vue'; diff --git a/packages/frontend/src/config.ts b/packages/frontend/src/config.ts deleted file mode 100644 index e3922a0cd5a7f1f609d572420231aa7950ed726c..0000000000000000000000000000000000000000 --- a/packages/frontend/src/config.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { miLocalStorage } from '@/local-storage.js'; - -const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href); -const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content; - -export const host = address.host; -export const hostname = address.hostname; -export const url = address.origin; -export const apiUrl = location.origin + '/api'; -export const wsOrigin = location.origin; -export const lang = miLocalStorage.getItem('lang') ?? 'en-US'; -export const langs = _LANGS_; -const preParseLocale = miLocalStorage.getItem('locale'); -export let locale = preParseLocale ? JSON.parse(preParseLocale) : null; -export const version = _VERSION_; -export const instanceName = siteName === 'Sharkey' || siteName == null ? host : siteName; -export const ui = miLocalStorage.getItem('ui'); -export const debug = miLocalStorage.getItem('debug') === 'true'; - -export function updateLocale(newLocale): void { - locale = newLocale; -} diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 9da3582e1a616dc4fbb34b197a82a9fed2658007..0d03282cee22c8f4fced75f2d4eb5eb756bbe7a7 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -6,7 +6,6 @@ import { shallowRef, computed, markRaw, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useStream } from '@/stream.js'; import { get, set } from '@/scripts/idb-proxy.js'; const storageCache = await get('emojis'); @@ -29,23 +28,20 @@ watch(customEmojis, emojis => { } }, { immediate: true }); -// TODO: ã“ã“ら辺副作用ãªã®ã§ã„ã„æ„Ÿã˜ã«ã™ã‚‹ -const stream = useStream(); - -stream.on('emojiAdded', emojiData => { - customEmojis.value = [emojiData.emoji, ...customEmojis.value]; +export function addCustomEmoji(emoji: Misskey.entities.EmojiSimple) { + customEmojis.value = [emoji, ...customEmojis.value]; set('emojis', customEmojis.value); -}); +} -stream.on('emojiUpdated', emojiData => { - customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.EmojiSimple ?? item); +export function updateCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) { + customEmojis.value = customEmojis.value.map(item => emojis.find(search => search.name === item.name) ?? item); set('emojis', customEmojis.value); -}); +} -stream.on('emojiDeleted', emojiData => { - customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name)); +export function removeCustomEmojis(emojis: Misskey.entities.EmojiSimple[]) { + customEmojis.value = customEmojis.value.filter(item => !emojis.some(search => search.name === item.name)); set('emojis', customEmojis.value); -}); +} export async function fetchCustomEmojis(force = false) { const now = Date.now(); diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts index f200f242ed23621d44a5d91a697b4039269762b0..615dd99fa8d4d384520b81eb78efc03762d3e293 100644 --- a/packages/frontend/src/directives/follow-append.ts +++ b/packages/frontend/src/directives/follow-append.ts @@ -4,7 +4,7 @@ */ import { Directive } from 'vue'; -import { getScrollContainer, getScrollPosition } from '@/scripts/scroll.js'; +import { getScrollContainer, getScrollPosition } from '@@/js/scroll.js'; export default { mounted(src, binding, vn) { diff --git a/packages/frontend/src/filters/date.ts b/packages/frontend/src/filters/date.ts index 2ffe93e868a2cc2905a34411e366cdee2c4640cc..d13d1a5e428ac29c474500fb6d5b2a00aad76cda 100644 --- a/packages/frontend/src/filters/date.ts +++ b/packages/frontend/src/filters/date.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { dateTimeFormat } from '@/scripts/intl-const.js'; +import { dateTimeFormat } from '@@/js/intl-const.js'; export default (d: Date | number | undefined) => dateTimeFormat.format(d); export const dateString = (d: string) => dateTimeFormat.format(new Date(d)); diff --git a/packages/frontend/src/filters/number.ts b/packages/frontend/src/filters/number.ts index 2e7cc60ff43dc4dff5937087a30bc6633fab65a9..10fb64deb4d703b1303aae7c11c5706fb4cb46cc 100644 --- a/packages/frontend/src/filters/number.ts +++ b/packages/frontend/src/filters/number.ts @@ -3,6 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { numberFormat } from '@/scripts/intl-const.js'; +import { numberFormat } from '@@/js/intl-const.js'; export default n => n == null ? 'N/A' : numberFormat.format(n); diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts index a87766764daed628c39e4b17dff106d8c330ec80..d9bc31676464e1c09da2b5d0115e15c5170f9682 100644 --- a/packages/frontend/src/filters/user.ts +++ b/packages/frontend/src/filters/user.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; export const acct = (user: Misskey.Acct) => { return Misskey.acct.toString(user); diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts index 10d6adbcd0ca9d203c748505aa108a8fc401ccc9..6ad503b0898a53004660609444b91ddaba9ea160 100644 --- a/packages/frontend/src/i18n.ts +++ b/packages/frontend/src/i18n.ts @@ -4,11 +4,11 @@ */ import { markRaw } from 'vue'; +import { I18n } from '@@/js/i18n.js'; import type { Locale } from '../../../locales/index.js'; -import { locale } from '@/config.js'; -import { I18n } from '@/scripts/i18n.js'; +import { locale } from '@@/js/config.js'; -export const i18n = markRaw(new I18n<Locale>(locale)); +export const i18n = markRaw(new I18n<Locale>(locale, _DEV_)); export function updateI18n(newLocale: Locale) { i18n.locale = newLocale; diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html index 733116b75f26e4c21344f1df860f9d69bd716402..55d6b6cffde8789cb20f839f422a8386661a8f1e 100644 --- a/packages/frontend/src/index.html +++ b/packages/frontend/src/index.html @@ -17,12 +17,12 @@ <meta http-equiv="Content-Security-Policy" content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/; - worker-src 'self'; - script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh; + worker-src 'self' blob:; + script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; - img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com; + img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com; media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; - connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org; + connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org https://api.friendlycaptcha.com; frame-src *;" /> <meta property="og:site_name" content="[DEV BUILD] Misskey" /> diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index 6847321d6c40d992191eef107ba740e2cf905998..71cb42b30c45f0840f5594ab97c43217321b51e5 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -7,7 +7,7 @@ import { computed, reactive } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { miLocalStorage } from '@/local-storage.js'; -import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js'; +import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js'; // TODO: ä»–ã®ã‚¿ãƒ–ã¨æ°¸ç¶šåŒ–ã•ã‚ŒãŸstateã‚’åŒæœŸ diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 2b2f59edb35c08971da4a9014174267966c9d0d5..89c0a4b8496184d3bb53f8563ca7767fc53296be 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -type Keys = +export type Keys = 'v' | 'lastVersion' | 'instance' | @@ -39,12 +39,22 @@ type Keys = `aiscript:${string}` | 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) 'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~); - `channelLastReadedAt:${string}` + `channelLastReadedAt:${string}` | + `idbfallback::${string}` + +// セッション毎ã«å»ƒæ£„ã•ã‚Œã‚‹LocalStorage代替(セーフモードãªã©ã§ä½¿ç”¨ã§ããã†ï¼‰ +//const safeSessionStorage = new Map<Keys, string>(); export const miLocalStorage = { - getItem: (key: Keys): string | null => window.localStorage.getItem(key), - setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value), - removeItem: (key: Keys): void => window.localStorage.removeItem(key), + getItem: (key: Keys): string | null => { + return window.localStorage.getItem(key); + }, + setItem: (key: Keys, value: string): void => { + window.localStorage.setItem(key, value); + }, + removeItem: (key: Keys): void => { + window.localStorage.removeItem(key); + }, getItemAsJson: (key: Keys): any | undefined => { const item = miLocalStorage.getItem(key); if (item === null) { @@ -52,5 +62,7 @@ export const miLocalStorage = { } return JSON.parse(item); }, - setItemAsJson: (key: Keys, value: any): void => window.localStorage.setItem(key, JSON.stringify(value)), + setItemAsJson: (key: Keys, value: any): void => { + miLocalStorage.setItem(key, JSON.stringify(value)); + }, }; diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index ccd7034968ce9a3b7d5cd18094dd229696f8cd81..6f236dc89e79ddcb0aa79bf2f60bce0632882ca1 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -12,7 +12,7 @@ import { openInstanceMenu } from '@/ui/_common_/common.js'; import { lookup } from '@/scripts/lookup.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { ui } from '@/config.js'; +import { ui } from '@@/js/config.js'; import { unisonReload } from '@/scripts/unison-reload.js'; export const navbarItemDef = reactive({ @@ -41,7 +41,7 @@ export const navbarItemDef = reactive({ followRequests: { title: i18n.ts.followRequests, icon: 'ti ti-user-plus', - show: computed(() => $i != null && $i.isLocked), + show: computed(() => $i != null && ($i.isLocked || $i.hasPendingReceivedFollowRequest || $i.hasPendingSentFollowRequest)), indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest), to: '/my/follow-requests', }, @@ -68,6 +68,12 @@ export const navbarItemDef = reactive({ lookup(); }, }, + following: { + title: i18n.ts.following, + icon: 'ph-user-check ph-bold ph-lg', + show: computed(() => $i != null && !$i.movedTo), + to: '/following-feed', + }, lists: { title: i18n.ts.lists, icon: 'ti ti-list', @@ -126,7 +132,7 @@ export const navbarItemDef = reactive({ ui: { title: i18n.ts.switchUi, icon: 'ti ti-devices', - action: (ev) => { + action: (ev: MouseEvent) => { os.popupMenu([{ text: i18n.ts.default, active: ui === 'default' || ui === null, diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts index 6a8ea09ed6af93d1bbef30fbfbec04f88f41c48b..25f853453a0dd67fbaec57742d325c453b9e9d8f 100644 --- a/packages/frontend/src/nirax.ts +++ b/packages/frontend/src/nirax.ts @@ -7,7 +7,14 @@ import { Component, onMounted, shallowRef, ShallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; -import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; + +function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} interface RouteDefBase { path: string; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index f6f4d62d50f5657452abf76cc9fadaae610c9878..e73aa77a3cc157cefeefa42e1b6d8a2e6a188e91 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -22,7 +22,7 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue'; import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue'; import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; @@ -245,6 +245,7 @@ export function confirm(props: { text?: string; okText?: string; cancelText?: string; + plain?: boolean; }): Promise<{ canceled: boolean }> { return new Promise(resolve => { const { dispose } = popup(MkDialog, { @@ -691,7 +692,7 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> { })); } -export function post(props: Record<string, any> = {}): Promise<void> { +export function post(props: Record<string, any> = {}): Promise<void | boolean> { pleaseLogin(undefined, (props.initialText || props.initialNote ? { type: 'share', params: { @@ -709,8 +710,8 @@ export function post(props: Record<string, any> = {}): Promise<void> { // 複数ã®post formã‚’é–‹ã„ãŸã¨ãã«å ´åˆã«ã‚ˆã£ã¦ã¯ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ // ã‚‚ã¡ã‚ん複数ã®post formã‚’é–‹ã‘ã‚‹ã“ã¨è‡ªä½“Misskeyサイドã®ãƒã‚°ãªã®ã ㌠const { dispose } = popup(MkPostFormDialog, props, { - closed: () => { - resolve(); + closed: (cancelled) => { + resolve(cancelled); dispose(); }, }); diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index dbc113a7664d2c45a035497a13886df03776944b..83f596b870c6bcedcc6e38a161be19b2197135de 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -29,7 +29,7 @@ import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkLink from '@/components/MkLink.vue'; -import { version } from '@/config.js'; +import { version } from '@@/js/config.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue index 938786949c8178616b0020a82498189d304544c5..7ebae62a7b2ef5a2109c4edb705ef82e8642c029 100644 --- a/packages/frontend/src/pages/about-sharkey.vue +++ b/packages/frontend/src/pages/about-sharkey.vue @@ -185,7 +185,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue'; -import { version } from '@/config.js'; +import { version } from '@@/js/config.js'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue index c378c0a0b8d29802ca4067c3296445b7c8029969..ca070c65a4f39af6d5224a082f82dfee5794dc3f 100644 --- a/packages/frontend/src/pages/about.overview.vue +++ b/packages/frontend/src/pages/about.overview.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"> <div style="overflow: clip;"> - <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/> + <img :src="instance.sidebarLogoUrl ?? instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/> <div :class="$style.bannerName"> <b>{{ instance.name ?? host }}</b> </div> @@ -116,6 +116,22 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSection> </FormSuspense> + <FormSection v-if="sponsors[0].length > 0"> + <template #label>Our lovely Sponsors</template> + <div :class="$style.contributors"> + <span + v-for="sponsor in sponsors[0]" + :key="sponsor" + style="margin-bottom: 0.5rem;" + > + <a :href="sponsor.website || sponsor.profile" target="_blank" :class="$style.contributor"> + <img :src="sponsor.image || `https://ui-avatars.com/api/?background=0D8ABC&color=fff&name=${sponsor.name}`" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">{{ sponsor.name }}</span> + </a> + </span> + </div> + </FormSection> + <FormSection> <template #label>Well-known resources</template> <div class="_gaps_s"> @@ -130,8 +146,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> +import { ref } from 'vue'; import sanitizeHtml from '@/scripts/sanitize-html.js'; -import { host, version } from '@/config.js'; +import { host, version } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import number from '@/filters/number.js'; @@ -144,7 +161,10 @@ import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkLink from '@/components/MkLink.vue'; +const sponsors = ref([]); + const initStats = () => misskeyApi('stats', {}); +await misskeyApi('sponsors', { instance: true }).then((res) => sponsors.value.push(res.sponsor_data)); </script> <style lang="scss" module> @@ -160,7 +180,7 @@ const initStats = () => misskeyApi('stats', {}); .bannerIcon { display: block; margin: 16px auto 0 auto; - height: 64px; + max-height: 96px; border-radius: var(--radius-sm);; } @@ -207,4 +227,37 @@ const initStats = () => misskeyApi('stats', {}); .ruleText { padding-top: 6px; } + +.contributors { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-gap: 12px; +} + +.contributor { + display: flex; + align-items: center; + padding: 12px; + background: var(--buttonBg); + border-radius: var(--radius-sm); + + &:hover { + text-decoration: none; + background: var(--buttonHoverBg); + } + + &.active { + color: var(--accent); + background: var(--buttonHoverBg); + } +} + +.contributorAvatar { + width: 30px; + border-radius: var(--radius-full); +} + +.contributorUsername { + margin-left: 12px; +} </style> diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index d8311186ab84676851af70999185278b9d3bf81c..60f6be51d42dc95770d50192c7c2c0d93937dca0 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -44,6 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> + <div v-else-if="tab === 'notes' && info" class="_gaps_m"> + <XNotes :fileId="fileId"/> + </div> <div v-else-if="tab === 'ip' && info" class="_gaps_m"> <MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> <MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline> @@ -67,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, ref } from 'vue'; +import { computed, defineAsyncComponent, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -88,6 +91,7 @@ const tab = ref('overview'); const file = ref<Misskey.entities.DriveFile | null>(null); const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null); const isSensitive = ref<boolean>(false); +const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue')); const props = defineProps<{ fileId: string, @@ -131,6 +135,10 @@ const headerTabs = computed(() => [{ title: i18n.ts.overview, icon: 'ti ti-info-circle', }, iAmModerator ? { + key: 'notes', + title: i18n.ts._fileViewer.attachedNotes, + icon: 'ti ti-pencil', +} : null, iAmModerator ? { key: 'ip', title: 'IP', icon: 'ti ti-password', diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 187ec66b423a5c1cda81de3469502909331a8fb7..22e16effe0272fde1e06a7c69ac0736ddf6266e8 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo> + <MkInfo v-if="isSystem">{{ i18n.ts.isSystemAccount }}</MkInfo> <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink> @@ -79,11 +79,11 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <div class="_gaps"> <MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch> - <MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> + <MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> <MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch> <div> - <MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton> + <MkButton v-if="user.host == null && !isSystem" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton> </div> <MkFolder> @@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton> <MkButton v-if="iAmModerator" inline danger @click="deleteAllFiles"><i class="ph-cloud ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton> </div> - <MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton> + <MkButton v-if="$i.isAdmin && !isSystem" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton> </div> </FormSection> </div> @@ -138,7 +138,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'announcements'" class="_gaps"> - <MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton> + <MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts._announcement.new }}</MkButton> <MkPagination :pagination="announcementsPagination"> <template #default="{ items }"> @@ -208,7 +208,7 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { acct } from '@/filters/user.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; @@ -227,15 +227,16 @@ const tab = ref(props.initialTab); const chartSrc = ref('per-user-notes'); const user = ref<null | Misskey.entities.UserDetailed>(); const init = ref<ReturnType<typeof createFetcher>>(); -const info = ref<any>(); +const info = ref<Misskey.entities.AdminShowUserResponse | null>(null); const ips = ref<Misskey.entities.AdminGetUserIpsResponse | null>(null); -const ap = ref<any>(null); +const ap = ref<Misskey.entities.ApGetResponse | null>(null); const moderator = ref(false); const silenced = ref(false); const approved = ref(false); const suspended = ref(false); const markedAsNSFW = ref(false); const moderationNote = ref(''); +const isSystem = computed(() => info.value?.isSystem ?? false); const filesPagination = { endpoint: 'admin/drive/files' as const, limit: 10, diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue index 61dc4f854921bf43acb0466c1440f149ba9a5f66..2a71e3efab3baa6a318d4a4fbc137398c1e68020 100644 --- a/packages/frontend/src/pages/admin/_header_.vue +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue'; import tinycolor from 'tinycolor2'; import { popupMenu } from '@/os.js'; -import { scrollToTop } from '@/scripts/scroll.js'; +import { scrollToTop } from '@@/js/scroll.js'; import MkButton from '@/components/MkButton.vue'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index bd442ccc691a4b4dcf0b0d39b854c52168847f42..6c8901b10b9363fdbb8e20edee35e1b24b40facb 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -65,18 +65,18 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTextarea v-model="ad.memo"> <template #label>{{ i18n.ts.memo }}</template> </MkTextarea> - <div class="buttons"> - <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"> + <div class="_buttons"> + <MkButton inline primary style="margin-right: 12px;" @click="save(ad)"> <i class="ti ti-device-floppy" ></i> {{ i18n.ts.save }} </MkButton> - <MkButton class="button" inline danger @click="remove(ad)"> + <MkButton inline danger @click="remove(ad)"> <i class="ti ti-trash"></i> {{ i18n.ts.remove }} </MkButton> </div> </div> - <MkButton class="button" @click="more()"> + <MkButton @click="more()"> <i class="ti ti-reload"></i>{{ i18n.ts.more }} </MkButton> </div> diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index b9e09c8d03a6c7be47441bc00bba2d462ab484fb..fd37311b21c4291e6c3580adc7d0763fd9a9fcc8 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -29,8 +29,16 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> </template> <template #caption>{{ announcement.text }}</template> + <template #footer> + <div class="_buttons"> + <MkButton rounded primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="announcement.id != null && announcement.isActive" rounded @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton> + <MkButton v-if="announcement.id != null && !announcement.isActive" rounded @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton> + <MkButton v-if="announcement.id != null" rounded danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </template> - <div class="_gaps_m"> + <div class="_gaps"> <MkInput v-model="announcement.title"> <template #label>{{ i18n.ts.title }}</template> </MkInput> @@ -64,16 +72,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} </MkSwitch> <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p> - <div class="buttons _buttons"> - <MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="announcement.id != null && announcement.isActive" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton> - <MkButton v-if="announcement.id != null && !announcement.isActive" class="button" inline @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton> - <MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> </div> </MkFolder> <MkLoading v-if="loadingMore"/> - <MkButton class="button" @click="more()"> + <MkButton @click="more()"> <i class="ti ti-reload"></i>{{ i18n.ts.more }} </MkButton> </template> @@ -170,7 +172,7 @@ function more() { loadingMore.value = true; misskeyApi('admin/announcements/list', { status: announcementsStatus.value, - untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id + untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id, }).then(announcementResponse => { announcements.value = announcements.value.concat(announcementResponse); loadingMore.value = false; diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 73c5e1919f41af20678e745ad0e39743846688b9..644436cde6d6d6a90f0602c37655eb4f26fd51e1 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -4,145 +4,166 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div> - <FormSuspense :p="init"> - <div class="_gaps_m"> - <MkRadios v-model="provider"> - <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> - <option value="hcaptcha">hCaptcha</option> - <option value="mcaptcha">mCaptcha</option> - <option value="recaptcha">reCAPTCHA</option> - <option value="turnstile">Turnstile</option> - </MkRadios> +<MkFolder> + <template #icon><i class="ti ti-shield"></i></template> + <template #label>{{ i18n.ts.botProtection }}</template> + <template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template> + <template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template> + <template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template> + <template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template> + <template v-else-if="botProtectionForm.savedState.provider === 'fc'" #suffix>FriendlyCaptcha</template> + <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> + <template v-if="botProtectionForm.modified.value" #footer> + <MkFormFooter :form="botProtectionForm"/> + </template> - <template v-if="provider === 'hcaptcha'"> - <MkInput v-model="hcaptchaSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> - </MkInput> - <MkInput v-model="hcaptchaSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> - </MkInput> - <FormSlot> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> - </FormSlot> - </template> - <template v-else-if="provider === 'mcaptcha'"> - <MkInput v-model="mcaptchaSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> - </MkInput> - <MkInput v-model="mcaptchaSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> - </MkInput> - <MkInput v-model="mcaptchaInstanceUrl"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> - </MkInput> - <FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl"> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="mcaptcha" :sitekey="mcaptchaSiteKey" :instanceUrl="mcaptchaInstanceUrl"/> - </FormSlot> - </template> - <template v-else-if="provider === 'recaptcha'"> - <MkInput v-model="recaptchaSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.recaptchaSiteKey }}</template> - </MkInput> - <MkInput v-model="recaptchaSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.recaptchaSecretKey }}</template> - </MkInput> - <FormSlot v-if="recaptchaSiteKey"> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/> - </FormSlot> - </template> - <template v-else-if="provider === 'turnstile'"> - <MkInput v-model="turnstileSiteKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.turnstileSiteKey }}</template> - </MkInput> - <MkInput v-model="turnstileSecretKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>{{ i18n.ts.turnstileSecretKey }}</template> - </MkInput> - <FormSlot> - <template #label>{{ i18n.ts.preview }}</template> - <MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/> - </FormSlot> - </template> + <div class="_gaps_m"> + <MkRadios v-model="botProtectionForm.state.provider"> + <option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> + <option value="hcaptcha">hCaptcha</option> + <option value="mcaptcha">mCaptcha</option> + <option value="recaptcha">reCAPTCHA</option> + <option value="turnstile">Turnstile</option> + <option value="fc">FriendlyCaptcha</option> + </MkRadios> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </div> - </FormSuspense> -</div> + <template v-if="botProtectionForm.state.provider === 'hcaptcha'"> + <MkInput v-model="botProtectionForm.state.hcaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.hcaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> + </MkInput> + <FormSlot> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> + </FormSlot> + </template> + <template v-else-if="botProtectionForm.state.provider === 'mcaptcha'"> + <MkInput v-model="botProtectionForm.state.mcaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.mcaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> + </MkInput> + <FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl"> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/> + </FormSlot> + </template> + <template v-else-if="botProtectionForm.state.provider === 'recaptcha'"> + <MkInput v-model="botProtectionForm.state.recaptchaSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.recaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.recaptchaSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.recaptchaSecretKey }}</template> + </MkInput> + <FormSlot v-if="botProtectionForm.state.recaptchaSiteKey"> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/> + </FormSlot> + </template> + <template v-else-if="botProtectionForm.state.provider === 'turnstile'"> + <MkInput v-model="botProtectionForm.state.turnstileSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.turnstileSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.turnstileSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.turnstileSecretKey }}</template> + </MkInput> + <FormSlot> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/> + </FormSlot> + </template> + <template v-else-if="botProtectionForm.state.provider === 'fc'"> + <MkInput v-model="botProtectionForm.state.fcSiteKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> + </MkInput> + <MkInput v-model="botProtectionForm.state.fcSecretKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> + </MkInput> + <FormSlot> + <template #label>{{ i18n.ts.preview }}</template> + <MkCaptcha provider="fc" :sitekey="botProtectionForm.state.fcSiteKey"/> + </FormSlot> + </template> + </div> +</MkFolder> </template> <script lang="ts" setup> import { defineAsyncComponent, ref } from 'vue'; -import type { CaptchaProvider } from '@/components/MkCaptcha.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import FormSlot from '@/components/form/slot.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; +import MkFolder from '@/components/MkFolder.vue'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); -const provider = ref<CaptchaProvider | null>(null); -const hcaptchaSiteKey = ref<string | null>(null); -const hcaptchaSecretKey = ref<string | null>(null); -const mcaptchaSiteKey = ref<string | null>(null); -const mcaptchaSecretKey = ref<string | null>(null); -const mcaptchaInstanceUrl = ref<string | null>(null); -const recaptchaSiteKey = ref<string | null>(null); -const recaptchaSecretKey = ref<string | null>(null); -const turnstileSiteKey = ref<string | null>(null); -const turnstileSecretKey = ref<string | null>(null); +const meta = await misskeyApi('admin/meta'); -async function init() { - const meta = await misskeyApi('admin/meta'); - hcaptchaSiteKey.value = meta.hcaptchaSiteKey; - hcaptchaSecretKey.value = meta.hcaptchaSecretKey; - mcaptchaSiteKey.value = meta.mcaptchaSiteKey; - mcaptchaSecretKey.value = meta.mcaptchaSecretKey; - mcaptchaInstanceUrl.value = meta.mcaptchaInstanceUrl; - recaptchaSiteKey.value = meta.recaptchaSiteKey; - recaptchaSecretKey.value = meta.recaptchaSecretKey; - turnstileSiteKey.value = meta.turnstileSiteKey; - turnstileSecretKey.value = meta.turnstileSecretKey; - - provider.value = meta.enableHcaptcha ? 'hcaptcha' : - meta.enableRecaptcha ? 'recaptcha' : - meta.enableTurnstile ? 'turnstile' : - meta.enableMcaptcha ? 'mcaptcha' : null; -} - -function save() { - os.apiWithDialog('admin/update-meta', { - enableHcaptcha: provider.value === 'hcaptcha', - hcaptchaSiteKey: hcaptchaSiteKey.value, - hcaptchaSecretKey: hcaptchaSecretKey.value, - enableMcaptcha: provider.value === 'mcaptcha', - mcaptchaSiteKey: mcaptchaSiteKey.value, - mcaptchaSecretKey: mcaptchaSecretKey.value, - mcaptchaInstanceUrl: mcaptchaInstanceUrl.value, - enableRecaptcha: provider.value === 'recaptcha', - recaptchaSiteKey: recaptchaSiteKey.value, - recaptchaSecretKey: recaptchaSecretKey.value, - enableTurnstile: provider.value === 'turnstile', - turnstileSiteKey: turnstileSiteKey.value, - turnstileSecretKey: turnstileSecretKey.value, - }).then(() => { - fetchInstance(true); +const botProtectionForm = useForm({ + provider: meta.enableHcaptcha + ? 'hcaptcha' + : meta.enableRecaptcha + ? 'recaptcha' + : meta.enableTurnstile + ? 'turnstile' + : meta.enableMcaptcha + ? 'mcaptcha' + : meta.enableFC + ? 'fc' + : null, + hcaptchaSiteKey: meta.hcaptchaSiteKey, + hcaptchaSecretKey: meta.hcaptchaSecretKey, + mcaptchaSiteKey: meta.mcaptchaSiteKey, + mcaptchaSecretKey: meta.mcaptchaSecretKey, + mcaptchaInstanceUrl: meta.mcaptchaInstanceUrl, + recaptchaSiteKey: meta.recaptchaSiteKey, + recaptchaSecretKey: meta.recaptchaSecretKey, + turnstileSiteKey: meta.turnstileSiteKey, + turnstileSecretKey: meta.turnstileSecretKey, + fcSiteKey: meta.fcSiteKey, + fcSecretKey: meta.fcSecretKey, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableHcaptcha: state.provider === 'hcaptcha', + hcaptchaSiteKey: state.hcaptchaSiteKey, + hcaptchaSecretKey: state.hcaptchaSecretKey, + enableMcaptcha: state.provider === 'mcaptcha', + mcaptchaSiteKey: state.mcaptchaSiteKey, + mcaptchaSecretKey: state.mcaptchaSecretKey, + mcaptchaInstanceUrl: state.mcaptchaInstanceUrl, + enableRecaptcha: state.provider === 'recaptcha', + recaptchaSiteKey: state.recaptchaSiteKey, + recaptchaSecretKey: state.recaptchaSecretKey, + enableTurnstile: state.provider === 'turnstile', + turnstileSiteKey: state.turnstileSiteKey, + turnstileSecretKey: state.turnstileSecretKey, + enableFC: state.provider === 'fc', + fcSiteKey: state.fcSiteKey, + fcSecretKey: state.fcSecretKey, }); -} + fetchInstance(true); +}); </script> diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 2e14aef0b946e63c7d14206d4b5ec8c98f91a03c..d3d52002fe325352ef23b808e8fade39767561ac 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -37,6 +37,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </MkInput> + <MkInput v-model="sidebarLogoUrl" type="url"> + <template #prefix><i class="ti ti-link"></i></template> + <template #label>{{ i18n.ts._serverSettings.sidebarLogoUrl }}</template> + <template #caption> + <div>{{ i18n.ts._serverSettings.sidebarLogoDescription }}</div> + <div>({{ i18n.ts._serverSettings.sidebarLogoUsageExample }})</div> + </template> + </MkInput> + <MkInput v-model="bannerUrl" type="url"> <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.bannerUrl }}</template> @@ -125,9 +134,10 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkColorInput from '@/components/MkColorInput.vue'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; const iconUrl = ref<string | null>(null); +const sidebarLogoUrl = ref<string | null>(null); const app192IconUrl = ref<string | null>(null); const app512IconUrl = ref<string | null>(null); const bannerUrl = ref<string | null>(null); @@ -146,6 +156,7 @@ const manifestJsonOverride = ref<string>('{}'); async function init() { const meta = await misskeyApi('admin/meta'); iconUrl.value = meta.iconUrl; + sidebarLogoUrl.value = meta.sidebarLogoUrl; app192IconUrl.value = meta.app192IconUrl; app512IconUrl.value = meta.app512IconUrl; bannerUrl.value = meta.bannerUrl; @@ -165,6 +176,7 @@ async function init() { function save() { os.apiWithDialog('admin/update-meta', { iconUrl: iconUrl.value, + sidebarLogoUrl: sidebarLogoUrl.value, app192IconUrl: app192IconUrl.value, app512IconUrl: app512IconUrl.value, bannerUrl: bannerUrl.value, diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index 4a858887f353a0c0774e39ef25d36df4383fad3f..ddfe5ae81f7b8573b1b7ecb7d1d2368c81ab9aee 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -100,7 +100,7 @@ async function init() { async function testEmail() { const { canceled, result: destination } = await os.inputText({ - title: i18n.ts.destination, + title: i18n.ts.emailDestination, type: 'email', default: instance.maintainerEmail ?? '', placeholder: 'test@example.com', diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index e4308e6030a0c0dfd2f7e4beaf328332d41b4b94..50e2c2dd5149c0ed5b43eaff46e88adb49f5d145 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> - <FormSection> + <MkFolder> <template #label>DeepL Translation</template> <div class="_gaps_m"> @@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="deeplIsPro"> <template #label>Pro account</template> </MkSwitch> + <MkSwitch v-model="deeplFreeMode"> <template #label>{{ i18n.ts.deeplFreeMode }}</template> </MkSwitch> @@ -27,17 +28,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>DeepLX-JS URL</template> <template #caption>{{ i18n.ts.deeplFreeModeDescription }}</template> </MkInput> + + <MkButton primary @click="save_deepl">Save</MkButton> </div> - </FormSection> + </MkFolder> </FormSuspense> </MkSpacer> - <template #footer> - <div :class="$style.footer"> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - </MkSpacer> - </div> - </template> </MkStickyContainer> </template> @@ -48,12 +44,12 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; -import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkFolder from '@/components/MkFolder.vue'; const deeplAuthKey = ref<string>(''); const deeplIsPro = ref<boolean>(false); @@ -68,7 +64,7 @@ async function init() { deeplFreeInstance.value = meta.deeplFreeInstance; } -function save() { +function save_deepl() { os.apiWithDialog('admin/update-meta', { deeplAuthKey: deeplAuthKey.value, deeplIsPro: deeplIsPro.value, @@ -88,10 +84,3 @@ definePageMetadata(() => ({ icon: 'ph-arrow-square-out ph-bold ph-lg', })); </script> - -<style lang="scss" module> -.footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} -</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index f547bedacb12d358921871b0b61dde16cfcb1349..e8d123060aeae4d8ecc804f8ca20348b7c80a481 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -63,7 +63,7 @@ const view = ref(null); const el = ref<HTMLDivElement | null>(null); const pageProps = ref({}); const noMaintainerInformation = computed(() => isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail)); -const noBotProtection = computed(() => !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile && !instance.enableMcaptcha); +const noBotProtection = computed(() => !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile && !instance.enableMcaptcha && !instance.enableFC); const noEmailServer = computed(() => !instance.enableEmail); const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl)); const thereIsUnresolvedAbuseReport = ref(false); @@ -214,16 +214,6 @@ const menuDef = computed(() => [{ text: i18n.ts.relays, to: '/admin/relays', active: currentPage.value?.route.name === 'relays', - }, { - icon: 'ti ti-ban', - text: i18n.ts.instanceBlocking, - to: '/admin/instance-block', - active: currentPage.value?.route.name === 'instance-block', - }, { - icon: 'ti ti-ghost', - text: i18n.ts.proxyAccount, - to: '/admin/proxy-account', - active: currentPage.value?.route.name === 'proxy-account', }, { icon: 'ph-arrow-square-out ph-bold ph-lg', text: i18n.ts.externalServices, @@ -235,10 +225,10 @@ const menuDef = computed(() => [{ to: '/admin/system-webhook', active: currentPage.value?.route.name === 'system-webhook', }, { - icon: 'ti ti-adjustments', - text: i18n.ts.other, - to: '/admin/other-settings', - active: currentPage.value?.route.name === 'other-settings', + icon: 'ti ti-bolt', + text: i18n.ts.performance, + to: '/admin/performance', + active: currentPage.value?.route.name === 'performance', }], }, { title: i18n.ts.info, diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue deleted file mode 100644 index e090616b26a2437fd37b4d1640113ffed7c40eeb..0000000000000000000000000000000000000000 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ /dev/null @@ -1,84 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<MkStickyContainer> - <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <template v-if="tab === 'block'"> - <MkTextarea v-model="blockedHosts"> - <span>{{ i18n.ts.blockedInstances }}</span> - <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> - </MkTextarea> - </template> - <template v-else-if="tab === 'silence'"> - <MkTextarea v-model="silencedHosts" class="_formBlock"> - <span>{{ i18n.ts.silencedInstances }}</span> - <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> - </MkTextarea> - <MkTextarea v-model="mediaSilencedHosts" class="_formBlock"> - <span>{{ i18n.ts.mediaSilencedInstances }}</span> - <template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template> - </MkTextarea> - </template> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </FormSuspense> - </MkSpacer> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { ref, computed } from 'vue'; -import XHeader from './_header_.vue'; -import MkButton from '@/components/MkButton.vue'; -import MkTextarea from '@/components/MkTextarea.vue'; -import FormSuspense from '@/components/form/suspense.vue'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { fetchInstance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; - -const blockedHosts = ref<string>(''); -const silencedHosts = ref<string>(''); -const mediaSilencedHosts = ref<string>(''); -const tab = ref('block'); - -async function init() { - const meta = await misskeyApi('admin/meta'); - blockedHosts.value = meta.blockedHosts.join('\n'); - silencedHosts.value = meta.silencedHosts.join('\n'); - mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); -} - -function save() { - os.apiWithDialog('admin/update-meta', { - blockedHosts: blockedHosts.value.split('\n') || [], - silencedHosts: silencedHosts.value.split('\n') || [], - mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [], - - }).then(() => { - fetchInstance(true); - }); -} - -const headerActions = computed(() => []); - -const headerTabs = computed(() => [{ - key: 'block', - title: i18n.ts.block, - icon: 'ti ti-ban', -}, { - key: 'silence', - title: i18n.ts.silence, - icon: 'ti ti-eye-off', -}]); - -definePageMetadata(() => ({ - title: i18n.ts.instanceBlocking, - icon: 'ti ti-ban', -})); -</script> diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 6297b9a1828e470e13fb80c0532cc48cdee328c8..bbcf2a6f77dbc6ff0694404accb4874fa12c207f 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -10,70 +10,130 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> <div class="_gaps_m"> - <MkSwitch v-model="enableRegistration"> + <MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration"> <template #label>{{ i18n.ts.enableRegistration }}</template> </MkSwitch> - <MkSwitch v-model="emailRequiredForSignup"> + <MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup"> <template #label>{{ i18n.ts.emailRequiredForSignup }}</template> </MkSwitch> - <MkSwitch v-model="approvalRequiredForSignup"> + <MkSwitch v-model="approvalRequiredForSignup" @change="onChange_approvalRequiredForSignup"> <template #label>{{ i18n.ts.approvalRequiredForSignup }}</template> </MkSwitch> <FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink> - <MkInput v-model="tosUrl" type="url"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.tosUrl }}</template> - </MkInput> - - <MkInput v-model="privacyPolicyUrl" type="url"> - <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts.privacyPolicyUrl }}</template> - </MkInput> - - <MkTextarea v-if="bubbleTimelineEnabled" v-model="bubbleTimeline"> + <MkFolder v-if="bubbleTimelineEnabled"> + <template #icon><i class="ph-drop ph-bold ph-lg"></i></template> <template #label>Bubble timeline</template> - <template #caption>Choose which instances should be displayed in the bubble.</template> - </MkTextarea> - <MkInput v-model="inquiryUrl" type="url"> + <div class="_gaps"> + <MkTextarea v-model="bubbleTimeline"> + <template #caption>Choose which instances should be displayed in the bubble.</template> + </MkTextarea> + <MkButton primary @click="save_bubbleTimeline">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> <template #prefix><i class="ti ti-link"></i></template> - <template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template> - <template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template> - </MkInput> + <template #label>{{ i18n.ts.trustedLinkUrlPatterns }}</template> + + <div class="_gaps"> + <MkTextarea v-model="trustedLinkUrlPatterns"> + <template #caption>{{ i18n.ts.trustedLinkUrlPatternsDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_trustedLinkUrlPatterns">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> - <MkTextarea v-model="preservedUsernames"> + <MkFolder> + <template #icon><i class="ti ti-lock-star"></i></template> <template #label>{{ i18n.ts.preservedUsernames }}</template> - <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template> - </MkTextarea> - <MkTextarea v-model="sensitiveWords"> + <div class="_gaps"> + <MkTextarea v-model="preservedUsernames"> + <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-message-exclamation"></i></template> <template #label>{{ i18n.ts.sensitiveWords }}</template> - <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> - </MkTextarea> - <MkTextarea v-model="prohibitedWords"> + <div class="_gaps"> + <MkTextarea v-model="sensitiveWords"> + <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> + </MkTextarea> + <MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-message-x"></i></template> <template #label>{{ i18n.ts.prohibitedWords }}</template> - <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template> - </MkTextarea> - <MkTextarea v-model="hiddenTags"> + <div class="_gaps"> + <MkTextarea v-model="prohibitedWords"> + <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template> + </MkTextarea> + <MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> <template #label>{{ i18n.ts.hiddenTags }}</template> - <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template> - </MkTextarea> + + <div class="_gaps"> + <MkTextarea v-model="hiddenTags"> + <template #caption>{{ i18n.ts.hiddenTagsDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> + <template #label>{{ i18n.ts.silencedInstances }}</template> + + <div class="_gaps"> + <MkTextarea v-model="silencedHosts"> + <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-eye-off"></i></template> + <template #label>{{ i18n.ts.mediaSilencedInstances }}</template> + + <div class="_gaps"> + <MkTextarea v-model="mediaSilencedHosts"> + <template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-ban"></i></template> + <template #label>{{ i18n.ts.blockedInstances }}</template> + + <div class="_gaps"> + <MkTextarea v-model="blockedHosts"> + <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> + </MkTextarea> + <MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton> + </div> + </MkFolder> </div> </FormSuspense> </MkSpacer> - <template #footer> - <div :class="$style.footer"> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - </MkSpacer> - </div> - </template> </MkStickyContainer> </div> </template> @@ -92,6 +152,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; +import MkFolder from '@/components/MkFolder.vue'; const enableRegistration = ref<boolean>(false); const emailRequiredForSignup = ref<boolean>(false); @@ -102,9 +163,10 @@ const prohibitedWords = ref<string>(''); const hiddenTags = ref<string>(''); const preservedUsernames = ref<string>(''); const bubbleTimeline = ref<string>(''); -const tosUrl = ref<string | null>(null); -const privacyPolicyUrl = ref<string | null>(null); -const inquiryUrl = ref<string | null>(null); +const trustedLinkUrlPatterns = ref<string>(''); +const blockedHosts = ref<string>(''); +const silencedHosts = ref<string>(''); +const mediaSilencedHosts = ref<string>(''); async function init() { const meta = await misskeyApi('admin/meta'); @@ -115,26 +177,105 @@ async function init() { prohibitedWords.value = meta.prohibitedWords.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n'); - tosUrl.value = meta.tosUrl; - privacyPolicyUrl.value = meta.privacyPolicyUrl; bubbleTimeline.value = meta.bubbleInstances.join('\n'); bubbleTimelineEnabled.value = meta.policies.btlAvailable; - inquiryUrl.value = meta.inquiryUrl; + trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n'); + blockedHosts.value = meta.blockedHosts.join('\n'); + silencedHosts.value = meta.silencedHosts.join('\n'); + mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); +} + +function onChange_enableRegistration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + disableRegistration: !value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_emailRequiredForSignup(value: boolean) { + os.apiWithDialog('admin/update-meta', { + emailRequiredForSignup: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_approvalRequiredForSignup(value: boolean) { + os.apiWithDialog('admin/update-meta', { + approvalRequiredForSignup: value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_bubbleTimeline() { + os.apiWithDialog('admin/update-meta', { + bubbleInstances: bubbleTimeline.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_trustedLinkUrlPatterns() { + os.apiWithDialog('admin/update-meta', { + trustedLinkUrlPatterns: trustedLinkUrlPatterns.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); } -function save() { +function save_preservedUsernames() { + os.apiWithDialog('admin/update-meta', { + preservedUsernames: preservedUsernames.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_sensitiveWords() { os.apiWithDialog('admin/update-meta', { - disableRegistration: !enableRegistration.value, - emailRequiredForSignup: emailRequiredForSignup.value, - approvalRequiredForSignup: approvalRequiredForSignup.value, - tosUrl: tosUrl.value, - privacyPolicyUrl: privacyPolicyUrl.value, - inquiryUrl: inquiryUrl.value, sensitiveWords: sensitiveWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_prohibitedWords() { + os.apiWithDialog('admin/update-meta', { prohibitedWords: prohibitedWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_hiddenTags() { + os.apiWithDialog('admin/update-meta', { hiddenTags: hiddenTags.value.split('\n'), - preservedUsernames: preservedUsernames.value.split('\n'), - bubbleInstances: bubbleTimeline.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_blockedHosts() { + os.apiWithDialog('admin/update-meta', { + blockedHosts: blockedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_silencedHosts() { + os.apiWithDialog('admin/update-meta', { + silencedHosts: silencedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_mediaSilencedHosts() { + os.apiWithDialog('admin/update-meta', { + mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [], }).then(() => { fetchInstance(true); }); @@ -147,10 +288,3 @@ definePageMetadata(() => ({ icon: 'ti ti-shield', })); </script> - -<style lang="scss" module> -.footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} -</style> diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index f6f276de53a54bfa85a35a2f40ce379b704b028c..6c81155c51ea1ef9172c1432721859804745421b 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -23,10 +23,15 @@ SPDX-License-Identifier: AGPL-3.0-only 'markSensitiveDriveFile', 'resetPassword', 'suspendRemoteInstance', + 'setRemoteInstanceNSFW', + 'unsetRemoteInstanceNSFW', + 'rejectRemoteInstanceReports', + 'acceptRemoteInstanceReports', ].includes(log.type), [$style.logRed]: [ 'suspend', 'approve', + 'decline', 'deleteRole', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', @@ -47,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'suspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'approve'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> + <span v-else-if="log.type === 'decline'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'unsuspend'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'resetPassword'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> <span v-else-if="log.type === 'assignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-arrow-right"></i> {{ log.info.roleName }}</span> @@ -61,6 +67,10 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span> <span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span> <span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span> + <span v-else-if="log.type === 'setRemoteInstanceNSFW'">: {{ log.info.host }}</span> + <span v-else-if="log.type === 'unsetRemoteInstanceNSFW'">: {{ log.info.host }}</span> + <span v-else-if="log.type === 'rejectRemoteInstanceReports'">: {{ log.info.host }}</span> + <span v-else-if="log.type === 'acceptRemoteInstanceReports'">: {{ log.info.host }}</span> <span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span> <span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span> <span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span> diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue deleted file mode 100644 index a92034f2d7c601c091bb69255c56e41bd98d132e..0000000000000000000000000000000000000000 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ /dev/null @@ -1,113 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<MkStickyContainer> - <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <div class="_gaps"> - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableServerMachineStats"> - <template #label>{{ i18n.ts.enableServerMachineStats }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableAchievements"> - <template #label>{{ i18n.ts.enableAchievements }}</template> - <template #caption>{{ i18n.ts.turnOffAchievements}}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableBotTrending"> - <template #label>{{ i18n.ts.enableBotTrending }}</template> - <template #caption>{{ i18n.ts.turnOffBotTrending }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableIdenticonGeneration"> - <template #label>{{ i18n.ts.enableIdenticonGeneration }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableChartsForRemoteUser"> - <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - - <div class="_panel" style="padding: 16px;"> - <MkSwitch v-model="enableChartsForFederatedInstances"> - <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> - <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> - </MkSwitch> - </div> - </div> - </FormSuspense> - </MkSpacer> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { ref, computed } from 'vue'; -import XHeader from './_header_.vue'; -import FormSuspense from '@/components/form/suspense.vue'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { fetchInstance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; -import MkSwitch from '@/components/MkSwitch.vue'; - -const enableServerMachineStats = ref<boolean>(false); -const enableAchievements = ref<boolean>(false); -const enableBotTrending = ref<boolean>(false); -const enableIdenticonGeneration = ref<boolean>(false); -const enableChartsForRemoteUser = ref<boolean>(false); -const enableChartsForFederatedInstances = ref<boolean>(false); - -async function init() { - const meta = await misskeyApi('admin/meta'); - enableServerMachineStats.value = meta.enableServerMachineStats; - enableAchievements.value = meta.enableAchievements; - enableBotTrending.value = meta.enableBotTrending; - enableIdenticonGeneration.value = meta.enableIdenticonGeneration; - enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser; - enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances; -} - -function save() { - os.apiWithDialog('admin/update-meta', { - enableServerMachineStats: enableServerMachineStats.value, - enableAchievements: enableAchievements.value, - enableBotTrending: enableBotTrending.value, - enableIdenticonGeneration: enableIdenticonGeneration.value, - enableChartsForRemoteUser: enableChartsForRemoteUser.value, - enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, - }).then(() => { - fetchInstance(true); - }); -} - -const headerActions = computed(() => [{ - asFullButton: true, - icon: 'ti ti-check', - text: i18n.ts.save, - handler: save, -}]); - -const headerTabs = computed(() => []); - -definePageMetadata(() => ({ - title: i18n.ts.other, - icon: 'ti ti-adjustments', -})); -</script> diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..584cd3e4d954fad9cc2b13cb84963899fe29c14f --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.ap-requests.stories.impl.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import { http, HttpResponse } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { commonHandlers } from '../../../.storybook/mocks.js'; +import overview_ap_requests from './overview.ap-requests.vue'; +export const Default = { + render(args) { + return { + components: { + overview_ap_requests, + }, + setup() { + return { + args, + }; + }, + template: '<overview_ap_requests />', + }; + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/charts/ap-request', async ({ request }) => { + action('POST /api/charts/ap-request')(await request.json()); + return HttpResponse.json({ + deliverFailed: [0, 0, 0, 2, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 1, 0, 0, 0, 3, 1, 1, 2, 0, 0], + deliverSucceeded: [0, 1, 51, 34, 136, 189, 51, 17, 17, 34, 1, 17, 18, 51, 34, 68, 287, 0, 17, 33, 32, 96, 96, 0, 49, 64, 0, 32, 0, 32, 81, 48, 65, 1, 16, 50, 90, 148, 33, 43, 72, 127, 17, 138, 78, 91, 78, 91, 13, 52], + inboxReceived: [507, 1173, 1096, 871, 958, 937, 908, 1026, 956, 909, 807, 1002, 832, 995, 1039, 1047, 1109, 930, 711, 835, 764, 679, 835, 958, 634, 654, 691, 895, 811, 676, 1044, 1389, 1318, 863, 887, 952, 1011, 1061, 592, 900, 611, 595, 604, 562, 607, 621, 854, 666, 1197, 644], + }); + }), + ], + }, + }, +} satisfies StoryObj<typeof overview_ap_requests>; diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue index d4c83f21b68571001245fb69034b25b9f4b774be..4bbb9210afa6264bdfc0ba413d9f5a2cbcd04f29 100644 --- a/packages/frontend/src/pages/admin/overview.ap-requests.vue +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onMounted, shallowRef, ref } from 'vue'; import { Chart } from 'chart.js'; import gradient from 'chartjs-plugin-gradient'; +import isChromatic from 'chromatic'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useChartTooltip } from '@/scripts/use-chart-tooltip.js'; import { chartVLine } from '@/scripts/chart-vline.js'; @@ -41,7 +42,7 @@ const { handler: externalTooltipHandler } = useChartTooltip(); const { handler: externalTooltipHandler2 } = useChartTooltip(); onMounted(async () => { - const now = new Date(); + const now = isChromatic() ? new Date('2024-08-31T10:00:00Z') : new Date(); const getDate = (ago: number) => { const y = now.getFullYear(); @@ -51,14 +52,14 @@ onMounted(async () => { return new Date(y, m, d - ago); }; - const format = (arr) => { + const format = (arr: number[]) => { return arr.map((v, i) => ({ x: getDate(i).getTime(), y: v, })); }; - const formatMinus = (arr) => { + const formatMinus = (arr: number[]) => { return arr.map((v, i) => ({ x: getDate(i).getTime(), y: -v, @@ -78,7 +79,6 @@ onMounted(async () => { type: 'line', data: { datasets: [{ - stack: 'a', parsing: false, label: 'Out: Succ', data: format(raw.deliverSucceeded).slice().reverse(), @@ -92,7 +92,6 @@ onMounted(async () => { fill: true, clip: 8, }, { - stack: 'a', parsing: false, label: 'Out: Fail', data: formatMinus(raw.deliverFailed).slice().reverse(), @@ -137,7 +136,6 @@ onMounted(async () => { min: getDate(chartLimit).getTime(), }, y: { - stacked: true, position: 'left', suggestedMax: 10, grid: { @@ -171,6 +169,9 @@ onMounted(async () => { duration: 0, }, external: externalTooltipHandler, + callbacks: { + label: context => `${context.dataset.label}: ${Math.abs(context.parsed.y)}`, + }, }, gradient, }, diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue index a09db2a6d52805ca1e54a26a4ae9b16850f401cd..292e2e1dbc9f94f098eac66e5214508348bc357e 100644 --- a/packages/frontend/src/pages/admin/overview.instances.vue +++ b/packages/frontend/src/pages/admin/overview.instances.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index c7478f252a269ccc1028aeed4d96fba7e4b09786..fb190f53258bc180dda4755bc6cc89801b39e7b7 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -36,7 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; import XChart from './overview.queue.chart.vue'; +import type { ApQueueDomain } from '@/pages/admin/queue.vue'; import number from '@/filters/number.js'; import { useStream } from '@/stream.js'; @@ -52,10 +54,10 @@ const chartDelayed = shallowRef<InstanceType<typeof XChart>>(); const chartWaiting = shallowRef<InstanceType<typeof XChart>>(); const props = defineProps<{ - domain: string; + domain: ApQueueDomain; }>(); -const onStats = (stats) => { +function onStats(stats: Misskey.entities.QueueStats) { activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; active.value = stats[props.domain].active; delayed.value = stats[props.domain].delayed; @@ -65,13 +67,13 @@ const onStats = (stats) => { chartActive.value.pushData(stats[props.domain].active); chartDelayed.value.pushData(stats[props.domain].delayed); chartWaiting.value.pushData(stats[props.domain].waiting); -}; +} -const onStatsLog = (statsLog) => { - const dataProcess = []; - const dataActive = []; - const dataDelayed = []; - const dataWaiting = []; +function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) { + const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = []; + const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = []; + const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = []; + const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = []; for (const stats of [...statsLog].reverse()) { dataProcess.push(stats[props.domain].activeSincePrevTick); @@ -84,7 +86,7 @@ const onStatsLog = (statsLog) => { chartActive.value.setData(dataActive); chartDelayed.value.setData(dataDelayed); chartWaiting.value.setData(dataWaiting); -}; +} onMounted(() => { connection.on('stats', onStats); diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 408be88d4792282d93439bcebe11b047dfb0644c..8c9d7a8197a23a6e749959b9aef34326c04e4d46 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import * as Misskey from 'misskey-js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import { defaultStore } from '@/store.js'; @@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, { .root { &:global { > .users { - .chart-move { - transition: transform 1s ease; - } - display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 12px; + .chart-move { + transition: transform 1s ease; + } + > .user:hover { text-decoration: none; } diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue new file mode 100644 index 0000000000000000000000000000000000000000..7e0a932f82925dec516f80a6996c25e053b3b566 --- /dev/null +++ b/packages/frontend/src/pages/admin/performance.vue @@ -0,0 +1,193 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> + <div class="_gaps"> + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats"> + <template #label>{{ i18n.ts.enableServerMachineStats }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration"> + <template #label>{{ i18n.ts.enableIdenticonGeneration }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser"> + <template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <div class="_panel" style="padding: 16px;"> + <MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances"> + <template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template> + <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template> + </MkSwitch> + </div> + + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-bolt"></i></template> + <template #label>Misskey® Fan-out Timeline Technologyâ„¢ (FTT)</template> + <template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="fttForm.modified.value" #footer> + <MkFormFooter :form="fttForm"/> + </template> + + <div class="_gaps"> + <MkSwitch v-model="fttForm.state.enableFanoutTimeline"> + <template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption> + <div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div> + <div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div> + </template> + </MkSwitch> + + <template v-if="fttForm.state.enableFanoutTimeline"> + <MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback"> + <template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template> + </MkSwitch> + + <MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number"> + <template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + + <MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number"> + <template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + + <MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number"> + <template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + + <MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number"> + <template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> + </template> + </div> + </MkFolder> + + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-bolt"></i></template> + <template #label>Misskey® Reactions Boost Technologyâ„¢ (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template> + <template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="rbtForm.modified.value" #footer> + <MkFormFooter :form="rbtForm"/> + </template> + + <div class="_gaps_m"> + <MkSwitch v-model="rbtForm.state.enableReactionsBuffering"> + <template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template> + </MkSwitch> + </div> + </MkFolder> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { ref, computed } from 'vue'; +import XHeader from './_header_.vue'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { fetchInstance } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkLink from '@/components/MkLink.vue'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; + +const meta = await misskeyApi('admin/meta'); + +const enableServerMachineStats = ref(meta.enableServerMachineStats); +const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration); +const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser); +const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances); + +function onChange_enableServerMachineStats(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableServerMachineStats: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableIdenticonGeneration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableIdenticonGeneration: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForRemoteUser(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForRemoteUser: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForFederatedInstances(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForFederatedInstances: value, + }).then(() => { + fetchInstance(true); + }); +} + +const fttForm = useForm({ + enableFanoutTimeline: meta.enableFanoutTimeline, + enableFanoutTimelineDbFallback: meta.enableFanoutTimelineDbFallback, + perLocalUserUserTimelineCacheMax: meta.perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax: meta.perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax: meta.perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax: meta.perUserListTimelineCacheMax, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableFanoutTimeline: state.enableFanoutTimeline, + enableFanoutTimelineDbFallback: state.enableFanoutTimelineDbFallback, + perLocalUserUserTimelineCacheMax: state.perLocalUserUserTimelineCacheMax, + perRemoteUserUserTimelineCacheMax: state.perRemoteUserUserTimelineCacheMax, + perUserHomeTimelineCacheMax: state.perUserHomeTimelineCacheMax, + perUserListTimelineCacheMax: state.perUserListTimelineCacheMax, + }); + fetchInstance(true); +}); + +const rbtForm = useForm({ + enableReactionsBuffering: meta.enableReactionsBuffering, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableReactionsBuffering: state.enableReactionsBuffering, + }); + fetchInstance(true); +}); + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata(() => ({ + title: i18n.ts.other, + icon: 'ti ti-adjustments', +})); +</script> diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue deleted file mode 100644 index 81db9f1da9c213a4b75ec7fff909c163892c50a8..0000000000000000000000000000000000000000 --- a/packages/frontend/src/pages/admin/proxy-account.vue +++ /dev/null @@ -1,71 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo> - <MkKeyValue> - <template #key>{{ i18n.ts.proxyAccount }}</template> - <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template> - </MkKeyValue> - - <MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton> - </FormSuspense> - </MkSpacer> -</MkStickyContainer> -</template> - -<script lang="ts" setup> -import { ref, computed } from 'vue'; -import * as Misskey from 'misskey-js'; -import MkKeyValue from '@/components/MkKeyValue.vue'; -import MkButton from '@/components/MkButton.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import FormSuspense from '@/components/form/suspense.vue'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { fetchInstance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; - -const proxyAccount = ref<Misskey.entities.UserDetailed | null>(null); -const proxyAccountId = ref<string | null>(null); - -async function init() { - const meta = await misskeyApi('admin/meta'); - proxyAccountId.value = meta.proxyAccountId; - if (proxyAccountId.value) { - proxyAccount.value = await misskeyApi('users/show', { userId: proxyAccountId.value }); - } -} - -function chooseProxyAccount() { - os.selectUser({ localOnly: true }).then(user => { - proxyAccount.value = user; - proxyAccountId.value = user.id; - save(); - }); -} - -function save() { - os.apiWithDialog('admin/update-meta', { - proxyAccountId: proxyAccountId.value, - }).then(() => { - fetchInstance(true); - }); -} - -const headerActions = computed(() => []); - -const headerTabs = computed(() => []); - -definePageMetadata(() => ({ - title: i18n.ts.proxyAccount, - icon: 'ti ti-ghost', -})); -</script> diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 8d3fe35320e31917340bf530cd9b87e96e560484..960a263a8628fedf82d10755d6065fbeaf75be29 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -49,7 +49,9 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; import XChart from './queue.chart.chart.vue'; +import type { ApQueueDomain } from '@/pages/admin/queue.vue'; import number from '@/filters/number.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; @@ -62,17 +64,17 @@ const activeSincePrevTick = ref(0); const active = ref(0); const delayed = ref(0); const waiting = ref(0); -const jobs = ref<(string | number)[][]>([]); +const jobs = ref<Misskey.Endpoints[`admin/queue/${ApQueueDomain}-delayed`]['res']>([]); const chartProcess = shallowRef<InstanceType<typeof XChart>>(); const chartActive = shallowRef<InstanceType<typeof XChart>>(); const chartDelayed = shallowRef<InstanceType<typeof XChart>>(); const chartWaiting = shallowRef<InstanceType<typeof XChart>>(); const props = defineProps<{ - domain: string; + domain: ApQueueDomain; }>(); -const onStats = (stats) => { +function onStats(stats: Misskey.entities.QueueStats) { activeSincePrevTick.value = stats[props.domain].activeSincePrevTick; active.value = stats[props.domain].active; delayed.value = stats[props.domain].delayed; @@ -82,13 +84,13 @@ const onStats = (stats) => { chartActive.value.pushData(stats[props.domain].active); chartDelayed.value.pushData(stats[props.domain].delayed); chartWaiting.value.pushData(stats[props.domain].waiting); -}; +} -const onStatsLog = (statsLog) => { - const dataProcess = []; - const dataActive = []; - const dataDelayed = []; - const dataWaiting = []; +function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) { + const dataProcess: Misskey.entities.QueueStats[ApQueueDomain]['activeSincePrevTick'][] = []; + const dataActive: Misskey.entities.QueueStats[ApQueueDomain]['active'][] = []; + const dataDelayed: Misskey.entities.QueueStats[ApQueueDomain]['delayed'][] = []; + const dataWaiting: Misskey.entities.QueueStats[ApQueueDomain]['waiting'][] = []; for (const stats of [...statsLog].reverse()) { dataProcess.push(stats[props.domain].activeSincePrevTick); @@ -101,14 +103,12 @@ const onStatsLog = (statsLog) => { chartActive.value.setData(dataActive); chartDelayed.value.setData(dataDelayed); chartWaiting.value.setData(dataWaiting); -}; +} onMounted(() => { - if (props.domain === 'inbox' || props.domain === 'deliver') { - misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => { - jobs.value = result; - }); - } + misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => { + jobs.value = result; + }); connection.on('stats', onStats); connection.on('statsLog', onStatsLog); diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index 8d77d927d7bfc04310794648d1611f40af7f385e..512039242e304f0933e4afc3677d61575b22e6fe 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -16,16 +16,18 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, type Ref } from 'vue'; import XQueue from './queue.chart.vue'; import XHeader from './_header_.vue'; import * as os from '@/os.js'; -import * as config from '@/config.js'; +import * as config from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; -const tab = ref('deliver'); +export type ApQueueDomain = 'deliver' | 'inbox'; + +const tab: Ref<ApQueueDomain> = ref('deliver'); function clear() { os.confirm({ diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 8200244cd79b915de4bbd15e0e03d821562766b8..1763db2323fc3b252a8d48924322d2a0a1e77354 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -630,6 +630,106 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRange> </div> </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])"> + <template #label>{{ i18n.ts._role._options.canImportAntennas }}</template> + <template #suffix> + <span v-if="role.policies.canImportAntennas.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportAntennas.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportAntennas)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportAntennas.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportAntennas.value" :disabled="role.policies.canImportAntennas.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportAntennas.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])"> + <template #label>{{ i18n.ts._role._options.canImportBlocking }}</template> + <template #suffix> + <span v-if="role.policies.canImportBlocking.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportBlocking.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportBlocking)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportBlocking.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportBlocking.value" :disabled="role.policies.canImportBlocking.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportBlocking.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])"> + <template #label>{{ i18n.ts._role._options.canImportFollowing }}</template> + <template #suffix> + <span v-if="role.policies.canImportFollowing.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportFollowing.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportFollowing)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportFollowing.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportFollowing.value" :disabled="role.policies.canImportFollowing.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportFollowing.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])"> + <template #label>{{ i18n.ts._role._options.canImportMuting }}</template> + <template #suffix> + <span v-if="role.policies.canImportMuting.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportMuting.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportMuting)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportMuting.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportMuting.value" :disabled="role.policies.canImportMuting.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportMuting.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserLists'])"> + <template #label>{{ i18n.ts._role._options.canImportUserLists }}</template> + <template #suffix> + <span v-if="role.policies.canImportUserLists.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canImportUserLists.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canImportUserLists)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canImportUserLists.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canImportUserLists.value" :disabled="role.policies.canImportUserLists.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canImportUserLists.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> </div> </FormSlot> </div> @@ -648,7 +748,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkRange from '@/components/MkRange.vue'; import FormSlot from '@/components/form/slot.vue'; import { i18n } from '@/i18n.js'; -import { ROLE_POLICIES } from '@/const.js'; +import { ROLE_POLICIES } from '@@/js/const.js'; import { instance } from '@/instance.js'; import { deepClone } from '@/scripts/clone.js'; diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 0a8bd0e898e5ebc3819f7137ff4a8258d0f13062..00a25446ab64060d62613e5eb5b0443a62a78c2a 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -11,6 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <MkFolder> <template #label>{{ i18n.ts._role.baseRole }}</template> + <template #footer> + <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton> + </template> <div class="_gaps_s"> <MkInput v-model="baseRoleQ" type="search"> <template #prefix><i class="ti ti-search"></i></template> @@ -233,7 +236,45 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </MkFolder> - <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportAntennas, 'canImportAntennas'])"> + <template #label>{{ i18n.ts._role._options.canImportAntennas }}</template> + <template #suffix>{{ policies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportAntennas"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportBlocking, 'canImportBlocking'])"> + <template #label>{{ i18n.ts._role._options.canImportBlocking }}</template> + <template #suffix>{{ policies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportBlocking"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportFollowing, 'canImportFollowing'])"> + <template #label>{{ i18n.ts._role._options.canImportFollowing }}</template> + <template #suffix>{{ policies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportFollowing"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportMuting, 'canImportMuting'])"> + <template #label>{{ i18n.ts._role._options.canImportMuting }}</template> + <template #suffix>{{ policies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportMuting"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + + <MkFolder v-if="matchQuery([i18n.ts._role._options.canImportUserLists, 'canImportUserList'])"> + <template #label>{{ i18n.ts._role._options.canImportUserLists }}</template> + <template #suffix>{{ policies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canImportUserLists"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> </div> </MkFolder> <MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton> @@ -259,6 +300,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, reactive, ref } from 'vue'; +import { ROLE_POLICIES } from '@@/js/const.js'; import XHeader from './_header_.vue'; import MkInput from '@/components/MkInput.vue'; import MkFolder from '@/components/MkFolder.vue'; @@ -273,7 +315,6 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance, fetchInstance } from '@/instance.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; -import { ROLE_POLICIES } from '@/const.js'; import { useRouter } from '@/router/supplier.js'; const router = useRouter(); diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 2e80622d7a146f4b324b5ca2cf4d3c23f6b30033..4358821092533adf216e4b02de0cad44ddd96937 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -7,76 +7,71 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <div class="_gaps_m"> - <MkFolder> - <template #icon><i class="ti ti-shield"></i></template> - <template #label>{{ i18n.ts.botProtection }}</template> - <template v-if="enableHcaptcha" #suffix>hCaptcha</template> - <template v-else-if="enableMcaptcha" #suffix>mCaptcha</template> - <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> - <template v-else-if="enableTurnstile" #suffix>Turnstile</template> - <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> + <div class="_gaps_m"> + <XBotProtection/> - <XBotProtection/> - </MkFolder> + <MkFolder> + <template #label>Active Email Validation</template> + <template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="emailValidationForm.modified.value" #footer> + <MkFormFooter :form="emailValidationForm"/> + </template> - <MkFolder> - <template #label>Active Email Validation</template> - <template v-if="enableActiveEmailValidation" #suffix>Enabled</template> - <template v-else #suffix>Disabled</template> + <div class="_gaps_m"> + <span>{{ i18n.ts.activeEmailValidationDescription }}</span> + <MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation"> + <template #label>Enable</template> + </MkSwitch> + <MkSwitch v-model="emailValidationForm.state.enableVerifymailApi"> + <template #label>Use Verifymail.io API</template> + </MkSwitch> + <MkInput v-model="emailValidationForm.state.verifymailAuthKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>Verifymail.io API Auth Key</template> + </MkInput> + <MkSwitch v-model="emailValidationForm.state.enableTruemailApi"> + <template #label>Use TrueMail API</template> + </MkSwitch> + <MkInput v-model="emailValidationForm.state.truemailInstance"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>TrueMail API Instance</template> + </MkInput> + <MkInput v-model="emailValidationForm.state.truemailAuthKey"> + <template #prefix><i class="ti ti-key"></i></template> + <template #label>TrueMail API Auth Key</template> + </MkInput> + </div> + </MkFolder> - <div class="_gaps_m"> - <span>{{ i18n.ts.activeEmailValidationDescription }}</span> - <MkSwitch v-model="enableActiveEmailValidation"> - <template #label>Enable</template> - </MkSwitch> - <MkSwitch v-model="enableVerifymailApi"> - <template #label>Use Verifymail.io API</template> - </MkSwitch> - <MkInput v-model="verifymailAuthKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>Verifymail.io API Auth Key</template> - </MkInput> - <MkSwitch v-model="enableTruemailApi"> - <template #label>Use TrueMail API</template> - </MkSwitch> - <MkInput v-model="truemailInstance"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>TrueMail API Instance</template> - </MkInput> - <MkInput v-model="truemailAuthKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>TrueMail API Auth Key</template> - </MkInput> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </div> - </MkFolder> + <MkFolder> + <template #label>Banned Email Domains</template> + <template v-if="bannedEmailDomainsForm.modified.value" #footer> + <MkFormFooter :form="bannedEmailDomainsForm"/> + </template> - <MkFolder> - <template #label>Banned Email Domains</template> + <div class="_gaps_m"> + <MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains"> + <template #label>Banned Email Domains List</template> + </MkTextarea> + </div> + </MkFolder> - <div class="_gaps_m"> - <MkTextarea v-model="bannedEmailDomains"> - <template #label>Banned Email Domains List</template> - </MkTextarea> - <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - </div> - </MkFolder> + <MkFolder> + <template #label>Log IP address</template> + <template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template> + <template v-else #suffix>Disabled</template> + <template v-if="ipLoggingForm.modified.value" #footer> + <MkFormFooter :form="ipLoggingForm"/> + </template> - <MkFolder> - <template #label>Log IP address</template> - <template v-if="enableIpLogging" #suffix>Enabled</template> - <template v-else #suffix>Disabled</template> - - <div class="_gaps_m"> - <MkSwitch v-model="enableIpLogging" @update:modelValue="save"> - <template #label>Enable</template> - </MkSwitch> - </div> - </MkFolder> - </div> - </FormSuspense> + <div class="_gaps_m"> + <MkSwitch v-model="ipLoggingForm.state.enableIpLogging"> + <template #label>Enable</template> + </MkSwitch> + </div> + </MkFolder> + </div> </MkSpacer> </MkStickyContainer> </template> @@ -88,60 +83,55 @@ import XHeader from './_header_.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import MkRange from '@/components/MkRange.vue'; import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; + +const meta = await misskeyApi('admin/meta'); -const enableHcaptcha = ref<boolean>(false); -const enableMcaptcha = ref<boolean>(false); -const enableRecaptcha = ref<boolean>(false); -const enableTurnstile = ref<boolean>(false); -const enableIpLogging = ref<boolean>(false); -const enableActiveEmailValidation = ref<boolean>(false); -const enableVerifymailApi = ref<boolean>(false); -const verifymailAuthKey = ref<string | null>(null); -const enableTruemailApi = ref<boolean>(false); -const truemailInstance = ref<string | null>(null); -const truemailAuthKey = ref<string | null>(null); -const bannedEmailDomains = ref<string>(''); +const ipLoggingForm = useForm({ + enableIpLogging: meta.enableIpLogging, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableIpLogging: state.enableIpLogging, + }); + fetchInstance(true); +}); -async function init() { - const meta = await misskeyApi('admin/meta'); - enableHcaptcha.value = meta.enableHcaptcha; - enableMcaptcha.value = meta.enableMcaptcha; - enableRecaptcha.value = meta.enableRecaptcha; - enableTurnstile.value = meta.enableTurnstile; - enableIpLogging.value = meta.enableIpLogging; - enableActiveEmailValidation.value = meta.enableActiveEmailValidation; - enableVerifymailApi.value = meta.enableVerifymailApi; - verifymailAuthKey.value = meta.verifymailAuthKey; - enableTruemailApi.value = meta.enableTruemailApi; - truemailInstance.value = meta.truemailInstance; - truemailAuthKey.value = meta.truemailAuthKey; - bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ''; -} +const emailValidationForm = useForm({ + enableActiveEmailValidation: meta.enableActiveEmailValidation, + enableVerifymailApi: meta.enableVerifymailApi, + verifymailAuthKey: meta.verifymailAuthKey, + enableTruemailApi: meta.enableTruemailApi, + truemailInstance: meta.truemailInstance, + truemailAuthKey: meta.truemailAuthKey, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableActiveEmailValidation: state.enableActiveEmailValidation, + enableVerifymailApi: state.enableVerifymailApi, + verifymailAuthKey: state.verifymailAuthKey, + enableTruemailApi: state.enableTruemailApi, + truemailInstance: state.truemailInstance, + truemailAuthKey: state.truemailAuthKey, + }); + fetchInstance(true); +}); -function save() { - os.apiWithDialog('admin/update-meta', { - enableIpLogging: enableIpLogging.value, - enableActiveEmailValidation: enableActiveEmailValidation.value, - enableVerifymailApi: enableVerifymailApi.value, - verifymailAuthKey: verifymailAuthKey.value, - enableTruemailApi: enableTruemailApi.value, - truemailInstance: truemailInstance.value, - truemailAuthKey: truemailAuthKey.value, - bannedEmailDomains: bannedEmailDomains.value.split('\n'), - }).then(() => { - fetchInstance(true); +const bannedEmailDomainsForm = useForm({ + bannedEmailDomains: meta.bannedEmailDomains?.join('\n') || '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + bannedEmailDomains: state.bannedEmailDomains.split('\n'), }); -} + fetchInstance(true); +}); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 90ac259c5328a048b6f98728d833df016791325f..04975dcbf30855bb8b21ca1ca7c72235c8dfdbd7 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -8,178 +8,216 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><XHeader :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> - <FormSuspense :p="init"> - <div class="_gaps_m"> - <MkInput v-model="name"> - <template #label>{{ i18n.ts.instanceName }}</template> - </MkInput> - - <MkInput v-model="shortName"> - <template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})</template> - <template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template> - </MkInput> - - <MkTextarea v-model="description"> - <template #label>{{ i18n.ts.instanceDescription }}</template> - </MkTextarea> - - <FormSplit :minWidth="300"> - <MkInput v-model="maintainerName"> - <template #label>{{ i18n.ts.maintainerName }}</template> + <div class="_gaps_m"> + <MkFolder :defaultOpen="true"> + <template #icon><i class="ti ti-info-circle"></i></template> + <template #label>{{ i18n.ts.info }}</template> + <template v-if="infoForm.modified.value" #footer> + <MkFormFooter :form="infoForm"/> + </template> + + <div class="_gaps"> + <MkInput v-model="infoForm.state.name"> + <template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template> </MkInput> - <MkInput v-model="maintainerEmail" type="email"> - <template #prefix><i class="ti ti-mail"></i></template> - <template #label>{{ i18n.ts.maintainerEmail }}</template> + <MkInput v-model="infoForm.state.shortName"> + <template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template> </MkInput> - </FormSplit> - - <MkInput v-model="repositoryUrl" type="url"> - <template #label>{{ i18n.ts.repositoryUrl }}</template> - <template #prefix><i class="ti ti-link"></i></template> - <template #caption>{{ i18n.ts.repositoryUrlDescription }}</template> - </MkInput> - - <MkInfo v-if="!instance.providesTarball && !repositoryUrl" warn> - {{ i18n.ts.repositoryUrlOrTarballRequired }} - </MkInfo> - - <MkInput v-model="impressumUrl" type="url"> - <template #label>{{ i18n.ts.impressumUrl }}</template> - <template #prefix><i class="ti ti-link"></i></template> - <template #caption>{{ i18n.ts.impressumDescription }}</template> - </MkInput> - - <MkInput v-model="donationUrl" type="url"> - <template #label>{{ i18n.ts.donationUrl }}</template> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> - </MkInput> - - <MkTextarea v-model="pinnedUsers"> - <template #label>{{ i18n.ts.pinnedUsers }}</template> - <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> - </MkTextarea> - <FormSection> - <template #label>{{ i18n.ts.files }}</template> + <MkTextarea v-model="infoForm.state.description"> + <template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkTextarea> - <div class="_gaps_m"> - <MkSwitch v-model="cacheRemoteFiles"> - <template #label>{{ i18n.ts.cacheRemoteFiles }}</template> - <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template> - </MkSwitch> + <FormSplit :minWidth="300"> + <MkInput v-model="infoForm.state.maintainerName"> + <template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkInput> - <template v-if="cacheRemoteFiles"> - <MkSwitch v-model="cacheRemoteSensitiveFiles"> - <template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template> - <template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template> - </MkSwitch> - </template> - </div> - </FormSection> + <MkInput v-model="infoForm.state.maintainerEmail" type="email"> + <template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-mail"></i></template> + </MkInput> + </FormSplit> - <FormSection> - <template #label>ServiceWorker</template> + <MkInput v-model="infoForm.state.tosUrl" type="url"> + <template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <div class="_gaps_m"> - <MkSwitch v-model="enableServiceWorker"> - <template #label>{{ i18n.ts.enableServiceworker }}</template> - <template #caption>{{ i18n.ts.serviceworkerInfo }}</template> - </MkSwitch> + <MkInput v-model="infoForm.state.privacyPolicyUrl" type="url"> + <template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <template v-if="enableServiceWorker"> - <MkInput v-model="swPublicKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>Public key</template> - </MkInput> + <MkInput v-model="infoForm.state.inquiryUrl" type="url"> + <template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <MkInput v-model="swPrivateKey"> - <template #prefix><i class="ti ti-key"></i></template> - <template #label>Private key</template> - </MkInput> - </template> - </div> - </FormSection> + <MkInput v-model="infoForm.state.repositoryUrl" type="url"> + <template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.repositoryUrlDescription }}</template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <FormSection> - <template #label>Misskey® Fan-out Timeline Technologyâ„¢ (FTT)</template> + <MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn> + {{ i18n.ts.repositoryUrlOrTarballRequired }} + </MkInfo> - <div class="_gaps_m"> - <MkSwitch v-model="enableFanoutTimeline"> - <template #label>{{ i18n.ts.enable }}</template> - <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template> - </MkSwitch> + <MkInput v-model="infoForm.state.impressumUrl" type="url"> + <template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.impressumDescription }}</template> + <template #prefix><i class="ti ti-link"></i></template> + </MkInput> - <MkSwitch v-model="enableFanoutTimelineDbFallback"> - <template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</template> - <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template> + <MkInput v-model="infoForm.state.donationUrl" type="url"> + <template #label>{{ i18n.ts.donationUrl }}</template> + <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + </MkInput> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-user-star"></i></template> + <template #label>{{ i18n.ts.pinnedUsers }}</template> + <template v-if="pinnedUsersForm.modified.value" #footer> + <MkFormFooter :form="pinnedUsersForm"/> + </template> + + <MkTextarea v-model="pinnedUsersForm.state.pinnedUsers"> + <template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> + </MkTextarea> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-cloud"></i></template> + <template #label>{{ i18n.ts.files }}</template> + <template v-if="filesForm.modified.value" #footer> + <MkFormFooter :form="filesForm"/> + </template> + + <div class="_gaps"> + <MkSwitch v-model="filesForm.state.cacheRemoteFiles"> + <template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template> + </MkSwitch> + + <template v-if="filesForm.state.cacheRemoteFiles"> + <MkSwitch v-model="filesForm.state.cacheRemoteSensitiveFiles"> + <template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="filesForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template> </MkSwitch> - - <MkInput v-model="perLocalUserUserTimelineCacheMax" type="number"> - <template #label>perLocalUserUserTimelineCacheMax</template> - </MkInput> - - <MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number"> - <template #label>perRemoteUserUserTimelineCacheMax</template> + </template> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-world-cog"></i></template> + <template #label>ServiceWorker</template> + <template v-if="serviceWorkerForm.modified.value" #footer> + <MkFormFooter :form="serviceWorkerForm"/> + </template> + + <div class="_gaps"> + <MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker"> + <template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.serviceworkerInfo }}</template> + </MkSwitch> + + <template v-if="serviceWorkerForm.state.enableServiceWorker"> + <MkInput v-model="serviceWorkerForm.state.swPublicKey"> + <template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-key"></i></template> </MkInput> - <MkInput v-model="perUserHomeTimelineCacheMax" type="number"> - <template #label>perUserHomeTimelineCacheMax</template> + <MkInput v-model="serviceWorkerForm.state.swPrivateKey"> + <template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #prefix><i class="ti ti-key"></i></template> </MkInput> - - <MkInput v-model="perUserListTimelineCacheMax" type="number"> - <template #label>perUserListTimelineCacheMax</template> + </template> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ph-faders ph-bold ph-lg ti-fw"></i></template> + <template #label>{{ i18n.ts.otherSettings }}</template> + <template v-if="otherForm.modified.value" #footer> + <MkFormFooter :form="otherForm"/> + </template> + + <div class="_gaps"> + <MkSwitch v-model="otherForm.state.enableAchievements"> + <template #label>{{ i18n.ts.enableAchievements }}<span v-if="otherForm.modifiedStates.enableAchievements" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.turnOffAchievements}}</template> + </MkSwitch> + + <MkSwitch v-model="otherForm.state.enableBotTrending"> + <template #label>{{ i18n.ts.enableBotTrending }}<span v-if="otherForm.modifiedStates.enableBotTrending" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.turnOffBotTrending }}</template> + </MkSwitch> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-ad"></i></template> + <template #label>{{ i18n.ts._ad.adsSettings }}</template> + <template v-if="adForm.modified.value" #footer> + <MkFormFooter :form="adForm"/> + </template> + + <div class="_gaps"> + <div class="_gaps_s"> + <MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number"> + <template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template> </MkInput> + <MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true"> + {{ i18n.ts._ad.adsTooClose }} + </MkInfo> </div> - </FormSection> - - <FormSection> - <template #label>{{ i18n.ts._ad.adsSettings }}</template> - - <div class="_gaps_m"> - <div class="_gaps_s"> - <MkInput v-model="notesPerOneAd" :min="0" type="number"> - <template #label>{{ i18n.ts._ad.notesPerOneAd }}</template> - <template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template> - </MkInput> - <MkInfo v-if="notesPerOneAd > 0 && notesPerOneAd < 20" :warn="true"> - {{ i18n.ts._ad.adsTooClose }} - </MkInfo> - </div> - </div> - </FormSection> - - <FormSection> - <template #label>{{ i18n.ts._urlPreviewSetting.title }}</template> - - <div class="_gaps_m"> - <MkSwitch v-model="urlPreviewEnabled"> - <template #label>{{ i18n.ts._urlPreviewSetting.enable }}</template> - </MkSwitch> - - <MkSwitch v-model="urlPreviewRequireContentLength"> - <template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</template> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-world-search"></i></template> + <template #label>{{ i18n.ts._urlPreviewSetting.title }}</template> + <template v-if="urlPreviewForm.modified.value" #footer> + <MkFormFooter :form="urlPreviewForm"/> + </template> + + <div class="_gaps"> + <MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled"> + <template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template> + </MkSwitch> + + <template v-if="urlPreviewForm.state.urlPreviewEnabled"> + <MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength"> + <template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template> </MkSwitch> - <MkInput v-model="urlPreviewMaximumContentLength" type="number"> - <template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number"> + <template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template> </MkInput> - <MkInput v-model="urlPreviewTimeout" type="number"> - <template #label>{{ i18n.ts._urlPreviewSetting.timeout }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number"> + <template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template> </MkInput> - <MkInput v-model="urlPreviewUserAgent" type="text"> - <template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text"> + <template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template> </MkInput> <div> - <MkInput v-model="urlPreviewSummaryProxyUrl" type="text"> - <template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</template> + <MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text"> + <template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template> <template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template> </MkInput> @@ -193,18 +231,51 @@ SPDX-License-Identifier: AGPL-3.0-only </ul> </div> </div> - </div> - </FormSection> - </div> - </FormSuspense> - </MkSpacer> - <template #footer> - <div :class="$style.footer"> - <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - </MkSpacer> + </template> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-planet"></i></template> + <template #label>{{ i18n.ts.federation }}</template> + <template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template> + <template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template> + <template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template> + <template v-if="federationForm.modified.value" #footer> + <MkFormFooter :form="federationForm"/> + </template> + + <div class="_gaps"> + <MkRadios v-model="federationForm.state.federation"> + <template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template> + <option value="all">{{ i18n.ts.all }}</option> + <option value="specified">{{ i18n.ts.specifyHost }}</option> + <option value="none">{{ i18n.ts.none }}</option> + </MkRadios> + + <MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts"> + <template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template> + <template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template> + </MkTextarea> + </div> + </MkFolder> + + <MkFolder> + <template #icon><i class="ti ti-ghost"></i></template> + <template #label>{{ i18n.ts.proxyAccount }}</template> + + <div class="_gaps"> + <MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo> + <MkKeyValue> + <template #key>{{ i18n.ts.proxyAccount }}</template> + <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template> + </MkKeyValue> + + <MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton> + </div> + </MkFolder> </div> - </template> + </MkSpacer> </MkStickyContainer> </div> </template> @@ -216,9 +287,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; -import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance, instance } from '@/instance.js'; @@ -226,99 +295,136 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; -import MkSelect from '@/components/MkSelect.vue'; - -const name = ref<string | null>(null); -const shortName = ref<string | null>(null); -const description = ref<string | null>(null); -const maintainerName = ref<string | null>(null); -const maintainerEmail = ref<string | null>(null); -const repositoryUrl = ref<string | null>(null); -const impressumUrl = ref<string | null>(null); -const donationUrl = ref<string | null>(null); -const pinnedUsers = ref<string>(''); -const cacheRemoteFiles = ref<boolean>(false); -const cacheRemoteSensitiveFiles = ref<boolean>(false); -const enableServiceWorker = ref<boolean>(false); -const swPublicKey = ref<string | null>(null); -const swPrivateKey = ref<string | null>(null); -const enableFanoutTimeline = ref<boolean>(false); -const enableFanoutTimelineDbFallback = ref<boolean>(false); -const perLocalUserUserTimelineCacheMax = ref<number>(0); -const perRemoteUserUserTimelineCacheMax = ref<number>(0); -const perUserHomeTimelineCacheMax = ref<number>(0); -const perUserListTimelineCacheMax = ref<number>(0); -const notesPerOneAd = ref<number>(0); -const urlPreviewEnabled = ref<boolean>(true); -const urlPreviewTimeout = ref<number>(10000); -const urlPreviewMaximumContentLength = ref<number>(1024 * 1024 * 10); -const urlPreviewRequireContentLength = ref<boolean>(true); -const urlPreviewUserAgent = ref<string | null>(null); -const urlPreviewSummaryProxyUrl = ref<string | null>(null); - -async function init(): Promise<void> { - const meta = await misskeyApi('admin/meta'); - name.value = meta.name; - shortName.value = meta.shortName; - description.value = meta.description; - maintainerName.value = meta.maintainerName; - maintainerEmail.value = meta.maintainerEmail; - repositoryUrl.value = meta.repositoryUrl; - impressumUrl.value = meta.impressumUrl; - donationUrl.value = meta.donationUrl; - pinnedUsers.value = meta.pinnedUsers.join('\n'); - cacheRemoteFiles.value = meta.cacheRemoteFiles; - cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles; - enableServiceWorker.value = meta.enableServiceWorker; - swPublicKey.value = meta.swPublickey; - swPrivateKey.value = meta.swPrivateKey; - enableFanoutTimeline.value = meta.enableFanoutTimeline; - enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback; - perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax; - perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax; - perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax; - perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax; - notesPerOneAd.value = meta.notesPerOneAd; - urlPreviewEnabled.value = meta.urlPreviewEnabled; - urlPreviewTimeout.value = meta.urlPreviewTimeout; - urlPreviewMaximumContentLength.value = meta.urlPreviewMaximumContentLength; - urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength; - urlPreviewUserAgent.value = meta.urlPreviewUserAgent; - urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl; -} +import MkKeyValue from '@/components/MkKeyValue.vue'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; +import MkRadios from '@/components/MkRadios.vue'; + +const meta = await misskeyApi('admin/meta'); + +const proxyAccount = ref(meta.proxyAccountId ? await misskeyApi('users/show', { userId: meta.proxyAccountId }) : null); + +const infoForm = useForm({ + name: meta.name ?? '', + shortName: meta.shortName ?? '', + description: meta.description ?? '', + maintainerName: meta.maintainerName ?? '', + maintainerEmail: meta.maintainerEmail ?? '', + tosUrl: meta.tosUrl ?? '', + privacyPolicyUrl: meta.privacyPolicyUrl ?? '', + inquiryUrl: meta.inquiryUrl ?? '', + repositoryUrl: meta.repositoryUrl ?? '', + impressumUrl: meta.impressumUrl ?? '', + donationUrl: meta.donationUrl ?? '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + name: state.name, + shortName: state.shortName === '' ? null : state.shortName, + description: state.description, + maintainerName: state.maintainerName, + maintainerEmail: state.maintainerEmail, + tosUrl: state.tosUrl, + privacyPolicyUrl: state.privacyPolicyUrl, + inquiryUrl: state.inquiryUrl, + repositoryUrl: state.repositoryUrl, + impressumUrl: state.impressumUrl, + donationUrl: state.donationUrl, + }); + fetchInstance(true); +}); -async function save() { +const pinnedUsersForm = useForm({ + pinnedUsers: meta.pinnedUsers.join('\n'), +}, async (state) => { await os.apiWithDialog('admin/update-meta', { - name: name.value, - shortName: shortName.value === '' ? null : shortName.value, - description: description.value, - maintainerName: maintainerName.value, - maintainerEmail: maintainerEmail.value, - repositoryUrl: repositoryUrl.value, - impressumUrl: impressumUrl.value, - donationUrl: donationUrl.value, - pinnedUsers: pinnedUsers.value.split('\n'), - cacheRemoteFiles: cacheRemoteFiles.value, - cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, - enableServiceWorker: enableServiceWorker.value, - swPublicKey: swPublicKey.value, - swPrivateKey: swPrivateKey.value, - enableFanoutTimeline: enableFanoutTimeline.value, - enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value, - perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value, - perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value, - perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value, - perUserListTimelineCacheMax: perUserListTimelineCacheMax.value, - notesPerOneAd: notesPerOneAd.value, - urlPreviewEnabled: urlPreviewEnabled.value, - urlPreviewTimeout: urlPreviewTimeout.value, - urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value, - urlPreviewRequireContentLength: urlPreviewRequireContentLength.value, - urlPreviewUserAgent: urlPreviewUserAgent.value, - urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value, + pinnedUsers: state.pinnedUsers.split('\n'), }); + fetchInstance(true); +}); +const filesForm = useForm({ + cacheRemoteFiles: meta.cacheRemoteFiles, + cacheRemoteSensitiveFiles: meta.cacheRemoteSensitiveFiles, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + cacheRemoteFiles: state.cacheRemoteFiles, + cacheRemoteSensitiveFiles: state.cacheRemoteSensitiveFiles, + }); + fetchInstance(true); +}); + +const serviceWorkerForm = useForm({ + enableServiceWorker: meta.enableServiceWorker, + swPublicKey: meta.swPublickey ?? '', + swPrivateKey: meta.swPrivateKey ?? '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableServiceWorker: state.enableServiceWorker, + swPublicKey: state.swPublicKey, + swPrivateKey: state.swPrivateKey, + }); fetchInstance(true); +}); + +const otherForm = useForm({ + enableAchievements: meta.enableAchievements, + enableBotTrending: meta.enableBotTrending, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableAchievements: state.enableAchievements, + enableBotTrending: state.enableBotTrending, + }); + fetchInstance(true); +}); + +const adForm = useForm({ + notesPerOneAd: meta.notesPerOneAd, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + notesPerOneAd: state.notesPerOneAd, + }); + fetchInstance(true); +}); + +const urlPreviewForm = useForm({ + urlPreviewEnabled: meta.urlPreviewEnabled, + urlPreviewTimeout: meta.urlPreviewTimeout, + urlPreviewMaximumContentLength: meta.urlPreviewMaximumContentLength, + urlPreviewRequireContentLength: meta.urlPreviewRequireContentLength, + urlPreviewUserAgent: meta.urlPreviewUserAgent ?? '', + urlPreviewSummaryProxyUrl: meta.urlPreviewSummaryProxyUrl ?? '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + urlPreviewEnabled: state.urlPreviewEnabled, + urlPreviewTimeout: state.urlPreviewTimeout, + urlPreviewMaximumContentLength: state.urlPreviewMaximumContentLength, + urlPreviewRequireContentLength: state.urlPreviewRequireContentLength, + urlPreviewUserAgent: state.urlPreviewUserAgent, + urlPreviewSummaryProxyUrl: state.urlPreviewSummaryProxyUrl, + }); + fetchInstance(true); +}); + +const federationForm = useForm({ + federation: meta.federation, + federationHosts: meta.federationHosts.join('\n'), +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + federation: state.federation, + federationHosts: state.federationHosts.split('\n'), + }); + fetchInstance(true); +}); + +function chooseProxyAccount() { + os.selectUser({ localOnly: true }).then(user => { + proxyAccount.value = user; + os.apiWithDialog('admin/update-meta', { + proxyAccountId: user.id, + }).then(() => { + fetchInstance(true); + }); + }); } const headerTabs = computed(() => []); @@ -330,11 +436,6 @@ definePageMetadata(() => ({ </script> <style lang="scss" module> -.footer { - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} - .subCaption { font-size: 0.85em; color: var(--fgTransparentWeak); diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue index 0c07122af33a4032f9f5bef0a4a3ae0f0cfbb477..4e767fba16bfd03c7414440dcee61f93ab43cae6 100644 --- a/packages/frontend/src/pages/admin/system-webhook.item.vue +++ b/packages/frontend/src/pages/admin/system-webhook.item.vue @@ -4,8 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.main"> - <span :class="$style.icon"> +<MkFolder> + <template #label>{{ entity.name || entity.url }}</template> + <template #icon> <i v-if="!entity.isActive" class="ti ti-player-pause"/> <i v-else-if="entity.latestStatus === null" class="ti ti-circle"/> <i @@ -14,23 +15,38 @@ SPDX-License-Identifier: AGPL-3.0-only :style="{ color: 'var(--success)' }" /> <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/> - </span> - <span :class="$style.text">{{ entity.name || entity.url }}</span> - <span :class="$style.suffix"> + </template> + <template #suffix> <MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/> - <button :class="$style.suffixButton" @click="onEditClick"> - <i class="ti ti-settings"></i> - </button> - <button :class="$style.suffixButton" @click="onDeleteClick"> - <i class="ti ti-trash"></i> - </button> - </span> -</div> + <span v-else>-</span> + </template> + <template #footer> + <div class="_buttons"> + <MkButton @click="onEditClick"> + <i class="ti ti-settings"></i> {{ i18n.ts.edit }} + </MkButton> + <MkButton danger @click="onDeleteClick"> + <i class="ti ti-trash"></i> {{ i18n.ts.delete }} + </MkButton> + </div> + </template> + + <div class="_gaps"> + <MkKeyValue> + <template #key>latestStatus</template> + <template #value>{{ entity.latestStatus ?? '-' }}</template> + </MkKeyValue> + </div> +</MkFolder> </template> <script lang="ts" setup> import { entities } from 'misskey-js'; import { toRefs } from 'vue'; +import MkFolder from '@/components/MkFolder.vue'; +import { i18n } from '@/i18n.js'; +import MkButton from '@/components/MkButton.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; const emit = defineEmits<{ (ev: 'edit', value: entities.SystemWebhook): void; @@ -54,64 +70,10 @@ function onDeleteClick() { </script> <style module lang="scss"> -.main { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 14px; - background: var(--buttonBg); - border: none; - border-radius: 6px; - font-size: 0.9em; - - &:hover { - text-decoration: none; - background: var(--buttonHoverBg); - } - - &.active { - color: var(--accent); - background: var(--buttonHoverBg); - } -} - .icon { margin-right: 0.75em; flex-shrink: 0; text-align: center; color: var(--fgTransparentWeak); } - -.text { - flex-shrink: 1; - white-space: normal; - padding-right: 12px; - text-align: center; -} - -.suffix { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gaps: 4px; - margin-left: auto; - margin-right: -8px; - opacity: 0.7; - white-space: nowrap; -} - -.suffixButton { - background: transparent; - border: none; - border-radius: 9999px; - margin-top: -8px; - margin-bottom: -8px; - padding: 8px; - - &:hover { - background: var(--buttonBg); - } -} </style> diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue index 7a40eec94494402105317836c6323f6b472b5148..c59abda24a21b12267067584dadda368191fd18d 100644 --- a/packages/frontend/src/pages/admin/system-webhook.vue +++ b/packages/frontend/src/pages/admin/system-webhook.vue @@ -11,8 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="900"> <div class="_gaps_m"> - <MkButton :class="$style.linkButton" full @click="onCreateWebhookClicked"> - {{ i18n.ts._webhookSettings.createWebhook }} + <MkButton primary @click="onCreateWebhookClicked"> + <i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }} </MkButton> <FormSection> @@ -89,8 +89,5 @@ definePageMetadata(() => ({ </script> <style module lang="scss"> -.linkButton { - text-align: left; - padding: 10px 18px; -} + </style> diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index e947ec9ba5dbe7206b3ce6cf13bfdbe92ee857bd..e57e212b60a143dd00d118b7667ba25efe737587 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader :actions="headerActions" :displayBackButton="true" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> <div ref="rootEl"> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index b9f45e219c110598fccf92a9728d9e36c1df3a34..75a44802f8b9468bb8d07249bb3d629c7cb96361 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <h1>{{ i18n.ts._auth.denied }}</h1> </div> <div v-if="state == 'accepted' && session"> - <h1>{{ session.app.isAuthorized ? i18n.ts['already-authorized'] : i18n.ts.allowed }}</h1> + <h1>{{ session.app.isAuthorized ? i18n.ts['already-authorized'] : i18n.ts._auth.allowed }}</h1> <p v-if="session.app.callbackUrl"> {{ i18n.ts._auth.callback }} <MkEllipsis/> diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index ad9ec3c4eecec5f4c20b2b2fd3be48793a950e88..b377314856a5e7c4828bcbc746b7bde16b20bc26 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -12,19 +12,31 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ avatarDecoration.name }}</template> <template #caption>{{ avatarDecoration.description }}</template> - <div class="_gaps_m"> - <MkInput v-model="avatarDecoration.name"> - <template #label>{{ i18n.ts.name }}</template> - </MkInput> - <MkTextarea v-model="avatarDecoration.description"> - <template #label>{{ i18n.ts.description }}</template> - </MkTextarea> - <MkInput v-model="avatarDecoration.url"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <div class="buttons _buttons"> - <MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <div :class="$style.editorRoot"> + <div :class="$style.editorWrapper"> + <div :class="$style.preview"> + <div :class="[$style.previewItem, $style.light]"> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/> + </div> + <div :class="[$style.previewItem, $style.dark]"> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/> + </div> + </div> + <div class="_gaps_m"> + <MkInput v-model="avatarDecoration.name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkTextarea v-model="avatarDecoration.description"> + <template #label>{{ i18n.ts.description }}</template> + </MkTextarea> + <MkInput v-model="avatarDecoration.url"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <div class="_buttons"> + <MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </div> </div> </div> </MkFolder> @@ -39,6 +51,7 @@ import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import { signinRequired } from '@/account.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; @@ -47,6 +60,8 @@ import MkFolder from '@/components/MkFolder.vue'; const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]); +const $i = signinRequired(); + function add() { avatarDecorations.value.unshift({ _id: Math.random().toString(36), @@ -99,3 +114,55 @@ definePageMetadata(() => ({ icon: 'ti ti-sparkles', })); </script> + +<style lang="scss" module> +.editorRoot { + container: editor / inline-size; +} + +.editorWrapper { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto auto; + gap: var(--margin); +} + +.preview { + display: grid; + place-items: center; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + gap: var(--margin); +} + +.previewItem { + width: 100%; + height: 100%; + min-height: 160px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius); + + &.light { + background: #eee; + } + + &.dark { + background: #222; + } +} + +@container editor (min-width: 600px) { + .editorWrapper { + grid-template-columns: 200px 1fr; + grid-template-rows: 1fr; + gap: calc(var(--margin) * 2); + } + + .preview { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } +} +</style> diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index e922599642846699bf0825f9a9be63199afa1695..790e16e47103cdde906af6740139b68013314d02 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -82,7 +82,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import MkNotes from '@/components/MkNotes.vue'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { favoritedChannelsCache } from '@/cache.js'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -341,7 +341,7 @@ definePageMetadata(() => ({ left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); } .bannerStatus { diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 506d906683f5e654b9b9b2fac6327cbbd4390605..52b852ad17441d2358f6a304b10314e8f7c8f585 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -39,11 +39,13 @@ import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import MkButton from '@/components/MkButton.vue'; import { clipsCache } from '@/cache.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ clipId: string, @@ -127,21 +129,41 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ clipsCache.delete(); }, }, ...(clip.value.isPublic ? [{ - icon: 'ti ti-link', - text: i18n.ts.copyUrl, - handler: async (): Promise<void> => { - copyToClipboard(`${url}/clips/${clip.value.id}`); - os.success(); - }, -}] : []), ...(clip.value.isPublic && isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, - handler: async (): Promise<void> => { - navigator.share({ - title: clip.value.name, - text: clip.value.description, - url: `${url}/clips/${clip.value.id}`, + handler: (ev: MouseEvent): void => { + const menuItems: MenuItem[] = []; + + menuItems.push({ + icon: 'ti ti-link', + text: i18n.ts.copyUrl, + action: () => { + copyToClipboard(`${url}/clips/${clip.value!.id}`); + os.success(); + }, + }, { + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + action: () => { + genEmbedCode('clips', clip.value!.id); + }, }); + + if (isSupportShare()) { + menuItems.push({ + icon: 'ti ti-share', + text: i18n.ts.share, + action: async () => { + navigator.share({ + title: clip.value!.name, + text: clip.value!.description ?? '', + url: `${url}/clips/${clip.value!.id}`, + }); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); }, }] : []), { icon: 'ti ti-trash', diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 4ed2a676788f251ddf7015bd4ee23ab3077f68f2..36e95cec2d2c5feb5304fe1239682cd11acd9e1a 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> </button> <button class="_button" :class="$style.kvEditBtn" @click="describe()"> - <MkKeyValue> + <MkKeyValue :class="$style.multiline"> <template #key>{{ i18n.ts.description }}</template> <template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template> </MkKeyValue> @@ -99,12 +99,12 @@ const file = ref<Misskey.entities.DriveFile>(); const folderHierarchy = computed(() => { if (!file.value) return [i18n.ts.drive]; const folderNames = [i18n.ts.drive]; - + function get(folder: Misskey.entities.DriveFolder) { if (folder.parent) get(folder.parent); folderNames.push(folder.name); } - + if (file.value.folder) get(file.value.folder); return folderNames; }); @@ -313,6 +313,10 @@ onMounted(async () => { padding: .5rem 1rem; } +.multiline { + white-space: pre-wrap; +} + .kvEditBtn { text-align: start; display: block; diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index 0f0b7e1ea8e67eba6767f643648f9e586c22c571..4db952eac239f24e8139b43f87d744782d7b34db 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -205,8 +205,8 @@ import { claimAchievement } from '@/scripts/achievements.js'; import { defaultStore } from '@/store.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; -import { apiUrl } from '@/config.js'; +import { useInterval } from '@@/js/use-interval.js'; +import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index d282ed48107af9be3a3813d47116897913b4eb64..fd6fadd0b37125d37fa22c4d0736f4d61399e991 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -11,6 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="title"> <template #label>{{ i18n.ts._play.title }}</template> </MkInput> + <MkSelect v-model="visibility"> + <template #label>{{ i18n.ts.visibility }}</template> + <template #caption>{{ i18n.ts._play.visibilityDescription }}</template> + <option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option> + <option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option> + </MkSelect> <MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true"> <template #label>{{ i18n.ts._play.summary }}</template> </MkTextarea> @@ -18,19 +24,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCodeEditor v-model="script" lang="is"> <template #label>{{ i18n.ts._play.script }}</template> </MkCodeEditor> - <MkSelect v-model="visibility"> - <template #label>{{ i18n.ts.visibility }}</template> - <template #caption>{{ i18n.ts._play.visibilityDescription }}</template> - <option :key="'public'" :value="'public'">{{ i18n.ts.public }}</option> - <option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option> - </MkSelect> - <div class="_buttons"> - <MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - <MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton> - <MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> - </div> </div> </MkSpacer> + <template #footer> + <div :class="$style.footer"> + <MkSpacer> + <div class="_buttons"> + <MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton @click="show"><i class="ti ti-eye"></i> {{ i18n.ts.show }}</MkButton> + <MkButton v-if="flash" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </MkSpacer> + </div> + </template> </MkStickyContainer> </template> @@ -459,3 +465,10 @@ definePageMetadata(() => ({ title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new, })); </script> +<style lang="scss" module> +.footer { + backdrop-filter: var(--blur, blur(15px)); + background: var(--acrylicBg); + border-top: solid .5px var(--divider); +} +</style> diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 1b277c936a04b8dda4b49fbe30dfe7fe790123a6..1229fcfd4e12ad2f651b1dea3594d2c8a28ab3fd 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -68,7 +68,7 @@ import { Interpreter, Parser, values } from '@syuilo/aiscript'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkAsUi from '@/components/MkAsUi.vue'; @@ -80,7 +80,7 @@ import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; import { pleaseLogin } from '@/scripts/please-login.js'; const props = defineProps<{ @@ -104,18 +104,23 @@ function fetchFlash() { function share(ev: MouseEvent) { if (!flash.value) return; - os.popupMenu([ - { - text: i18n.ts.shareWithNote, - icon: 'ph-repeat ph-bold ph-lg ti-fw', - action: shareWithNote, - }, - ...(isSupportShare() ? [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ + text: i18n.ts.shareWithNote, + icon: 'ph-repeat ph-bold ph-lg ti-fw', + action: shareWithNote, + }); + + if (isSupportShare()) { + menuItems.push({ text: i18n.ts.share, icon: 'ti ti-share', action: shareWithNavigator, - }] : []), - ], ev.currentTarget ?? ev.target); + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } function copyLink() { diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index d50887b2e9f1df7f67044c112b10adc289dcd4dc..400dfdbe6dc97ef51846982565ad3b5a4a2eb9c2 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -5,39 +5,43 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><MkPageHeader/></template> + <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> - <MkPagination ref="paginationComponent" :pagination="pagination"> - <template #empty> - <div class="_fullinfo"> - <img :src="infoImageUrl" class="_ghost"/> - <div>{{ i18n.ts.noFollowRequests }}</div> - </div> - </template> - <template #default="{items}"> - <div class="mk-follow-requests"> - <div v-for="req in items" :key="req.id" class="user _panel"> - <MkAvatar class="avatar" :user="req.follower" indicator link preview/> - <div class="body"> - <div class="name"> - <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA> - <p class="acct">@{{ acct(req.follower) }}</p> - </div> - <div class="commands"> - <MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton> - <MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton> + <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> + <div :key="tab" class="_gaps"> + <MkPagination ref="paginationComponent" :pagination="pagination"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.noFollowRequests }}</div> + </div> + </template> + <template #default="{items}"> + <div class="mk-follow-requests"> + <div v-for="req in items" :key="req.id" class="user _panel"> + <MkAvatar class="avatar" :user="displayUser(req)" indicator link preview/> + <div class="body"> + <div class="name"> + <MkA v-user-preview="displayUser(req).id" class="name" :to="userPage(displayUser(req))"><MkUserName :user="displayUser(req)"/></MkA> + <p class="acct">@{{ acct(displayUser(req)) }}</p> + </div> + <div v-if="tab === 'list'" class="commands"> + <MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton> + <MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton> + </div> + </div> </div> </div> - </div> - </div> - </template> - </MkPagination> + </template> + </MkPagination> + </div> + </MkHorizontalSwipe> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> -import { shallowRef, computed } from 'vue'; +import { shallowRef, computed, ref } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import MkButton from '@/components/MkButton.vue'; import { userPage, acct } from '@/filters/user.js'; @@ -45,29 +49,53 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { infoImageUrl } from '@/instance.js'; +import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import { $i } from '@/account'; const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); -const pagination = { - endpoint: 'following/requests/list' as const, - limit: 10, -}; +const pagination = computed(() => tab.value === 'list' + ? { + endpoint: 'following/requests/list' as const, + limit: 10, + } + : { + endpoint: 'following/requests/sent' as const, + limit: 10, + }, +); function accept(user) { misskeyApi('following/requests/accept', { userId: user.id }).then(() => { - paginationComponent.value.reload(); + paginationComponent.value?.reload(); }); } function reject(user) { misskeyApi('following/requests/reject', { userId: user.id }).then(() => { - paginationComponent.value.reload(); + paginationComponent.value?.reload(); }); } +function displayUser(req) { + return tab.value === 'list' ? req.follower : req.followee; +} + const headerActions = computed(() => []); -const headerTabs = computed(() => []); +const headerTabs = computed(() => [ + { + key: 'list', + title: i18n.ts.followRequests, + icon: 'ph-envelope ph-bold ph-lg', + }, { + key: 'sent', + title: i18n.ts.pendingFollowRequests, + icon: 'ph-paper-plane-tilt ph-bold ph-lg', + }, +]); + +const tab = ref($i?.isLocked || !$i.hasPendingSentFollowRequest ? 'list' : 'sent'); definePageMetadata(() => ({ title: i18n.ts.followRequests, diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue new file mode 100644 index 0000000000000000000000000000000000000000..d45f572739217b82ad707f98412f3e2e84617fdf --- /dev/null +++ b/packages/frontend/src/pages/following-feed.vue @@ -0,0 +1,282 @@ +<!-- +SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root"> + <div :class="$style.header"> + <MkPageHeader v-model:tab="userList" :tabs="headerTabs" :actions="headerActions" :displayBackButton="true" @update:tab="onChangeTab"/> + <MkInfo v-if="showRemoteWarning" :class="$style.remoteWarning" warn closable @close="remoteWarningDismissed = true">{{ i18n.ts.remoteFollowersWarning }}</MkInfo> + </div> + + <div ref="noteScroll" :class="$style.notes"> + <MkHorizontalSwipe v-model:tab="userList" :tabs="headerTabs"> + <MkPullToRefresh :refresher="() => reloadLatestNotes()"> + <MkPagination ref="latestNotesPaging" :pagination="latestNotesPagination" @init="onListReady"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost" :alt="i18n.ts.noNotes" aria-hidden="true"/> + <div>{{ i18n.ts.noNotes }}</div> + </div> + </template> + + <template #default="{ items: notes }"> + <MkDateSeparatedList v-slot="{ item: note }" :items="notes" :class="$style.panel" :noGap="true"> + <SkFollowingFeedEntry v-if="!isHardMuted(note)" :isMuted="isSoftMuted(note)" :note="note" @select="userSelected"/> + </MkDateSeparatedList> + </template> + </MkPagination> + </MkPullToRefresh> + </MkHorizontalSwipe> + </div> + + <div v-if="isWideViewport" ref="userScroll" :class="$style.user"> + <MkHorizontalSwipe v-if="selectedUserId" v-model:tab="userList" :tabs="headerTabs"> + <SkUserRecentNotes ref="userRecentNotes" :userId="selectedUserId" :withNonPublic="withNonPublic" :withQuotes="withQuotes" :withBots="withBots" :withReplies="withReplies" :onlyFiles="onlyFiles"/> + </MkHorizontalSwipe> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed, Ref, ref, shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import { getScrollContainer } from '@@/js/scroll.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { i18n } from '@/i18n.js'; +import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; +import { infoImageUrl } from '@/instance.js'; +import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; +import { Tab } from '@/components/global/MkPageHeader.tabs.vue'; +import { PageHeaderItem } from '@/types/page-header.js'; +import SkFollowingFeedEntry from '@/components/SkFollowingFeedEntry.vue'; +import { useRouter } from '@/router/supplier.js'; +import MkPageHeader from '@/components/global/MkPageHeader.vue'; +import { $i } from '@/account.js'; +import { checkWordMute } from '@/scripts/check-word-mute.js'; +import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue'; +import { useScrollPositionManager } from '@/nirax.js'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import { createModel, createOptions, followersTab, followingTab, mutualsTab } from '@/scripts/following-feed-utils.js'; + +const { + userList, + withNonPublic, + withQuotes, + withBots, + withReplies, + onlyFiles, + remoteWarningDismissed, +} = createModel(); + +const router = useRouter(); + +const userRecentNotes = shallowRef<InstanceType<typeof SkUserRecentNotes>>(); +const userScroll = shallowRef<HTMLElement>(); +const noteScroll = shallowRef<HTMLElement>(); + +const showRemoteWarning = computed(() => userList.value === 'followers' && !remoteWarningDismissed.value); + +// We have to disable the per-user feed on small displays, and it must be done through JS instead of CSS. +// Otherwise, the second column will waste resources in the background. +const wideViewportQuery = window.matchMedia('(min-width: 750px)'); +const isWideViewport: Ref<boolean> = ref(wideViewportQuery.matches); +wideViewportQuery.addEventListener('change', () => isWideViewport.value = wideViewportQuery.matches); + +const selectedUserId: Ref<string | null> = ref(null); + +function userSelected(user: Misskey.entities.UserLite): void { + if (isWideViewport.value) { + selectedUserId.value = user.id; + } else { + router.push(`/following-feed/${user.id}`); + } +} + +async function reloadLatestNotes() { + await latestNotesPaging.value?.reload(); +} + +async function reloadUserNotes() { + await userRecentNotes.value?.reload(); +} + +async function reload() { + await Promise.all([ + reloadLatestNotes(), + reloadUserNotes(), + ]); +} + +async function onListReady(): Promise<void> { + if (!selectedUserId.value && latestNotesPaging.value?.items.size) { + // This looks messy, but actually just gets the first user ID. + const selectedNote = latestNotesPaging.value.items.values().next().value; + + // We know this to be non-null because of the size check above. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + selectedUserId.value = selectedNote!.userId; + } +} + +async function onChangeTab(): Promise<void> { + selectedUserId.value = null; +} + +function isSoftMuted(note: Misskey.entities.Note): boolean { + return isMuted(note, $i?.mutedWords); +} + +function isHardMuted(note: Misskey.entities.Note): boolean { + return isMuted(note, $i?.hardMutedWords); +} + +// Match the typing used by Misskey +type Mutes = (string | string[])[] | null | undefined; + +// Adapted from MkNote.ts +function isMuted(note: Misskey.entities.Note, mutes: Mutes): boolean { + return checkMute(note, mutes) + || checkMute(note.reply, mutes) + || checkMute(note.renote, mutes); +} + +// Adapted from check-word-mute.ts +function checkMute(note: Misskey.entities.Note | undefined | null, mutes: Mutes): boolean { + if (!note) { + return false; + } + + if (!mutes || mutes.length < 1) { + return false; + } + + return checkWordMute(note, $i, mutes); +} + +const latestNotesPaging = shallowRef<InstanceType<typeof MkPagination>>(); + +const latestNotesPagination: Paging<'notes/following'> = { + endpoint: 'notes/following' as const, + limit: 20, + params: computed(() => ({ + list: userList.value, + filesOnly: onlyFiles.value, + includeNonPublic: withNonPublic.value, + includeReplies: withReplies.value, + includeQuotes: withQuotes.value, + includeBots: withBots.value, + })), +}; + +const headerActions: PageHeaderItem[] = [ + { + icon: 'ti ti-refresh', + text: i18n.ts.reload, + handler: () => reload(), + }, + createOptions(), +]; + +const headerTabs = computed(() => [ + { + key: followingTab, + icon: 'ph-user-check ph-bold ph-lg', + title: i18n.ts.following, + } satisfies Tab, + { + key: mutualsTab, + icon: 'ph-user-switch ph-bold ph-lg', + title: i18n.ts.mutuals, + } satisfies Tab, + { + key: followersTab, + icon: 'ph-user ph-bold ph-lg', + title: i18n.ts.followers, + } satisfies Tab, +]); + +useScrollPositionManager(() => getScrollContainer(userScroll.value ?? null), router); +useScrollPositionManager(() => getScrollContainer(noteScroll.value ?? null), router); +definePageMetadata(() => ({ + title: i18n.ts.following, + icon: 'ph-user-check ph-bold ph-lg', +})); + +</script> + +<style lang="scss" module> +//This inspection complains about duplicate "height" properties, but this is needed because "dvh" units are not supported in all browsers. +//The earlier "vh" provide a "close enough" approximation for older browsers. +//noinspection CssOverwrittenProperties +.root { + display: grid; + grid-template-columns: min-content 1fr min-content; + grid-template-rows: min-content 1fr; + grid-template-areas: + "header header header" + "lm notes rm"; + gap: 12px; + + height: 100vh; + height: 100dvh; + + // The universal layout inserts a "spacer" thing that causes a stray scroll bar. + // We have to create fake "space" for it to "roll up" and back into the viewport, which removes the scrollbar. + margin-bottom: calc(-1 * var(--minBottomSpacing)); + + // Some "just in case" backup properties. + // These should not be needed, but help to maintain the layout if the above trick ever stops working. + overflow: hidden; + position: sticky; + top: 0; +} + +.header { + grid-area: header; +} + +.notes { + grid-area: notes; + overflow-y: auto; +} + +.user { + grid-area: user; + overflow-y: auto; +} + +.remoteWarning { + margin: 12px 12px 0 12px; +} + +.userInfo { + margin-bottom: 12px; +} + +@media (min-width: 750px) { + .root { + grid-template-columns: min-content 4fr 6fr min-content; + grid-template-rows: min-content 1fr; + grid-template-areas: + "header header header header" + "lm notes user rm"; + gap: 24px; + } + + .remoteWarning { + margin: 24px 24px 0 24px; + } + + .userInfo { + margin-bottom: 24px; + } +} + +.panel { + background: var(--panel); +} +</style> diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 913758ba7e969d53d72f32ceeee2425bd599dd5d..6ed119c0c4cab9821900a13f87e64b8b70588fcf 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -72,7 +72,7 @@ import MkContainer from '@/components/MkContainer.vue'; import MkPagination from '@/components/MkPagination.vue'; import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; @@ -80,7 +80,7 @@ import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; const router = useRouter(); @@ -171,35 +171,35 @@ function reportAbuse() { function showMenu(ev: MouseEvent) { if (!post.value) return; - const menu: MenuItem[] = [ - ...($i && $i.id !== post.value.userId ? [ - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }, - ...($i.isModerator || $i.isAdmin ? [ - { - type: 'divider' as const, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: () => os.confirm({ - type: 'warning', - text: i18n.ts.deleteConfirm, - }).then(({ canceled }) => { - if (canceled || !post.value) return; - - os.apiWithDialog('gallery/posts/delete', { postId: post.value.id }); - }), - }, - ] : []), - ] : []), - ]; - - os.popupMenu(menu, ev.currentTarget ?? ev.target); + const menuItems: MenuItem[] = []; + + if ($i && $i.id !== post.value.userId) { + menuItems.push({ + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: reportAbuse, + }); + + if ($i.isModerator || $i.isAdmin) { + menuItems.push({ + type: 'divider', + }, { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: () => os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }).then(({ canceled }) => { + if (canceled || !post.value) return; + + os.apiWithDialog('gallery/posts/delete', { postId: post.value.id }); + }), + }); + } + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } watch(() => props.postId, fetchPost, { immediate: true }); diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 4ff26197d8427d7ccfe06d763a795bb4fb384e38..a5e6e5ac338fd5f363413ab31b44d8b0b6c385b6 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -43,12 +43,19 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._delivery._type[suspensionState] }} </template> </MkKeyValue> - <MkButton v-if="suspensionState === 'none'" :disabled="!instance" danger @click="stopDelivery">{{ i18n.ts._delivery.stop }}</MkButton> - <MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton> - <MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> - <MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> - <MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch> - <MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch> + <div class="_buttons"> + <MkButton inline :disabled="!instance" danger @click="deleteAllFiles">{{ i18n.ts.deleteAllFiles }}</MkButton> + <MkButton inline :disabled="!instance" danger @click="severAllFollowRelations">{{ i18n.ts.severAllFollowRelations }}</MkButton> + </div> + <MkSwitch v-model="isSuspended" :disabled="!instance" @update:modelValue="toggleSuspended">{{ i18n.ts._delivery.stop }}</MkSwitch> + <MkInfo v-if="isBaseBlocked" warn>{{ i18n.ts.blockedByBase }}</MkInfo> + <MkSwitch v-model="isBlocked" :disabled="!meta || !instance || isBaseBlocked" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> + <MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo> + <MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> + <MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">{{ i18n.ts.markInstanceAsNSFW }}</MkSwitch> + <MkSwitch v-model="rejectReports" :disabled="!instance" @update:modelValue="toggleRejectReports">{{ i18n.ts.rejectReports }}</MkSwitch> + <MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo> + <MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch> <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkTextarea v-model="moderationNote" manualSave> <template #label>{{ i18n.ts.moderationNote }}</template> @@ -123,6 +130,36 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> </MkPagination> </div> + <div v-else-if="tab === 'following'" key="following" class="_gaps_m"> + <MkPagination v-slot="{items}" :pagination="followingPagination"> + <div class="follow-relations-list"> + <div v-for="followRelationship in items" :key="followRelationship.id" class="follow-relation"> + <MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.followee.updatedAt)}`" :to="`/admin/user/${followRelationship.followee.id}`" class="user"> + <MkUserCardMini :user="followRelationship.followee" :withChart="false"/> + </MkA> + <span class="arrow">→</span> + <MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.follower.updatedAt)}`" :to="`/admin/user/${followRelationship.follower.id}`" class="user"> + <MkUserCardMini :user="followRelationship.follower" :withChart="false"/> + </MkA> + </div> + </div> + </MkPagination> + </div> + <div v-else-if="tab === 'followers'" key="followers" class="_gaps_m"> + <MkPagination v-slot="{items}" :pagination="followersPagination"> + <div class="follow-relations-list"> + <div v-for="followRelationship in items" :key="followRelationship.id" class="follow-relation"> + <MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.followee.updatedAt)}`" :to="`/admin/user/${followRelationship.followee.id}`" class="user"> + <MkUserCardMini :user="followRelationship.followee" :withChart="false"/> + </MkA> + <span class="arrow">â†</span> + <MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.follower.updatedAt)}`" :to="`/admin/user/${followRelationship.follower.id}`" class="user"> + <MkUserCardMini :user="followRelationship.follower" :withChart="false"/> + </MkA> + </div> + </div> + </MkPagination> + </div> <div v-else-if="tab === 'raw'" key="raw" class="_gaps_m"> <MkObjectView tall :value="instance"> </MkObjectView> @@ -135,7 +172,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed, watch } from 'vue'; import * as Misskey from 'misskey-js'; -import MkChart from '@/components/MkChart.vue'; +import MkChart, { type ChartSrc } from '@/components/MkChart.vue'; import MkObjectView from '@/components/MkObjectView.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; @@ -151,11 +188,13 @@ import { iAmModerator, iAmAdmin } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; -import MkPagination from '@/components/MkPagination.vue'; +import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { dateString } from '@/filters/date.js'; import MkTextarea from '@/components/MkTextarea.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import { $i } from '@/account.js'; const props = defineProps<{ host: string; @@ -163,19 +202,36 @@ const props = defineProps<{ const tab = ref('overview'); -const chartSrc = ref('instance-requests'); +const chartSrc = ref<ChartSrc>('instance-requests'); const meta = ref<Misskey.entities.AdminMetaResponse | null>(null); const instance = ref<Misskey.entities.FederationInstance | null>(null); const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none'); +const isSuspended = ref(false); const isBlocked = ref(false); const isSilenced = ref(false); const isNSFW = ref(false); +const rejectReports = ref(false); const isMediaSilenced = ref(false); const faviconUrl = ref<string | null>(null); const moderationNote = ref(''); +const baseDomains = computed(() => { + const domains: string[] = []; + + const parts = props.host.toLowerCase().split('.'); + for (let s = 1; s < parts.length; s++) { + const domain = parts.slice(s).join('.'); + domains.push(domain); + } + + return domains; +}); +const isBaseBlocked = computed(() => meta.value && baseDomains.value.some(d => meta.value?.blockedHosts.includes(d))); +const isBaseSilenced = computed(() => meta.value && baseDomains.value.some(d => meta.value?.silencedHosts.includes(d))); +const isBaseMediaSilenced = computed(() => meta.value && baseDomains.value.some(d => meta.value?.mediaSilencedHosts.includes(d))); + const usersPagination = { - endpoint: iAmModerator ? 'admin/show-users' : 'users' as const, + endpoint: iAmModerator ? 'admin/show-users' : 'users', limit: 10, params: { sort: '+updatedAt', @@ -183,11 +239,34 @@ const usersPagination = { hostname: props.host, }, offsetMode: true, +} satisfies Paging; + +const followingPagination = { + endpoint: 'federation/following' as const, + limit: 10, + params: { + host: props.host, + includeFollower: true, + }, + offsetMode: false, }; -watch(moderationNote, async () => { - await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value }); -}); +const followersPagination = { + endpoint: 'federation/followers' as const, + limit: 10, + params: { + host: props.host, + includeFollower: true, + }, + offsetMode: false, +}; + +if (iAmModerator) { + watch(moderationNote, async () => { + if (instance.value == null) return; + await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value }); + }); +} async function fetch(): Promise<void> { if (iAmAdmin) { @@ -197,15 +276,18 @@ async function fetch(): Promise<void> { host: props.host, }); suspensionState.value = instance.value?.suspensionState ?? 'none'; + isSuspended.value = suspensionState.value !== 'none'; isBlocked.value = instance.value?.isBlocked ?? false; isSilenced.value = instance.value?.isSilenced ?? false; isNSFW.value = instance.value?.isNSFW ?? false; + rejectReports.value = instance.value?.rejectReports ?? false; isMediaSilenced.value = instance.value?.isMediaSilenced ?? false; faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); moderationNote.value = instance.value?.moderationNote ?? ''; } async function toggleBlock(): Promise<void> { + if (!iAmAdmin) return; if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; @@ -215,6 +297,7 @@ async function toggleBlock(): Promise<void> { } async function toggleSilenced(): Promise<void> { + if (!iAmAdmin) return; if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; @@ -225,6 +308,7 @@ async function toggleSilenced(): Promise<void> { } async function toggleMediaSilenced(): Promise<void> { + if (!iAmAdmin) return; if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?'); const { host } = instance.value; @@ -234,33 +318,36 @@ async function toggleMediaSilenced(): Promise<void> { }); } -async function stopDelivery(): Promise<void> { +async function toggleSuspended(): Promise<void> { + if (!iAmModerator) return; if (!instance.value) throw new Error('No instance?'); - suspensionState.value = 'manuallySuspended'; + suspensionState.value = isSuspended.value ? 'manuallySuspended' : 'none'; await misskeyApi('admin/federation/update-instance', { host: instance.value.host, - isSuspended: true, + isSuspended: isSuspended.value, }); } -async function resumeDelivery(): Promise<void> { +async function toggleNSFW(): Promise<void> { + if (!iAmModerator) return; if (!instance.value) throw new Error('No instance?'); - suspensionState.value = 'none'; await misskeyApi('admin/federation/update-instance', { host: instance.value.host, - isSuspended: false, + isNSFW: isNSFW.value, }); } -async function toggleNSFW(): Promise<void> { +async function toggleRejectReports(): Promise<void> { + if (!iAmModerator) return; if (!instance.value) throw new Error('No instance?'); await misskeyApi('admin/federation/update-instance', { host: instance.value.host, - isNSFW: isNSFW.value, + rejectReports: rejectReports.value, }); } function refreshMetadata(): void { + if (!iAmModerator) return; if (!instance.value) throw new Error('No instance?'); misskeyApi('admin/federation/refresh-remote-instance-metadata', { host: instance.value.host, @@ -270,6 +357,50 @@ function refreshMetadata(): void { }); } +async function deleteAllFiles(): Promise<void> { + if (!iAmModerator) return; + if (!instance.value) throw new Error('No instance?'); + + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteAllFilesConfirm, + }); + if (confirm.canceled) return; + + await Promise.all([ + misskeyApi('admin/federation/delete-all-files', { + host: instance.value.host, + }), + os.alert({ + text: i18n.ts.deleteAllFilesQueued, + }), + ]); +} + +async function severAllFollowRelations(): Promise<void> { + if (!iAmModerator) return; + if (!instance.value) throw new Error('No instance?'); + + const confirm = await os.confirm({ + type: 'warning', + text: i18n.tsx.severAllFollowRelationsConfirm({ + instanceName: instance.value.name ?? instance.value.host, + followingCount: instance.value.followingCount, + followersCount: instance.value.followersCount, + }), + }); + if (confirm.canceled) return; + + await Promise.all([ + misskeyApi('admin/federation/remove-all-following', { + host: instance.value.host, + }), + os.alert({ + text: i18n.tsx.severAllFollowRelationsQueued({ host: instance.value.host }), + }), + ]); +} + fetch(); const headerActions = computed(() => [{ @@ -292,12 +423,28 @@ const headerTabs = computed(() => [{ key: 'users', title: i18n.ts.users, icon: 'ti ti-users', -}, { +}, ...getFollowingTabs(), { key: 'raw', title: 'Raw', icon: 'ti ti-code', }]); +function getFollowingTabs() { + if (!$i) return []; + return [ + { + key: 'following', + title: i18n.ts.following, + icon: 'ti ti-arrow-right', + }, + { + key: 'followers', + title: i18n.ts.followers, + icon: 'ti ti-arrow-left', + }, + ]; +} + definePageMetadata(() => ({ title: props.host, icon: 'ti ti-server', @@ -334,4 +481,31 @@ definePageMetadata(() => ({ } } } + +.follow-relations-list { + display: flex; + flex-direction: column; + gap: 12px; + + .follow-relation { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: nowrap; + justify-content: space-between; + + .user { + flex: 1; + max-width: 45%; + flex-shrink: 0; + overflow: hidden; + text-overflow: ellipsis; + } + + .arrow { + font-size: 1.5em; + flex-shrink: 0; + } + } +} </style> diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index a2ceb222feeac22490ca9f03ecabc28189ef7688..5f195693ccc8a23f12aa9a3044c45b33bce69da1 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -134,12 +134,14 @@ async function removeUser(item, ev) { async function showMembershipMenu(item, ev) { const withRepliesRef = ref(item.withReplies); + os.popupMenu([{ type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, icon: 'ti ti-messages', ref: withRepliesRef, }], ev.currentTarget ?? ev.target); + watch(withRepliesRef, withReplies => { misskeyApi('users/lists/update-membership', { listId: list.value!.id, diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 28f583829653684e0053c8757ded31b24652dba4..bd93fc8369659ef96d04c9ee915e24a6a7a84302 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -30,7 +30,7 @@ import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; const tab = ref('all'); const includeTypes = ref<string[] | null>(null); diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 6bc3c0908a4998a10474cc65f8d8fbea618ebf94..ac9f3e74013d87e8e6325cd7081dbd8a1ef8534d 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -69,6 +69,7 @@ import MkButton from '@/components/MkButton.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; +import { url } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { selectFile } from '@/scripts/select-file.js'; diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index fc8627a772c10ad3d6e9c50e01962848b9d1bf86..12b70fa64f41a62ba4469f058935a5f6768b1dac 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -104,7 +104,7 @@ import XPage from '@/components/page/page.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import MkMediaImage from '@/components/MkMediaImage.vue'; import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; @@ -121,7 +121,7 @@ import { instance } from '@/instance.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; const router = useRouter(); @@ -165,18 +165,23 @@ function fetchPage() { function share(ev: MouseEvent) { if (!page.value) return; - os.popupMenu([ - { - text: i18n.ts.shareWithNote, - icon: 'ti ti-pencil', - action: shareWithNote, - }, - ...(isSupportShare() ? [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ + text: i18n.ts.shareWithNote, + icon: 'ti ti-pencil', + action: shareWithNote, + }); + + if (isSupportShare()) { + menuItems.push({ text: i18n.ts.share, icon: 'ti ti-share', action: shareWithNavigator, - }] : []), - ], ev.currentTarget ?? ev.target); + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } function copyLink() { @@ -256,51 +261,59 @@ function reportAbuse() { function showMenu(ev: MouseEvent) { if (!page.value) return; - const menu: MenuItem[] = [ - ...($i && $i.id === page.value.userId ? [ - { - icon: 'ti ti-code', - text: i18n.ts._pages.viewSource, - action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`), - }, - ...($i.pinnedPageId === page.value.id ? [{ + const menuItems: MenuItem[] = []; + + if ($i && $i.id === page.value.userId) { + menuItems.push({ + icon: 'ti ti-pencil', + text: i18n.ts._pages.editThisPage, + action: () => router.push(`/pages/edit/${page.value.id}`), + }); + + if ($i.pinnedPageId === page.value.id) { + menuItems.push({ icon: 'ti ti-pinned-off', text: i18n.ts.unpin, action: () => pin(false), - }] : [{ + }); + } else { + menuItems.push({ icon: 'ti ti-pin', text: i18n.ts.pin, action: () => pin(true), - }]), - ] : []), - ...($i && $i.id !== page.value.userId ? [ - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }, - ...($i.isModerator || $i.isAdmin ? [ - { - type: 'divider' as const, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: () => os.confirm({ - type: 'warning', - text: i18n.ts.deleteConfirm, - }).then(({ canceled }) => { - if (canceled || !page.value) return; - - os.apiWithDialog('pages/delete', { pageId: page.value.id }); - }), - }, - ] : []), - ] : []), - ]; - - os.popupMenu(menu, ev.currentTarget ?? ev.target); + }); + } + } else if ($i && $i.id !== page.value.userId) { + menuItems.push({ + icon: 'ti ti-code', + text: i18n.ts._pages.viewSource, + action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`), + }, { + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: reportAbuse, + }); + + if ($i.isModerator || $i.isAdmin) { + menuItems.push({ + type: 'divider', + }, { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: () => os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }).then(({ canceled }) => { + if (canceled || !page.value) return; + + os.apiWithDialog('pages/delete', { pageId: page.value.id }); + }), + }); + } + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } watch(() => path.value, fetchPage, { immediate: true }); @@ -433,13 +446,12 @@ definePageMetadata(() => ({ .pageBannerTitleUser { --height: 32px; flex-shrink: 0; + line-height: var(--height); .avatar { height: var(--height); width: var(--height); } - - line-height: var(--height); } .pageBannerTitleSubActions { diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index bac1d2bb700752e7bfc2f142d9c2266acec87412..8dcb8fa477a898a72f331e8f3adbefdb099d21c4 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton> <FormSection v-if="keys"> - <template #label>{{ i18n.ts.keys }}</template> + <template #label>{{ i18n.ts._registry.keys }}</template> <div class="_gaps_s"> <FormLink v-for="key in keys" :to="`/registry/value/${props.domain}/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink> </div> diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 7d9cefa5c9f44e7d2b91806967ac4289c0b910f3..54e66f6e169bf685c1216e06884cf11937a7d17f 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -149,9 +149,9 @@ import MkButton from '@/components/MkButton.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import { deepClone } from '@/scripts/clone.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { signinRequired } from '@/account.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { userPage } from '@/filters/user.js'; diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index 31c0003130ca9a6c8c26e238d889e0e0f2880cc5..08bb3cb76cea203f05b7e579b17c215ff854d62e 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -121,7 +121,7 @@ import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { useRouter } from '@/router/supplier.js'; const $i = signinRequired(); diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 97a793753d8b18598e30513b90798fd6ecd29535..10ea3717abbaf895e48cdd2eb610f6b65d367d0e 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -20,9 +20,9 @@ import { useStream } from '@/stream.js'; import { signinRequired } from '@/account.js'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const $i = signinRequired(); diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index 51a03e441801df034807441aca575e3780ed9432..d823861b4ac634e73c3f5fa41cc69aafb8afb7d4 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -117,7 +117,7 @@ import { $i } from '@/account.js'; import MkPagination from '@/components/MkPagination.vue'; import { useRouter } from '@/router/supplier.js'; import * as os from '@/os.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { pleaseLogin } from '@/scripts/please-login.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 45edbc5da284b91f8a12c9235134b0f7826fdb66..9ee3fd7f01b2db7d1488e23e93d067d967f71d70 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template> + <template #header><MkPageHeader v-model:tab="tab" :displayBackButton="true" :tabs="headerTabs"/></template> <MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200"> <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> @@ -43,7 +43,7 @@ import MkUserList from '@/components/MkUserList.vue'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { serverErrorImageUrl, infoImageUrl } from '@/instance.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index 9aaa8ff9c633cc589ec93d663a76b4ec23404ea5..155d8b82d71cbc25e0f87fab4b38c4d34052d063 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -30,6 +30,24 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkContainer> + <MkContainer :foldable="true" :expanded="false"> + <template #header>{{ i18n.ts.uiInspector }}</template> + <div :class="$style.uiInspector"> + <div v-for="c in components" :key="c.value.id" :class="{ [$style.uiInspectorUnShown]: !showns.has(c.value.id) }"> + <div :class="$style.uiInspectorType">{{ c.value.type }}</div> + <div :class="$style.uiInspectorId">{{ c.value.id }}</div> + <button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))"> + <i v-if="uiInspectorOpenedComponents.get(c)" class="ti ti-chevron-up icon"></i> + <i v-else class="ti ti-chevron-down icon"></i> + </button> + <div v-if="uiInspectorOpenedComponents.get(c)"> + <MkTextarea :modelValue="stringifyUiProps(c.value)" code readonly></MkTextarea> + </div> + </div> + <div :class="$style.uiInspectorDescription">{{ i18n.ts.uiInspectorDescription }}</div> + </div> + </MkContainer> + <div class=""> {{ i18n.ts.scratchpadDescription }} </div> @@ -43,6 +61,7 @@ import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue'; import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import MkContainer from '@/components/MkContainer.vue'; import MkButton from '@/components/MkButton.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; import * as os from '@/os.js'; @@ -61,6 +80,7 @@ const logs = ref<any[]>([]); const root = ref<AsUiRoot>(); const components = ref<Ref<AsUiComponent>[]>([]); const uiKey = ref(0); +const uiInspectorOpenedComponents = ref(new Map<string, boolean>); const saved = miLocalStorage.getItem('scratchpad'); if (saved) { @@ -71,6 +91,14 @@ watch(code, () => { miLocalStorage.setItem('scratchpad', code.value); }); +function stringifyUiProps(uiProps) { + return JSON.stringify( + { ...uiProps, type: undefined, id: undefined }, + (k, v) => typeof v === 'function' ? '<function>' : v, + 2 + ); +} + async function run() { if (aiscript) aiscript.abort(); root.value = undefined; @@ -152,6 +180,20 @@ const headerActions = computed(() => []); const headerTabs = computed(() => []); +const showns = computed(() => { + const result = new Set<string>(); + (function addChildrenToResult(c: AsUiComponent) { + result.add(c.id); + if (c.children) { + const childComponents = components.value.filter(v => c.children.includes(v.value.id)); + for (const child of childComponents) { + addChildrenToResult(child.value); + } + } + })(root.value); + return result; +}); + definePageMetadata(() => ({ title: i18n.ts.scratchpad, icon: 'ti ti-terminal-2', @@ -192,4 +234,39 @@ definePageMetadata(() => ({ } } } + +.uiInspector { + display: grid; + gap: 8px; + padding: 16px; +} + +.uiInspectorUnShown { + color: var(--fgTransparent); +} + +.uiInspectorType { + display: inline-block; + border: hidden; + border-radius: 10px; + background-color: var(--panelHighlight); + padding: 2px 8px; + font-size: 12px; +} + +.uiInspectorId { + display: inline-block; + padding-left: 8px; +} + +.uiInspectorDescription { + display: block; + font-size: 12px; + padding-top: 16px; +} + +.uiInspectorPropsToggle { + background: none; + border: none; +} </style> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue index 0767fa7864c6b5493a7bf7a00a0bf08d0ce882f1..9f7852a71d74e057345bf65150aaa8edfee21207 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click="emit('click')" > <div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div> - <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/> + <MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY, showBelow }]" forceShowDecoration/> <i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i> </div> </template> @@ -32,6 +32,7 @@ const props = defineProps<{ flipH?: boolean; offsetX?: number; offsetY?: number; + showBelow?: boolean; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index ce1d4e48d829801d93968870ee1e87060113ac31..4ec4610279a583d08c5666530219fc7c7fdef88b 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -29,6 +29,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRange v-model="offsetY" continuousUpdate :min="-0.25" :max="0.25" :step="0.025" :textConverter="(v) => `${Math.floor(v * 100)}%`"> <template #label>Y {{ i18n.ts.position }}</template> </MkRange> + <MkSwitch v-model="showBelow"> + <template #label>{{ i18n.ts.showBelowAvatar }}</template> + </MkSwitch> <MkSwitch v-model="flipH"> <template #label>{{ i18n.ts.flip }}</template> </MkSwitch> @@ -71,12 +74,14 @@ const emit = defineEmits<{ flipH: boolean; offsetX: number; offsetY: number; + showBelow: boolean; }): void; (ev: 'update', payload: { angle: number; flipH: boolean; offsetX: number; offsetY: number; + showBelow: boolean; }): void; (ev: 'detach'): void; }>(); @@ -87,6 +92,7 @@ const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIn const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false); const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0); const offsetY = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetY : null) ?? 0); +const showBelow = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].showBelow : null) ?? false); const decorationsForPreview = computed(() => { const decoration = { @@ -96,6 +102,8 @@ const decorationsForPreview = computed(() => { flipH: flipH.value, offsetX: offsetX.value, offsetY: offsetY.value, + showBelow: showBelow.value, + blink: true, }; const decorations = [...$i.avatarDecorations]; if (props.usingIndex != null) { @@ -116,6 +124,7 @@ async function update() { flipH: flipH.value, offsetX: offsetX.value, offsetY: offsetY.value, + showBelow: showBelow.value, }); dialog.value.close(); } @@ -126,6 +135,7 @@ async function attach() { flipH: flipH.value, offsetX: offsetX.value, offsetY: offsetY.value, + showBelow: showBelow.value, }); dialog.value.close(); } diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index 77229d3349d04c5fa331f67076083877411ff40f..5324a6b7f74d3d6ccb663ca93fbf421385458820 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only :flipH="avatarDecoration.flipH" :offsetX="avatarDecoration.offsetX" :offsetY="avatarDecoration.offsetY" + :showBelow="avatarDecoration.showBelow" :active="true" @click="openDecoration(avatarDecoration, i)" /> @@ -78,6 +79,7 @@ function openDecoration(avatarDecoration, index?: number) { flipH: payload.flipH, offsetX: payload.offsetX, offsetY: payload.offsetY, + showBelow: payload.showBelow, }; const update = [...$i.avatarDecorations, decoration]; await os.apiWithDialog('i/update', { @@ -92,6 +94,7 @@ function openDecoration(avatarDecoration, index?: number) { flipH: payload.flipH, offsetX: payload.offsetX, offsetY: payload.offsetY, + showBelow: payload.showBelow, }; const update = [...$i.avatarDecorations]; update[index] = decoration; diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 3f7db1b779dd44832a13973d5ecb8dfdf9998d8a..432295aa7f1fe6216066a45c9a616f9ecaa0a11a 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option v-for="x in sortOptions" :key="x.value" :value="x.value">{{ x.displayName }}</option> </MkSelect> <div v-if="!fetching"> - <MkPagination v-slot="{items}" :pagination="pagination"> + <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination"> <div class="_gaps"> <div v-for="file in items" :key="file.id" @@ -48,8 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { computed, ref, watch, type StyleValue } from 'vue'; +import { computed, ref, shallowRef, watch, type StyleValue } from 'vue'; import tinycolor from 'tinycolor2'; +import * as Misskey from 'misskey-js'; +import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import MkPagination from '@/components/MkPagination.vue'; @@ -58,13 +60,16 @@ import { i18n } from '@/i18n.js'; import bytes from '@/filters/bytes.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkSelect from '@/components/MkSelect.vue'; -import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; + +const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>(); const sortMode = ref('+size'); const pagination = { endpoint: 'drive/files' as const, limit: 10, params: computed(() => ({ sort: sortMode.value })), + offsetMode: true, }; const sortOptions = [ @@ -109,6 +114,46 @@ function genUsageBar(fsize: number): StyleValue { }; } +async function deleteFile(file: Misskey.entities.DriveFile) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), + }); + + if (canceled) return; + misskeyApi('drive/files/delete', { + fileId: file.id, + }); + + if (paginationComponent.value) { + paginationComponent.value.items.delete(file.id); + } +} + +function getDriveFileMenu(file: Misskey.entities.DriveFile): MenuItem[] { + const menuItems: MenuItem[] = []; + + menuItems.push({ + text: i18n.ts.copyUrl, + icon: 'ti ti-link', + action: () => copyToClipboard(file.url), + }, { + type: 'a', + href: file.url, + target: '_blank', + text: i18n.ts.download, + icon: 'ti ti-download', + download: file.name, + }, { type: 'divider' }, { + text: i18n.ts.delete, + icon: 'ti ti-trash', + danger: true, + action: () => deleteFile(file), + }); + + return menuItems; +} + function onClick(ev: MouseEvent, file) { os.popupMenu(getDriveFileMenu(file), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index fa0963784432c432233b04c4e576954faa43bf0a..c1e325863176bcf730f39c4e7d274ce1e4a4cfb2 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.keepOriginalFilename }}</template> <template #caption>{{ i18n.ts.keepOriginalFilenameDescription }}</template> </MkSwitch> - <MkSwitch v-model="alwaysMarkNsfw" @update:modelValue="saveProfile()"> + <MkSwitch v-model="defaultSensitive" @update:modelValue="saveProfile()"> <template #label>{{ i18n.ts.alwaysMarkSensitive }}</template> </MkSwitch> </div> @@ -80,7 +80,7 @@ const fetching = ref(true); const usage = ref<number | null>(null); const capacity = ref<number | null>(null); const uploadFolder = ref<Misskey.entities.DriveFolder | null>(null); -const alwaysMarkNsfw = ref($i.alwaysMarkNsfw); +const defaultSensitive = ref($i.defaultSensitive); const meterStyle = computed(() => { if (!capacity.value || !usage.value) return {}; @@ -127,14 +127,14 @@ function chooseUploadFolder() { function saveProfile() { misskeyApi('i/update', { - alwaysMarkNsfw: !!alwaysMarkNsfw.value, + defaultSensitive: !!defaultSensitive.value, }).catch(err => { os.alert({ type: 'error', title: i18n.ts.error, text: err.message, }); - alwaysMarkNsfw.value = true; + defaultSensitive.value = true; }); } diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue index 1df723c850366ddaeb3d0241d3bd01d8ea5e7c6a..bb919c6c5395e4dab506da97a623fc396b08f3b0 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -124,10 +124,13 @@ SPDX-License-Identifier: AGPL-3.0-only <option :value="4">{{ i18n.ts.large }}+</option> </MkRadios> - <MkSwitch v-model="emojiPickerUseDrawerForMobile"> - {{ i18n.ts.useDrawerReactionPickerForMobile }} + <MkSelect v-model="emojiPickerStyle"> + <template #label>{{ i18n.ts.style }}</template> <template #caption>{{ i18n.ts.needReloadToApply }}</template> - </MkSwitch> + <option value="auto">{{ i18n.ts.auto }}</option> + <option value="popup">{{ i18n.ts.popup }}</option> + <option value="drawer">{{ i18n.ts.drawer }}</option> + </MkSelect> </div> </FormSection> </div> @@ -140,7 +143,7 @@ import MkRadios from '@/components/MkRadios.vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; import FromSlot from '@/components/form/slot.vue'; -import MkSwitch from '@/components/MkSwitch.vue'; +import MkSelect from '@/components/MkSelect.vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; @@ -159,7 +162,7 @@ const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmoji const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale')); const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth')); const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight')); -const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile')); +const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle')); const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev); const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev); diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 637c1b24b9fc88ffc8cf3685b1edf0f40f94c77d..b42c2f15030d422c6a59cc124e5d07c728f2b1e8 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -14,16 +14,17 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink> </template> </I18n> + <!-- + <br /> + <I18n :src="i18n.ts.i18nInfoSharkey" tag="span"> + <template #link> + <MkLink url="https://crowdin.com/project/misskey">INSERT THINGY</MkLink> + </template> + </I18n> + --> </template> </MkSelect> - <MkRadios v-model="hemisphere"> - <template #label>{{ i18n.ts.hemisphere }}</template> - <option value="N">{{ i18n.ts._hemisphere.N }}</option> - <option value="S">{{ i18n.ts._hemisphere.S }}</option> - <template #caption>{{ i18n.ts._hemisphere.caption }}</template> - </MkRadios> - <MkRadios v-model="overridedDeviceKind"> <template #label>{{ i18n.ts.overridedDeviceKind }}</template> <option :value="null">{{ i18n.ts.auto }}</option> @@ -68,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch> <MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch> <MkSwitch v-model="showTickerOnReplies">{{ i18n.ts.showTickerOnReplies }}</MkSwitch> + <MkSwitch v-model="disableCatSpeak">{{ i18n.ts.disableCatSpeak }}</MkSwitch> <MkSelect v-model="searchEngine" placeholder="Other"> <template #label>{{ i18n.ts.searchEngine }}</template> <option @@ -169,6 +171,8 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option> </MkRadios> + <MkSwitch v-model="notificationClickable">{{ i18n.ts.allowClickingNotifications }}</MkSwitch> + <MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton> </div> </FormSection> @@ -186,12 +190,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch> <MkSwitch v-model="showAvatarDecorations">{{ i18n.ts.showAvatarDecorations }}</MkSwitch> <MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch> - <MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch> <MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch> <MkSwitch v-model="oneko">{{ i18n.ts.oneko }}</MkSwitch> <MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch> <MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch> </div> + + <MkSelect v-model="menuStyle"> + <template #label>{{ i18n.ts.menuStyle }}</template> + <option value="auto">{{ i18n.ts.auto }}</option> + <option value="popup">{{ i18n.ts.popup }}</option> + <option value="drawer">{{ i18n.ts.drawer }}</option> + </MkSelect> + <div> <MkRadios v-model="emojiStyle"> <template #label>{{ i18n.ts.emojiStyle }}</template> @@ -230,11 +241,11 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch> <MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch> <MkSwitch v-model="clickToOpen">{{ i18n.ts.clickToOpen }}</MkSwitch> - <MkSwitch v-model="showBots">{{ i18n.ts.showBots }}</MkSwitch> <MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch> <MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch> <MkSwitch v-model="alwaysConfirmFollow">{{ i18n.ts.alwaysConfirmFollow }}</MkSwitch> <MkSwitch v-model="confirmWhenRevealingSensitiveMedia">{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</MkSwitch> + <MkSwitch v-model="warnExternalUrl">{{ i18n.ts.warnExternalUrl }}</MkSwitch> </div> <MkSelect v-model="serverDisconnectedBehavior"> <template #label>{{ i18n.ts.whenServerDisconnected }}</template> @@ -306,6 +317,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.other }}</template> <div class="_gaps"> + <MkRadios v-model="hemisphere"> + <template #label>{{ i18n.ts.hemisphere }}</template> + <option value="N">{{ i18n.ts._hemisphere.N }}</option> + <option value="S">{{ i18n.ts._hemisphere.S }}</option> + <template #caption>{{ i18n.ts._hemisphere.caption }}</template> + </MkRadios> <MkFolder> <template #label>{{ i18n.ts.additionalEmojiDictionary }}</template> <div class="_buttons"> @@ -325,6 +342,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, reactive, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { langs } from '@@/js/config.js'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import MkRadios from '@/components/MkRadios.vue'; @@ -336,12 +354,11 @@ import FormSection from '@/components/form/section.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/MkLink.vue'; import MkInfo from '@/components/MkInfo.vue'; -import { langs } from '@/config.js'; import { searchEngineMap } from '@/scripts/search-engine-map.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -356,16 +373,6 @@ const cornerRadius = ref(miLocalStorage.getItem('cornerRadius')); const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null); const dataSaver = ref(defaultStore.state.dataSaver); -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere')); const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind')); const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior')); @@ -376,14 +383,6 @@ const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthO const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes')); const collapseNotesRepliedTo = computed(defaultStore.makeGetterSetter('collapseNotesRepliedTo')); const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen')); -// copied from src/pages/timeline.vue -const showBots = computed<boolean>({ - get: () => defaultStore.reactiveState.tl.value.filter.withBots, - set: (newValue) => { - const out = deepMerge({ filter: { withBots: newValue } }, defaultStore.state.tl); - defaultStore.set('tl', out); - }, -}); const collapseFiles = computed(defaultStore.makeGetterSetter('collapseFiles')); const autoloadConversation = computed(defaultStore.makeGetterSetter('autoloadConversation')); const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v)); @@ -395,11 +394,12 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm')); const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount')); const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle')); -const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); +const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle')); const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds')); const oneko = computed(defaultStore.makeGetterSetter('oneko')); const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); +const disableCatSpeak = computed(defaultStore.makeGetterSetter('disableCatSpeak')); const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia')); const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); const enableFaviconNotificationDot = computed(defaultStore.makeGetterSetter('enableFaviconNotificationDot')); @@ -417,6 +417,7 @@ const showAvatarDecorations = computed(defaultStore.makeGetterSetter('showAvatar const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance')); const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition')); const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis')); +const notificationClickable = computed(defaultStore.makeGetterSetter('notificationClickable')); const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn')); const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline')); const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications')); @@ -435,6 +436,7 @@ const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('u const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow')); const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia')); const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu')); +const warnExternalUrl = computed(defaultStore.makeGetterSetter('warnExternalUrl')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -496,8 +498,9 @@ watch([ alwaysConfirmFollow, confirmWhenRevealingSensitiveMedia, contextMenu, + warnExternalUrl, ], async () => { - await reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const; diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 9c02b604c07c493af2d21ad21c7aa75ab423ccbb..e000c608fe37783db2256d9ac1b55b9a4beff639 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </div> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportFollowing"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkSwitch v-model="withReplies"> @@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportUserLists"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportMuting"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportBlocking"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -123,7 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder v-if="$i && !$i.movedTo"> + <MkFolder v-if="$i && !$i.movedTo && $i.policies.canImportAntennas"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index 7f8460e316409cf33cbc998369885ba96a36286f..a0e6cad9c8d4484b32f463bac4de2b50f8b06eb9 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -54,7 +54,7 @@ import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { defaultStore } from '@/store.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -67,16 +67,6 @@ const items = ref(defaultStore.state.menu.map(x => ({ const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - async function addItem() { const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k)); const { canceled, result: item } = await os.select({ @@ -100,7 +90,7 @@ function removeItem(index: number) { async function save() { defaultStore.set('menu', items.value.map(x => x.type)); - await reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); } function reset() { @@ -111,7 +101,7 @@ function reset() { } watch(menuDisplay, async () => { - await reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index a861f6ee0d227116267ac3bcfa007e6f066487f8..491102fece49475bbf47871734ec5c4c7c62af5e 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -69,12 +69,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; -import { notificationTypes } from '@/const.js'; +import { notificationTypes } from '@@/js/const.js'; const $i = signinRequired(); -const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted']; -const notificationTypesWithoutSender = ['achievementEarned']; +const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[]; +const notificationTypesWithoutSender = ['achievementEarned'] as const satisfies (typeof notificationTypes[number])[]; const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 86bbae431d93c2f7e1c5ddab6e5b49860b66d292..eb9d158c1e08a494427fc6a74d9cd2fc2bed85a4 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -62,8 +62,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.experimentalFeatures }}</template> <div class="_gaps_m"> - <MkSwitch v-model="enableCondensedLineForAcct"> - <template #label>Enable condensed line for acct</template> + <MkSwitch v-model="enableCondensedLine"> + <template #label>Enable condensed line</template> </MkSwitch> </div> </MkFolder> @@ -109,13 +109,13 @@ import { defaultStore } from '@/store.js'; import { signout, signinRequired } from '@/account.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import FormSection from '@/components/form/section.vue'; const $i = signinRequired(); const reportError = computed(defaultStore.makeGetterSetter('reportError')); -const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct')); +const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine')); const devMode = computed(defaultStore.makeGetterSetter('devMode')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); @@ -143,16 +143,6 @@ async function deleteAccount() { await signout(); } -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - async function updateRepliesAll(withReplies: boolean) { const { canceled } = await os.confirm({ type: 'warning', @@ -178,9 +168,9 @@ const exportData = () => { }; watch([ - enableCondensedLineForAcct, + enableCondensedLine, ], async () => { - await reloadAsk(); + await reloadAsk(); }); const headerActions = computed(() => []); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index f9fd494ce95f272ac12a9d973c2a7c1ea3c4beb9..8036ef5555bc0230bd78d15bb587d8f0f2d581d5 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> <div :class="$style.buttons"> - <MkButton inline primary @click="saveNew">{{ ts._preferencesBackups.saveNew }}</MkButton> - <MkButton inline @click="loadFile">{{ ts._preferencesBackups.loadFile }}</MkButton> + <MkButton inline primary @click="saveNew">{{ i18n.ts._preferencesBackups.saveNew }}</MkButton> + <MkButton inline @click="loadFile">{{ i18n.ts._preferencesBackups.loadFile }}</MkButton> </div> <FormSection> - <template #label>{{ ts._preferencesBackups.list }}</template> + <template #label>{{ i18n.ts._preferencesBackups.list }}</template> <template v-if="profiles && Object.keys(profiles).length > 0"> <div class="_gaps_s"> <div @@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.prevent.stop="$event => menu($event, id)" > <div :class="$style.profileName">{{ profile.name }}</div> - <div :class="$style.profileTime">{{ t('_preferencesBackups.createdAt', { date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div> - <div v-if="profile.updatedAt" :class="$style.profileTime">{{ t('_preferencesBackups.updatedAt', { date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div> + <div :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.createdAt({ date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div> + <div v-if="profile.updatedAt" :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.updatedAt({ date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div> </div> </div> </template> <div v-else-if="profiles"> - <MkInfo>{{ ts._preferencesBackups.noBackups }}</MkInfo> + <MkInfo>{{ i18n.ts._preferencesBackups.noBackups }}</MkInfo> </div> <MkLoading v-else/> </FormSection> @@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, onUnmounted, ref } from 'vue'; import { v4 as uuid } from 'uuid'; +import { version, host } from '@@/js/config.js'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -49,10 +50,8 @@ import { unisonReload } from '@/scripts/unison-reload.js'; import { useStream } from '@/stream.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; -import { version, host } from '@/config.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; -const { t, ts } = i18n; const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'collapseRenotes', @@ -77,9 +76,10 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'enableFaviconNotificationDot', 'imageNewTab', 'dataSaver', + 'disableCatSpeak', 'disableShowingAnimatedImages', 'emojiStyle', - 'disableDrawer', + 'menuStyle', 'useBlurEffectForModal', 'useBlurEffect', 'showFixedPostForm', @@ -91,7 +91,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'emojiPickerScale', 'emojiPickerWidth', 'emojiPickerHeight', - 'emojiPickerUseDrawerForMobile', + 'emojiPickerStyle', 'defaultSideView', 'menuDisplay', 'reportError', @@ -109,7 +109,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'mediaListWithOneImageAppearance', 'notificationPosition', 'notificationStackAxis', - 'enableCondensedLineForAcct', 'keepScreenOn', 'defaultWithReplies', 'disableStreamingTimeline', @@ -210,15 +209,15 @@ async function saveNew(): Promise<void> { if (!profiles.value) return; const { canceled, result: name } = await os.inputText({ - title: ts._preferencesBackups.inputName, + title: i18n.ts._preferencesBackups.inputName, default: '', }); if (canceled) return; if (Object.values(profiles.value).some(x => x.name === name)) { return os.alert({ - title: ts._preferencesBackups.cannotSave, - text: t('_preferencesBackups.nameAlreadyExists', { name }), + title: i18n.ts._preferencesBackups.cannotSave, + text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }), }); } @@ -247,8 +246,8 @@ function loadFile(): void { if (file.type !== 'application/json') { return os.alert({ type: 'error', - title: ts._preferencesBackups.cannotLoad, - text: ts._preferencesBackups.invalidFile, + title: i18n.ts._preferencesBackups.cannotLoad, + text: i18n.ts._preferencesBackups.invalidFile, }); } @@ -259,7 +258,7 @@ function loadFile(): void { } catch (err) { return os.alert({ type: 'error', - title: ts._preferencesBackups.cannotLoad, + title: i18n.ts._preferencesBackups.cannotLoad, text: (err as any)?.message ?? '', }); } @@ -285,8 +284,8 @@ async function applyProfile(id: string): Promise<void> { const { canceled: cancel1 } = await os.confirm({ type: 'warning', - title: ts._preferencesBackups.apply, - text: t('_preferencesBackups.applyConfirm', { name: profile.name }), + title: i18n.ts._preferencesBackups.apply, + text: i18n.tsx._preferencesBackups.applyConfirm({ name: profile.name }), }); if (cancel1) return; @@ -345,7 +344,7 @@ async function applyProfile(id: string): Promise<void> { const { canceled: cancel2 } = await os.confirm({ type: 'info', - text: ts.reloadToApplySetting, + text: i18n.ts.reloadToApplySetting, }); if (cancel2) return; @@ -357,8 +356,8 @@ async function deleteProfile(id: string): Promise<void> { const { canceled } = await os.confirm({ type: 'info', - title: ts.delete, - text: t('deleteAreYouSure', { x: profiles.value[id].name }), + title: i18n.ts.delete, + text: i18n.tsx.deleteAreYouSure({ x: profiles.value[id].name }), }); if (canceled) return; @@ -373,8 +372,8 @@ async function save(id: string): Promise<void> { const { canceled } = await os.confirm({ type: 'info', - title: ts._preferencesBackups.save, - text: t('_preferencesBackups.saveConfirm', { name }), + title: i18n.ts._preferencesBackups.save, + text: i18n.tsx._preferencesBackups.saveConfirm({ name }), }); if (canceled) return; @@ -393,15 +392,15 @@ async function rename(id: string): Promise<void> { if (!profiles.value) return; const { canceled: cancel1, result: name } = await os.inputText({ - title: ts._preferencesBackups.inputName, + title: i18n.ts._preferencesBackups.inputName, default: '', }); if (cancel1 || profiles.value[id].name === name) return; if (Object.values(profiles.value).some(x => x.name === name)) { return os.alert({ - title: ts._preferencesBackups.cannotSave, - text: t('_preferencesBackups.nameAlreadyExists', { name }), + title: i18n.ts._preferencesBackups.cannotSave, + text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }), }); } @@ -409,8 +408,8 @@ async function rename(id: string): Promise<void> { const { canceled: cancel2 } = await os.confirm({ type: 'info', - title: ts.rename, - text: t('_preferencesBackups.renameConfirm', { old: registry.name, new: name }), + title: i18n.ts.rename, + text: i18n.tsx._preferencesBackups.renameConfirm({ old: registry.name, new: name }), }); if (cancel2) return; @@ -422,25 +421,25 @@ function menu(ev: MouseEvent, profileId: string) { if (!profiles.value) return; return os.popupMenu([{ - text: ts._preferencesBackups.apply, + text: i18n.ts._preferencesBackups.apply, icon: 'ti ti-check', action: () => applyProfile(profileId), }, { type: 'a', - text: ts.download, + text: i18n.ts.download, icon: 'ti ti-download', href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })), download: `${profiles.value[profileId].name}.json`, }, { type: 'divider' }, { - text: ts.rename, + text: i18n.ts.rename, icon: 'ti ti-forms', action: () => rename(profileId), }, { - text: ts._preferencesBackups.save, + text: i18n.ts._preferencesBackups.save, icon: 'ti ti-device-floppy', action: () => save(profileId), }, { type: 'divider' }, { - text: ts.delete, + text: i18n.ts.delete, icon: 'ti ti-trash', action: () => deleteProfile(profileId), danger: true, @@ -462,7 +461,7 @@ onUnmounted(() => { }); definePageMetadata(() => ({ - title: ts.preferencesBackups, + title: i18n.ts.preferencesBackups, icon: 'ti ti-device-floppy', })); </script> diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 6cc19db127b01f45a1293c3db5b83c13ae61e6d6..c94cd512f3a56f5d465cfdb3698d787e6b06d9e8 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -94,15 +94,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts._profile.metadataDescription }}</template> </FormSlot> - <MkFolder> - <template #label>{{ i18n.ts.advancedSettings }}</template> - - <div class="_gaps_m"> - <MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> - <MkSwitch v-if="profile.isCat" v-model="profile.speakAsCat">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></MkSwitch> - <MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> - </div> - </MkFolder> + <MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false"> + <template #label>{{ i18n.ts._profile.followedMessage }}<span class="_beta">{{ i18n.ts.beta }}</span></template> + <template #caption> + <div>{{ i18n.ts._profile.followedMessageDescription }}</div> + <div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div> + </template> + </MkInput> <MkSelect v-model="reactionAcceptance"> <template #label>{{ i18n.ts.reactionAcceptance }}</template> @@ -112,6 +110,16 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option> <option value="likeOnly">{{ i18n.ts.likeOnly }}</option> </MkSelect> + + <MkFolder> + <template #label>{{ i18n.ts.advancedSettings }}</template> + + <div class="_gaps_m"> + <MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> + <MkSwitch v-if="profile.isCat" v-model="profile.speakAsCat">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></MkSwitch> + <MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> + </div> + </MkFolder> </div> </template> @@ -153,6 +161,7 @@ const setMaxBirthDate = () => { const profile = reactive({ name: $i.name, description: $i.description, + followedMessage: $i.followedMessage, location: $i.location, birthday: $i.birthday, listenbrainz: $i.listenbrainz, @@ -209,6 +218,8 @@ function save() { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing description: profile.description || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + followedMessage: profile.followedMessage || null, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing location: profile.location || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing birthday: profile.birthday || null, diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index ad07a6b5392a841dfdfb54a7dabcfbda89bf6699..e7aef55a53a765b1e7106e304b36a2213432db47 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -88,19 +88,9 @@ import { uniqueBy } from '@/scripts/array.js'; import { fetchThemes, getThemes } from '@/theme-store.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; +import { reloadAsk } from '@/scripts/reload-ask.js'; import * as os from '@/os.js'; -async function reloadAsk() { - const { canceled } = await os.confirm({ - type: 'info', - text: i18n.ts.reloadToApplySetting, - }); - if (canceled) return; - - unisonReload(); -} - const installedThemes = ref(getThemes()); const builtinThemes = getBuiltinThemesRef(); @@ -148,13 +138,13 @@ watch(syncDeviceDarkMode, () => { } }); -watch(wallpaper, () => { +watch(wallpaper, async () => { if (wallpaper.value == null) { miLocalStorage.removeItem('wallpaper'); } else { miLocalStorage.setItem('wallpaper', wallpaper.value); } - reloadAsk(); + await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); }); onActivated(() => { diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index 058ef69c35e3745243dbd5d15dcc63794bd1b9ab..adeaf8550cd3600e513192435e66d458863b9db1 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -21,14 +21,41 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <template #label>{{ i18n.ts._webhookSettings.trigger }}</template> - <div class="_gaps_s"> - <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> - <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch> - <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> - <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> - <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> - <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> - <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> + <div class="_gaps"> + <div class="_gaps_s"> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_follow)" @click="test('follow')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_followed">{{ i18n.ts._webhookSettings._events.followed }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_followed)" @click="test('followed')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_note)" @click="test('note')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reply)" @click="test('reply')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton> + </div> + <div :class="$style.switchBox"> + <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> + <MkButton transparent :class="$style.testButton" :disabled="!(active && event_mention)" @click="test('mention')"><i class="ti ti-send"></i></MkButton> + </div> + </div> + + <div :class="$style.description"> + {{ i18n.ts._webhookSettings.testRemarks }} + </div> </div> </FormSection> @@ -43,6 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, computed } from 'vue'; +import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import FormSection from '@/components/form/section.vue'; import MkSwitch from '@/components/MkSwitch.vue'; @@ -76,8 +104,8 @@ const event_renote = ref(webhook.on.includes('renote')); const event_reaction = ref(webhook.on.includes('reaction')); const event_mention = ref(webhook.on.includes('mention')); -async function save(): Promise<void> { - const events = []; +function save() { + const events: Misskey.entities.UserWebhook['on'] = []; if (event_follow.value) events.push('follow'); if (event_followed.value) events.push('followed'); if (event_note.value) events.push('note'); @@ -110,8 +138,21 @@ async function del(): Promise<void> { router.push('/settings/webhook'); } +async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> { + await os.apiWithDialog('i/webhooks/test', { + webhookId: props.webhookId, + type, + override: { + secret: secret.value, + url: url.value, + }, + }); +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars const headerActions = computed(() => []); +// eslint-disable-next-line @typescript-eslint/no-unused-vars const headerTabs = computed(() => []); definePageMetadata(() => ({ @@ -119,3 +160,30 @@ definePageMetadata(() => ({ icon: 'ti ti-webhook', })); </script> + +<style module lang="scss"> +.switchBox { + display: flex; + align-items: center; + justify-content: start; + + .testButton { + $buttonSize: 28px; + padding: 0; + width: $buttonSize; + min-width: $buttonSize; + max-width: $buttonSize; + height: $buttonSize; + margin-left: auto; + line-height: inherit; + font-size: 90%; + border-radius: 9999px; + } +} + +.description { + font-size: 0.85em; + padding: 8px 0 0 0; + color: var(--fgTransparentWeak); +} +</style> diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 9b77392872fbfee9ee137ee83002ae335cba9c74..0d261b1af39275d4178eb4a5ddbacdeb078d8c81 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader :actions="headerActions" :displayBackButton="true" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> <MkNotes ref="notes" class="" :pagination="pagination"/> </MkSpacer> @@ -28,6 +28,7 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; const props = defineProps<{ tag: string; @@ -51,7 +52,19 @@ async function post() { notes.value?.pagingComponent?.reload(); } -const headerActions = computed(() => []); +const headerActions = computed(() => [{ + icon: 'ti ti-dots', + label: i18n.ts.more, + handler: (ev: MouseEvent) => { + os.popupMenu([{ + text: i18n.ts.genEmbedCode, + icon: 'ti ti-code', + action: () => { + genEmbedCode('tags', props.tag); + }, + }], ev.currentTarget ?? ev.target); + } +}]); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 1cdccdc244b7ca35ff68db9833db1247f707ac1b..2fa6eb81ba7f13f020caeec4977762ea1a1f0784 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -79,6 +79,8 @@ import tinycolor from 'tinycolor2'; import { v4 as uuid } from 'uuid'; import JSON5 from 'json5'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import MkButton from '@/components/MkButton.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkTextarea from '@/components/MkTextarea.vue'; @@ -86,9 +88,7 @@ import MkFolder from '@/components/MkFolder.vue'; import { $i } from '@/account.js'; import { Theme, applyTheme } from '@/scripts/theme.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import * as os from '@/os.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { addTheme } from '@/theme-store.js'; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 20d8abccf6e2772e63c209456e0d2f64f0035629..60974da971f95569a9a08fa3cff7c8c2ab1b5cca 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.tl"> <MkTimeline ref="tlComponent" - :key="src + withRenotes + withReplies + onlyFiles" + :key="src + withRenotes + withBots + withReplies + onlyFiles" :src="src.split(':')[0]" :list="src.split(':')[1]" :withRenotes="withRenotes" @@ -41,7 +41,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkPostForm from '@/components/MkPostForm.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; @@ -51,13 +51,16 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js'; +import { useRouter } from '@/router/supplier.js'; provide('shouldOmitHeaderTitle', true); +const router = useRouter(); + const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>(); const rootEl = shallowRef<HTMLElement>(); @@ -195,7 +198,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { }), (channels.length === 0 ? undefined : { type: 'divider' }), { - type: 'link' as const, + type: 'link', icon: 'ti ti-plus', text: i18n.ts.createNew, to: '/channels', @@ -264,16 +267,28 @@ const headerActions = computed(() => { icon: 'ti ti-dots', text: i18n.ts.options, handler: (ev) => { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, - }, isBasicTimeline(src.value) && hasWithReplies(src.value) ? { + }, { type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, - } : undefined, { + text: i18n.ts.showBots, + ref: withBots, + }); + + if (isBasicTimeline(src.value) && hasWithReplies(src.value)) { + menuItems.push({ + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles, + }); + } + + menuItems.push({ type: 'switch', text: i18n.ts.withSensitive, ref: withSensitive, @@ -282,7 +297,9 @@ const headerActions = computed(() => { text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false, - }], ev.currentTarget ?? ev.target); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); }, }, ]; @@ -309,6 +326,11 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList icon: basicTimelineIconClass(tl), iconOnly: true, })), { + icon: 'ph-user-check ph-bold ph-lg', + title: i18n.ts.following, + iconOnly: true, + onClick: () => router.push('/following-feed'), +}, { icon: 'ti ti-list', title: i18n.ts.lists, iconOnly: true, diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index 6566263c010f6fc834ba67d372e5e5802078570c..396e6eb14a0ee469c229eb5499e25eee95904cab 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> - <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <template #header><MkPageHeader :actions="headerActions" :displayBackButton="true" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> <div ref="rootEl"> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, watch, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkTimeline from '@/components/MkTimeline.vue'; -import { scroll } from '@/scripts/scroll.js'; +import { scroll } from '@@/js/scroll.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index b997fe1c3f1ac34512f252f055605581d6ee43a1..279f301d78f9f66cb5e580d984739dd14517d7bd 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -30,7 +30,15 @@ SPDX-License-Identifier: AGPL-3.0-only </button> </div> </div> - <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span> + <ul v-if="$i && $i.id != user.id" :class="$style.infoBadges"> + <li v-if="user.isFollowed && user.isFollowing">{{ i18n.ts.mutuals }}</li> + <li v-else-if="user.isFollowing">{{ i18n.ts.following }}</li> + <li v-else-if="user.isFollowed">{{ i18n.ts.followsYou }}</li> + <li v-if="user.isMuted">{{ i18n.ts.muted }}</li> + <li v-if="user.isRenoteMuted">{{ i18n.ts.renoteMuted }}</li> + <li v-if="user.isBlocking">{{ i18n.ts.blocked }}</li> + <li v-if="user.isBlocked && $i.isModerator">{{ i18n.ts.blockingYou }}</li> + </ul> <div class="actions"> <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button> <MkFollowButton v-if="$i?.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> @@ -46,6 +54,11 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> </div> </div> + <div v-if="user.followedMessage != null" class="followedMessage"> + <div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;"> + <Mfm :text="user.followedMessage" :author="user"/> + </div> + </div> <div v-if="user.roles.length > 0" class="roles"> <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> <MkA v-adaptive-bg :to="`/roles/${role.id}`"> @@ -120,19 +133,16 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="contents _gaps"> - <div v-if="user.pinnedNotes.length > 0" class="_gaps"> - <MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/> - </div> - <MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> + <MkInfo v-if="user.pinnedNotes.length === 0 && $i?.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> <template v-if="narrow"> <MkLazy> - <XFiles :key="user.id" :user="user"/> + <XFiles :key="user.id" :user="user" :collapsed="true"/> </MkLazy> <MkLazy> - <XActivity :key="user.id" :user="user"/> + <XActivity :key="user.id" :user="user" :collapsed="true"/> </MkLazy> <MkLazy> - <XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user"/> + <XListenBrainz v-if="user.listenbrainz && listenbrainzdata" :key="user.id" :user="user" :collapsed="true"/> </MkLazy> </template> <!-- <div v-if="!disableNotes"> @@ -142,14 +152,27 @@ SPDX-License-Identifier: AGPL-3.0-only </div> --> <MkStickyContainer> <template #header> + <!-- You can't use v-if on these, as MkTab first *deletes* and replaces all children with native HTML elements. --> + <!-- Instead, we add a "no notes" placeholder and default to null (all notes) if there's nothing pinned. --> + <!-- It also converts all comments into text! --> <MkTab v-model="noteview" :class="$style.tab"> + <option value="pinned">{{ i18n.ts.pinnedOnly }}</option> <option :value="null">{{ i18n.ts.notes }}</option> <option value="all">{{ i18n.ts.all }}</option> <option value="files">{{ i18n.ts.withFiles }}</option> </MkTab> </template> <MkLazy> - <MkNotes :class="$style.tl" :noGap="true" :pagination="AllPagination"/> + <div v-if="noteview === 'pinned'" class="_gaps"> + <div v-if="user.pinnedNotes.length < 1" class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost" aria-hidden="true" :alt="i18n.ts.noNotes"/> + <div>{{ i18n.ts.noNotes }}</div> + </div> + <div v-else class="_panel"> + <MkNote v-for="note of user.pinnedNotes" :key="note.id" class="note" :class="$style.pinnedNote" :note="note" :pinned="true"/> + </div> + </div> + <MkNotes v-else :class="$style.tl" :noGap="true" :pagination="AllPagination"/> </MkLazy> </MkStickyContainer> </div> @@ -175,7 +198,7 @@ import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkButton from '@/components/MkButton.vue'; -import { getScrollPosition } from '@/scripts/scroll.js'; +import { getScrollPosition } from '@@/js/scroll.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; @@ -189,11 +212,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { useRouter } from '@/router/supplier.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +import { infoImageUrl } from '@/instance.js'; const MkNote = defineAsyncComponent(() => - (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : - (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : - null + defaultStore.state.noteDesign === 'sharkey' + ? import('@/components/SkNote.vue') + : import('@/components/MkNote.vue'), ); function calcAge(birthdate: string): number { @@ -213,7 +237,7 @@ function calcAge(birthdate: string): number { const XFiles = defineAsyncComponent(() => import('./index.files.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); -const XListenBrainz = defineAsyncComponent(() => import("./index.listenbrainz.vue")); +const XListenBrainz = defineAsyncComponent(() => import('./index.listenbrainz.vue')); //const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); const props = withDefaults(defineProps<{ @@ -245,7 +269,7 @@ if (props.user.listenbrainz) { const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, { method: 'GET', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, }); const data = await response.json(); @@ -262,11 +286,11 @@ const background = computed(() => { if (props.user.backgroundUrl == null) return {}; if (defaultStore.state.disableShowingAnimatedImages) { return { - '--backgroundImageStatic': `url('${getStaticImageUrl(props.user.backgroundUrl)}')` + '--backgroundImageStatic': `url('${getStaticImageUrl(props.user.backgroundUrl)}')`, }; } else { return { - '--backgroundImageStatic': `url('${props.user.backgroundUrl}')` + '--backgroundImageStatic': `url('${props.user.backgroundUrl}')`, }; } }); @@ -279,7 +303,7 @@ const pagination = { endpoint: 'users/featured-notes' as const, limit: 10, params: computed(() => ({ - userId: props.user.id + userId: props.user.id, })), }; @@ -445,17 +469,6 @@ onUnmounted(() => { background: linear-gradient(transparent, rgba(#000, 0.7)); } - > .followed { - position: absolute; - top: 12px; - left: 12px; - padding: 4px 8px; - color: #fff; - background: rgba(0, 0, 0, 0.7); - font-size: 0.7em; - border-radius: var(--radius-sm); - } - > .actions { position: absolute; top: 12px; @@ -552,6 +565,11 @@ onUnmounted(() => { filter: drop-shadow(1px 1px 3px rgba(#000, 0.2)); } + > .followedMessage { + padding: 24px 24px 0 154px; + font-size: 0.9em; + } + > .roles { padding: 24px 24px 0 154px; font-size: 0.95em; @@ -734,6 +752,10 @@ onUnmounted(() => { margin: auto; } + > .followedMessage { + padding: 16px 16px 0 16px; + } + > .roles { padding: 16px 16px 0 16px; justify-content: center; @@ -780,7 +802,7 @@ onUnmounted(() => { } .tab { - margin: calc(var(--margin) / 2) 0; + margin-bottom: calc(var(--margin) / 2); padding: calc(var(--margin) / 2) 0; background: color-mix(in srgb, var(--bg) 65%, transparent); backdrop-filter: var(--blur, blur(15px)); @@ -797,4 +819,34 @@ onUnmounted(() => { margin-left: 4px; color: var(--success); } + +.pinnedNote:not(:last-child) { + border-bottom: solid 0.5px var(--divider); +} + +.infoBadges { + position: absolute; + top: 12px; + left: 12px; + + display: flex; + flex-direction: row; + + padding: 0; + margin: 0; + + > * { + padding: 4px 8px; + color: #fff; + background: rgba(0, 0, 0, 0.7); + font-size: 0.7em; + border-radius: var(--radius-sm); + list-style-type: none; + margin-left: 0; + } + + > :not(:first-child) { + margin-left: 8px; + } +} </style> diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue index 45bc35067b2f7021192ada44c997d54397f0c1f0..ba94b5c822c5d584f571261cbd929d88c543564a 100644 --- a/packages/frontend/src/pages/user/index.activity.vue +++ b/packages/frontend/src/pages/user/index.activity.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkContainer> +<MkContainer :foldable="true" :expanded="!collapsed"> <template #icon><i class="ti ti-chart-line"></i></template> <template #header>{{ i18n.ts.activity }}</template> <template #func="{ buttonStyleClass }"> @@ -30,8 +30,10 @@ import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.User; limit?: number; + collapsed?: boolean; }>(), { limit: 50, + collapsed: false, }); const chartSrc = ref('per-user-notes'); diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index 1430ae1296f02c04c8cbcc183035c9f3528711e2..23fd4ca23e82d47a31715eeda8380fa6410128e2 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkContainer :max-height="300" :foldable="true"> +<MkContainer :max-height="300" :foldable="true" :expanded="!collapsed"> <template #icon><i class="ti ti-photo"></i></template> <template #header>{{ i18n.ts.files }}</template> <div :class="$style.root"> @@ -43,9 +43,12 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed; -}>(); + collapsed?: boolean; +}>(), { + collapsed: false, +}); const fetching = ref(true); const files = ref<{ diff --git a/packages/frontend/src/pages/user/index.listenbrainz.vue b/packages/frontend/src/pages/user/index.listenbrainz.vue index 18092d9d875a0c4691cd12b814d42f460ccb4f39..1c9ef8dd226a6eb42053cfab5bd8389b9ae572ce 100644 --- a/packages/frontend/src/pages/user/index.listenbrainz.vue +++ b/packages/frontend/src/pages/user/index.listenbrainz.vue @@ -4,44 +4,43 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> - <MkContainer :foldable="true"> - <template #header - ><i - class="ph-headphones ph-bold ph-lg" - style="margin-right: 0.5em" - ></i - >Music</template - > +<MkContainer :foldable="true" :expanded="!collapsed"> + <template #header> + <i + class="ph-headphones ph-bold ph-lg" + style="margin-right: 0.5em" + ></i>Music + </template> - <div style="padding: 8px"> - <div class="flex"> - <a :href="listenbrainz.musicbrainzurl"> - <img class="image" :src="listenbrainz.img" :alt="listenbrainz.title" /> - <div class="flex flex-col items-start"> - <p class="text-sm font-bold">Now Playing: {{ listenbrainz.title }}</p> - <p class="text-xs font-medium">{{ listenbrainz.artist }}</p> - </div> - </a> - <a :href="listenbrainz.listenbrainzurl"> - <div class="playicon"> - <i class="ph-play ph-bold ph-lg"></i> - </div> - </a> - </div> + <div style="padding: 8px"> + <div class="flex"> + <a :href="listenbrainz.musicbrainzurl"> + <img class="image" :src="listenbrainz.img" :alt="listenbrainz.title"/> + <div class="flex flex-col items-start"> + <p class="text-sm font-bold">Now Playing: {{ listenbrainz.title }}</p> + <p class="text-xs font-medium">{{ listenbrainz.artist }}</p> + </div> + </a> + <a :href="listenbrainz.listenbrainzurl"> + <div class="playicon"> + <i class="ph-play ph-bold ph-lg"></i> + </div> + </a> </div> - </MkContainer> + </div> +</MkContainer> </template> <script lang="ts" setup> -/* eslint-disable no-mixed-spaces-and-tabs */ -import {} from "vue"; -import * as misskey from "misskey-js"; -import MkContainer from "@/components/MkContainer.vue"; +import * as misskey from 'misskey-js'; +import MkContainer from '@/components/MkContainer.vue'; const props = withDefaults( defineProps<{ - user: misskey.entities.User; - }>(), - {}, + user: misskey.entities.UserDetailed; + collapsed?: boolean; + }>(), { + collapsed: false, + }, ); const listenbrainz = { title: '', artist: '', lastlisten: '', img: '', musicbrainzurl: '', listenbrainzurl: '' }; if (props.user.listenbrainz) { @@ -49,12 +48,12 @@ if (props.user.listenbrainz) { const response = await fetch(`https://api.listenbrainz.org/1/metadata/lookup/?artist_name=${artist}&recording_name=${title}`, { method: 'GET', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, }); const data = await response.json(); if (!data.recording_name) { - return null; + return null; } const titler: string = data.recording_name; const artistr: string = data.artist_credit_name; @@ -64,35 +63,33 @@ if (props.user.listenbrainz) { return [titler, artistr, img, musicbrainzurl, listenbrainzurl]; }; const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - }); - const data = await response.json(); + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); if (data.payload.listens && data.payload.listens.length !== 0) { - const title: string = data.payload.listens[0].track_metadata.track_name; - const artist: string = data.payload.listens[0].track_metadata.artist_name; - const lastlisten: string = data.payload.listens[0].playing_now; - const img: string = 'https://coverartarchive.org/img/big_logo.svg'; - await getLMData(title, artist).then((data) => { - if (!data) { - listenbrainz.title = title; - listenbrainz.img = img; - listenbrainz.artist = artist; - listenbrainz.lastlisten = lastlisten; - return; - } else { - listenbrainz.title = data[0]; - listenbrainz.img = data[2]; - listenbrainz.artist = data[1]; - listenbrainz.lastlisten = lastlisten; - listenbrainz.musicbrainzurl = data[3]; - listenbrainz.listenbrainzurl = data[4]; - return; - } - }); - } + const title: string = data.payload.listens[0].track_metadata.track_name; + const artist: string = data.payload.listens[0].track_metadata.artist_name; + const lastlisten: string = data.payload.listens[0].playing_now; + const img = 'https://coverartarchive.org/img/big_logo.svg'; + await getLMData(title, artist).then((lmData) => { + if (!lmData) { + listenbrainz.title = title; + listenbrainz.img = img; + listenbrainz.artist = artist; + listenbrainz.lastlisten = lastlisten; + } else { + listenbrainz.title = lmData[0]; + listenbrainz.img = lmData[2]; + listenbrainz.artist = lmData[1]; + listenbrainz.lastlisten = lastlisten; + listenbrainz.musicbrainzurl = lmData[3]; + listenbrainz.listenbrainzurl = lmData[4]; + } + }); + } } </script> @@ -119,8 +116,7 @@ if (props.user.listenbrainz) { } .text-sm { font-size: 0.875rem; - margin: 0; - margin-bottom: 0.3rem; + margin: 0 0 0.3rem; } .font-bold { font-weight: 700; diff --git a/packages/frontend/src/pages/user/recent-notes.vue b/packages/frontend/src/pages/user/recent-notes.vue new file mode 100644 index 0000000000000000000000000000000000000000..c16a1c5fed91c68fffead4f1a9d7f30d7cf8f830 --- /dev/null +++ b/packages/frontend/src/pages/user/recent-notes.vue @@ -0,0 +1,66 @@ +<!-- +SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer ref="userScroll"> + <template #header> + <MkPageHeader :actions="headerActions" :displayBackButton="true"/> + </template> + <SkUserRecentNotes ref="userRecentNotes" :userId="userId" :withNonPublic="withNonPublic" :withQuotes="withQuotes" :withBots="withBots" :withReplies="withReplies" :onlyFiles="onlyFiles"/> +</MkStickyContainer> +</template> + +<script setup lang="ts"> + +import { computed, shallowRef } from 'vue'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { i18n } from '@/i18n.js'; +import { PageHeaderItem } from '@/types/page-header.js'; +import MkPageHeader from '@/components/global/MkPageHeader.vue'; +import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue'; +import { acct } from '@/filters/user.js'; +import { createModel, createOptions } from '@/scripts/following-feed-utils.js'; +import MkStickyContainer from '@/components/global/MkStickyContainer.vue'; + +defineProps<{ + userId: string; +}>(); + +const userRecentNotes = shallowRef<InstanceType<typeof SkUserRecentNotes>>(); +const user = computed(() => userRecentNotes.value?.user); + +const { + withNonPublic, + withQuotes, + withBots, + withReplies, + onlyFiles, +} = createModel(); + +const headerActions: PageHeaderItem[] = [ + { + icon: 'ti ti-refresh', + text: i18n.ts.reload, + handler: () => userRecentNotes.value?.reload(), + }, + createOptions(), +]; + +// Based on user/index.vue +definePageMetadata(() => ({ + title: i18n.ts.user, + icon: 'ti ti-user', + ...user.value ? { + title: user.value.name ? ` (@${user.value.username})` : `@${user.value.username}`, + subtitle: `@${acct(user.value)}`, + userName: user.value, + avatar: user.value, + path: `/@${user.value.username}`, + share: { + title: user.value.name, + }, + } : {}, +})); +</script> diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 31911649ac8170238da4825462624bd9761c9f13..5a41100bf1c842256f80fdae10553ebfec98fdd1 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; -import { host, version } from '@/config.js'; +import { host, version } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue index 252b1a2955c49f029b8f9c944556653d08ef5def..ee8d4e1d625b80aee822353ecf50f1ae10b2e12d 100644 --- a/packages/frontend/src/pages/welcome.timeline.note.vue +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -84,7 +84,7 @@ onUpdated(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), var(--X15)); + background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); } } diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 045f424cdaedbec86ab3cfa9c9c6c8a9c13d777e..16d558cc91626cc8068ef6c66ea7b7c501383505 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -24,7 +24,7 @@ import * as Misskey from 'misskey-js'; import { onUpdated, ref, shallowRef } from 'vue'; import XNote from '@/pages/welcome.timeline.note.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; const notes = ref<Misskey.entities.Note[]>([]); const isScrolling = ref(false); diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue index 915fe35025ea72885c01c0547a85927b2c7b948e..38d257506c48062378280223fc78598fca943605 100644 --- a/packages/frontend/src/pages/welcome.vue +++ b/packages/frontend/src/pages/welcome.vue @@ -15,7 +15,7 @@ import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XSetup from './welcome.setup.vue'; import XEntrance from './welcome.entrance.a.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { fetchInstance } from '@/instance.js'; diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index 81233a5a5e451447948ad9bc7c636165b7477086..c0034d414ce679aa993728439313bae5f10cab3f 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -6,8 +6,10 @@ import { ref } from 'vue'; import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; -import { inputText } from '@/os.js'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; +import { warningExternalWebsite } from '@/scripts/warning-external-website.js'; const parser = new Parser(); const pluginContexts = new Map<string, Interpreter>(); @@ -92,7 +94,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<s }), 'Plugin:open_url': values.FN_NATIVE(([url]) => { utils.assertString(url); - window.open(url.value, '_blank', 'noopener'); + warningExternalWebsite(url.value); }), 'Plugin:config': values.OBJ(config), }; diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 14110d1f9bd0cbb9494d5692fa9e548d2818f1b3..686ac6920acfa2d528c154c9e34105897bb8d2cb 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue'; -import type { RouteDef } from '@/nirax.js'; -import { IRouter, Router } from '@/nirax.js'; +import { AsyncComponentLoader, defineAsyncComponent } from 'vue'; +import type { IRouter, RouteDef } from '@/nirax.js'; +import { Router } from '@/nirax.js'; import { $i, iAmModerator } from '@/account.js'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; -import { setMainRouter } from '@/router/main.js'; -const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ +export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ loader: loader, loadingComponent: MkLoading, errorComponent: MkError, @@ -227,6 +226,14 @@ const routes: RouteDef[] = [{ path: '/explore', component: page(() => import('@/pages/explore.vue')), hash: 'initialTab', +}, { + path: '/following-feed', + component: page(() => import('@/pages/following-feed.vue')), + loginRequired: true, +}, { + path: '/following-feed/:userId', + component: page(() => import('@/pages/user/recent-notes.vue')), + loginRequired: true, }, { path: '/search', component: page(() => import('@/pages/search.vue')), @@ -463,22 +470,14 @@ const routes: RouteDef[] = [{ path: '/relays', name: 'relays', component: page(() => import('@/pages/admin/relays.vue')), - }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('@/pages/admin/instance-block.vue')), - }, { - path: '/proxy-account', - name: 'proxy-account', - component: page(() => import('@/pages/admin/proxy-account.vue')), }, { path: '/external-services', name: 'external-services', component: page(() => import('@/pages/admin/external-services.vue')), }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('@/pages/admin/other-settings.vue')), + path: '/performance', + name: 'performance', + component: page(() => import('@/pages/admin/performance.vue')), }, { path: '/server-rules', name: 'server-rules', @@ -601,36 +600,6 @@ const routes: RouteDef[] = [{ component: page(() => import('@/pages/not-found.vue')), }]; -function createRouterImpl(path: string): IRouter { +export function createMainRouter(path: string): IRouter { return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue'))); } - -/** - * {@link Router}ã«ã‚ˆã‚‹ç”»é¢é·ç§»ã‚’å¯èƒ½ã¨ã™ã‚‹ãŸã‚ã«{@link mainRouter}をセットアップã™ã‚‹ã€‚ - * ã¾ãŸã€{@link Router}ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’作æˆã™ã‚‹ãŸã‚ã®ãƒ•ã‚¡ã‚¯ãƒˆãƒªã‚‚{@link provide}経由ã§å…¬é–‹ã™ã‚‹ï¼ˆ`routerFactory`ã¨ã„ã†ã‚ーã§å–å¾—å¯èƒ½ï¼‰ - */ -export function setupRouter(app: App) { - app.provide('routerFactory', createRouterImpl); - - const mainRouter = createRouterImpl(location.pathname + location.search + location.hash); - - window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); - }); - - mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.addListener('same', () => { - window.scroll({ top: 0, behavior: 'smooth' }); - }); - - mainRouter.addListener('replace', ctx => { - window.history.replaceState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.init(); - - setMainRouter(mainRouter); -} diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts index 7a3fde131ed840139b459b4ad1148a7bb8e367d8..709c508741b414dc323061f1dbccc4c4571a1801 100644 --- a/packages/frontend/src/router/main.ts +++ b/packages/frontend/src/router/main.ts @@ -3,10 +3,41 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { ShallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; +import type { App, ShallowRef } from 'vue'; + +/** + * {@link Router}ã«ã‚ˆã‚‹ç”»é¢é·ç§»ã‚’å¯èƒ½ã¨ã™ã‚‹ãŸã‚ã«{@link mainRouter}をセットアップã™ã‚‹ã€‚ + * ã¾ãŸã€{@link Router}ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’作æˆã™ã‚‹ãŸã‚ã®ãƒ•ã‚¡ã‚¯ãƒˆãƒªã‚‚{@link provide}経由ã§å…¬é–‹ã™ã‚‹ï¼ˆ`routerFactory`ã¨ã„ã†ã‚ーã§å–å¾—å¯èƒ½ï¼‰ + */ +export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)): void { + app.provide('routerFactory', routerFactory); + + const mainRouter = routerFactory(location.pathname + location.search + location.hash); + + window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); + }); + + mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.addListener('same', () => { + window.scroll({ top: 0, behavior: 'smooth' }); + }); + + mainRouter.addListener('replace', ctx => { + window.history.replaceState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.init(); + + setMainRouter(mainRouter); +} + function getMainRouter(): IRouter { const router = mainRouterHolder; if (!router) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 98a0c61752cb66c446dd5b552f18795e218e4901..46aed49330f79750f55f3cf98d0190b14ab0603d 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -4,13 +4,13 @@ */ import { utils, values } from '@syuilo/aiscript'; +import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { url, lang } from '@/config.js'; -import { nyaize } from '@/scripts/nyaize.js'; +import { url, lang } from '@@/js/config.js'; export function aiScriptReadline(q: string): Promise<string> { return new Promise(ok => { @@ -87,7 +87,7 @@ export function createAiScriptEnv(opts) { }), 'Mk:nyaize': values.FN_NATIVE(([text]) => { utils.assertString(text); - return values.STR(nyaize(text.value)); + return values.STR(Misskey.nyaize(text.value)); }), }; } diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index fa3fcac2e79a992e82e3c2ffd58e4123da4712ee..2b386bebb81aa1f79b390b7157768aaeb8e49f72 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -27,6 +27,8 @@ export type AsUiContainer = AsUiComponentBase & { font?: 'serif' | 'sans-serif' | 'monospace'; borderWidth?: number; borderColor?: string; + borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'; + borderRadius?: number; padding?: number; rounded?: boolean; hidden?: boolean; @@ -173,6 +175,10 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, if (borderWidth) utils.assertNumber(borderWidth); const borderColor = def.value.get('borderColor'); if (borderColor) utils.assertString(borderColor); + const borderStyle = def.value.get('borderStyle'); + if (borderStyle) utils.assertString(borderStyle); + const borderRadius = def.value.get('borderRadius'); + if (borderRadius) utils.assertNumber(borderRadius); const padding = def.value.get('padding'); if (padding) utils.assertNumber(padding); const rounded = def.value.get('rounded'); @@ -191,6 +197,8 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, font: font?.value, borderWidth: borderWidth?.value, borderColor: borderColor?.value, + borderStyle: borderStyle?.value, + borderRadius: borderRadius?.value, padding: padding?.value, rounded: rounded?.value, hidden: hidden?.value, diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts index 8fc857f84fce06babd2417ad944cc96222e96322..c3c3f419a978c064f94107e1de0421813dba6add 100644 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { UnicodeEmojiDef } from './emojilist.js'; +import { UnicodeEmojiDef } from '@@/js/emojilist.js'; export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean { if (typeof emoji === 'string') return true; // UnicodeEmojiDefã«ã‚‚ç„¡ã„絵文å—ã§ã‚ã‚Œã°æ–‡å—列ã§æ¥ã‚‹ã€‚Unicode絵文å—ã§ã‚ã‚‹ã“ã¨ã«ã¯å¤‰ã‚ã‚Šãªã„ã®ã§å¸¸ã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³å¯èƒ½ã¨ã™ã‚‹; diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index e94027d3022a516e4e08d28b3a1c9743347b380b..6710d9826e8d6ce643d03d9293af56f164842dc4 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -3,17 +3,17 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getHighlighterCore, loadWasm } from 'shiki/core'; +import { createHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { unique } from './array.js'; import { deepClone } from './clone.js'; import { deepMerge } from './merge.js'; import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; import { ColdDeviceStorage } from '@/store.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; let _highlighter: HighlighterCore | null = null; @@ -69,7 +69,7 @@ async function initHighlighter() { ]); const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); - const highlighter = await getHighlighterCore({ + const highlighter = await createHighlighterCore({ themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index eb2da5ad8665c47ebe6c7d55b7c8f9684a79c407..81278b17ea6de36820ae97f5ae45412fc2ab556a 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js'; +import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js'; import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement; diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..064d6b72e35cf7f6adcfb1de470ab55fac9dfe71 --- /dev/null +++ b/packages/frontend/src/scripts/following-feed-utils.ts @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { computed } from 'vue'; +import { defaultStore } from '@/store.js'; +import { deepMerge } from '@/scripts/merge.js'; +import { PageHeaderItem } from '@/types/page-header.js'; +import { i18n } from '@/i18n.js'; +import { popupMenu } from '@/os.js'; + +export const followingTab = 'following' as const; +export const mutualsTab = 'mutuals' as const; +export const followersTab = 'followers' as const; +export type FollowingFeedTab = typeof followingTab | typeof mutualsTab | typeof followersTab; + +export function createOptions(): PageHeaderItem { + const { + userList, + withNonPublic, + withQuotes, + withBots, + withReplies, + onlyFiles, + } = createModel(); + + return { + icon: 'ti ti-dots', + text: i18n.ts.options, + handler: ev => + popupMenu([ + { + type: 'switch', + text: i18n.ts.showNonPublicNotes, + ref: withNonPublic, + disabled: userList.value === 'followers', + }, + { + type: 'switch', + text: i18n.ts.showQuotes, + ref: withQuotes, + }, + { + type: 'switch', + text: i18n.ts.showBots, + ref: withBots, + }, + { + type: 'switch', + text: i18n.ts.showReplies, + ref: withReplies, + disabled: onlyFiles, + }, + { + type: 'divider', + }, + { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: withReplies, + }, + ], ev.currentTarget ?? ev.target), + }; +} + +export function createModel() { + const userList = computed({ + get: () => defaultStore.reactiveState.followingFeed.value.userList, + set: value => saveFollowingFilter('userList', value), + }); + + const withNonPublic = computed({ + get: () => { + if (userList.value === 'followers') return false; + return defaultStore.reactiveState.followingFeed.value.withNonPublic; + }, + set: value => saveFollowingFilter('withNonPublic', value), + }); + const withQuotes = computed({ + get: () => defaultStore.reactiveState.followingFeed.value.withQuotes, + set: value => saveFollowingFilter('withQuotes', value), + }); + const withBots = computed({ + get: () => defaultStore.reactiveState.followingFeed.value.withBots, + set: value => saveFollowingFilter('withBots', value), + }); + const withReplies = computed({ + get: () => defaultStore.reactiveState.followingFeed.value.withReplies, + set: value => saveFollowingFilter('withReplies', value), + }); + const onlyFiles = computed({ + get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles, + set: value => saveFollowingFilter('onlyFiles', value), + }); + + const remoteWarningDismissed = computed({ + get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed, + set: value => saveFollowingFilter('remoteWarningDismissed', value), + }); + + return { + userList, + withNonPublic, + withQuotes, + withBots, + withReplies, + onlyFiles, + remoteWarningDismissed, + }; +} + +// Based on timeline.saveTlFilter() +function saveFollowingFilter<Key extends keyof typeof defaultStore.state.followingFeed>(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) { + const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed); + return defaultStore.set('followingFeed', out); +} diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index 60884d08d39b933a0eb83e8b82cd7e1568543fbb..a85ee01e26e55f4a7d36cd89e6fc215777b5019b 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { host as localHost } from '@/config.js'; +import { host as localHost } from '@@/js/config.js'; export async function genSearchQuery(v: any, q: string) { let host: string; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 108648d640ad0defe1a53f80987715042a3a0ff5..c8ab9238d3b72996938d36790c6d872417be6e47 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -9,7 +9,7 @@ import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; function rename(file: Misskey.entities.DriveFile) { @@ -87,8 +87,10 @@ async function deleteFile(file: Misskey.entities.DriveFile) { export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { const isImage = file.type.startsWith('image/'); - let menu; - menu = [{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'link', to: `/my/drive/file/${file.id}`, text: i18n.ts._fileViewer.title, @@ -109,14 +111,20 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => describe(file), - }, ...isImage ? [{ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () => os.cropImage(file, { - aspectRatio: NaN, - uploadFolder: folder ? folder.id : folder, - }), - }] : [], { type: 'divider' }, { + }); + + if (isImage) { + menuItems.push({ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () => os.cropImage(file, { + aspectRatio: NaN, + uploadFolder: folder ? folder.id : folder, + }), + }); + } + + menuItems.push({ type: 'divider' }, { text: i18n.ts.createNoteFromTheFile, icon: 'ti ti-pencil', action: () => os.post({ @@ -138,17 +146,17 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss icon: 'ti ti-trash', danger: true, action: () => deleteFile(file), - }]; + }); if (defaultStore.state.devMode) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, action: () => { copyToClipboard(file.id); }, - }]); + }); } - return menu; + return menuItems; } diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts new file mode 100644 index 0000000000000000000000000000000000000000..158ab9c7f81e2d13e4273e47284c1da8b58470be --- /dev/null +++ b/packages/frontend/src/scripts/get-embed-code.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { defineAsyncComponent } from 'vue'; +import { v4 as uuid } from 'uuid'; +import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js'; +import { url } from '@@/js/config.js'; +import * as os from '@/os.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js'; + +const MOBILE_THRESHOLD = 500; + +/** + * パラメータをæ£è¦åŒ–ã™ã‚‹ï¼ˆåŸ‹ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ä½œæˆç”¨ï¼‰ + * @param params パラメータ + * @returns æ£è¦åŒ–ã•ã‚ŒãŸãƒ‘ラメータ + */ +export function normalizeEmbedParams(params: EmbedParams): Record<string, string> { + // paramsã®valueã‚’ã™ã¹ã¦stringã«å¤‰æ›ã€‚undefinedã‚„nullã¯ãƒ—ãƒãƒ‘ティã”ã¨æ¶ˆã™ + const normalizedParams: Record<string, string> = {}; + for (const key in params) { + // デフォルトã®å€¤ã¨åŒã˜ãªã‚‰paramsã«å«ã‚ãªã„ + if (params[key] == null || params[key] === defaultEmbedParams[key]) { + continue; + } + switch (typeof params[key]) { + case 'number': + normalizedParams[key] = params[key].toString(); + break; + case 'boolean': + normalizedParams[key] = params[key] ? 'true' : 'false'; + break; + default: + normalizedParams[key] = params[key]; + break; + } + } + return normalizedParams; +} + +/** + * 埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’生æˆï¼ˆiframe IDã®ç™ºç•ªã‚‚やる) + */ +export function getEmbedCode(path: string, params?: EmbedParams): string { + const iframeId = 'v1_' + uuid(); // å°†æ¥embed.jsã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒä¸ŠãŒã£ãŸã¨ã用ã«v1_を付ã‘ã¦ãŠã + + let paramString = ''; + if (params) { + const searchParams = new URLSearchParams(normalizeEmbedParams(params)); + paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString(); + } + + const iframeCode = [ + `<iframe src="${url + path + paramString}" data-misskey-embed-id="${iframeId}" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" style="border: none; width: 100%; max-width: 500px; height: 300px; color-scheme: light dark;"></iframe>`, + `<script defer src="${url}/embed.js"></script>`, + ]; + return iframeCode.join('\n'); +} + +/** + * 埋ã‚è¾¼ã¿ã‚³ãƒ¼ãƒ‰ã‚’生æˆã—ã¦ã‚³ãƒ”ーã™ã‚‹ï¼ˆã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºæ©Ÿèƒ½ã¤ã) + * + * カスタマイズ機能ãŒã„らãªã„å ´åˆï¼ˆäº‹å‰ã«ãƒ‘ラメータを指定ã™ã‚‹å ´åˆï¼‰ã¯ getEmbedCode を直接使ã£ã¦ãã ã•ã„ + */ +export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { + const _params = { ...params }; + + if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { + _params.maxHeight = 700; + } + + // PCã˜ã‚ƒãªã„å ´åˆã¯ã‚³ãƒ¼ãƒ‰ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚ºç”»é¢ã‚’出ã•ãšã«ãã®ã¾ã¾ã‚³ãƒ”ー + if (window.innerWidth < MOBILE_THRESHOLD) { + copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); + os.success(); + } else { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { + entity, + id, + params: _params, + }, { + closed: () => dispose(), + }); + } +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index a7ec4ce6d7735acd81a7c5183de681ad522d830b..4f3fb656651694e6fc10fa73c162c3ccf92ae33a 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -12,15 +12,16 @@ import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@/config.js'; +import { url } from '@@/js/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache, favoritedChannelsCache } from '@/cache.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -66,6 +67,11 @@ export async function getNoteClipMenu(props: { }); if (props.currentClip?.id === clip.id) props.isDeleted.value = true; } + } else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') { + os.alert({ + type: 'error', + text: i18n.ts.clipNoteLimitExceeded, + }); } else { os.alert({ type: 'error', @@ -93,11 +99,13 @@ export async function getNoteClipMenu(props: { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', + default: null, label: i18n.ts.name, }, description: { type: 'string', required: false, + default: null, multiline: true, label: i18n.ts.description, }, @@ -162,6 +170,19 @@ export function getCopyNoteOriginLinkMenu(note: misskey.entities.Note, text: str }; } +function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { + if (note.url != null || note.uri != null) return undefined; + if (['specified', 'followers'].includes(note.visibility)) return undefined; + + return { + icon: 'ti ti-code', + text, + action: (): void => { + genEmbedCode('notes', note.id); + }, + }; +} + export function getNoteMenu(props: { note: Misskey.entities.Note; translation: Ref<Misskey.entities.NotesTranslateResponse | null>; @@ -267,7 +288,7 @@ export function getNoteMenu(props: { title: i18n.ts.numberOfDays, }); - if (canceled) return; + if (canceled || days == null) return; os.apiWithDialog('admin/promo/create', { noteId: appearNote.id, @@ -298,170 +319,184 @@ export function getNoteMenu(props: { props.translation.value = res; } - let menu: MenuItem[]; + const menuItems: MenuItem[] = []; + if ($i) { const statePromise = misskeyApi('notes/state', { noteId: appearNote.id, }); - menu = [ - ...( - props.currentClip?.userId === $i.id ? [{ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, { type: 'divider' }] : [] - ), { - icon: 'ti ti-info-circle', - text: i18n.ts.details, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? + if (props.currentClip?.userId === $i.id) { + menuItems.push({ + icon: 'ti ti-backspace', + text: i18n.ts.unclip, + danger: true, + action: unclip, + }, { type: 'divider' }); + } + + menuItems.push({ + icon: 'ti ti-info-circle', + text: i18n.ts.details, + action: openDetail, + }, { + icon: 'ti ti-copy', + text: i18n.ts.copyContent, + action: copyContent, + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); + + if (appearNote.url || appearNote.uri) { + menuItems.push( getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - : undefined, - (appearNote.url || appearNote.uri) ? { + ); + menuItems.push({ icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined, - ...(isSupportShare() ? [{ + }); + } else { + menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); + } + + if (isSupportShare()) { + menuItems.push({ icon: 'ti ti-share', text: i18n.ts.share, action: share, - }] : []), - $i && $i.policies.canUseTranslator && instance.translatorAvailable ? { + }); + } + + if ($i.policies.canUseTranslator && instance.translatorAvailable) { + menuItems.push({ icon: 'ti ti-language-hiragana', text: i18n.ts.translate, action: translate, - } : undefined, - { type: 'divider' }, - statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - }), - { - type: 'parent' as const, - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - children: () => getNoteClipMenu(props), + }); + } + + menuItems.push({ type: 'divider' }); + + menuItems.push(statePromise.then(state => state.isFavorited ? { + icon: 'ti ti-star-off', + text: i18n.ts.unfavorite, + action: () => toggleFavorite(false), + } : { + icon: 'ti ti-star', + text: i18n.ts.favorite, + action: () => toggleFavorite(true), + })); + + menuItems.push({ + type: 'parent', + icon: 'ti ti-paperclip', + text: i18n.ts.clip, + children: () => getNoteClipMenu(props), + }); + + menuItems.push(statePromise.then(state => state.isMutedThread ? { + icon: 'ti ti-message-off', + text: i18n.ts.unmuteThread, + action: () => toggleThreadMute(false), + } : { + icon: 'ti ti-message-off', + text: i18n.ts.muteThread, + action: () => toggleThreadMute(true), + })); + + if (appearNote.userId === $i.id) { + if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) { + menuItems.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => togglePin(false), + }); + } else { + menuItems.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => togglePin(true), + }); + } + } + + menuItems.push({ + type: 'parent', + icon: 'ti ti-user', + text: i18n.ts.user, + children: async () => { + const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); + const { menu, cleanup } = getUserMenu(user); + cleanups.push(cleanup); + return menu; }, - statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - }), - appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? { - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - } : { - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - } : undefined, - { - type: 'parent' as const, - icon: 'ti ti-user', - text: i18n.ts.user, + }); + + if (appearNote.userId !== $i.id) { + menuItems.push({ type: 'divider' }); + menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse)); + } + + if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) { + menuItems.push({ type: 'divider' }); + menuItems.push({ + type: 'parent', + icon: 'ti ti-device-tv', + text: i18n.ts.channel, children: async () => { - const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); - const { menu, cleanup } = getUserMenu(user); - cleanups.push(cleanup); - return menu; - }, - }, - /* - ...($i.isModerator || $i.isAdmin ? [ - { type: 'divider' }, - { - icon: 'ti ti-speakerphone', - text: i18n.ts.promote, - action: promote - }] - : [] - ),*/ - ...(appearNote.userId !== $i.id ? [ - { type: 'divider' }, - appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined, - ] - : [] - ), - ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [ - { type: 'divider' }, - { - type: 'parent' as const, - icon: 'ti ti-device-tv', - text: i18n.ts.channel, - children: async () => { - const channelChildMenu = [] as MenuItem[]; - - const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); - - if (channel.pinnedNoteIds.includes(appearNote.id)) { - channelChildMenu.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), - }), - }); - } else { - channelChildMenu.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], - }), - }); - } - return channelChildMenu; - }, + const channelChildMenu = [] as MenuItem[]; + + const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); + + if (channel.pinnedNoteIds.includes(appearNote.id)) { + channelChildMenu.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), + }), + }); + } else { + channelChildMenu.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], + }), + }); + } + return channelChildMenu; }, - ] - : [] - ), - ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ - { type: 'divider' }, - appearNote.userId === $i.id ? { + }); + } + + if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) { + menuItems.push({ type: 'divider' }); + if (appearNote.userId === $i.id) { + menuItems.push({ icon: 'ph-pencil-simple ph-bold ph-lg', text: i18n.ts.edit, action: edit, - } : undefined, - { + }); + menuItems.push({ icon: 'ti ti-edit', text: i18n.ts.deleteAndEdit, danger: true, action: delEdit, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }] - : [] - )] - .filter(x => x !== undefined); + }); + } + menuItems.push({ + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: del, + }); + } } else { - menu = [{ + menuItems.push({ icon: 'ti ti-info-circle', text: i18n.ts.details, action: openDetail, @@ -469,38 +504,45 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? - getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - : undefined, - (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); - }, - } : undefined] - .filter(x => x !== undefined); + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); + + if (appearNote.url || appearNote.uri) { + menuItems.push( + getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') + ); + menuItems.push({ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); + }, + }); + } else { + menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); + } } if (noteActions.length > 0) { - menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({ + menuItems.push({ type: 'divider' }); + + menuItems.push(...noteActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(appearNote); }, - }))]); + }))); } if (defaultStore.state.devMode) { - menu = menu.concat([{ type: "divider" }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyNoteId, action: () => { copyToClipboard(appearNote.id); + os.success(); }, - }]); + }); } const cleanup = () => { @@ -511,7 +553,7 @@ export function getNoteMenu(props: { }; return { - menu, + menu: menuItems, cleanup, }; } diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts index 6fd9947ac1e75eff85eb7790d926f3c88955b432..58d486bf9b166d4d3798413cde389f78d6a2e1ac 100644 --- a/packages/frontend/src/scripts/get-note-summary.ts +++ b/packages/frontend/src/scripts/get-note-summary.ts @@ -27,13 +27,13 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // 本文 if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ''; + summary += `CW: ${note.cw}`; + } else if (note.text) { + summary += note.text; } // ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ã‚‹ã¨ã - if ((note.files || []).length !== 0) { + if (note.files && note.files.length !== 0) { summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`; } @@ -44,7 +44,7 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // 返信ã®ã¨ã if (note.replyId) { - if (note.reply) { + if (note.reply && !note.cw) { summary += `\n\nRE: ${getNoteSummary(note.reply)}`; } else { summary += '\n\nRE: ...'; @@ -53,7 +53,7 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // Renoteã®ã¨ã if (note.renoteId) { - if (note.renote) { + if (note.renote && !note.cw) { summary += `\n\nRN: ${getNoteSummary(note.renote)}`; } else { summary += '\n\nRN: ...'; diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 33f16a68aa618d4e8c6c8ae9b3b03b0324d1b20c..d15279d6333935a4e77431bde1b9622bae44faae 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -8,7 +8,7 @@ import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { host, url } from '@/config.js'; +import { host, url } from '@@/js/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; @@ -17,7 +17,8 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; -import { MenuItem } from '@/types/menu.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; +import type { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; @@ -147,123 +148,154 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - let menu: MenuItem[] = [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ icon: 'ti ti-at', text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host ?? host}`); }, - }, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{ - icon: 'ti ti-search', - text: i18n.ts.searchThisUsersNotes, - action: () => { - router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); - }, - }] : []) - , ...(iAmModerator ? [{ - icon: 'ti ti-user-exclamation', - text: i18n.ts.moderation, - action: () => { - router.push(`/admin/user/${user.id}`); - }, - }] : []), { + }); + + if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) { + menuItems.push({ + icon: 'ti ti-search', + text: i18n.ts.searchThisUsersNotes, + action: () => { + router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); + }, + }); + } + + if (iAmModerator) { + menuItems.push({ + icon: 'ti ti-user-exclamation', + text: i18n.ts.moderation, + action: () => { + router.push(`/admin/user/${user.id}`); + }, + }); + } + + menuItems.push({ icon: 'ti ti-rss', text: i18n.ts.copyRSS, action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, - }, ...(user.host != null && user.url != null ? [{ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - if (user.url == null) return; - window.open(user.url, '_blank', 'noopener'); - }, - }] : []), { + }); + + if (user.host != null && user.url != null) { + menuItems.push({ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + if (user.url == null) return; + window.open(user.url, '_blank', 'noopener'); + }, + }); + } else { + menuItems.push({ + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + type: 'parent', + children: [{ + text: i18n.ts.noteOfThisUser, + action: () => { + genEmbedCode('user-timeline', user.id); + }, + }], // TODO: ユーザーカードã®åŸ‹ã‚è¾¼ã¿ãªã© + }); + } + + menuItems.push({ icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }, ...($i ? [{ - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; - os.post({ specified: user, initialText: `${canonical} ` }); - }, - }, { type: 'divider' }, { - icon: 'ti ti-pencil', - text: i18n.ts.editMemo, - action: () => { - editMemo(); - }, - }, { - type: 'parent', - icon: 'ti ti-list', - text: i18n.ts.addToList, - children: async () => { - const lists = await userListsCache.fetch(); - return lists.map(list => { - const isListed = ref(list.userIds.includes(user.id)); - cleanups.push(watch(isListed, () => { - if (isListed.value) { - os.apiWithDialog('users/lists/push', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds.push(user.id); - }); - } else { - os.apiWithDialog('users/lists/pull', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds.splice(list.userIds.indexOf(user.id), 1); + }); + + if ($i) { + menuItems.push({ + icon: 'ti ti-mail', + text: i18n.ts.sendMessage, + action: () => { + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; + os.post({ specified: user, initialText: `${canonical} ` }); + }, + }, { type: 'divider' }, { + icon: 'ti ti-pencil', + text: i18n.ts.editMemo, + action: editMemo, + }, { + type: 'parent', + icon: 'ti ti-list', + text: i18n.ts.addToList, + children: async () => { + const lists = await userListsCache.fetch(); + return lists.map(list => { + const isListed = ref(list.userIds?.includes(user.id) ?? false); + cleanups.push(watch(isListed, () => { + if (isListed.value) { + os.apiWithDialog('users/lists/push', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds?.push(user.id); + }); + } else { + os.apiWithDialog('users/lists/pull', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds?.splice(list.userIds?.indexOf(user.id), 1); + }); + } + })); + + return { + type: 'switch', + text: list.name, + ref: isListed, + }; + }); + }, + }, { + type: 'parent', + icon: 'ti ti-antenna', + text: i18n.ts.addToAntenna, + children: async () => { + const antennas = await antennasCache.fetch(); + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; + return antennas.filter((a) => a.src === 'users').map(antenna => ({ + text: antenna.name, + action: async () => { + await os.apiWithDialog('antennas/update', { + antennaId: antenna.id, + name: antenna.name, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + src: antenna.src, + userListId: antenna.userListId, + users: [...antenna.users, canonical], + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, }); - } + antennasCache.delete(); + }, })); - - return { - type: 'switch', - text: list.name, - ref: isListed, - }; - }); - }, - }, { - type: 'parent', - icon: 'ti ti-antenna', - text: i18n.ts.addToAntenna, - children: async () => { - const antennas = await antennasCache.fetch(); - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; - return antennas.filter((a) => a.src === 'users').map(antenna => ({ - text: antenna.name, - action: async () => { - await os.apiWithDialog('antennas/update', { - antennaId: antenna.id, - name: antenna.name, - keywords: antenna.keywords, - excludeKeywords: antenna.excludeKeywords, - src: antenna.src, - userListId: antenna.userListId, - users: [...antenna.users, canonical], - caseSensitive: antenna.caseSensitive, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - notify: antenna.notify, - }); - antennasCache.delete(); - }, - })); - }, - }] : [])] as any; + }, + }); + } if ($i && meId !== user.id) { if (iAmModerator) { - menu = menu.concat([{ + menuItems.push({ type: 'parent', icon: 'ti ti-badges', text: i18n.ts.roles, @@ -301,13 +333,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }, })); }, - }]); + }); } // フォãƒãƒ¼ã—ãŸã¨ã—ã¦ã‚‚ user.isFollowing ã¯ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ æ›´æ–°ã•ã‚Œãªã„ã®ã§ä¸ä¾¿ãªãŸã‚ //if (user.isFollowing) { - const withRepliesRef = ref(user.withReplies); - menu = menu.concat([{ + const withRepliesRef = ref(user.withReplies ?? false); + + menuItems.push({ type: 'switch', icon: 'ti ti-messages', text: i18n.ts.showRepliesToOthersInTimeline, @@ -316,7 +349,8 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, action: toggleNotify, - }]); + }); + watch(withRepliesRef, (withReplies) => { misskeyApi('following/update', { userId: user.id, @@ -327,7 +361,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); //} - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, @@ -339,70 +373,68 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, - }]); + }); if (user.isFollowed) { - menu = menu.concat([{ + menuItems.push({ icon: 'ti ti-link-off', text: i18n.ts.breakFollow, action: invalidateFollow, - }]); + }); } - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-exclamation-circle', text: i18n.ts.reportAbuse, action: reportAbuse, - }]); + }); } if (user.host !== null) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-refresh', text: i18n.ts.updateRemoteUser, action: userInfoUpdate, - }]); + }); } if (defaultStore.state.devMode) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyUserId, action: () => { copyToClipboard(user.id); }, - }]); + }); } if ($i && meId === user.id) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-pencil', text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); }, - }]); + }); } if (userActions.length > 0) { - menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({ + menuItems.push({ type: 'divider' }, ...userActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(user); }, - }))]); + }))); } - const cleanup = () => { - if (_DEV_) console.log('user menu cleanup', cleanups); - for (const cl of cleanups) { - cl(); - } - }; - return { - menu, - cleanup, + menu: menuItems, + cleanup: () => { + if (_DEV_) console.log('user menu cleanup', cleanups); + for (const cl of cleanups) { + cl(); + } + }, }; } diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 6b511f2a5fc78ed8be89a74a393014984746f16f..20f51660c725ddc15a79b50d80a675ea0559477e 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,10 +10,11 @@ import { set as iset, del as idel, } from 'idb-keyval'; +import { miLocalStorage } from '@/local-storage.js'; -const fallbackName = (key: string) => `idbfallback::${key}`; +const PREFIX = 'idbfallback::'; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; // iframe.contentWindow.indexedDB.deleteDatabase() ãŒchromeã®ãƒã‚°ã§ä½¿ç”¨ã§ããªã„ãŸã‚ã€indexedDBを無効化ã—ã¦ã„る。 // ãƒã‚°ãŒæ²»ã£ã¦å†åº¦æœ‰åŠ¹åŒ–ã™ã‚‹ã®ã§ã‚ã‚Œã°ã€cypressã®ã‚³ãƒžãƒ³ãƒ‰å†…ã®ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆã‚’外ã™ã“㨠@@ -38,15 +39,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return JSON.parse(window.localStorage.getItem(fallbackName(key))); + return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); + return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); } export async function del(key: string) { if (idbAvailable) return idel(key); - return window.localStorage.removeItem(fallbackName(key)); + return miLocalStorage.removeItem(`${PREFIX}${key}`); } diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts index 1517e4e1e8fa7be0ea821ce84f7d2af2d77380bb..867ebf19edadd4cacc8dbf33944e7d84aad54e70 100644 --- a/packages/frontend/src/scripts/initialize-sw.ts +++ b/packages/frontend/src/scripts/initialize-sw.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@/config.js'; +import { lang } from '@@/js/config.js'; export async function initializeSw() { if (!('serviceWorker' in navigator)) return; diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts index aaa4f0a86edf4898662d1a3f6caeaea88f057c4c..385f59ec39c068f213d9151ed7c38289f85a0b12 100644 --- a/packages/frontend/src/scripts/intl-const.ts +++ b/packages/frontend/src/scripts/intl-const.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@/config.js'; +import { lang } from '@@/js/config.js'; export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 099a22163af44a0e8c2e6c3444bda13e2706fada..78eba35eaddb941518276539fb9286df400e0fa6 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -3,51 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { query } from '@/scripts/url.js'; -import { url } from '@/config.js'; +import { MediaProxy } from '@@/js/media-proxy.js'; +import { url } from '@@/js/config.js'; import { instance } from '@/instance.js'; -export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { - const localProxy = `${url}/proxy`; +let _mediaProxy: MediaProxy | null = null; - if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { - // ã‚‚ã†æ—¢ã«proxyã£ã½ãã†ã ã£ãŸã‚‰urlã‚’å–り出㙠- imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; +export function getProxiedImageUrl(...args: Parameters<MediaProxy['getProxiedImageUrl']>): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${mustOrigin ? localProxy : instance.mediaProxy}/${ - type === 'preview' ? 'preview.webp' - : 'image.webp' - }?${query({ - url: imageUrl, - ...(!noFallback ? { 'fallback': '1' } : {}), - ...(type ? { [type]: '1' } : {}), - ...(mustOrigin ? { origin: '1' } : {}), - })}`; + return _mediaProxy.getProxiedImageUrl(...args); } -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} - -export function getStaticImageUrl(baseUrl: string): string { - const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); - - if (u.href.startsWith(`${url}/emoji/`)) { - // ã‚‚ã†æ—¢ã«emojiã£ã½ãã†ã ã£ãŸã‚‰searchParams付ã‘ã‚‹ã ã‘ - u.searchParams.set('static', '1'); - return u.href; +export function getProxiedImageUrlNullable(...args: Parameters<MediaProxy['getProxiedImageUrlNullable']>): string | null { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - if (u.href.startsWith(instance.mediaProxy + '/')) { - // ã‚‚ã†æ—¢ã«proxyã£ã½ãã†ã ã£ãŸã‚‰searchParams付ã‘ã‚‹ã ã‘ - u.searchParams.set('static', '1'); - return u.href; + return _mediaProxy.getProxiedImageUrlNullable(...args); +} + +export function getStaticImageUrl(...args: Parameters<MediaProxy['getStaticImageUrl']>): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${instance.mediaProxy}/static.webp?${query({ - url: u.href, - static: '1', - })}`; + return _mediaProxy.getStaticImageUrl(...args); } diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 63acf9d3debcec849f0bbd0280b0746891d44e4a..2911469cddb9d671721c93dc5c9f4f312c29e596 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -6,7 +6,7 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@/const.js'; +import { MFM_TAGS } from '@@/js/const.js'; import type { MenuItem } from '@/types/menu.js'; /** diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index 49fb6f9e59c670f6ac0d7972b55fee911ce4ca35..1b1159fd015a8f3e67697d9dc7f43d193b531176 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -5,7 +5,7 @@ import * as Misskey from 'misskey-js'; import { ref } from 'vue'; -import { apiUrl } from '@/config.js'; +import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts index 53b2a9e44151857adfcd262a7e8065ebbf3b2303..39c6df65004a996c2ccd55de7947901333dace95 100644 --- a/packages/frontend/src/scripts/player-url-transform.ts +++ b/packages/frontend/src/scripts/player-url-transform.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { hostname } from '@/config.js'; +import { hostname } from '@@/js/config.js'; export function transformPlayerUrl(url: string): string { const urlObj = new URL(url); diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 1caa2dfc21017074e4233f2176e312e94d6a5f3a..5b141222e84f2e5f5f272964bd73843b82464ad0 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from './url.js'; -import * as config from '@/config.js'; +import { appendQuery } from '@@/js/url.js'; +import * as config from '@@/js/config.js'; export function popout(path: string, w?: HTMLElement) { let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 31a9ac1ad9dca54f33892827884254d48bee010b..11b6f52ddd0db4fb7d6f4e0589422ed6babd27a8 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -18,7 +18,7 @@ export type MiPostMessageEvent = { * 親フレームã«ã‚¤ãƒ™ãƒ³ãƒˆã‚’é€ä¿¡ */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.postMessage({ + window.parent.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts new file mode 100644 index 0000000000000000000000000000000000000000..733d91b85ab42625e51e0004a63a16ba9d0cde52 --- /dev/null +++ b/packages/frontend/src/scripts/reload-ask.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; + +let isReloadConfirming = false; + +export async function reloadAsk(opts: { + unison?: boolean; + reason?: string; +}) { + if (isReloadConfirming) { + return; + } + + isReloadConfirming = true; + + const { canceled } = await os.confirm(opts.reason == null ? { + type: 'info', + text: i18n.ts.reloadConfirm, + } : { + type: 'info', + title: i18n.ts.reloadConfirm, + text: opts.reason, + }).finally(() => { + isReloadConfirming = false; + }); + + if (canceled) return; + + if (opts.unison) { + unisonReload(); + } else { + location.reload(); + } +} diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts deleted file mode 100644 index 6bfcef6c362c208d4b3dbc4cbb3eb8a84e9c73d7..0000000000000000000000000000000000000000 --- a/packages/frontend/src/scripts/safe-parse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeParseFloat(str: unknown): number | null { - if (typeof str !== 'string' || str === '') return null; - const num = parseFloat(str); - if (isNaN(num)) return null; - return num; -} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 0edf4e9eba0f32f6bded54812f4a71afb74a1379..0000000000000000000000000000000000000000 --- a/packages/frontend/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/frontend/src/scripts/show-system-account-dialog.ts b/packages/frontend/src/scripts/show-system-account-dialog.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c28d901fc5ba507c9c97a4eeeb93f4edd512f71 --- /dev/null +++ b/packages/frontend/src/scripts/show-system-account-dialog.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; + +export function showSystemAccountDialog(): Promise<void> { + return os.alert({ + type: 'error', + title: i18n.ts.systemAccountTitle, + text: i18n.ts.systemAccountDescription, + }); +} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb0e607fcb6ad6a57e10a735ea8259696ef5ae9e --- /dev/null +++ b/packages/frontend/src/scripts/stream-mock.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Misskey from 'misskey-js'; +import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; + +type AnyOf<T extends Record<any, any>> = T[keyof T]; +type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never; + +/** + * Websocket無効化時ã«ä½¿ã†Streamã®ãƒ¢ãƒƒã‚¯ï¼ˆãªã«ã‚‚ã—ãªã„) + */ +export class StreamMock extends EventEmitter<StreamEvents> implements IStream { + public readonly state = 'initializing'; + + constructor(...args: ConstructorParameters<typeof Misskey.Stream>) { + super(); + // do nothing + } + + public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> { + return new ChannelConnectionMock(this, channel, name); + } + + public removeSharedConnection(connection: any): void { + // do nothing + } + + public removeSharedConnectionPool(pool: any): void { + // do nothing + } + + public disconnectToChannel(): void { + // do nothing + } + + public send(typeOrPayload: string): void + public send(typeOrPayload: string, payload: any): void + public send(typeOrPayload: Record<string, any> | any[]): void + public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void { + // do nothing + } + + public ping(): void { + // do nothing + } + + public heartbeat(): void { + // do nothing + } + + public close(): void { + // do nothing + } +} + +class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> { + public id = ''; + public name?: string; // for debug + public inCount = 0; // for debug + public outCount = 0; // for debug + public channel: string; + + constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) { + super(); + + this.channel = args[0]; + this.name = args[1]; + } + + public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void { + // do nothing + } + + public dispose(): void { + // do nothing + } +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index e59643b09cf4bedab6c4ccbe769e9be87341d895..bd3cddde67962a09319ea8bd3e8c97ee6a9c311d 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,11 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { @@ -54,7 +54,7 @@ export const getBuiltinThemes = () => Promise.all( 'd-u0', 'rosepine', 'rosepine-dawn', - ].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), + ].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); export const getBuiltinThemesRef = () => { @@ -78,6 +78,8 @@ export function applyTheme(theme: Theme, persist = true) { const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; + document.documentElement.dataset.colorScheme = colorScheme; + // Deep copy const _theme = deepClone(theme); diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 3e947183c9eaa370c22377983bcaf077625e831c..22dce609c692b3de69158eb6bb64fde3b1a1b2b4 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -9,10 +9,11 @@ import { v4 as uuid } from 'uuid'; import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; -import { apiUrl } from '@/config.js'; +import { apiUrl } from '@@/js/config.js'; import { $i } from '@/account.js'; import { alert } from '@/os.js'; import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; type Uploading = { id: string; @@ -39,6 +40,15 @@ export function uploadFile( if (folder && typeof folder === 'object') folder = folder.id; + if (file.size > instance.maxFileSize) { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, + }); + return Promise.reject(); + } + return new Promise((resolve, reject) => { const id = uuid(); diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts new file mode 100644 index 0000000000000000000000000000000000000000..0d505fe4668baf668261bee95809afe2ff72f003 --- /dev/null +++ b/packages/frontend/src/scripts/use-form.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { computed, Reactive, reactive, watch } from 'vue'; + +function copy<T>(v: T): T { + return JSON.parse(JSON.stringify(v)); +} + +function unwrapReactive<T>(v: Reactive<T>): T { + return JSON.parse(JSON.stringify(v)); +} + +export function useForm<T extends Record<string, any>>(initialState: T, save: (newState: T) => Promise<void>) { + const currentState = reactive<T>(copy(initialState)); + const previousState = reactive<T>(copy(initialState)); + + const modifiedStates = reactive<Record<keyof T, boolean>>({} as any); + for (const key in currentState) { + modifiedStates[key] = false; + } + const modified = computed(() => Object.values(modifiedStates).some(v => v)); + const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length); + + watch([currentState, previousState], () => { + for (const key in modifiedStates) { + modifiedStates[key] = currentState[key] !== previousState[key]; + } + }, { deep: true }); + + async function _save() { + await save(unwrapReactive(currentState)); + for (const key in currentState) { + previousState[key] = copy(currentState[key]); + } + } + + function discard() { + for (const key in currentState) { + currentState[key] = copy(previousState[key]); + } + } + + return { + state: currentState, + savedState: previousState, + modifiedStates, + modified, + modifiedCount, + save: _save, + discard, + }; +} diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index 1b3626bff5f7a035de42ca348490a5da749d410c..89aa023f23250a2f940ffa1bf4f49007670612db 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -13,16 +13,16 @@ import { misskeyApi } from './misskey-api.js'; export function useNoteCapture(props: { rootEl: ShallowRef<HTMLElement | undefined>; note: Ref<Misskey.entities.Note>; - pureNote: Ref<Misskey.entities.Note>; + pureNote?: Ref<Misskey.entities.Note>; isDeletedRef: Ref<boolean>; - onReplyCallback: (replyNote: Misskey.entities.Note) => void | undefined; - onDeleteCallback: (id: Misskey.entities.Note['id']) => void | undefined; + onReplyCallback?: (replyNote: Misskey.entities.Note) => void | Promise<void>; + onDeleteCallback?: (id: Misskey.entities.Note['id']) => void | Promise<void>; }) { const note = props.note; const pureNote = props.pureNote !== undefined ? props.pureNote : props.note; const connection = $i ? useStream() : null; - async function onStreamNoteUpdated(noteData): void { + async function onStreamNoteUpdated(noteData): Promise<void> { const { type, id, body } = noteData; if ((id !== note.value.id) && (id !== pureNote.value.id)) return; @@ -39,7 +39,7 @@ export function useNoteCapture(props: { await props.onReplyCallback(replyNote); } catch { /* empty */ } - + break; } @@ -81,7 +81,7 @@ export function useNoteCapture(props: { case 'pollVoted': { const choice = body.choice; - const choices = [...note.value.poll.choices]; + const choices = [...note.value.poll!.choices]; choices[choice] = { ...choices[choice], votes: choices[choice].votes + 1, @@ -90,7 +90,7 @@ export function useNoteCapture(props: { } : {}), }; - note.value.poll.choices = choices; + note.value.poll!.choices = choices; break; } @@ -106,7 +106,7 @@ export function useNoteCapture(props: { const editedNote = await misskeyApi('notes/show', { noteId: id, }); - + const keys = new Set<string>(); Object.keys(editedNote) .concat(Object.keys(note.value)) diff --git a/packages/frontend/src/scripts/warning-external-website.ts b/packages/frontend/src/scripts/warning-external-website.ts new file mode 100644 index 0000000000000000000000000000000000000000..67158c64385d30cd4b194a957acdf792a5b87c67 --- /dev/null +++ b/packages/frontend/src/scripts/warning-external-website.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { instance } from '@/instance.js'; +import { defaultStore } from '@/store.js'; +import * as os from '@/os.js'; +import MkUrlWarningDialog from '@/components/MkUrlWarningDialog.vue'; + +const extractDomain = /^(https?:\/\/|\/\/)?([^@/\s]+@)?(www\.)?([^:/\s]+)/i; +const isRegExp = /^\/(.+)\/(.*)$/; + +export async function warningExternalWebsite(url: string) { + const domain = extractDomain.exec(url)?.[4]; + + if (!domain) return false; + + const isTrustedByInstance = instance.trustedLinkUrlPatterns.some(expression => { + const r = isRegExp.exec(expression); + + if (r) { + return new RegExp(r[1], r[2]).test(url); + } else if (expression.includes(' ')) { + return expression.split(' ').every(keyword => url.includes(keyword)); + } else { + return domain.endsWith(expression); + } + }); + + const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(domain); + const isDisabledByUser = !defaultStore.reactiveState.warnExternalUrl.value; + + if (!isTrustedByInstance && !isTrustedByUser && !isDisabledByUser) { + const confirm = await new Promise<{ canceled: boolean }>(resolve => { + const { dispose } = os.popup(MkUrlWarningDialog, { + url, + }, { + done: result => { + resolve(result ?? { canceled: true }); + }, + closed: () => dispose(), + }); + }); + + if (confirm.canceled) return false; + + return window.open(url, '_blank', 'nofollow noopener popup=false'); + } + + return window.open(url, '_blank', 'nofollow noopener popup=false'); +} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index dda320dbacc7188e6fbc8481acbb7c76c94fe2d3..fd84ad2e4d579931a6a8f83b68e58fa204eb04fe 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -5,11 +5,14 @@ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { hemisphere } from '@@/js/intl-const.js'; +import lightTheme from '@@/themes/l-cherry.json5'; +import darkTheme from '@@/themes/d-ice.json5'; import { miLocalStorage } from './local-storage.js'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; +import type { FollowingFeedTab } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; -import { hemisphere } from '@/scripts/intl-const.js'; interface PostFormAction { title: string, @@ -165,6 +168,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: 'public' as 'public' | 'home' | 'followers', }, + trustedDomains: { + where: 'account', + default: [] as string[], + }, + warnExternalUrl: { + where: 'account', + default: true, + }, menu: { where: 'deviceAccount', @@ -231,6 +242,18 @@ export const defaultStore = markRaw(new Storage('base', { where: 'deviceAccount', default: [] as Misskey.entities.UserList[], }, + followingFeed: { + where: 'account', + default: { + withNonPublic: false, + withQuotes: false, + withBots: true, + withReplies: false, + onlyFiles: false, + userList: 'following' as FollowingFeedTab, + remoteWarningDismissed: false, + }, + }, overridedDeviceKind: { where: 'device', @@ -288,13 +311,17 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: window.matchMedia('(prefers-reduced-motion)').matches, }, + disableCatSpeak: { + where: 'account', + default: false, + }, emojiStyle: { where: 'device', default: 'twemoji', // twemoji / fluentEmoji / native }, - disableDrawer: { + menuStyle: { where: 'device', - default: false, + default: 'auto' as 'auto' | 'popup' | 'drawer', }, useBlurEffectForModal: { where: 'device', @@ -356,9 +383,9 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 2, }, - emojiPickerUseDrawerForMobile: { + emojiPickerStyle: { where: 'device', - default: true, + default: 'auto' as 'auto' | 'popup' | 'drawer', }, recentlyUsedEmojis: { where: 'device', @@ -456,10 +483,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'horizontal' as 'vertical' | 'horizontal', }, - enableCondensedLineForAcct: { + notificationClickable: { where: 'device', default: false, }, + enableCondensedLine: { + where: 'device', + default: true, + }, additionalUnicodeEmojiIndexes: { where: 'device', default: {} as Record<string, Record<string, string[]>>, @@ -524,10 +555,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - contextMenu: { + contextMenu: { where: 'device', default: 'app' as 'app' | 'appWithShift' | 'native', - }, + }, sound_masterVolume: { where: 'device', @@ -586,8 +617,6 @@ interface Watcher { /** * 常ã«ãƒ¡ãƒ¢ãƒªã«ãƒãƒ¼ãƒ‰ã—ã¦ãŠãå¿…è¦ãŒãªã„よã†ãªè¨å®šæƒ…å ±ã‚’ä¿ç®¡ã™ã‚‹ã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸(éžãƒªã‚¢ã‚¯ãƒ†ã‚£ãƒ–) */ -import lightTheme from '@/themes/l-cherry.json5'; -import darkTheme from '@/themes/d-ice.json5'; export class ColdDeviceStorage { public static default = { @@ -624,7 +653,7 @@ export class ColdDeviceStorage { public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼ã³å‡ºã—å´ã®ãƒã‚°ç‰ã§ undefined ãŒæ¥ã‚‹ã“ã¨ãŒã‚ã‚‹ // undefined ã‚’æ–‡å—列ã¨ã—㦠miLocalStorage ã«å…¥ã‚Œã‚‹ã¨å‚ç…§ã™ã‚‹éš›ã® JSON.parse ã§ã‚³ã‚±ã¦ä¸å…·åˆã®å…ƒã«ãªã‚‹ãŸã‚無視 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index 0d5bd78b09af845f20d1a99c3803833f9013c711..e63dac951c0033d8bdc45d37b198af06af3dac32 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -6,18 +6,21 @@ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; -import { wsOrigin } from '@/config.js'; +import { wsOrigin } from '@@/js/config.js'; +// TODO: No Websocketモードã§StreamMockãŒä½¿ãˆãㆠ+//import { StreamMock } from '@/scripts/stream-mock.js'; // heart beat interval in ms const HEART_BEAT_INTERVAL = 1000 * 60; -let stream: Misskey.Stream | null = null; -let timeoutHeartBeat: ReturnType<typeof setTimeout> | null = null; +let stream: Misskey.IStream | null = null; +let timeoutHeartBeat: number | null = null; let lastHeartbeatCall = 0; -export function useStream(): Misskey.Stream { +export function useStream(): Misskey.IStream { if (stream) return stream; + // TODO: No Websocketモードもã“ã“ã§åˆ¤å®š stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 62ba7a08d55e45bce8e6f4d5b54cbd3f2875b8c4..d990a706b3ca2dffa0cbae7e63940702df669203 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -33,21 +33,14 @@ --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); --minBottomSpacing: var(--minBottomSpacingMobile); + //--ad: rgb(255 169 0 / 10%); + @media (max-width: 500px) { --margin: var(--marginHalf); } --avatar: 48px; --thread-width: 2px; - - //--ad: rgb(255 169 0 / 10%); - --eventFollow: #36aed2; - --eventRenote: #36d298; - --eventReply: #007aff; - --eventReactionHeart: #dd2e44; - --eventReaction: #e99a0b; - --eventAchievement: #cb9a11; - --eventOther: #88a6b7; } html.radius-misskey { @@ -289,11 +282,11 @@ rt { background: var(--accent); &:not(:disabled):hover { - background: var(--X8); + background: hsl(from var(--accent) h s calc(l + 5)); } &:not(:disabled):active { - background: var(--X9); + background: hsl(from var(--accent) h s calc(l - 5)); } } @@ -303,11 +296,11 @@ rt { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } &:not(:disabled):active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); } } @@ -412,6 +405,16 @@ rt { vertical-align: top; } +._modified { + margin-left: 0.7em; + font-size: 65%; + padding: 2px 3px; + color: var(--warn); + border: solid 1px var(--warn); + border-radius: 4px; + vertical-align: top; +} + ._table { > ._row { display: flex; @@ -481,7 +484,7 @@ rt { --fg: #693410; } -html[data-color-mode=dark] ._woodenFrame { +html[data-color-scheme=dark] ._woodenFrame { --bg: #1d0c02; --fg: #F1E8DC; --panel: #192320; diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 17079b3ddc705e9aa08127e1b8e9a9155f6d730c..8355ae3061580fb92c142b69904d2015385ad144 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -7,7 +7,7 @@ import { defineAsyncComponent } from 'vue'; import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -41,7 +41,9 @@ function toolsMenuItems(): MenuItem[] { } export function openInstanceMenu(ev: MouseEvent) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ text: instance.name ?? host, type: 'label', }, { @@ -69,12 +71,18 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.ads, icon: 'ti ti-ad', to: '/ads', - }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? { - type: 'link', - to: '/invite', - text: i18n.ts.invite, - icon: 'ti ti-user-plus', - } : undefined, { + }); + + if ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) { + menuItems.push({ + type: 'link', + to: '/invite', + text: i18n.ts.invite, + icon: 'ti ti-user-plus', + }); + } + + menuItems.push({ type: 'parent', text: i18n.ts.tools, icon: 'ti ti-tool', @@ -84,50 +92,80 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.inquiry, icon: 'ti ti-help-circle', to: '/contact', - }, (instance.impressumUrl) ? { - type: 'a', - text: i18n.ts.impressum, - icon: 'ti ti-file-invoice', - href: instance.impressumUrl, - target: '_blank', - } : undefined, (instance.tosUrl) ? { - type: 'a', - text: i18n.ts.termsOfService, - icon: 'ti ti-notebook', - href: instance.tosUrl, - target: '_blank', - } : undefined, (instance.privacyPolicyUrl) ? { - type: 'a', - text: i18n.ts.privacyPolicy, - icon: 'ti ti-shield-lock', - href: instance.privacyPolicyUrl, - target: '_blank', - } : undefined, (instance.donationUrl) ? { - type: 'a', - text: i18n.ts.donation, - icon: 'ph-hand-coins ph-bold ph-lg', - href: instance.donationUrl, - target: '_blank', - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) ? undefined : { type: 'divider' }, { + }); + + if (instance.impressumUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.impressum, + icon: 'ti ti-file-invoice', + href: instance.impressumUrl, + target: '_blank', + }); + } + + if (instance.tosUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.termsOfService, + icon: 'ti ti-notebook', + href: instance.tosUrl, + target: '_blank', + }); + } + + if (instance.privacyPolicyUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.privacyPolicy, + icon: 'ti ti-shield-lock', + href: instance.privacyPolicyUrl, + target: '_blank', + }); + } + + if (instance.donationUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.donation, + icon: 'ph-hand-coins ph-bold ph-lg', + href: instance.donationUrl, + target: '_blank', + }); + } + + if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) { + menuItems.push({ type: 'divider' }); + } + + menuItems.push({ type: 'a', text: i18n.ts.document, icon: 'ti ti-bulb', href: 'https://misskey-hub.net/docs/for-users/', target: '_blank', - }, ($i) ? { - text: i18n.ts._initialTutorial.launchTutorial, - icon: 'ti ti-presentation', - action: () => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { - closed: () => dispose(), - }); - }, - } : undefined, { + }); + + if ($i) { + menuItems.push({ + text: i18n.ts._initialTutorial.launchTutorial, + icon: 'ti ti-presentation', + action: () => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { + closed: () => dispose(), + }); + }, + }); + } + + menuItems.push({ type: 'link', text: i18n.ts.aboutMisskey, icon: 'sk-icons sk-shark sk-icons-lg', to: '/about-sharkey', - }], ev.currentTarget ?? ev.target, { + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target, { align: 'left', }); } diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 442b6479dd29c6af2a50789cab1023516a29b8ac..a8ff2a4c8d1f75473339a56f49afdc23a26e3af8 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -30,7 +30,11 @@ SPDX-License-Identifier: AGPL-3.0-only :enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''" > - <div v-for="notification in notifications" :key="notification.id" :class="$style.notification"> + <div + v-for="notification in notifications" :key="notification.id" :class="$style.notification" :style="{ + pointerEvents: getPointerEvents() + }" + > <XNotification :notification="notification"/> </div> </TransitionGroup> @@ -39,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="pendingApiRequestsCount > 0" id="wait"></div> -<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div> +<div v-if="dev" id="devTicker"><span style="animation: dev-ticker-blink 2s infinite;">DEV BUILD</span></div> <div v-if="$i && $i.isBot" id="botWarn"><span>{{ i18n.ts.loggedInAsBot }}</span></div> @@ -101,6 +105,10 @@ if ($i) { swInject(); } } + +function getPointerEvents() { + return defaultStore.state.notificationClickable ? undefined : 'none'; +} </script> <style lang="scss" module> @@ -122,7 +130,6 @@ if ($i) { position: fixed; z-index: 3900000; padding: 0 var(--margin); - pointer-events: none; display: flex; &.notificationsPosition_leftTop { @@ -263,10 +270,6 @@ if ($i) { font-size: 14px; pointer-events: none; user-select: none; - - > span { - animation: dev-ticker-blink 2s infinite; - } } #devTicker { @@ -280,9 +283,5 @@ if ($i) { font-size: 14px; pointer-events: none; user-select: none; - - > span { - animation: dev-ticker-blink 2s infinite; - } } </style> diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index a3f9d6cf2cc5a6f58a6206f43024aeba5c461c96..f3244b5697d69dd2193e63d72ee327402b7f8b18 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.top"> <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> <button class="_button" :class="$style.instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> + <img :src="instance.sidebarLogoUrl || instance.iconUrl || '/apple-touch-icon.png'" alt="" :class="instance.sidebarLogoUrl ? $style.wideInstanceIcon : $style.instanceIcon"/> </button> </div> <div :class="$style.middle"> @@ -82,6 +82,8 @@ function more() { <style lang="scss" module> .root { + --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5); + display: flex; flex-direction: column; } @@ -91,7 +93,7 @@ function more() { top: 0; z-index: 1; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -121,11 +123,18 @@ function more() { aspect-ratio: 1; } +.wideInstanceIcon { + display: inline-block; + min-width: 38px; + max-width: 100%; + max-height: 80px; +} + .bottom { position: sticky; bottom: 0; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 1f1fd37def98a6019635b38d2e7b87b232936f1f..17690df4121cc22026678ad683a24c1edc80d69e 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.top"> <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div> <button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/> + <img :src="instance.sidebarLogoUrl && !iconOnly ? instance.sidebarLogoUrl : instance.iconUrl || '/apple-touch-icon.png'" alt="" :class="instance.sidebarLogoUrl && !iconOnly ? $style.wideInstanceIcon : $style.instanceIcon"/> </button> </div> <div :class="$style.middle"> @@ -111,6 +111,7 @@ function more(ev: MouseEvent) { .root { --nav-width: 250px; --nav-icon-only-width: 80px; + --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5); flex: 0 0 var(--nav-width); width: var(--nav-width); @@ -144,7 +145,7 @@ function more(ev: MouseEvent) { top: 0; z-index: 1; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -183,11 +184,18 @@ function more(ev: MouseEvent) { aspect-ratio: 1; } + .wideInstanceIcon { + display: inline-block; + min-width: 38px; + max-width: 100%; + max-height: 80px; + } + .bottom { position: sticky; bottom: 0; padding-top: 20px; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -378,7 +386,7 @@ function more(ev: MouseEvent) { top: 0; z-index: 1; padding: 20px 0; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } @@ -408,7 +416,7 @@ function more(ev: MouseEvent) { position: sticky; bottom: 0; padding-top: 20px; - background: var(--X14); + background: var(--nav-bg-transparent); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); } diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index 8dad6666235daac237d61f9b00e6b511087e0366..e234bb3a33a0393cad69fecbd24786ad33956625 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -35,7 +35,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index 6e1d06eec1f99e620334bd66e055c4f9bda66eee..550fc39b001bf57145da603baa7ca3844b020279 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { shuffle } from '@/scripts/shuffle.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 67f8b109c484a6d15855b510867852e0cd05e64e..078b595dca744b2bb830746cf4c9fa2fc238fb15 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -35,7 +35,7 @@ import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index 872c69810c750011736e1571ca79e260d081cdcb..690366307b1c5a711ff34a80a1579d58a8ee38b6 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -40,6 +40,14 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') --nameMargin: 10px; font-size: 0.85em; + display: flex; + vertical-align: bottom; + width: 100%; + line-height: var(--height); + height: var(--height); + overflow: clip; + contain: strict; + &.verySmall { --nameMargin: 7px; --height: 16px; @@ -64,14 +72,6 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') font-size: 0.9em; } - display: flex; - vertical-align: bottom; - width: 100%; - line-height: var(--height); - height: var(--height); - overflow: clip; - contain: strict; - &.black { background: #000; color: #fff; diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue index 244bac6f102eadcc8b7e17c424b564be9762d953..96030c7897c9286c4dc16589432608bcd6ac7f32 100644 --- a/packages/frontend/src/ui/_common_/upload.vue +++ b/packages/frontend/src/ui/_common_/upload.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="top"> <p class="name"><MkLoading :em="true"/>{{ ctx.name }}</p> <p class="status"> - <span v-if="ctx.progressValue === undefined" class="initing">{{ i18n.ts.waiting }}<MkEllipsis/></span> + <span v-if="ctx.progressValue === undefined" class="initing">{{ i18n.ts.uploading }}</span> <span v-if="ctx.progressValue !== undefined" class="kb">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span> <span v-if="ctx.progressValue !== undefined" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span> </p> diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index d49df8e8ac651eb44b94988d4d86e56933e7356e..96cc24c9b9c21621b2fcacb62239c70436fa6911 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="divider"></div> <div class="about"> <button v-click-anime class="item _button" @click="openInstanceMenu"> - <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/> + <img :src="instance.sidebarLogoUrl && !iconOnly ? instance.sidebarLogoUrl : instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" :class="{ wideIcon: instance.sidebarLogoUrl && !iconOnly }" class="_ghost" /> </button> </div> <!--<MisskeyLogo class="misskey"/>--> @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue'; import { openInstanceMenu } from './_common_/common.js'; -// import { host } from '@/config.js'; +// import { host } from '@@/js/config.js'; import * as os from '@/os.js'; import { navbarItemDef } from '@/navbar.js'; import { openAccountMenu as openAccountMenu_, $i } from '@/account.js'; @@ -66,7 +66,6 @@ import { i18n } from '@/i18n.js'; const WINDOW_THRESHOLD = 1400; const menu = ref(defaultStore.state.menu); -const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay')); const otherNavItemIndicated = computed<boolean>(() => { for (const def in navbarItemDef) { if (menu.value.includes(def)) continue; @@ -81,10 +80,12 @@ const iconOnly = ref(false); const settingsWindowed = ref(false); function calcViewState() { - iconOnly.value = (window.innerWidth <= WINDOW_THRESHOLD) || (menuDisplay.value === 'sideIcon'); + iconOnly.value = (window.innerWidth <= WINDOW_THRESHOLD) || (defaultStore.state.menuDisplay === 'sideIcon'); settingsWindowed.value = (window.innerWidth > WINDOW_THRESHOLD); } +calcViewState(); + function more(ev: MouseEvent) { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { src: ev.currentTarget ?? ev.target, @@ -166,7 +167,6 @@ watch(defaultStore.reactiveState.menuDisplay, () => { top: 0; z-index: 1; padding: 16px 0; - background: var(--bg); > .button { min-width: 0; @@ -180,12 +180,15 @@ watch(defaultStore.reactiveState.menuDisplay, () => { > .item { display: block; - width: 32px; margin: 0 auto; img { display: block; - width: 100%; + width: 32px; + &.wideIcon { + width: 80%; + margin: 0 auto; + } } } } diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index ce5a22a61d4665f235f8fe75e9dafaca1be0ca1f..31bb1ddc14928a4db1a41ec235d70498e9ab2ed6 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue'; import XSidebar from './classic.sidebar.vue'; import XCommon from './_common_/common.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { StickySidebar } from '@/scripts/sticky-sidebar.js'; import * as os from '@/os.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; @@ -57,6 +57,8 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { mainRouter } from '@/router/main.js'; +import { isLink } from '@@/js/is-link.js'; + const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue')); const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); @@ -104,12 +106,6 @@ function top() { } function onContextmenu(ev: MouseEvent) { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection().toString() !== '') return; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 44f1af5f8fdde0fa8c486f50cb9f206c3ca36b4a..cd4e256056332e717829b6e294b3cb0ffdd7e73d 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -118,7 +118,7 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; import { mainRouter } from '@/router/main.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); @@ -451,6 +451,7 @@ body { &:active { color: var(--accent); + background: hsl(from var(--panel) h s calc(l - 2)); } } @@ -460,12 +461,12 @@ body { color: var(--fgOnAccent); &:hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } &:active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } } diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index 987bd4db557e4a7888ec38719f634bb359ac89f6..a41639e71c46cf551c06f992086c6e9b9b0e8388 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -22,7 +22,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { antennasCache } from '@/cache.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 518df7a6fcd530c9764ccf435b6f0a12b33708b6..661d45b110354b6fc783e0dedf950d007d4840a7 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -29,7 +29,7 @@ import * as os from '@/os.js'; import { favoritedChannelsCache } from '@/cache.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 5fed70fc9055763e45531d682e39eab195fa5310..5ed3aa754f377f1294736e566ac2dcc7502fb17e 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -46,7 +46,7 @@ import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column } from './deck-store.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; provide('shouldHeaderThin', true); provide('shouldOmitHeaderTitle', true); @@ -104,7 +104,27 @@ function toggleActive() { } function getMenu() { - let items: MenuItem[] = [{ + const menuItems: MenuItem[] = []; + + if (props.menu) { + menuItems.push(...props.menu, { + type: 'divider', + }); + } + + if (props.refresher) { + menuItems.push({ + icon: 'ti ti-refresh', + text: i18n.ts.reload, + action: () => { + if (props.refresher) { + props.refresher(); + } + }, + }); + } + + menuItems.push({ icon: 'ti ti-settings', text: i18n.ts._deck.configureColumn, action: async () => { @@ -129,74 +149,73 @@ function getMenu() { if (canceled) return; updateColumn(props.column.id, result); }, + }); + + const moveToMenuItems: MenuItem[] = []; + + moveToMenuItems.push({ + icon: 'ti ti-arrow-left', + text: i18n.ts._deck.swapLeft, + action: () => { + swapLeftColumn(props.column.id); + }, }, { - type: 'parent', - text: i18n.ts.move + '...', - icon: 'ti ti-arrows-move', - children: [{ - icon: 'ti ti-arrow-left', - text: i18n.ts._deck.swapLeft, - action: () => { - swapLeftColumn(props.column.id); - }, - }, { - icon: 'ti ti-arrow-right', - text: i18n.ts._deck.swapRight, - action: () => { - swapRightColumn(props.column.id); - }, - }, props.isStacked ? { + icon: 'ti ti-arrow-right', + text: i18n.ts._deck.swapRight, + action: () => { + swapRightColumn(props.column.id); + }, + }); + + if (props.isStacked) { + moveToMenuItems.push({ icon: 'ti ti-arrow-up', text: i18n.ts._deck.swapUp, action: () => { swapUpColumn(props.column.id); }, - } : undefined, props.isStacked ? { + }, { icon: 'ti ti-arrow-down', text: i18n.ts._deck.swapDown, action: () => { swapDownColumn(props.column.id); }, - } : undefined], + }); + } + + menuItems.push({ + type: 'parent', + text: i18n.ts.move + '...', + icon: 'ti ti-arrows-move', + children: moveToMenuItems, }, { icon: 'ti ti-stack-2', text: i18n.ts._deck.stackLeft, action: () => { stackLeftColumn(props.column.id); }, - }, props.isStacked ? { - icon: 'ti ti-window-maximize', - text: i18n.ts._deck.popRight, - action: () => { - popRightColumn(props.column.id); - }, - } : undefined, { type: 'divider' }, { + }); + + if (props.isStacked) { + menuItems.push({ + icon: 'ti ti-window-maximize', + text: i18n.ts._deck.popRight, + action: () => { + popRightColumn(props.column.id); + }, + }); + } + + menuItems.push({ type: 'divider' }, { icon: 'ti ti-trash', text: i18n.ts.remove, danger: true, action: () => { removeColumn(props.column.id); }, - }]; - - if (props.menu) { - items.unshift({ type: 'divider' }); - items = props.menu.concat(items); - } - - if (props.refresher) { - items = [{ - icon: 'ti ti-refresh', - text: i18n.ts.reload, - action: () => { - if (props.refresher) { - props.refresher(); - } - }, - }, ...items]; - } + }); - return items; + return menuItems; } function showSettingsMenu(ev: MouseEvent) { @@ -324,11 +343,11 @@ function onDrop(ev) { > .body { background: transparent !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: transparent; } - scrollbar-color: var(--scrollbarHandle) transparent; } } @@ -338,11 +357,11 @@ function onDrop(ev) { > .body { background: var(--bg) !important; overflow-y: scroll !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: inherit; } - scrollbar-color: var(--scrollbarHandle) transparent; } } } @@ -423,10 +442,10 @@ function onDrop(ev) { box-sizing: border-box; container-type: size; background-color: var(--bg); + scrollbar-color: var(--scrollbarHandle) var(--panel); &::-webkit-scrollbar-track { background: var(--panel); } - scrollbar-color: var(--scrollbarHandle) var(--panel); } </style> diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index a0e318f7eb7c629065970673dea86cdb5d345241..8762fb0cce1ed444dc7a0adc8940ba1e87775b3c 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -22,7 +22,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; import { userListsCache } from '@/cache.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 79c967191709a6e7fb635c4a1d31cbe490a79f37..f8c712c3713d7b2933493a2591dec1addf747c49 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -26,7 +26,8 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; +import { isLink } from '@@/js/is-link.js'; import { mainRouter } from '@/router/main.js'; defineProps<{ @@ -52,12 +53,6 @@ function back() { function onContextmenu(ev: MouseEvent) { if (!ev.target) return; - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; if (isLink(ev.target as HTMLElement)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index a375e9c574f82276f5fac5d8f77fe93d49df84f1..beb4237978c3b48440a77827fbc1feb6b0b525b7 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -21,7 +21,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 17afa12551cbbda7e1d4136cbfe3c6e3e049831e..8315f7fca572bb88a341f67e55b26df974851e65 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -115,29 +115,41 @@ function onNote() { sound.playMisskeySfxFile(soundSetting.value); } -const menu = computed<MenuItem[]>(() => [{ - icon: 'ti ti-pencil', - text: i18n.ts.timeline, - action: setType, -}, { - icon: 'ti ti-bell', - text: i18n.ts._deck.newNoteNotificationSettings, - action: () => soundSettingsButton(soundSetting), -}, { - type: 'switch', - text: i18n.ts.showRenotes, - ref: withRenotes, -}, hasWithReplies(props.column.tl) ? { - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, -} : undefined, { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: hasWithReplies(props.column.tl) ? withReplies : false, -}]); +const menu = computed<MenuItem[]>(() => { + const menuItems: MenuItem[] = []; + + menuItems.push({ + icon: 'ti ti-pencil', + text: i18n.ts.timeline, + action: setType, + }, { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), + }, { + type: 'switch', + text: i18n.ts.showRenotes, + ref: withRenotes, + }); + + if (hasWithReplies(props.column.tl)) { + menuItems.push({ + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles, + }); + } + + menuItems.push({ + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: hasWithReplies(props.column.tl) ? withReplies : false, + }); + + return menuItems; +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index db5eb19c2079e680ce6219fac5a45489ab1c2f44..9e41c48c5bcf972c8e78ac1262df12fb8b89700c 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, provide, ref } from 'vue'; import XCommon from './_common_/common.vue'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import { mainRouter } from '@/router/main.js'; const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index'); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index d2b5d8cc42152de90bd4b9242a4e8dc2dfb9fc32..2fdaca775bc1d49463e7844730b62c50e2de7a5e 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue'; import XCommon from './_common_/common.vue'; import type MkStickyContainer from '@/components/global/MkStickyContainer.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import * as os from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -108,9 +108,10 @@ import { $i } from '@/account.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; -import { CURRENT_STICKY_BOTTOM } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; import { useScrollPositionManager } from '@/nirax.js'; import { mainRouter } from '@/router/main.js'; +import { isLink } from '@@/js/is-link.js'; const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue')); const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue')); @@ -195,12 +196,6 @@ onMounted(() => { }); const onContextmenu = (ev) => { - const isLink = (el: HTMLElement) => { - if (el.tagName === 'A') return true; - if (el.parentElement) { - return isLink(el.parentElement); - } - }; if (isLink(ev.target)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; @@ -423,10 +418,12 @@ $widgets-hide-threshold: 1090px; color: var(--fg); &:hover { + background: var(--panelHighlight); color: var(--accent); } &:active { + background: hsl(from var(--panel) h s calc(l - 2)); color: var(--accent); } } @@ -437,12 +434,12 @@ $widgets-hide-threshold: 1090px; color: var(--fgOnAccent); &:hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } &:active { - background: linear-gradient(90deg, var(--X8), var(--X8)); + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); color: var(--fgOnAccent); } } diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index ccc8d8fd973efeac84ebeed9ad26e6f7e8a36245..510b2a4342b3ccf2f05c8a1a0eda12cbb4aae8a4 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, provide, ref, computed } from 'vue'; import XCommon from './_common_/common.vue'; -import { instanceName } from '@/config.js'; +import { instanceName } from '@@/js/config.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; import XSigninDialog from '@/components/MkSigninDialog.vue'; diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index 47502b2af1feae601323484c5476743e86a49829..ac13d7822f6f56ae85d3f71dd1a89b2214e07c67 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, provide, ref } from 'vue'; import XCommon from './_common_/common.vue'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; -import { instanceName, ui } from '@/config.js'; +import { instanceName, ui } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; import { mainRouter } from '@/router/main.js'; diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 49fd103d37eb60f8ce8ed72ff2f7bcac741b6ab8..bcfaaf00ab4ff10060ae21a5c0af7131bee5370a 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { useInterval } from '@@/js/use-interval.js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import { $i } from '@/account.js'; diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 19843f3949c582d5b9195173564fa5df270877eb..e4ac1acfc7b4c9049833c93b2d61ff4ae7206e26 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -42,7 +42,7 @@ import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; const name = 'calendar'; diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index 2ea1253c2b42b354786ea9d93a6672da223b0bde..e91a77beab330e53159497e229f2599d16cca26c 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -32,7 +32,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue index 76ccdb397119a7e5516763a49008fd34b6e825e2..d090372b9a26a9cd31317a21d9e0322288655795 100644 --- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue +++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue @@ -26,7 +26,7 @@ import MkContainer from '@/components/MkContainer.vue'; import MkTagCloud from '@/components/MkTagCloud.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const name = 'instanceCloud'; diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index ce8b0dd60254c275704989ea0be42960d76f09ae..014cf01a5d7210e40892c8b30790243272d266cb 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; -import { host } from '@/config.js'; +import { host } from '@@/js/config.js'; import { instance } from '@/instance.js'; const name = 'instanceInfo'; diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index b69152b4feaa3be66ca57a2dfb46c68e0d8753b6..ee89beb944c00b11af20ec1455e2dfa50445f362 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header>{{ i18n.ts._widgets.memo }}</template> <div :class="$style.root"> - <textarea v-model="text" :style="`height: ${widgetProps.height}px;`" :class="$style.textarea" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea> + <textarea v-model="text" :style="`height: ${widgetProps.height}px;`" :class="$style.textarea" @input="onChange"></textarea> <button :class="$style.save" :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button> </div> </MkContainer> diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue index 5c89a06c62062dd03f92c41d4ce768d09b3a1049..d56ee96ac1751e374eb1cbaefa35e1a03f5a062c 100644 --- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue +++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue @@ -18,7 +18,7 @@ import { ref } from 'vue'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import number from '@/filters/number.js'; diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index e5758662cc49479c8ef5b79ff5b8f8bbb7d1a2ce..511777a5709c427bc8e453833283aefa6b3da6e4 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -28,9 +28,9 @@ import * as Misskey from 'misskey-js'; import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; -import { url as base } from '@/config.js'; +import { url as base } from '@@/js/config.js'; import { i18n } from '@/i18n.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { infoImageUrl } from '@/instance.js'; const name = 'rss'; diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index 16306ef5ba9b39ca8fed919115d266aba9a1f576..b393ecd74b9d6f0bdf2c95d742cd19f85f20b60b 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -34,8 +34,8 @@ import MarqueeText from '@/components/MkMarquee.vue'; import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import { shuffle } from '@/scripts/shuffle.js'; -import { url as base } from '@/config.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { url as base } from '@@/js/config.js'; +import { useInterval } from '@@/js/use-interval.js'; const name = 'rssTicker'; diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue index 294c97e2932d0ae065c12409653025d5ccafd191..1a328be7ce2ae6c212bdf1f009435388c02679c2 100644 --- a/packages/frontend/src/widgets/WidgetSearch.vue +++ b/packages/frontend/src/widgets/WidgetSearch.vue @@ -24,6 +24,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import * as os from '@/os.js'; import { useRouter } from '@/router/supplier.js'; import { GetFormResultType } from '@/scripts/form.js'; +import { notesSearchAvailable } from '@/scripts/check-permissions.js'; const name = 'search'; @@ -71,7 +72,7 @@ function options(ev) { { type: 'button', icon: 'ph-image ph-bold ph-lg', - text: 'With Images', + text: `With Images ${filetype.value === 'image' ? '✔' : ''}`, action: () => { filetype.value = 'image'; }, @@ -79,7 +80,7 @@ function options(ev) { { type: 'button', icon: 'ph-music-notes-simple ph-bold ph-lg', - text: 'With Audios', + text: `With Audios ${filetype.value === 'audio' ? '✔' : ''}`, action: () => { filetype.value = 'audio'; }, @@ -87,12 +88,19 @@ function options(ev) { { type: 'button', icon: 'ph-video ph-bold ph-lg', - text: 'With Videos', + text: `With Videos ${filetype.value === 'video' ? '✔' : ''}`, action: () => { filetype.value = 'video'; }, }], - }], ev.currentTarget ?? ev.target); + }, + ...(filetype.value ? [{ + text: 'Clear Filter', + icon: 'ti ti-trash', + action: () => { + filetype.value = null; + }, + }] : [])], ev.currentTarget ?? ev.target); } async function search() { @@ -128,6 +136,14 @@ async function search() { return; } + if (!notesSearchAvailable) { + os.alert({ + type: 'warning', + text: i18n.ts.notesSearchNotAvailable, + }); + return; + } + notePagination.value = { endpoint: 'notes/search', limit: 10, diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue index b8efd3bda9fc7808ab0a5bfe339da3b34d444b22..3fea1d70534429af206a158e11e926fe0fe505e8 100644 --- a/packages/frontend/src/widgets/WidgetSlideshow.vue +++ b/packages/frontend/src/widgets/WidgetSlideshow.vue @@ -23,7 +23,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid import { GetFormResultType } from '@/scripts/form.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; const name = 'slideshow'; diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index d02f9b8e224abdba1a8005ae620b1b1f4ff1b193..a4685fd1fc6ca7fffcc2f0b7742f83f2343d8129 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -40,6 +40,7 @@ import MkContainer from '@/components/MkContainer.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { i18n } from '@/i18n.js'; import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; +import type { MenuItem } from '@/types/menu.js'; const name = 'timeline'; @@ -109,11 +110,26 @@ const choose = async (ev) => { setSrc('list'); }, })); - os.popupMenu([...availableBasicTimelines().map(tl => ({ + + const menuItems: MenuItem[] = []; + + menuItems.push(...availableBasicTimelines().map(tl => ({ text: i18n.ts._timelines[tl], icon: basicTimelineIconClass(tl), action: () => { setSrc(tl); }, - })), antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { + }))); + + if (antennaItems.length > 0) { + menuItems.push({ type: 'divider' }); + menuItems.push(...antennaItems); + } + + if (listItems.length > 0) { + menuItems.push({ type: 'divider' }); + menuItems.push(...listItems); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); }; diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 4299181a273f93c23758e6cd3325bf12df0c1377..a41db513e8d1eeca371bd557d5bef3d87657b1a5 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import MkMiniChart from '@/components/MkMiniChart.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index d9f4dc49ea87aeeb621e6e111470383f3c4d184c..72391d622eeb3b470f74bd2362c23acd3478d48f 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -31,7 +31,7 @@ import { GetFormResultType } from '@/scripts/form.js'; import MkContainer from '@/components/MkContainer.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue index 27d32342073526dd6c9769566ce4c30a9de13060..469075e2c49c9a15593ed8e9238aedb9bf205462 100644 --- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -138,7 +138,7 @@ function onStats(connStats: Misskey.entities.ServerStats) { } function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { - for (const revStats of statsLog.reverse()) { + for (const revStats of statsLog.toReversed()) { onStats(revStats); } } diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue index d46aaa5f69372aa6ec312c4da07cb45d0fd75409..d78494b8d26642c7cd29e33773a28ddbc0e58d5e 100644 --- a/packages/frontend/src/widgets/server-metric/net.vue +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -111,7 +111,7 @@ function onStats(connStats: Misskey.entities.ServerStats) { } function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) { - for (const revStats of statsLog.reverse()) { + for (const revStats of statsLog.toReversed()) { onStats(revStats); } } diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts index 9a2989b37348794c5347efab6eb85d4cc23cb4f5..cf686efd0d127651a1aaadc142ce0be00087547e 100644 --- a/packages/frontend/test/emoji.test.ts +++ b/packages/frontend/test/emoji.test.ts @@ -6,7 +6,7 @@ import { describe, test, assert, afterEach } from 'vitest'; import { render, cleanup, type RenderResult } from '@testing-library/vue'; import { defaultStoreState } from './init.js'; -import { getEmojiName } from '@/scripts/emojilist.js'; +import { getEmojiName } from '@@/js/emojilist.js'; import { components } from '@/components/index.js'; import { directives } from '@/directives/index.js'; import MkEmoji from '@/components/global/MkEmoji.vue'; diff --git a/packages/frontend/test/i18n.test.ts b/packages/frontend/test/i18n.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..149815d39c76f42674a9875cad37f777a17b6f8a --- /dev/null +++ b/packages/frontend/test/i18n.test.ts @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, it } from 'vitest'; +import { I18n } from '../../frontend-shared/js/i18n.js'; // @@ã§å‚ç…§ã§ããªã‹ã£ãŸã®ã§ +import { ParameterizedString } from '../../../locales/index.js'; + +/* eslint "sharkey/locale":"off" */ + +// TODO: ã“ã®ãƒ†ã‚¹ãƒˆã¯frontend-sharedã«ç§»å‹•ã™ã‚‹ + +describe('i18n', () => { + it('t', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.t('foo')).toBe('foo'); + expect(i18n.t('bar.baz')).toBe('baz'); + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + it('ts', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.ts.foo).toBe('foo'); + expect(i18n.ts.bar.baz).toBe('baz'); + }); + it('tsx', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); +}); diff --git a/packages/frontend/test/scroll.test.ts b/packages/frontend/test/scroll.test.ts index a0b56b7221bbadb5cd63b7af51e5bbf425072ad2..32a5a1c55811931b67d6e5c85de8cb6fd6b6f709 100644 --- a/packages/frontend/test/scroll.test.ts +++ b/packages/frontend/test/scroll.test.ts @@ -5,7 +5,7 @@ import { describe, test, assert, afterEach } from 'vitest'; import { Window } from 'happy-dom'; -import { onScrollBottom, onScrollTop } from '@/scripts/scroll.js'; +import { onScrollBottom, onScrollTop } from '@@/js/scroll.js'; describe('Scroll', () => { describe('onScrollTop', () => { diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index fe4d20289492776252989f792ada957b8d70ce47..bbf9d653cf349296c9dbd9db73f885adedf84d09 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -21,9 +21,11 @@ "allowSyntheticDefaultImports": true, "isolatedModules": true, "useDefineForClassFields": true, + "skipLibCheck": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@@/*": ["../frontend-shared/*"] }, "typeRoots": [ "./@types", @@ -44,10 +46,14 @@ }, "compileOnSave": false, "include": [ - "./**/*.ts", - "./**/*.vue" + "./src/**/*.ts", + "./src/**/*.vue", + "./test/**/*.ts", + "./test/**/*.vue", + "./@types/**/*.ts" ], "exclude": [ + "node_modules", ".storybook/**/*" ] } diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts index 07cf3b4a69e06c3e946b59ca31e8efc1298463bf..d588f83138e8c88bde2507260a140e74686310fe 100644 --- a/packages/frontend/vite.config.local-dev.ts +++ b/packages/frontend/vite.config.local-dev.ts @@ -15,6 +15,7 @@ const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')) const httpUrl = `http://localhost:${port}/`; const websocketUrl = `ws://localhost:${port}/`; +const embedUrl = `http://localhost:5174/`; // activitypubリクエストã¯Proxyを通ã—ã€ãれ以外ã¯Viteã®é–‹ç™ºã‚µãƒ¼ãƒãƒ¼ã‚’返㙠function varyHandler(req: IncomingMessage) { @@ -51,6 +52,12 @@ const devConfig: UserConfig = { ws: true, }, '/favicon.ico': httpUrl, + '/robots.txt': httpUrl, + '/embed.js': httpUrl, + '/embed': { + target: embedUrl, + ws: true, + }, '/identicon': { target: httpUrl, rewrite(path) { diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 674fdbf6801bc2ba826771f2b4c0190e393f1f5d..f8bd433335d1bfb70ee5487734eb7640413256aa 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -2,13 +2,13 @@ import path from 'path'; import pluginReplace from '@rollup/plugin-replace'; import pluginVue from '@vitejs/plugin-vue'; import { type UserConfig, defineConfig } from 'vite'; - +import { localesVersion } from '../../locales/version.js'; import locales from '../../locales/index.js'; import meta from '../../package.json'; import packageInfo from './package.json' with { type: 'json' }; import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js'; import pluginJson5 from './vite.json5.js'; -import { pluginReplaceIcons } from './vite.replaceIcons.ts'; +import { pluginReplaceIcons } from './vite.replaceIcons.js'; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm']; @@ -66,6 +66,9 @@ export function getConfig(): UserConfig { server: { port: 5173, + headers: { // ãªã‚“ã‹åŠ¹ã‹ãªã„ + 'X-Frame-Options': 'DENY', + }, }, plugins: [ @@ -89,6 +92,7 @@ export function getConfig(): UserConfig { extensions, alias: { '@/': __dirname + '/src/', + '@@/': __dirname + '/../frontend-shared/', '/client-assets/': __dirname + '/assets/', '/static-assets/': __dirname + '/../backend/assets/', '/fluent-emojis/': __dirname + '/../../fluent-emojis/dist/', @@ -110,6 +114,7 @@ export function getConfig(): UserConfig { define: { _VERSION_: JSON.stringify(meta.version), _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _LANGS_VERSION_: JSON.stringify(localesVersion), _ENV_: JSON.stringify(process.env.NODE_ENV), _DEV_: process.env.NODE_ENV !== 'production', _PERF_PREFIX_: JSON.stringify('Misskey:'), @@ -151,7 +156,7 @@ export function getConfig(): UserConfig { }, }, cssCodeSplit: true, - outDir: __dirname + '/../../built/_vite_', + outDir: __dirname + '/../../built/_frontend_vite_', assetsDir: '.', emptyOutDir: false, sourcemap: process.env.NODE_ENV === 'development', diff --git a/packages/frontend/vite.replaceIcons.ts b/packages/frontend/vite.replaceIcons.ts index 92ac568ef34e9d2db1b4cf82a2f4d79c8cc77efe..ae9a8f81d62009c01de5abc3d96376686af01a6c 100644 --- a/packages/frontend/vite.replaceIcons.ts +++ b/packages/frontend/vite.replaceIcons.ts @@ -144,6 +144,7 @@ export function pluginReplaceIcons() { 'ti ti-api': 'ph-key ph-bold ph-lg', 'ti ti-app-window': 'ph-app-window ph-bold ph-lg', 'ti ti-apple': 'ph-orange-slice ph-bold ph-lg', + 'ti ti-archive': 'ph-archive ph-bold ph-lg', 'ti ti-arrow-back-up': 'ph-arrow-u-up-left ph-bold ph-lg', 'ti ti-arrow-bar-to-down': 'ph-arrow-line-down ph-bold ph-lg', 'ti ti-arrow-big-right': 'ph-arrow-fat-right ph-bold ph-lg', @@ -214,6 +215,7 @@ export function pluginReplaceIcons() { 'ti ti-device-mobile': 'ph-device-mobile ph-bold ph-lg', 'ti ti-device-tablet': 'ph-device-tablet ph-bold ph-lg', 'ti ti-device-tv': 'ph-television ph-bold ph-lg', + 'ti ti-device-usb': 'ph-usb ph-bold ph-lg', 'ti ti-devices': 'ph-devices ph-bold ph-lg', 'ti ti-dice': 'ph ph-dice-five ph-bold ph-lg', 'ti ti-dice-5': 'ph ph-dice-five ph-bold ph-lg', @@ -262,6 +264,7 @@ export function pluginReplaceIcons() { 'ti ti-list-search': 'ph-list ph-bold ph-lg-search', 'ti ti-lock': 'ph-lock ph-bold ph-lg', 'ti ti-lock-open': 'ph-lock-open ph-bold ph-lg', + 'ti ti-lock-star': 'ph-shield-star ph-bold ph-lg', 'ti ti-mail': 'ph-envelope ph-bold ph-lg', 'ti ti-map-pin': 'ph-map-pin ph-bold ph-lg', 'ti ti-maximize': 'ph-frame-corners ph-bold ph-lg', @@ -269,7 +272,9 @@ export function pluginReplaceIcons() { 'ti ti-menu': 'ph-list ph-bold ph-lg', 'ti ti-menu-2': 'ph-list ph-bold ph-lg', 'ti ti-message': 'ph-envelope ph-bold ph-lg', + 'ti ti-message-exclamation': 'ph-exclamation ph-bold ph-lg', 'ti ti-message-off': 'ph-bell-slash ph-bold ph-lg', + 'ti ti-message-x': 'ph-prohibit ph-bold ph-lg', 'ti ti-messages': 'ph-envelope ph-bold ph-lg', 'ti ti-messages-off': 'ph-envelope-open ph-bold ph-lg', 'ti ti-minimize': 'ph-arrows-in-simple ph-bold ph-lg', @@ -344,7 +349,7 @@ export function pluginReplaceIcons() { 'ti ti-universe': 'ph-rocket-launch ph-bold ph-lg', 'ti ti-upload': 'ph-upload ph-bold ph-lg', 'ti ti-user': 'ph-user ph-bold ph-lg', - 'ti ti-user-check': 'ph-check ph-bold ph-lg', + 'ti ti-user-check': 'ph-user-check ph-bold ph-lg', 'ti ti-user-circle': 'ph-user-circle ph-bold ph-lg', 'ti ti-user-edit': 'ph-user-list ph-bold ph-lg', 'ti ti-user-exclamation': 'ph-warning-circle ph-bold ph-lg', @@ -352,6 +357,7 @@ export function pluginReplaceIcons() { 'ti ti-user-plus': 'ph-user-plus ph-bold ph-lg', 'ti ti-user-search': 'ph-user-circle ph-bold ph-lg', 'ti ti-user-shield': 'ph-newspaper-clipping ph-bold ph-lg', + 'ti ti-user-star': 'ph-user-focus ph-bold ph-lg', 'ti ti-users': 'ph-users ph-bold ph-lg', 'ti ti-video': 'ph-video ph-bold ph-lg', 'ti ti-volume': 'ph-speaker-high ph-bold ph-lg', @@ -361,6 +367,7 @@ export function pluginReplaceIcons() { 'ti ti-window-maximize': 'ph-frame-corners ph-bold ph-lg', 'ti ti-world': 'ph-globe-hemisphere-west ph-bold ph-lg', 'ti ti-world-download': 'ph-cloud-arrow-down ph-bold ph-lg', + 'ti ti-world-cog': 'ph-gear-six ph-bold ph-lg', 'ti ti-world-search': 'ph-binoculars ph-bold ph-lg', 'ti ti-world-upload': 'ph-cloud-arrow-up ph-bold ph-lg', 'ti ti-world-x': 'ph-planet ph-bold ph-lg', diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json index dbb5686af26eb58c9d2d3d57a7e0650c77ae3885..bd9e4fc2844d1006dd85c0fac6d94e83c3e7a272 100644 --- a/packages/megalodon/package.json +++ b/packages/megalodon/package.json @@ -6,7 +6,6 @@ "typings": "./lib/src/index.d.ts", "scripts": { "build": "tsc -p ./", - "lint": "eslint --ext .js,.ts src --cache", "doc": "typedoc --out ../docs ./src", "test": "NODE_ENV=test jest -u --maxWorkers=3" }, @@ -63,7 +62,7 @@ "@types/parse-link-header": "^2.0.3", "@types/uuid": "^9.0.7", "@types/ws": "^8.5.10", - "axios": "1.6.0", + "axios": "1.7.4", "dayjs": "^1.11.10", "form-data": "^4.0.0", "https-proxy-agent": "^7.0.2", @@ -73,7 +72,7 @@ "socks-proxy-agent": "^8.0.2", "typescript": "5.1.6", "uuid": "^9.0.1", - "ws": "8.14.2" + "ws": "8.17.1" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.12.0", diff --git a/packages/megalodon/tsconfig.json b/packages/megalodon/tsconfig.json index b2b4a984b1a68869fafee116ccdc489c8d96745b..6327d7c31db10affb8fa8b29d1d62bf8d6fecac6 100644 --- a/packages/megalodon/tsconfig.json +++ b/packages/megalodon/tsconfig.json @@ -34,6 +34,7 @@ "noUnusedParameters": true, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "skipLibCheck": true, /* Module Resolution Options */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ diff --git a/packages/misskey-bubble-game/build.js b/packages/misskey-bubble-game/build.js index e626c97a5967d6c20facd1cd107f46f28451a63a..a80b71646f43800c48d502dc828142c4e0bd367c 100644 --- a/packages/misskey-bubble-game/build.js +++ b/packages/misskey-bubble-game/build.js @@ -1,32 +1,32 @@ -import * as esbuild from "esbuild"; -import { build } from "esbuild"; -import { globSync } from "glob"; -import { execa } from "execa"; -import fs from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as esbuild from 'esbuild'; +import { build } from 'esbuild'; +import { globSync } from 'glob'; +import { execa } from 'execa'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); -const entryPoints = globSync("./src/**/**.{ts,tsx}"); +const entryPoints = globSync('./src/**/**.{ts,tsx}'); /** @type {import('esbuild').BuildOptions} */ const options = { entryPoints, minify: process.env.NODE_ENV === 'production', - outdir: "./built", - target: "es2022", - platform: "browser", - format: "esm", + outdir: './built', + target: 'es2022', + platform: 'browser', + format: 'esm', sourcemap: 'linked', }; // builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ fs.rmSync('./built', { recursive: true, force: true }); -if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) { +if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { await watchSrc(); } else { await buildSrc(); @@ -36,7 +36,7 @@ async function buildSrc() { console.log(`[${_package.name}] start building...`); await build(options) - .then(it => { + .then(() => { console.log(`[${_package.name}] build succeeded.`); }) .catch((err) => { @@ -65,7 +65,7 @@ function buildDts() { { stdout: process.stdout, stderr: process.stderr, - } + }, ); } @@ -86,7 +86,7 @@ async function watchSrc() { }, }]; - console.log(`[${_package.name}] start watching...`) + console.log(`[${_package.name}] start watching...`); const context = await esbuild.context({ ...options, plugins }); await context.watch(); diff --git a/packages/misskey-bubble-game/eslint.config.js b/packages/misskey-bubble-game/eslint.config.js index 86c21a22a3a78a7619a698d3d1c9200868e3e3a3..749971a72b581d8b08cb143a5f7711e468781eb4 100644 --- a/packages/misskey-bubble-game/eslint.config.js +++ b/packages/misskey-bubble-game/eslint.config.js @@ -1,6 +1,7 @@ import tsParser from '@typescript-eslint/parser'; import sharedConfig from '../shared/eslint.config.js'; +// eslint-disable-next-line import/no-default-export export default [ ...sharedConfig, { @@ -24,4 +25,13 @@ export default [ }, }, }, + { + ignores: [ + "**/lib/", + "**/temp/", + "**/built/", + "**/coverage/", + "**/node_modules/", + ] + }, ]; diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json index acd6ab9dd2d0c67f6aa13908a11e0bf893ff9c1c..2b280f17fc3626bbe468456bbaf9e330b5ca1f03 100644 --- a/packages/misskey-bubble-game/package.json +++ b/packages/misskey-bubble-game/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "node ./build.js", "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", - "eslint": "eslint './**/*.{js,jsx,ts,tsx}' --cache", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", "typecheck": "tsc --noEmit", "lint": "pnpm typecheck && pnpm eslint" }, diff --git a/packages/misskey-bubble-game/src/game.ts b/packages/misskey-bubble-game/src/game.ts index 3bce4b1dcfe8dd2e15ad8645889dcf039c5d9959..7f230e39cb8f511a9d02c1a31de598892025da0d 100644 --- a/packages/misskey-bubble-game/src/game.ts +++ b/packages/misskey-bubble-game/src/game.ts @@ -199,13 +199,12 @@ export class DropAndFusionGame extends EventEmitter<{ }; if (mono.shape === 'circle') { return Matter.Bodies.circle(x, y, mono.sizeX / 2, options); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (mono.shape === 'rectangle') { return Matter.Bodies.rectangle(x, y, mono.sizeX, mono.sizeY, options); - } else if (mono.shape === 'custom') { - return Matter.Bodies.fromVertices(x, y, mono.vertices!.map(i => i.map(j => ({ - x: (j.x / mono.verticesSize!) * mono.sizeX, - y: (j.y / mono.verticesSize!) * mono.sizeY, + } else if (mono.shape === 'custom' && mono.vertices != null && mono.verticesSize != null) { //eslint-disable-line @typescript-eslint/no-unnecessary-condition + return Matter.Bodies.fromVertices(x, y, mono.vertices.map(i => i.map(j => ({ + x: (j.x / mono.verticesSize!) * mono.sizeX, //eslint-disable-line @typescript-eslint/no-non-null-assertion + y: (j.y / mono.verticesSize!) * mono.sizeY, //eslint-disable-line @typescript-eslint/no-non-null-assertion }))), options); } else { throw new Error('unrecognized shape'); @@ -227,7 +226,12 @@ export class DropAndFusionGame extends EventEmitter<{ this.gameOverReadyBodyIds = this.gameOverReadyBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id); Matter.Composite.remove(this.engine.world, [bodyA, bodyB]); - const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!; + const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label); + + if (currentMono == null) { + throw new Error('Current Mono Not Found'); + } + const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1) ?? null; if (nextMono) { @@ -362,14 +366,18 @@ export class DropAndFusionGame extends EventEmitter<{ } public getActiveMonos() { - return this.engine.world.bodies.map(x => this.monoDefinitions.find((mono) => mono.id === x.label)!).filter(x => x !== undefined); + return this.engine.world.bodies + .map(x => this.monoDefinitions.find((mono) => mono.id === x.label)) + .filter(x => x !== undefined); } public drop(_x: number) { if (this.isGameOver) return; if (this.frame - this.latestDroppedAt < this.DROP_COOLTIME) return; - const head = this.stock.shift()!; + const head = this.stock.shift(); + if (!head) return; + this.stock.push({ id: this.rng().toString(), mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], @@ -411,13 +419,15 @@ export class DropAndFusionGame extends EventEmitter<{ }); if (this.holding) { - const head = this.stock.shift()!; + const head = this.stock.shift(); + if (!head) return; this.stock.unshift(this.holding); this.holding = head; this.emit('changeHolding', this.holding); this.emit('changeStock', this.stock); } else { - const head = this.stock.shift()!; + const head = this.stock.shift(); + if (!head) return; this.holding = head; this.stock.push({ id: this.rng().toString(), diff --git a/packages/misskey-bubble-game/tsconfig.json b/packages/misskey-bubble-game/tsconfig.json index 6e34e332e000246724fd39812527733960a20185..f467951ef6f6168a713b0d6665bd66ba43c9111d 100644 --- a/packages/misskey-bubble-game/tsconfig.json +++ b/packages/misskey-bubble-game/tsconfig.json @@ -15,6 +15,7 @@ "experimentalDecorators": true, "noImplicitReturns": true, "esModuleInterop": true, + "skipLibCheck": true, "typeRoots": [ "./node_modules/@types" ], diff --git a/packages/misskey-js/eslint.config.js b/packages/misskey-js/eslint.config.js index d8173f30e9ac53c6dfe8a1e379aa8cfc9b815fe8..f33c4c4d2516a8bb5db39fdd8568349f946d3069 100644 --- a/packages/misskey-js/eslint.config.js +++ b/packages/misskey-js/eslint.config.js @@ -26,4 +26,13 @@ export default [ }, }, }, + { + ignores: [ + "**/lib/", + "**/temp/", + "**/built/", + "**/coverage/", + "**/node_modules/", + ] + }, ]; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 87fda5568eca67598eab813ea6476e5c99eab5f2..5af1a4112f8e95a2e5152ad610599142d91ed8fb 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -133,6 +133,9 @@ type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations // @public (undocumented) type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; @@ -367,6 +370,9 @@ type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show'] // @public (undocumented) type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json']; +// @public (undocumented) +type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json']; @@ -566,7 +572,7 @@ type Channel = components['schemas']['Channel']; // Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export abstract class ChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> { +export abstract class ChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> { constructor(stream: Stream, channel: string, name?: string); // (undocumented) channel: string; @@ -699,7 +705,7 @@ export type Channels = { }; hashtag: { params: { - q?: string; + q: string[][]; }; events: { note: (payload: Note) => void; @@ -1188,6 +1194,10 @@ export type Endpoints = Overwrite<Endpoints_2, { req: SigninRequest; res: SigninResponse; }; + 'signin-with-passkey': { + req: SigninWithPasskeyRequest; + res: SigninWithPasskeyResponse; + }; 'admin/roles/create': { req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride; @@ -1219,6 +1229,8 @@ declare namespace entities { SignupPendingRequest, SignupPendingResponse, SigninRequest, + SigninWithPasskeyRequest, + SigninWithPasskeyResponse, SigninResponse, PartialRolePolicyOverride, EmptyRequest, @@ -1319,6 +1331,7 @@ declare namespace entities { AdminUnsilenceUserRequest, AdminSuspendUserRequest, AdminApproveUserRequest, + AdminDeclineUserRequest, AdminUnsuspendUserRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, @@ -1344,6 +1357,7 @@ declare namespace entities { AdminSystemWebhookShowResponse, AdminSystemWebhookUpdateRequest, AdminSystemWebhookUpdateResponse, + AdminSystemWebhookTestRequest, AnnouncementsRequest, AnnouncementsResponse, AnnouncementsShowRequest, @@ -1502,6 +1516,8 @@ declare namespace entities { FollowingRequestsCancelResponse, FollowingRequestsListRequest, FollowingRequestsListResponse, + FollowingRequestsSentRequest, + FollowingRequestsSentResponse, FollowingRequestsRejectRequest, GalleryFeaturedRequest, GalleryFeaturedResponse, @@ -1605,6 +1621,7 @@ declare namespace entities { IWebhooksShowResponse, IWebhooksUpdateRequest, IWebhooksDeleteRequest, + IWebhooksTestRequest, InviteCreateResponse, InviteDeleteRequest, InviteListRequest, @@ -1642,6 +1659,8 @@ declare namespace entities { NotesFavoritesDeleteRequest, NotesFeaturedRequest, NotesFeaturedResponse, + NotesFollowingRequest, + NotesFollowingResponse, NotesGlobalTimelineRequest, NotesGlobalTimelineResponse, NotesBubbleTimelineRequest, @@ -1655,6 +1674,7 @@ declare namespace entities { NotesPollsRecommendationRequest, NotesPollsRecommendationResponse, NotesPollsVoteRequest, + NotesPollsRefreshRequest, NotesReactionsRequest, NotesReactionsResponse, NotesReactionsCreateRequest, @@ -2021,6 +2041,12 @@ type FollowingRequestsListResponse = operations['following___requests___list'][' // @public (undocumented) type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json']; +// @public (undocumented) +type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json']; + +// @public (undocumented) +type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json']; + // @public (undocumented) type FollowingUpdateAllRequest = operations['following___update-all']['requestBody']['content']['application/json']; @@ -2165,6 +2191,24 @@ type IAuthorizedAppsResponse = operations['i___authorized-apps']['responses']['2 // @public (undocumented) type IChangePasswordRequest = operations['i___change-password']['requestBody']['content']['application/json']; +// @public (undocumented) +export interface IChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> { + // (undocumented) + channel: string; + // (undocumented) + dispose(): void; + // (undocumented) + id: string; + // (undocumented) + inCount: number; + // (undocumented) + name?: string; + // (undocumented) + outCount: number; + // (undocumented) + send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void; +} + // @public (undocumented) type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json']; @@ -2333,6 +2377,40 @@ type ISigninHistoryResponse = operations['i___signin-history']['responses']['200 // @public (undocumented) function isPureRenote(note: Note): note is PureRenote; +// @public (undocumented) +export interface IStream extends EventEmitter<StreamEvents> { + // (undocumented) + close(): void; + // Warning: (ae-forgotten-export) The symbol "NonSharedConnection" needs to be exported by the entry point index.d.ts + // + // (undocumented) + disconnectToChannel(connection: NonSharedConnection): void; + // (undocumented) + heartbeat(): void; + // (undocumented) + ping(): void; + // Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts + // + // (undocumented) + removeSharedConnection(connection: SharedConnection): void; + // Warning: (ae-forgotten-export) The symbol "Pool" needs to be exported by the entry point index.d.ts + // + // (undocumented) + removeSharedConnectionPool(pool: Pool): void; + // (undocumented) + send(typeOrPayload: string): void; + // (undocumented) + send(typeOrPayload: string, payload: unknown): void; + // (undocumented) + send(typeOrPayload: Record<string, unknown> | unknown[]): void; + // (undocumented) + send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void; + // (undocumented) + state: 'initializing' | 'reconnecting' | 'connected'; + // (undocumented) + useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): IChannelConnection<Channels[C]>; +} + // @public (undocumented) type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; @@ -2369,6 +2447,9 @@ type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['co // @public (undocumented) type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json']; +// @public (undocumented) +type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json']; + // @public (undocumented) type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json']; @@ -2411,6 +2492,9 @@ type ModerationLog = { } | { type: 'approve'; info: ModerationLogPayloads['approve']; +} | { + type: 'decline'; + info: ModerationLogPayloads['decline']; } | { type: 'suspend'; info: ModerationLogPayloads['suspend']; @@ -2648,6 +2732,12 @@ type NotesFeaturedRequest = operations['notes___featured']['requestBody']['conte // @public (undocumented) type NotesFeaturedResponse = operations['notes___featured']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesFollowingRequest = operations['notes___following']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesFollowingResponse = operations['notes___following']['responses']['200']['content']['application/json']; + // @public (undocumented) type NotesGlobalTimelineRequest = operations['notes___global-timeline']['requestBody']['content']['application/json']; @@ -2681,6 +2771,9 @@ type NotesPollsRecommendationRequest = operations['notes___polls___recommendatio // @public (undocumented) type NotesPollsRecommendationResponse = operations['notes___polls___recommendation']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesPollsRefreshRequest = operations['notes___polls___refresh']['requestBody']['content']['application/json']; + // @public (undocumented) type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json']; @@ -2783,6 +2876,9 @@ type NotificationsCreateRequest = operations['notifications___create']['requestB // @public (undocumented) export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited"]; +// @public (undocumented) +export function nyaize(text: string): string; + // @public (undocumented) type Page = components['schemas']['Page']; @@ -2841,7 +2937,7 @@ type PartialRolePolicyOverride = Partial<{ }>; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; @@ -3042,6 +3138,19 @@ type SigninResponse = { i: string; }; +// @public (undocumented) +type SigninWithPasskeyRequest = { + credential?: object; + context?: string; +}; + +// @public (undocumented) +type SigninWithPasskeyResponse = { + option?: object; + context?: string; + signinResponse?: SigninResponse; +}; + // @public (undocumented) type SignupPendingRequest = { code: string; @@ -3076,10 +3185,8 @@ type SponsorsRequest = operations['sponsors']['requestBody']['content']['applica // @public (undocumented) type StatsResponse = operations['stats']['responses']['200']['content']['application/json']; -// Warning: (ae-forgotten-export) The symbol "StreamEvents" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -export class Stream extends EventEmitter<StreamEvents> { +export class Stream extends EventEmitter<StreamEvents> implements IStream { constructor(origin: string, user: { token: string; } | null, options?: { @@ -3087,20 +3194,14 @@ export class Stream extends EventEmitter<StreamEvents> { }); // (undocumented) close(): void; - // Warning: (ae-forgotten-export) The symbol "NonSharedConnection" needs to be exported by the entry point index.d.ts - // // (undocumented) disconnectToChannel(connection: NonSharedConnection): void; // (undocumented) heartbeat(): void; // (undocumented) ping(): void; - // Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts - // // (undocumented) removeSharedConnection(connection: SharedConnection): void; - // Warning: (ae-forgotten-export) The symbol "Pool" needs to be exported by the entry point index.d.ts - // // (undocumented) removeSharedConnectionPool(pool: Pool): void; // (undocumented) @@ -3115,6 +3216,14 @@ export class Stream extends EventEmitter<StreamEvents> { useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnection<Channels[C]>; } +// Warning: (ae-forgotten-export) The symbol "BroadcastEvents" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type StreamEvents = { + _connected_: void; + _disconnected_: void; +} & BroadcastEvents; + // Warning: (ae-forgotten-export) The symbol "SwitchCase" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "IsCaseMatched" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "GetCaseResult" needs to be exported by the entry point index.d.ts diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json index f9bd0a4e5d06e0bde402d824578fbb482a46d2bf..1d31c732374941b812cccc96ed04180c6d6efecd 100644 --- a/packages/misskey-js/generator/package.json +++ b/packages/misskey-js/generator/package.json @@ -7,15 +7,15 @@ "generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix --cache" }, "devDependencies": { - "@readme/openapi-parser": "2.5.0", + "@readme/openapi-parser": "2.6.0", "@types/node": "20.9.1", - "@typescript-eslint/eslint-plugin": "6.11.0", - "@typescript-eslint/parser": "6.11.0", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", "openapi-types": "12.1.3", "openapi-typescript": "6.7.3", - "ts-case-convert": "2.0.2", + "ts-case-convert": "2.0.7", "tsx": "4.4.0", - "typescript": "5.3.3" + "typescript": "5.6.2" }, "files": [ "built" diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts index 4ae00a4522db6ccfa8adb3608e8cc789d27080ad..88f2ae9ee96e6251727923ccb16f1fdc32108d27 100644 --- a/packages/misskey-js/generator/src/generator.ts +++ b/packages/misskey-js/generator/src/generator.ts @@ -96,15 +96,11 @@ async function generateEndpoints( endpoint.request = req; const reqType = new EndpointReqMediaType(path, req); - endpointReqMediaTypesSet.add(reqType.getMediaType()); - endpointReqMediaTypes.push(reqType); - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); + if (reqType.getMediaType() !== 'application/json') { + endpointReqMediaTypesSet.add(reqType.getMediaType()); + endpointReqMediaTypes.push(reqType); + } } - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); } if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) { @@ -158,16 +154,19 @@ async function generateEndpoints( endpointOutputLine.push(''); function generateEndpointReqMediaTypesType() { - return `Record<keyof Endpoints, ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}>`; + return `{ [K in keyof Endpoints]?: ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}; }`; } - endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`); + endpointOutputLine.push(`/** + * NOTE: The content-type for all endpoints not listed here is application/json. + */`); + endpointOutputLine.push('export const endpointReqTypes = {'); endpointOutputLine.push( ...endpointReqMediaTypes.map(it => '\t' + it.toLine()), ); - endpointOutputLine.push('};'); + endpointOutputLine.push(`} as const satisfies ${generateEndpointReqMediaTypesType()};`); endpointOutputLine.push(''); await writeFile(endpointOutputPath, endpointOutputLine.join('\n')); diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 5ba4b93411acda005554b38de71dda78cf6f2016..05cda465745e6c44669f3c7ebd3bb4daf4c4c6a6 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.8.0", + "version": "2024.9.1", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", @@ -22,7 +22,7 @@ "tsd": "tsd", "api": "pnpm api-extractor run --local --verbose", "api-prod": "pnpm api-extractor run --verbose", - "eslint": "eslint './**/*.{js,jsx,ts,tsx}' --cache", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", "typecheck": "tsc --noEmit", "lint": "pnpm typecheck && pnpm eslint", "jest": "jest --coverage --detectOpenHandles", @@ -35,9 +35,9 @@ "directory": "packages/misskey-js" }, "devDependencies": { - "@microsoft/api-extractor": "7.47.4", + "@microsoft/api-extractor": "7.47.9", "@swc/jest": "0.2.36", - "@types/jest": "29.5.12", + "@types/jest": "29.5.13", "@types/node": "20.14.12", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", @@ -46,11 +46,11 @@ "jest-websocket-mock": "2.5.0", "mock-socket": "9.3.1", "ncp": "2.0.0", - "nodemon": "3.1.4", - "execa": "9.3.0", - "tsd": "0.31.1", - "typescript": "5.5.4", - "esbuild": "0.23.0", + "nodemon": "3.1.7", + "execa": "9.4.0", + "tsd": "0.31.2", + "typescript": "5.6.2", + "esbuild": "0.23.1", "glob": "11.0.0" }, "files": [ diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts index ea1df57f3d77cf10564b949a2f235d9025de9eac..ed1282957fbe3e613c767e0bc078bbb8138a9407 100644 --- a/packages/misskey-js/src/api.ts +++ b/packages/misskey-js/src/api.ts @@ -56,6 +56,10 @@ export class APIClient { return obj !== null && typeof obj === 'object' && !Array.isArray(obj); } + private assertSpecialEpReqType(ep: keyof Endpoints): ep is keyof typeof endpointReqTypes { + return ep in endpointReqTypes; + } + public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>( endpoint: E, params: P = {} as P, @@ -63,9 +67,12 @@ export class APIClient { ): Promise<SwitchCaseResponseType<E, P>> { return new Promise((resolve, reject) => { let mediaType = 'application/json'; - if (endpoint in endpointReqTypes) { + // (autogenãŒãƒã‚°ã£ãŸã¨ãã®ãŸã‚ã€å¿µã®ç‚ºnullãƒã‚§ãƒƒã‚¯ã‚‚è¡Œã†ï¼‰ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.assertSpecialEpReqType(endpoint) && endpointReqTypes[endpoint] != null) { mediaType = endpointReqTypes[endpoint]; } + let payload: FormData | string = '{}'; if (mediaType === 'application/json') { @@ -100,7 +107,7 @@ export class APIClient { method: 'POST', body: payload, headers: { - 'Content-Type': endpointReqTypes[endpoint], + 'Content-Type': mediaType, }, credentials: 'omit', cache: 'no-cache', diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index 5ee4194db213b0a0122bcf9f36152d666c9f92d0..4c3f2e157859965c04a9a3951e6f63d01b91aaea 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -5,6 +5,8 @@ import { PartialRolePolicyOverride, SigninRequest, SigninResponse, + SigninWithPasskeyRequest, + SigninWithPasskeyResponse, SignupPendingRequest, SignupPendingResponse, SignupRequest, @@ -82,6 +84,10 @@ export type Endpoints = Overwrite< req: SigninRequest; res: SigninResponse; }, + 'signin-with-passkey': { + req: SigninWithPasskeyRequest; + res: SigninWithPasskeyResponse; + } 'admin/roles/create': { req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride }>; res: AdminRolesCreateResponse; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index c13485621b5709b631aac1e96ba079547bbbc4ad..3fa67b79903f28923ef48358eec547d688d4b24c 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -812,6 +812,17 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user* + */ + request<E extends 'admin/decline-user', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -1015,6 +1026,18 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook* + */ + request<E extends 'admin/system-webhook/test', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -1867,7 +1890,7 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *No* + * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'federation/followers', P extends Endpoints[E]['req']>( endpoint: E, @@ -1878,7 +1901,7 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *No* + * **Credential required**: *Yes* / **Permission**: *read:account* */ request<E extends 'federation/following', P extends Endpoints[E]['req']>( endpoint: E, @@ -2029,6 +2052,17 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:following* + */ + request<E extends 'following/requests/sent', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -2909,6 +2943,18 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request<E extends 'i/webhooks/test', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -3174,6 +3220,17 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request<E extends 'notes/following', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -3251,6 +3308,17 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:federation* + */ + request<E extends 'notes/polls/refresh', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -4258,7 +4326,7 @@ declare module '../api.js' { ): Promise<SwitchCaseResponseType<E, P>>; /** - * Get Sharkey GH Sponsors + * Get Sharkey Sponsors or Instance Sponsors * * **Credential required**: *No* */ diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 628825d5044c7f0dae683a8aafe0e833a80583b8..609c9f99d8a3d73bb80436d57bd307ce25677854 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -97,6 +97,7 @@ import type { AdminUnsilenceUserRequest, AdminSuspendUserRequest, AdminApproveUserRequest, + AdminDeclineUserRequest, AdminUnsuspendUserRequest, AdminUpdateMetaRequest, AdminDeleteAccountRequest, @@ -122,6 +123,7 @@ import type { AdminSystemWebhookShowResponse, AdminSystemWebhookUpdateRequest, AdminSystemWebhookUpdateResponse, + AdminSystemWebhookTestRequest, AnnouncementsRequest, AnnouncementsResponse, AnnouncementsShowRequest, @@ -280,6 +282,8 @@ import type { FollowingRequestsCancelResponse, FollowingRequestsListRequest, FollowingRequestsListResponse, + FollowingRequestsSentRequest, + FollowingRequestsSentResponse, FollowingRequestsRejectRequest, GalleryFeaturedRequest, GalleryFeaturedResponse, @@ -383,6 +387,7 @@ import type { IWebhooksShowResponse, IWebhooksUpdateRequest, IWebhooksDeleteRequest, + IWebhooksTestRequest, InviteCreateResponse, InviteDeleteRequest, InviteListRequest, @@ -420,6 +425,8 @@ import type { NotesFavoritesDeleteRequest, NotesFeaturedRequest, NotesFeaturedResponse, + NotesFollowingRequest, + NotesFollowingResponse, NotesGlobalTimelineRequest, NotesGlobalTimelineResponse, NotesBubbleTimelineRequest, @@ -433,6 +440,7 @@ import type { NotesPollsRecommendationRequest, NotesPollsRecommendationResponse, NotesPollsVoteRequest, + NotesPollsRefreshRequest, NotesReactionsRequest, NotesReactionsResponse, NotesReactionsCreateRequest, @@ -662,6 +670,7 @@ export type Endpoints = { 'admin/unsilence-user': { req: AdminUnsilenceUserRequest; res: EmptyResponse }; 'admin/suspend-user': { req: AdminSuspendUserRequest; res: EmptyResponse }; 'admin/approve-user': { req: AdminApproveUserRequest; res: EmptyResponse }; + 'admin/decline-user': { req: AdminDeclineUserRequest; res: EmptyResponse }; 'admin/unsuspend-user': { req: AdminUnsuspendUserRequest; res: EmptyResponse }; 'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse }; 'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse }; @@ -680,6 +689,7 @@ export type Endpoints = { 'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse }; 'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse }; 'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse }; + 'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse }; 'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse }; 'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse }; 'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse }; @@ -772,6 +782,7 @@ export type Endpoints = { 'following/requests/accept': { req: FollowingRequestsAcceptRequest; res: EmptyResponse }; 'following/requests/cancel': { req: FollowingRequestsCancelRequest; res: FollowingRequestsCancelResponse }; 'following/requests/list': { req: FollowingRequestsListRequest; res: FollowingRequestsListResponse }; + 'following/requests/sent': { req: FollowingRequestsSentRequest; res: FollowingRequestsSentResponse }; 'following/requests/reject': { req: FollowingRequestsRejectRequest; res: EmptyResponse }; 'gallery/featured': { req: GalleryFeaturedRequest; res: GalleryFeaturedResponse }; 'gallery/popular': { req: EmptyRequest; res: GalleryPopularResponse }; @@ -849,6 +860,7 @@ export type Endpoints = { 'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse }; 'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse }; 'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse }; + 'i/webhooks/test': { req: IWebhooksTestRequest; res: EmptyResponse }; 'invite/create': { req: EmptyRequest; res: InviteCreateResponse }; 'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse }; 'invite/list': { req: InviteListRequest; res: InviteListResponse }; @@ -873,6 +885,7 @@ export type Endpoints = { 'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse }; 'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse }; 'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse }; + 'notes/following': { req: NotesFollowingRequest; res: NotesFollowingResponse }; 'notes/global-timeline': { req: NotesGlobalTimelineRequest; res: NotesGlobalTimelineResponse }; 'notes/bubble-timeline': { req: NotesBubbleTimelineRequest; res: NotesBubbleTimelineResponse }; 'notes/hybrid-timeline': { req: NotesHybridTimelineRequest; res: NotesHybridTimelineResponse }; @@ -880,6 +893,7 @@ export type Endpoints = { 'notes/mentions': { req: NotesMentionsRequest; res: NotesMentionsResponse }; 'notes/polls/recommendation': { req: NotesPollsRecommendationRequest; res: NotesPollsRecommendationResponse }; 'notes/polls/vote': { req: NotesPollsVoteRequest; res: EmptyResponse }; + 'notes/polls/refresh': { req: NotesPollsRefreshRequest; res: EmptyResponse }; 'notes/reactions': { req: NotesReactionsRequest; res: NotesReactionsResponse }; 'notes/reactions/create': { req: NotesReactionsCreateRequest; res: EmptyResponse }; 'notes/reactions/delete': { req: NotesReactionsDeleteRequest; res: EmptyResponse }; @@ -983,397 +997,9 @@ export type Endpoints = { 'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse }; } -export const endpointReqTypes: Record<keyof Endpoints, 'application/json' | 'multipart/form-data'> = { - 'admin/meta': 'application/json', - 'admin/abuse-user-reports': 'application/json', - 'admin/abuse-report/notification-recipient/list': 'application/json', - 'admin/abuse-report/notification-recipient/show': 'application/json', - 'admin/abuse-report/notification-recipient/create': 'application/json', - 'admin/abuse-report/notification-recipient/update': 'application/json', - 'admin/abuse-report/notification-recipient/delete': 'application/json', - 'admin/accounts/create': 'application/json', - 'admin/accounts/delete': 'application/json', - 'admin/accounts/find-by-email': 'application/json', - 'admin/ad/create': 'application/json', - 'admin/ad/delete': 'application/json', - 'admin/ad/list': 'application/json', - 'admin/ad/update': 'application/json', - 'admin/announcements/create': 'application/json', - 'admin/announcements/delete': 'application/json', - 'admin/announcements/list': 'application/json', - 'admin/announcements/update': 'application/json', - 'admin/avatar-decorations/create': 'application/json', - 'admin/avatar-decorations/delete': 'application/json', - 'admin/avatar-decorations/list': 'application/json', - 'admin/avatar-decorations/update': 'application/json', - 'admin/delete-all-files-of-a-user': 'application/json', - 'admin/unset-user-avatar': 'application/json', - 'admin/unset-user-banner': 'application/json', - 'admin/drive/clean-remote-files': 'application/json', - 'admin/drive/cleanup': 'application/json', - 'admin/drive/files': 'application/json', - 'admin/drive/show-file': 'application/json', - 'admin/emoji/add-aliases-bulk': 'application/json', - 'admin/emoji/add': 'application/json', - 'admin/emoji/copy': 'application/json', - 'admin/emoji/delete-bulk': 'application/json', - 'admin/emoji/delete': 'application/json', - 'admin/emoji/import-zip': 'application/json', - 'admin/emoji/list-remote': 'application/json', - 'admin/emoji/list': 'application/json', - 'admin/emoji/remove-aliases-bulk': 'application/json', - 'admin/emoji/set-aliases-bulk': 'application/json', - 'admin/emoji/set-category-bulk': 'application/json', - 'admin/emoji/set-license-bulk': 'application/json', - 'admin/emoji/update': 'application/json', - 'admin/federation/delete-all-files': 'application/json', - 'admin/federation/refresh-remote-instance-metadata': 'application/json', - 'admin/federation/remove-all-following': 'application/json', - 'admin/federation/update-instance': 'application/json', - 'admin/get-index-stats': 'application/json', - 'admin/get-table-stats': 'application/json', - 'admin/get-user-ips': 'application/json', - 'admin/invite/create': 'application/json', - 'admin/invite/list': 'application/json', - 'admin/promo/create': 'application/json', - 'admin/queue/clear': 'application/json', - 'admin/queue/deliver-delayed': 'application/json', - 'admin/queue/inbox-delayed': 'application/json', - 'admin/queue/promote': 'application/json', - 'admin/queue/stats': 'application/json', - 'admin/relays/add': 'application/json', - 'admin/relays/list': 'application/json', - 'admin/relays/remove': 'application/json', - 'admin/reset-password': 'application/json', - 'admin/resolve-abuse-user-report': 'application/json', - 'admin/send-email': 'application/json', - 'admin/server-info': 'application/json', - 'admin/show-moderation-logs': 'application/json', - 'admin/show-user': 'application/json', - 'admin/show-users': 'application/json', - 'admin/nsfw-user': 'application/json', - 'admin/unnsfw-user': 'application/json', - 'admin/silence-user': 'application/json', - 'admin/unsilence-user': 'application/json', - 'admin/suspend-user': 'application/json', - 'admin/approve-user': 'application/json', - 'admin/unsuspend-user': 'application/json', - 'admin/update-meta': 'application/json', - 'admin/delete-account': 'application/json', - 'admin/update-user-note': 'application/json', - 'admin/roles/create': 'application/json', - 'admin/roles/delete': 'application/json', - 'admin/roles/list': 'application/json', - 'admin/roles/show': 'application/json', - 'admin/roles/update': 'application/json', - 'admin/roles/assign': 'application/json', - 'admin/roles/unassign': 'application/json', - 'admin/roles/update-default-policies': 'application/json', - 'admin/roles/users': 'application/json', - 'admin/system-webhook/create': 'application/json', - 'admin/system-webhook/delete': 'application/json', - 'admin/system-webhook/list': 'application/json', - 'admin/system-webhook/show': 'application/json', - 'admin/system-webhook/update': 'application/json', - 'announcements': 'application/json', - 'announcements/show': 'application/json', - 'antennas/create': 'application/json', - 'antennas/delete': 'application/json', - 'antennas/list': 'application/json', - 'antennas/notes': 'application/json', - 'antennas/show': 'application/json', - 'antennas/update': 'application/json', - 'ap/get': 'application/json', - 'ap/show': 'application/json', - 'app/create': 'application/json', - 'app/show': 'application/json', - 'auth/accept': 'application/json', - 'auth/session/generate': 'application/json', - 'auth/session/show': 'application/json', - 'auth/session/userkey': 'application/json', - 'blocking/create': 'application/json', - 'blocking/delete': 'application/json', - 'blocking/list': 'application/json', - 'channels/create': 'application/json', - 'channels/featured': 'application/json', - 'channels/follow': 'application/json', - 'channels/followed': 'application/json', - 'channels/owned': 'application/json', - 'channels/show': 'application/json', - 'channels/timeline': 'application/json', - 'channels/unfollow': 'application/json', - 'channels/update': 'application/json', - 'channels/favorite': 'application/json', - 'channels/unfavorite': 'application/json', - 'channels/my-favorites': 'application/json', - 'channels/search': 'application/json', - 'charts/active-users': 'application/json', - 'charts/ap-request': 'application/json', - 'charts/drive': 'application/json', - 'charts/federation': 'application/json', - 'charts/instance': 'application/json', - 'charts/notes': 'application/json', - 'charts/user/drive': 'application/json', - 'charts/user/following': 'application/json', - 'charts/user/notes': 'application/json', - 'charts/user/pv': 'application/json', - 'charts/user/reactions': 'application/json', - 'charts/users': 'application/json', - 'clips/add-note': 'application/json', - 'clips/remove-note': 'application/json', - 'clips/create': 'application/json', - 'clips/delete': 'application/json', - 'clips/list': 'application/json', - 'clips/notes': 'application/json', - 'clips/show': 'application/json', - 'clips/update': 'application/json', - 'clips/favorite': 'application/json', - 'clips/unfavorite': 'application/json', - 'clips/my-favorites': 'application/json', - 'drive': 'application/json', - 'drive/files': 'application/json', - 'drive/files/attached-notes': 'application/json', - 'drive/files/check-existence': 'application/json', +/** + * NOTE: The content-type for all endpoints not listed here is application/json. + */ +export const endpointReqTypes = { 'drive/files/create': 'multipart/form-data', - 'drive/files/delete': 'application/json', - 'drive/files/find-by-hash': 'application/json', - 'drive/files/find': 'application/json', - 'drive/files/show': 'application/json', - 'drive/files/update': 'application/json', - 'drive/files/upload-from-url': 'application/json', - 'drive/folders': 'application/json', - 'drive/folders/create': 'application/json', - 'drive/folders/delete': 'application/json', - 'drive/folders/find': 'application/json', - 'drive/folders/show': 'application/json', - 'drive/folders/update': 'application/json', - 'drive/stream': 'application/json', - 'email-address/available': 'application/json', - 'endpoint': 'application/json', - 'endpoints': 'application/json', - 'export-custom-emojis': 'application/json', - 'federation/followers': 'application/json', - 'federation/following': 'application/json', - 'federation/instances': 'application/json', - 'federation/show-instance': 'application/json', - 'federation/update-remote-user': 'application/json', - 'federation/users': 'application/json', - 'federation/stats': 'application/json', - 'following/create': 'application/json', - 'following/delete': 'application/json', - 'following/update': 'application/json', - 'following/update-all': 'application/json', - 'following/invalidate': 'application/json', - 'following/requests/accept': 'application/json', - 'following/requests/cancel': 'application/json', - 'following/requests/list': 'application/json', - 'following/requests/reject': 'application/json', - 'gallery/featured': 'application/json', - 'gallery/popular': 'application/json', - 'gallery/posts': 'application/json', - 'gallery/posts/create': 'application/json', - 'gallery/posts/delete': 'application/json', - 'gallery/posts/like': 'application/json', - 'gallery/posts/show': 'application/json', - 'gallery/posts/unlike': 'application/json', - 'gallery/posts/update': 'application/json', - 'get-online-users-count': 'application/json', - 'get-avatar-decorations': 'application/json', - 'hashtags/list': 'application/json', - 'hashtags/search': 'application/json', - 'hashtags/show': 'application/json', - 'hashtags/trend': 'application/json', - 'hashtags/users': 'application/json', - 'i': 'application/json', - 'i/2fa/done': 'application/json', - 'i/2fa/key-done': 'application/json', - 'i/2fa/password-less': 'application/json', - 'i/2fa/register-key': 'application/json', - 'i/2fa/register': 'application/json', - 'i/2fa/update-key': 'application/json', - 'i/2fa/remove-key': 'application/json', - 'i/2fa/unregister': 'application/json', - 'i/apps': 'application/json', - 'i/authorized-apps': 'application/json', - 'i/claim-achievement': 'application/json', - 'i/change-password': 'application/json', - 'i/delete-account': 'application/json', - 'i/export-data': 'application/json', - 'i/export-blocking': 'application/json', - 'i/export-following': 'application/json', - 'i/export-mute': 'application/json', - 'i/export-notes': 'application/json', - 'i/export-clips': 'application/json', - 'i/export-favorites': 'application/json', - 'i/export-user-lists': 'application/json', - 'i/export-antennas': 'application/json', - 'i/favorites': 'application/json', - 'i/gallery/likes': 'application/json', - 'i/gallery/posts': 'application/json', - 'i/import-blocking': 'application/json', - 'i/import-following': 'application/json', - 'i/import-notes': 'application/json', - 'i/import-muting': 'application/json', - 'i/import-user-lists': 'application/json', - 'i/import-antennas': 'application/json', - 'i/notifications': 'application/json', - 'i/notifications-grouped': 'application/json', - 'i/page-likes': 'application/json', - 'i/pages': 'application/json', - 'i/pin': 'application/json', - 'i/read-all-unread-notes': 'application/json', - 'i/read-announcement': 'application/json', - 'i/regenerate-token': 'application/json', - 'i/registry/get-all': 'application/json', - 'i/registry/get-unsecure': 'application/json', - 'i/registry/get-detail': 'application/json', - 'i/registry/get': 'application/json', - 'i/registry/keys-with-type': 'application/json', - 'i/registry/keys': 'application/json', - 'i/registry/remove': 'application/json', - 'i/registry/scopes-with-domain': 'application/json', - 'i/registry/set': 'application/json', - 'i/revoke-token': 'application/json', - 'i/signin-history': 'application/json', - 'i/unpin': 'application/json', - 'i/update-email': 'application/json', - 'i/update': 'application/json', - 'i/move': 'application/json', - 'i/webhooks/create': 'application/json', - 'i/webhooks/list': 'application/json', - 'i/webhooks/show': 'application/json', - 'i/webhooks/update': 'application/json', - 'i/webhooks/delete': 'application/json', - 'invite/create': 'application/json', - 'invite/delete': 'application/json', - 'invite/list': 'application/json', - 'invite/limit': 'application/json', - 'meta': 'application/json', - 'emojis': 'application/json', - 'emoji': 'application/json', - 'miauth/gen-token': 'application/json', - 'mute/create': 'application/json', - 'mute/delete': 'application/json', - 'mute/list': 'application/json', - 'renote-mute/create': 'application/json', - 'renote-mute/delete': 'application/json', - 'renote-mute/list': 'application/json', - 'my/apps': 'application/json', - 'notes': 'application/json', - 'notes/children': 'application/json', - 'notes/clips': 'application/json', - 'notes/conversation': 'application/json', - 'notes/create': 'application/json', - 'notes/delete': 'application/json', - 'notes/favorites/create': 'application/json', - 'notes/favorites/delete': 'application/json', - 'notes/featured': 'application/json', - 'notes/global-timeline': 'application/json', - 'notes/bubble-timeline': 'application/json', - 'notes/hybrid-timeline': 'application/json', - 'notes/local-timeline': 'application/json', - 'notes/mentions': 'application/json', - 'notes/polls/recommendation': 'application/json', - 'notes/polls/vote': 'application/json', - 'notes/reactions': 'application/json', - 'notes/reactions/create': 'application/json', - 'notes/reactions/delete': 'application/json', - 'notes/like': 'application/json', - 'notes/renotes': 'application/json', - 'notes/replies': 'application/json', - 'notes/search-by-tag': 'application/json', - 'notes/search': 'application/json', - 'notes/show': 'application/json', - 'notes/state': 'application/json', - 'notes/thread-muting/create': 'application/json', - 'notes/thread-muting/delete': 'application/json', - 'notes/timeline': 'application/json', - 'notes/translate': 'application/json', - 'notes/unrenote': 'application/json', - 'notes/user-list-timeline': 'application/json', - 'notes/edit': 'application/json', - 'notes/versions': 'application/json', - 'notifications/create': 'application/json', - 'notifications/flush': 'application/json', - 'notifications/mark-all-as-read': 'application/json', - 'notifications/test-notification': 'application/json', - 'page-push': 'application/json', - 'pages/create': 'application/json', - 'pages/delete': 'application/json', - 'pages/featured': 'application/json', - 'pages/like': 'application/json', - 'pages/show': 'application/json', - 'pages/unlike': 'application/json', - 'pages/update': 'application/json', - 'flash/create': 'application/json', - 'flash/delete': 'application/json', - 'flash/featured': 'application/json', - 'flash/like': 'application/json', - 'flash/show': 'application/json', - 'flash/unlike': 'application/json', - 'flash/update': 'application/json', - 'flash/my': 'application/json', - 'flash/my-likes': 'application/json', - 'ping': 'application/json', - 'pinned-users': 'application/json', - 'promo/read': 'application/json', - 'roles/list': 'application/json', - 'roles/show': 'application/json', - 'roles/users': 'application/json', - 'roles/notes': 'application/json', - 'request-reset-password': 'application/json', - 'reset-db': 'application/json', - 'reset-password': 'application/json', - 'server-info': 'application/json', - 'stats': 'application/json', - 'sw/show-registration': 'application/json', - 'sw/update-registration': 'application/json', - 'sw/register': 'application/json', - 'sw/unregister': 'application/json', - 'test': 'application/json', - 'username/available': 'application/json', - 'users': 'application/json', - 'users/clips': 'application/json', - 'users/followers': 'application/json', - 'users/following': 'application/json', - 'users/gallery/posts': 'application/json', - 'users/get-frequently-replied-users': 'application/json', - 'users/featured-notes': 'application/json', - 'users/lists/create': 'application/json', - 'users/lists/delete': 'application/json', - 'users/lists/list': 'application/json', - 'users/lists/pull': 'application/json', - 'users/lists/push': 'application/json', - 'users/lists/show': 'application/json', - 'users/lists/favorite': 'application/json', - 'users/lists/unfavorite': 'application/json', - 'users/lists/update': 'application/json', - 'users/lists/create-from-public': 'application/json', - 'users/lists/update-membership': 'application/json', - 'users/lists/get-memberships': 'application/json', - 'users/notes': 'application/json', - 'users/pages': 'application/json', - 'users/flashs': 'application/json', - 'users/reactions': 'application/json', - 'users/recommendation': 'application/json', - 'users/relation': 'application/json', - 'users/report-abuse': 'application/json', - 'users/search-by-username-and-host': 'application/json', - 'users/search': 'application/json', - 'users/show': 'application/json', - 'users/achievements': 'application/json', - 'users/update-memo': 'application/json', - 'fetch-rss': 'application/json', - 'fetch-external-resources': 'application/json', - 'retention': 'application/json', - 'sponsors': 'application/json', - 'bubble-game/register': 'application/json', - 'bubble-game/ranking': 'application/json', - 'reversi/cancel-match': 'application/json', - 'reversi/games': 'application/json', - 'reversi/match': 'application/json', - 'reversi/invitations': 'application/json', - 'reversi/show-game': 'application/json', - 'reversi/surrender': 'application/json', - 'reversi/verify': 'application/json', -}; +} as const satisfies { [K in keyof Endpoints]?: 'multipart/form-data'; }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 0228ad47e60e6a3e1c10b1f707f3fbed95c162c3..999dd4dd545254b50f4618919ba836ee4c56bb8d 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -100,6 +100,7 @@ export type AdminSilenceUserRequest = operations['admin___silence-user']['reques export type AdminUnsilenceUserRequest = operations['admin___unsilence-user']['requestBody']['content']['application/json']; export type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json']; export type AdminApproveUserRequest = operations['admin___approve-user']['requestBody']['content']['application/json']; +export type AdminDeclineUserRequest = operations['admin___decline-user']['requestBody']['content']['application/json']; export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json']; export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json']; @@ -125,6 +126,7 @@ export type AdminSystemWebhookShowRequest = operations['admin___system-webhook__ export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json']; export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json']; export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json']; +export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json']; export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json']; export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json']; export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json']; @@ -283,6 +285,8 @@ export type FollowingRequestsCancelRequest = operations['following___requests___ export type FollowingRequestsCancelResponse = operations['following___requests___cancel']['responses']['200']['content']['application/json']; export type FollowingRequestsListRequest = operations['following___requests___list']['requestBody']['content']['application/json']; export type FollowingRequestsListResponse = operations['following___requests___list']['responses']['200']['content']['application/json']; +export type FollowingRequestsSentRequest = operations['following___requests___sent']['requestBody']['content']['application/json']; +export type FollowingRequestsSentResponse = operations['following___requests___sent']['responses']['200']['content']['application/json']; export type FollowingRequestsRejectRequest = operations['following___requests___reject']['requestBody']['content']['application/json']; export type GalleryFeaturedRequest = operations['gallery___featured']['requestBody']['content']['application/json']; export type GalleryFeaturedResponse = operations['gallery___featured']['responses']['200']['content']['application/json']; @@ -386,6 +390,7 @@ export type IWebhooksShowRequest = operations['i___webhooks___show']['requestBod export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json']; export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json']; export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json']; +export type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json']; export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json']; export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json']; export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json']; @@ -423,6 +428,8 @@ export type NotesFavoritesCreateRequest = operations['notes___favorites___create export type NotesFavoritesDeleteRequest = operations['notes___favorites___delete']['requestBody']['content']['application/json']; export type NotesFeaturedRequest = operations['notes___featured']['requestBody']['content']['application/json']; export type NotesFeaturedResponse = operations['notes___featured']['responses']['200']['content']['application/json']; +export type NotesFollowingRequest = operations['notes___following']['requestBody']['content']['application/json']; +export type NotesFollowingResponse = operations['notes___following']['responses']['200']['content']['application/json']; export type NotesGlobalTimelineRequest = operations['notes___global-timeline']['requestBody']['content']['application/json']; export type NotesGlobalTimelineResponse = operations['notes___global-timeline']['responses']['200']['content']['application/json']; export type NotesBubbleTimelineRequest = operations['notes___bubble-timeline']['requestBody']['content']['application/json']; @@ -436,6 +443,7 @@ export type NotesMentionsResponse = operations['notes___mentions']['responses'][ export type NotesPollsRecommendationRequest = operations['notes___polls___recommendation']['requestBody']['content']['application/json']; export type NotesPollsRecommendationResponse = operations['notes___polls___recommendation']['responses']['200']['content']['application/json']; export type NotesPollsVoteRequest = operations['notes___polls___vote']['requestBody']['content']['application/json']; +export type NotesPollsRefreshRequest = operations['notes___polls___refresh']['requestBody']['content']['application/json']; export type NotesReactionsRequest = operations['notes___reactions']['requestBody']['content']['application/json']; export type NotesReactionsResponse = operations['notes___reactions']['responses']['200']['content']['application/json']; export type NotesReactionsCreateRequest = operations['notes___reactions___create']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 562c4981909a544728764f74eaf622d129add204..e275e4b475c88935f5884d1942b3f60f6a4fd5b1 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -675,6 +675,15 @@ export type paths = { */ post: operations['admin___approve-user']; }; + '/admin/decline-user': { + /** + * admin/decline-user + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user* + */ + post: operations['admin___decline-user']; + }; '/admin/unsuspend-user': { /** * admin/unsuspend-user @@ -842,6 +851,16 @@ export type paths = { */ post: operations['admin___system-webhook___update']; }; + '/admin/system-webhook/test': { + /** + * admin/system-webhook/test + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook* + */ + post: operations['admin___system-webhook___test']; + }; '/announcements': { /** * announcements @@ -1626,7 +1645,7 @@ export type paths = { * federation/followers * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *Yes* / **Permission**: *read:account* */ post: operations['federation___followers']; }; @@ -1635,7 +1654,7 @@ export type paths = { * federation/following * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *Yes* / **Permission**: *read:account* */ post: operations['federation___following']; }; @@ -1770,6 +1789,15 @@ export type paths = { */ post: operations['following___requests___list']; }; + '/following/requests/sent': { + /** + * following/requests/sent + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:following* + */ + post: operations['following___requests___sent']; + }; '/following/requests/reject': { /** * following/requests/reject @@ -2510,6 +2538,16 @@ export type paths = { */ post: operations['i___webhooks___delete']; }; + '/i/webhooks/test': { + /** + * i/webhooks/test + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['i___webhooks___test']; + }; '/invite/create': { /** * invite/create @@ -2748,6 +2786,22 @@ export type paths = { */ post: operations['notes___featured']; }; + '/notes/following': { + /** + * notes/following + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + get: operations['notes___following']; + /** + * notes/following + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['notes___following']; + }; '/notes/global-timeline': { /** * notes/global-timeline @@ -2811,6 +2865,15 @@ export type paths = { */ post: operations['notes___polls___vote']; }; + '/notes/polls/refresh': { + /** + * notes/polls/refresh + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:federation* + */ + post: operations['notes___polls___refresh']; + }; '/notes/reactions': { /** * notes/reactions @@ -3673,7 +3736,7 @@ export type paths = { '/sponsors': { /** * sponsors - * @description Get Sharkey GH Sponsors + * @description Get Sharkey Sponsors or Instance Sponsors * * **Credential required**: *No* */ @@ -3814,16 +3877,19 @@ export type components = { url: string; offsetX?: number; offsetY?: number; + showBelow?: boolean; }[]; /** @default false */ isAdmin?: boolean; /** @default false */ isModerator?: boolean; - isSilenced: boolean; + /** @default false */ + isSystem?: boolean; noindex: boolean; isBot?: boolean; isCat?: boolean; speakAsCat?: boolean; + isSilenced: boolean; instance?: { name: string | null; softwareName: string | null; @@ -3864,6 +3930,7 @@ export type components = { backgroundUrl: string | null; backgroundBlurhash: string | null; isLocked: boolean; + isSilenced: boolean; /** @example false */ isSuspended: boolean; /** @example Hi masters, I am Ai! */ @@ -3872,7 +3939,7 @@ export type components = { /** @example 2018-03-12 */ birthday: string | null; /** @example Steve */ - ListenBrainz: string | null; + listenbrainz: string | null; /** @example ja-JP */ lang: string | null; fields: { @@ -3899,6 +3966,7 @@ export type components = { /** @default false */ securityKeys: boolean; roles: components['schemas']['RoleLite'][]; + followedMessage?: string | null; memo: string | null; moderationNote?: string; isFollowing?: boolean; @@ -3920,11 +3988,13 @@ export type components = { bannerId: string | null; /** Format: id */ backgroundId: string | null; + followedMessage: string | null; isModerator: boolean | null; isAdmin: boolean | null; injectFeaturedNote: boolean; receiveAnnouncementEmail: boolean; alwaysMarkNsfw: boolean; + defaultSensitive: boolean; autoSensitive: boolean; carefulBot: boolean; autoAcceptFollowed: boolean; @@ -3943,6 +4013,7 @@ export type components = { hasUnreadChannel: boolean; hasUnreadNotification: boolean; hasPendingReceivedFollowRequest: boolean; + hasPendingSentFollowRequest: boolean; unreadNotificationsCount: number; mutedWords: string[][]; hardMutedWords: string[][]; @@ -4359,7 +4430,7 @@ export type components = { user: components['schemas']['UserLite']; /** Format: id */ userId: string; - } | { + } | ({ /** Format: id */ id: string; /** Format: date-time */ @@ -4369,7 +4440,8 @@ export type components = { user: components['schemas']['UserLite']; /** Format: id */ userId: string; - } | { + message: string | null; + }) | { /** Format: id */ id: string; /** Format: date-time */ @@ -4377,15 +4449,27 @@ export type components = { /** @enum {string} */ type: 'roleAssigned'; role: components['schemas']['Role']; - } | { + } | ({ /** Format: id */ id: string; /** Format: date-time */ createdAt: string; /** @enum {string} */ type: 'achievementEarned'; - achievement: string; - } | { + /** @enum {string} */ + achievement: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead'; + }) | ({ + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + type: 'exportCompleted'; + /** @enum {string} */ + exportedEntity: 'antenna' | 'blocking' | 'clip' | 'customEmoji' | 'favorite' | 'following' | 'muting' | 'note' | 'userList'; + /** Format: id */ + fileId: string; + }) | ({ /** Format: id */ id: string; /** Format: date-time */ @@ -4393,9 +4477,9 @@ export type components = { /** @enum {string} */ type: 'app'; body: string; - header: string; - icon: string; - } | { + header: string | null; + icon: string | null; + }) | { /** Format: id */ id: string; /** Format: date-time */ @@ -4753,6 +4837,7 @@ export type components = { /** Format: date-time */ latestRequestReceivedAt: string | null; isNSFW: boolean; + rejectReports: boolean; moderationNote?: string | null; }; GalleryPost: { @@ -4946,6 +5031,11 @@ export type components = { userEachUserListsLimit: number; rateLimitFactor: number; avatarDecorationLimit: number; + canImportAntennas: boolean; + canImportBlocking: boolean; + canImportFollowing: boolean; + canImportMuting: boolean; + canImportUserLists: boolean; }; ReversiGameLite: { /** Format: id */ @@ -5052,6 +5142,8 @@ export type components = { recaptchaSiteKey: string | null; enableTurnstile: boolean; turnstileSiteKey: string | null; + enableFC: boolean; + fcSiteKey: string | null; enableAchievements: boolean | null; swPublickey: string | null; /** @default /assets/ai.png */ @@ -5061,7 +5153,13 @@ export type components = { infoImageUrl: string | null; notFoundImageUrl: string | null; iconUrl: string | null; + sidebarLogoUrl: string | null; maxNoteTextLength: number; + maxRemoteNoteTextLength: number; + maxCwLength: number; + maxRemoteCwLength: number; + maxAltTextLength: number; + maxRemoteAltTextLength: number; ads: { /** * Format: id @@ -5096,6 +5194,8 @@ export type components = { * @enum {string} */ noteSearchableScope: 'local' | 'global'; + trustedLinkUrlPatterns: string[]; + maxFileSize: number; }; MetaDetailedOnly: { features?: { @@ -5183,6 +5283,8 @@ export type operations = { recaptchaSiteKey: string | null; enableTurnstile: boolean; turnstileSiteKey: string | null; + enableFC: boolean; + fcSiteKey: string | null; swPublickey: string | null; /** @default /assets/ai.png */ mascotImageUrl: string | null; @@ -5193,10 +5295,11 @@ export type operations = { iconUrl: string | null; app192IconUrl: string | null; app512IconUrl: string | null; + sidebarLogoUrl: string | null; enableEmail: boolean; enableServiceWorker: boolean; translatorAvailable: boolean; - silencedHosts?: string[]; + silencedHosts: string[]; mediaSilencedHosts: string[]; pinnedUsers: string[]; hiddenTags: string[]; @@ -5210,6 +5313,7 @@ export type operations = { mcaptchaSecretKey: string | null; recaptchaSecretKey: string | null; turnstileSecretKey: string | null; + fcSecretKey: string | null; sensitiveMediaDetection: string; sensitiveMediaDetectionSensitivity: string; setSensitiveFlagAutomatically: boolean; @@ -5256,6 +5360,7 @@ export type operations = { perRemoteUserUserTimelineCacheMax: number; perUserHomeTimelineCacheMax: number; perUserListTimelineCacheMax: number; + enableReactionsBuffering: boolean; notesPerOneAd: number; backgroundImageUrl: string | null; deeplAuthKey: string | null; @@ -5291,6 +5396,9 @@ export type operations = { urlPreviewRequireContentLength: boolean; urlPreviewUserAgent: string | null; urlPreviewSummaryProxyUrl: string | null; + trustedLinkUrlPatterns: string[]; + federation: string; + federationHosts: string[]; }; }; }; @@ -7950,6 +8058,7 @@ export type operations = { host: string; isSuspended?: boolean; isNSFW?: boolean; + rejectReports?: boolean; moderationNote?: string; }; }; @@ -8375,7 +8484,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': ((string | number)[])[]; + 'application/json': [string, number][]; }; }; /** @description Client error */ @@ -8421,7 +8530,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': ((string | number)[])[]; + 'application/json': [string, number][]; }; }; /** @description Client error */ @@ -9055,6 +9164,8 @@ export type operations = { 'application/json': { email: string | null; emailVerified: boolean; + approved: boolean; + followedMessage: string | null; autoAcceptFollowed: boolean; noCrawle: boolean; preventAiLearning: boolean; @@ -9194,6 +9305,7 @@ export type operations = { }]>; }; isModerator: boolean; + isSystem: boolean; isSilenced: boolean; isSuspended: boolean; isHibernated: boolean; @@ -9629,6 +9741,58 @@ export type operations = { }; }; }; + /** + * admin/decline-user + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:decline-user* + */ + 'admin___decline-user': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * admin/unsuspend-user * @description No description provided. @@ -9706,6 +9870,7 @@ export type operations = { iconUrl?: string | null; app192IconUrl?: string | null; app512IconUrl?: string | null; + sidebarLogoUrl?: string | null; backgroundImageUrl?: string | null; logoImageUrl?: string | null; name?: string | null; @@ -9731,6 +9896,9 @@ export type operations = { enableTurnstile?: boolean; turnstileSiteKey?: string | null; turnstileSecretKey?: string | null; + enableFC?: boolean; + fcSiteKey?: string | null; + fcSecretKey?: string | null; /** @enum {string} */ sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote'; /** @enum {string} */ @@ -9800,6 +9968,7 @@ export type operations = { perRemoteUserUserTimelineCacheMax?: number; perUserHomeTimelineCacheMax?: number; perUserListTimelineCacheMax?: number; + enableReactionsBuffering?: boolean; notesPerOneAd?: number; silencedHosts?: string[] | null; mediaSilencedHosts?: string[] | null; @@ -9811,6 +9980,10 @@ export type operations = { urlPreviewRequireContentLength?: boolean; urlPreviewUserAgent?: string | null; urlPreviewSummaryProxyUrl?: string | null; + trustedLinkUrlPatterns?: string[] | null; + /** @enum {string} */ + federation?: 'all' | 'none' | 'specified'; + federationHosts?: string[]; }; }; }; @@ -10752,6 +10925,71 @@ export type operations = { }; }; }; + /** + * admin/system-webhook/test + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook* + */ + 'admin___system-webhook___test': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + webhookId: string; + /** @enum {string} */ + type: 'abuseReport' | 'abuseReportResolved' | 'userCreated'; + override?: { + url?: string; + secret?: string; + }; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * announcements * @description No description provided. @@ -15325,7 +15563,7 @@ export type operations = { * federation/followers * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *Yes* / **Permission**: *read:account* */ federation___followers: { requestBody: { @@ -15338,6 +15576,10 @@ export type operations = { untilId?: string; /** @default 10 */ limit?: number; + /** @default false */ + includeFollower?: boolean; + /** @default true */ + includeFollowee?: boolean; }; }; }; @@ -15384,7 +15626,7 @@ export type operations = { * federation/following * @description No description provided. * - * **Credential required**: *No* + * **Credential required**: *Yes* / **Permission**: *read:account* */ federation___following: { requestBody: { @@ -15397,6 +15639,10 @@ export type operations = { untilId?: string; /** @default 10 */ limit?: number; + /** @default false */ + includeFollower?: boolean; + /** @default true */ + includeFollowee?: boolean; }; }; }; @@ -16207,6 +16453,69 @@ export type operations = { }; }; }; + /** + * following/requests/sent + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:following* + */ + following___requests___sent: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** Format: id */ + id: string; + follower: components['schemas']['UserLite']; + followee: components['schemas']['UserLite']; + }[]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * following/requests/reject * @description No description provided. @@ -19029,8 +19338,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -19097,8 +19406,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -20210,6 +20519,7 @@ export type operations = { 'application/json': { name?: string | null; description?: string | null; + followedMessage?: string | null; location?: string | null; birthday?: string | null; listenbrainz?: string | null; @@ -20224,6 +20534,7 @@ export type operations = { flipH?: boolean | null; offsetX?: number | null; offsetY?: number | null; + showBelow?: boolean | null; })[]; /** Format: misskey:id */ bannerId?: string | null; @@ -20248,6 +20559,7 @@ export type operations = { injectFeaturedNote?: boolean; receiveAnnouncementEmail?: boolean; alwaysMarkNsfw?: boolean; + defaultSensitive?: boolean; autoSensitive?: boolean; /** @enum {string} */ followingVisibility?: 'public' | 'followers' | 'private'; @@ -20801,6 +21113,71 @@ export type operations = { }; }; }; + /** + * i/webhooks/test + * @description No description provided. + * + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + i___webhooks___test: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + webhookId: string; + /** @enum {string} */ + type: 'mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited'; + override?: { + url?: string; + secret?: string; + }; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * invite/create * @description No description provided. @@ -22173,6 +22550,81 @@ export type operations = { }; }; }; + /** + * notes/following + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + notes___following: { + requestBody: { + content: { + 'application/json': { + /** + * @default following + * @enum {string} + */ + list?: 'following' | 'followers' | 'mutuals'; + /** @default false */ + filesOnly?: boolean; + /** @default false */ + includeNonPublic?: boolean; + /** @default false */ + includeReplies?: boolean; + /** @default false */ + includeQuotes?: boolean; + /** @default true */ + includeBots?: boolean; + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Note'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * notes/global-timeline * @description No description provided. @@ -22623,6 +23075,58 @@ export type operations = { }; }; }; + /** + * notes/polls/refresh + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:federation* + */ + notes___polls___refresh: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * notes/reactions * @description No description provided. @@ -27055,7 +27559,15 @@ export type operations = { /** @default false */ withReplies?: boolean; /** @default true */ + withRepliesToSelf?: boolean; + /** @default true */ + withQuotes?: boolean; + /** @default true */ withRenotes?: boolean; + /** @default true */ + withBots?: boolean; + /** @default true */ + withNonPublic?: boolean; /** @default false */ withChannelNotes?: boolean; /** @default 10 */ @@ -27988,7 +28500,7 @@ export type operations = { }; /** * sponsors - * @description Get Sharkey GH Sponsors + * @description Get Sharkey Sponsors or Instance Sponsors * * **Credential required**: *No* */ @@ -27998,6 +28510,8 @@ export type operations = { 'application/json': { /** @default false */ forceUpdate?: boolean; + /** @default false */ + instance?: boolean; }; }; }; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 890b43639d150a75a44d85bafa874ef5f062a27f..c99b8f55707c9d8cc6845a5237afffc68c770add 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -78,6 +78,7 @@ export const permissions = [ 'read:admin:show-user', 'write:admin:suspend-user', 'write:admin:approve-user', + 'write:admin:decline-user', 'write:admin:nsfw-user', 'write:admin:unnsfw-user', 'write:admin:silence-user', @@ -204,6 +205,11 @@ export type ModerationLogPayloads = { userUsername: string; userHost: string | null; }; + decline: { + userId: string; + userUsername: string; + userHost: string | null; + }; unsuspend: { userId: string; userUsername: string; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 6a405f81a5514f8092cb61448c161078001b5255..f85574d9d5036b2c04ad7fce68b5c712af135c4b 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -50,6 +50,9 @@ export type ModerationLog = { } | { type: 'approve'; info: ModerationLogPayloads['approve']; +} | { + type: 'decline'; + info: ModerationLogPayloads['decline']; } | { type: 'suspend'; info: ModerationLogPayloads['suspend']; @@ -274,6 +277,17 @@ export type SigninRequest = { token?: string; }; +export type SigninWithPasskeyRequest = { + credential?: object; + context?: string; +}; + +export type SigninWithPasskeyResponse = { + option?: object; + context?: string; + signinResponse?: SigninResponse; +}; + export type SigninResponse = { id: User['id'], i: string, diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index ace9738e6a19ffd6d2e6a253dbc54455ee09df54..e4c9364aa1c2a0a184e45061020d75ce14fb6692 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -1,15 +1,6 @@ -import { type Endpoints } from './api.types.js'; import Stream, { Connection } from './streaming.js'; -import { type Channels } from './streaming.types.js'; -import { type Acct } from './acct.js'; import * as consts from './consts.js'; -export type { - Endpoints, - Channels, - Acct, -}; - export { Stream, Connection as ChannelConnection, @@ -31,4 +22,21 @@ import * as api from './api.js'; import * as entities from './entities.js'; import * as acct from './acct.js'; import * as note from './note.js'; -export { api, entities, acct, note }; +import { nyaize } from './nyaize.js'; +export { api, entities, acct, note, nyaize }; + +//#region standalone types +import type { Endpoints } from './api.types.js'; +import type { StreamEvents, IStream, IChannelConnection } from './streaming.js'; +import type { Channels } from './streaming.types.js'; +import type { Acct } from './acct.js'; + +export type { + Endpoints, + Channels, + Acct, + StreamEvents, + IStream, + IChannelConnection, +}; +//#endregion diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/misskey-js/src/nyaize.ts similarity index 81% rename from packages/frontend/src/scripts/nyaize.ts rename to packages/misskey-js/src/nyaize.ts index 5e6fa298d14c80be20568e3b31df07bcc11fbb9d..d5ee54776f4e9f85cb560f745cc7b291fa6cf1a6 100644 --- a/packages/frontend/src/scripts/nyaize.ts +++ b/packages/misskey-js/src/nyaize.ts @@ -7,10 +7,10 @@ const koRegex1 = /[나-낳]/g; const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; -function ifAfter(prefix, fn) { +function ifAfter(prefix: string, fn: (x: string) => string) { const preLen = prefix.length; const regex = new RegExp(prefix, 'i'); - return (x, pos, string) => { + return (x: string, pos: number, string: string) => { return pos > 0 && string.substring(pos - preLen, pos).match(regex) ? fn(x) : x; }; } @@ -24,9 +24,9 @@ export function nyaize(text: string): string { .replace(/ing/gi, ifAfter('morn', x => x === 'ING' ? 'YAN' : 'yan')) .replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan')) // ko-KR - .replace(koRegex1, match => String.fromCharCode( + .replace(koRegex1, match => !isNaN(match.charCodeAt(0)) ? String.fromCharCode( match.charCodeAt(0) + 'ëƒ'.charCodeAt(0) - '나'.charCodeAt(0), - )) + ) : match) .replace(koRegex2, '다냥') .replace(koRegex3, '냥'); } diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts index d1d131cfc134d95aa34416dff09817bd989d8ed9..ffb46c77f6c179002023228d804f5a3d78247291 100644 --- a/packages/misskey-js/src/streaming.ts +++ b/packages/misskey-js/src/streaming.ts @@ -17,16 +17,32 @@ export function urlQuery(obj: Record<string, string | number | boolean | undefin type AnyOf<T extends Record<PropertyKey, unknown>> = T[keyof T]; -type StreamEvents = { +export type StreamEvents = { _connected_: void; _disconnected_: void; } & BroadcastEvents; +export interface IStream extends EventEmitter<StreamEvents> { + state: 'initializing' | 'reconnecting' | 'connected'; + + useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): IChannelConnection<Channels[C]>; + removeSharedConnection(connection: SharedConnection): void; + removeSharedConnectionPool(pool: Pool): void; + disconnectToChannel(connection: NonSharedConnection): void; + send(typeOrPayload: string): void; + send(typeOrPayload: string, payload: unknown): void; + send(typeOrPayload: Record<string, unknown> | unknown[]): void; + send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void; + ping(): void; + heartbeat(): void; + close(): void; +} + /** * Misskey stream connection */ // eslint-disable-next-line import/no-default-export -export default class Stream extends EventEmitter<StreamEvents> { +export default class Stream extends EventEmitter<StreamEvents> implements IStream { private stream: _ReconnectingWebsocket.default; public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; private sharedConnectionPools: Pool[] = []; @@ -277,7 +293,18 @@ class Pool { } } -export abstract class Connection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> { +export interface IChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> { + id: string; + name?: string; + inCount: number; + outCount: number; + channel: string; + + send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void; + dispose(): void; +} + +export abstract class Connection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> { public channel: string; protected stream: Stream; public abstract id: string; diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 7455bdcd7fccec9adf97e8cafbf328efa308f8a8..186db05d618da43b62042cd271b5147a76d4d5a0 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -140,7 +140,7 @@ export type Channels = { }; hashtag: { params: { - q?: string; + q: string[][]; }; events: { note: (payload: Note) => void; @@ -249,7 +249,7 @@ export type Channels = { } }; -export type NoteUpdatedEvent = { +export type NoteUpdatedEvent = { id: Note['id'] } & ({ type: 'reacted'; body: { reaction: string; @@ -280,7 +280,7 @@ export type NoteUpdatedEvent = { choice: number; userId: User['id']; }; -}; +}); export type BroadcastEvents = { noteUpdated: (payload: NoteUpdatedEvent) => void; diff --git a/packages/frontend/test/nyaize.test.ts b/packages/misskey-js/test/nyaize.test.ts similarity index 74% rename from packages/frontend/test/nyaize.test.ts rename to packages/misskey-js/test/nyaize.test.ts index 28b90c315244cd75706f225532a28a5b1b6b07e9..24b19bc2b90dbe471e61c83e5cf5079b9e5f362d 100644 --- a/packages/frontend/test/nyaize.test.ts +++ b/packages/misskey-js/test/nyaize.test.ts @@ -3,15 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, test, assert, afterEach } from 'vitest'; -import { nyaize } from '@/scripts/nyaize.js'; +import { nyaize } from '../src/nyaize.js'; function runTests(cases) { - for (const c of cases) { - const [input,expected] = c; - const got = nyaize(input); - assert.strictEqual(got, expected); - } + for (const c of cases) { + const [input,expected] = c; + const got = nyaize(input); + expect(got).toEqual(expected); + } } describe('nyaize', () => { diff --git a/packages/misskey-js/tsconfig.json b/packages/misskey-js/tsconfig.json index f7bbc4730454e937f210a032cfcd90f978b445a8..95128b8fab18b0aafe6676921bb5a35d55040e93 100644 --- a/packages/misskey-js/tsconfig.json +++ b/packages/misskey-js/tsconfig.json @@ -16,6 +16,7 @@ "noImplicitReturns": true, "esModuleInterop": true, "exactOptionalPropertyTypes": true, + "skipLibCheck": true, "typeRoots": [ "./node_modules/@types" ], diff --git a/packages/misskey-reversi/build.js b/packages/misskey-reversi/build.js index e626c97a5967d6c20facd1cd107f46f28451a63a..a80b71646f43800c48d502dc828142c4e0bd367c 100644 --- a/packages/misskey-reversi/build.js +++ b/packages/misskey-reversi/build.js @@ -1,32 +1,32 @@ -import * as esbuild from "esbuild"; -import { build } from "esbuild"; -import { globSync } from "glob"; -import { execa } from "execa"; -import fs from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname } from "node:path"; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as esbuild from 'esbuild'; +import { build } from 'esbuild'; +import { globSync } from 'glob'; +import { execa } from 'execa'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); -const entryPoints = globSync("./src/**/**.{ts,tsx}"); +const entryPoints = globSync('./src/**/**.{ts,tsx}'); /** @type {import('esbuild').BuildOptions} */ const options = { entryPoints, minify: process.env.NODE_ENV === 'production', - outdir: "./built", - target: "es2022", - platform: "browser", - format: "esm", + outdir: './built', + target: 'es2022', + platform: 'browser', + format: 'esm', sourcemap: 'linked', }; // builté…下をã™ã¹ã¦å‰Šé™¤ã™ã‚‹ fs.rmSync('./built', { recursive: true, force: true }); -if (process.argv.map(arg => arg.toLowerCase()).includes("--watch")) { +if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { await watchSrc(); } else { await buildSrc(); @@ -36,7 +36,7 @@ async function buildSrc() { console.log(`[${_package.name}] start building...`); await build(options) - .then(it => { + .then(() => { console.log(`[${_package.name}] build succeeded.`); }) .catch((err) => { @@ -65,7 +65,7 @@ function buildDts() { { stdout: process.stdout, stderr: process.stderr, - } + }, ); } @@ -86,7 +86,7 @@ async function watchSrc() { }, }]; - console.log(`[${_package.name}] start watching...`) + console.log(`[${_package.name}] start watching...`); const context = await esbuild.context({ ...options, plugins }); await context.watch(); diff --git a/packages/misskey-reversi/eslint.config.js b/packages/misskey-reversi/eslint.config.js index 3f81df714508b46e3d547e601ca7ce3dcd5e007b..8453d9b8e707c0597ae922e58feaa023cf9539f0 100644 --- a/packages/misskey-reversi/eslint.config.js +++ b/packages/misskey-reversi/eslint.config.js @@ -1,6 +1,7 @@ import tsParser from '@typescript-eslint/parser'; import sharedConfig from '../shared/eslint.config.js'; +// eslint-disable-next-line import/no-default-export export default [ ...sharedConfig, { diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json index f32b31d4d9aea2aeac6e7659dc09ff9f8ebb481f..6386f4011881f8f42def158309cb2b94147047b3 100644 --- a/packages/misskey-reversi/package.json +++ b/packages/misskey-reversi/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "node ./build.js", "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", - "eslint": "eslint './**/*.{js,jsx,ts,tsx}' --cache", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", "typecheck": "tsc --noEmit", "lint": "pnpm typecheck && pnpm eslint" }, diff --git a/packages/misskey-reversi/src/game.ts b/packages/misskey-reversi/src/game.ts index 4afca9898cf092901017afb9f53ec279d3fe0001..35cc44feb4d94c0e4ae9492d71751c8717792b6e 100644 --- a/packages/misskey-reversi/src/game.ts +++ b/packages/misskey-reversi/src/game.ts @@ -53,9 +53,13 @@ export class Game { //#region Options this.opts = opts; + + /* eslint-disable @typescript-eslint/no-unnecessary-condition */ if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; + /* eslint-enable */ + //#endregion //#region Parse map data @@ -123,12 +127,13 @@ export class Game { // ターン計算 this.turn = this.canPutSomewhere(!this.prevColor) ? !this.prevColor : - this.canPutSomewhere(this.prevColor!) ? this.prevColor : + this.canPutSomewhere(this.prevColor!) ? this.prevColor : //eslint-disable-line @typescript-eslint/no-non-null-assertion null; } public undo() { - const undo = this.logs.pop()!; + const undo = this.logs.pop(); + if (undo == null) return; this.prevColor = undo.color; this.prevPos = undo.pos; this.board[undo.pos] = null; @@ -183,7 +188,7 @@ export class Game { const found: number[] = []; // 挟ã‚ã‚‹ã‹ã‚‚ã—ã‚Œãªã„相手ã®çŸ³ã‚’入れã¦ãŠãé…列 let [x, y] = this.posToXy(initPos); - while (true) { + while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition [x, y] = nextPos(x, y); // 座標ãŒæŒ‡ã—示ã™ä½ç½®ãŒãƒœãƒ¼ãƒ‰å¤–ã«å‡ºãŸã¨ã diff --git a/packages/misskey-reversi/tsconfig.json b/packages/misskey-reversi/tsconfig.json index 6e34e332e000246724fd39812527733960a20185..f467951ef6f6168a713b0d6665bd66ba43c9111d 100644 --- a/packages/misskey-reversi/tsconfig.json +++ b/packages/misskey-reversi/tsconfig.json @@ -15,6 +15,7 @@ "experimentalDecorators": true, "noImplicitReturns": true, "esModuleInterop": true, + "skipLibCheck": true, "typeRoots": [ "./node_modules/@types" ], diff --git a/packages/sw/build.js b/packages/sw/build.js index 9522d061e020d5414433c18aff784070f7539153..c9754086643f463c752de907142ca6e676506e73 100644 --- a/packages/sw/build.js +++ b/packages/sw/build.js @@ -8,10 +8,11 @@ import { fileURLToPath } from 'node:url'; import * as esbuild from 'esbuild'; import locales from '../../locales/index.js'; -import meta from '../../package.json' with { type: "json" }; +import meta from '../../package.json' with { type: 'json' }; +import { localesVersion } from '../../locales/version.js'; const watch = process.argv[2]?.includes('watch'); -const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const __dirname = fileURLToPath(new URL('.', import.meta.url)); console.log('Starting SW building...'); @@ -23,6 +24,7 @@ const buildOptions = { _DEV_: JSON.stringify(process.env.NODE_ENV !== 'production'), _ENV_: JSON.stringify(process.env.NODE_ENV ?? ''), // `NODE_ENV`ãŒ`undefined`ãªã¨ã`JSON.stringify`ãŒ`undefined`ã‚’è¿”ã—ã¦ã‚¨ãƒ©ãƒ¼ã«ãªã£ã¦ã—ã¾ã†ã®ã§`??`を使ã£ã¦ã„ã‚‹ _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _LANGS_VERSION_: JSON.stringify(localesVersion), _PERF_PREFIX_: JSON.stringify('Misskey:'), _VERSION_: JSON.stringify(meta.version), }, diff --git a/packages/sw/eslint.config.js b/packages/sw/eslint.config.js index c62a2eadc6d5f03feef800d0bf4dcefae5441d98..7878eb14b0f83f4bafcfef06883f95e45364cad7 100644 --- a/packages/sw/eslint.config.js +++ b/packages/sw/eslint.config.js @@ -12,6 +12,7 @@ export default [ require: false, _DEV_: false, _LANGS_: false, + _LANGS_VERSION_: false, _VERSION_: false, _ENV_: false, _PERF_PREFIX_: false, diff --git a/packages/sw/package.json b/packages/sw/package.json index 081f648f8adb76d1eae03860eda5e68771f69149..ed63a026624a61c83f878f730634159bddf06293 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -5,20 +5,20 @@ "watch": "nodemon -w ../../package.json -e json --exec \"node build.js watch\"", "build": "node build.js", "typecheck": "tsc --noEmit", - "eslint": "eslint --quiet src/**/*.ts --cache", + "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache", "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { - "esbuild": "0.23.0", + "esbuild": "0.23.1", "idb-keyval": "6.2.1", "misskey-js": "workspace:*" }, "devDependencies": { "@typescript-eslint/parser": "7.17.0", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", - "eslint-plugin-import": "2.29.1", - "nodemon": "3.1.4", - "typescript": "5.5.4" + "eslint-plugin-import": "2.30.0", + "nodemon": "3.1.7", + "typescript": "5.6.2" }, "type": "module" } diff --git a/packages/sw/src/@types/global.d.ts b/packages/sw/src/@types/global.d.ts index bf63810e6d6e3847f8b7ad3e5d7402aca8c1b66f..334fa815117d84515bb318c981e3272193e6abdc 100644 --- a/packages/sw/src/@types/global.d.ts +++ b/packages/sw/src/@types/global.d.ts @@ -7,6 +7,7 @@ type FIXME = any; declare const _LANGS_: string[][]; +declare const _LANGS_VERSION_: string; declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 32b12f4b4fa926e984615003c7303d593f6849a8..9c56e338c765927a91a6dca81d8b191d8d93286b 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -41,11 +41,10 @@ export async function createNotification<K extends keyof PushNotificationDataMap async function composeNotification(data: PushNotificationDataMap[keyof PushNotificationDataMap]): Promise<[string, NotificationOptions] | null> { const i18n = await (swLang.i18n ?? swLang.fetchLocale()); - const { t } = i18n; switch (data.type) { /* case 'driveFileCreated': // TODO (Server Side) - return [t('_notification.fileUploaded'), { + return [i18n.ts._notification.fileUploaded, { body: body.name, icon: body.url, data @@ -58,52 +57,52 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif const account = await getAccountFromId(data.userId); if (!account) return null; const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token); - return [t('_notification.youWereFollowed'), { + return [i18n.ts._notification.youWereFollowed, { body: getUserName(data.body.user), - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('user-plus'), data, actions: userDetail.isFollowing ? [] : [ { action: 'follow', - title: t('_notification._actions.followBack'), + title: i18n.ts._notification._actions.followBack, }, ], }]; } case 'mention': - return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youGotMention({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('at'), data, actions: [ { action: 'reply', - title: t('_notification._actions.reply'), + title: i18n.ts._notification._actions.reply, }, ], }]; case 'reply': - return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youGotReply({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('arrow-back-up'), data, actions: [ { action: 'reply', - title: t('_notification._actions.reply'), + title: i18n.ts._notification._actions.reply, }, ], }]; case 'renote': - return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youRenoted({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('repeat'), data, actions: [ @@ -115,29 +114,29 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif }]; case 'quote': - return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { + return [i18n.tsx._notification.youGotQuote({ name: getUserName(data.body.user) }), { body: data.body.note.text ?? '', - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('quote'), data, actions: [ { action: 'reply', - title: t('_notification._actions.reply'), + title: i18n.ts._notification._actions.reply, }, ...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [ { action: 'renote', - title: t('_notification._actions.renote'), + title: i18n.ts._notification._actions.renote, }, ] : []), ], }]; case 'note': - return [t('_notification.newNote') + ': ' + getUserName(data.body.user), { + return [i18n.ts._notification.newNote + ': ' + getUserName(data.body.user), { body: data.body.note.text ?? '', - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, data, }]; @@ -164,7 +163,7 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif const tag = `reaction:${data.body.note.id}`; return [`${reaction} ${getUserName(data.body.user)}`, { body: data.body.note.text ?? '', - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, tag, badge, data, @@ -178,41 +177,60 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif } case 'receiveFollowRequest': - return [t('_notification.youReceivedFollowRequest'), { + return [i18n.ts._notification.youReceivedFollowRequest, { body: getUserName(data.body.user), - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('user-plus'), data, actions: [ { action: 'accept', - title: t('accept'), + title: i18n.ts.accept, }, { action: 'reject', - title: t('reject'), + title: i18n.ts.reject, }, ], }]; case 'followRequestAccepted': - return [t('_notification.yourFollowRequestAccepted'), { + return [i18n.ts._notification.yourFollowRequestAccepted, { body: getUserName(data.body.user), - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('circle-check'), data, }]; case 'achievementEarned': - return [t('_notification.achievementEarned'), { - body: t(`_achievements._types._${data.body.achievement}.title`), + return [i18n.ts._notification.achievementEarned, { + body: i18n.ts._achievements._types[`_${data.body.achievement}`].title, badge: iconUrl('medal'), data, tag: `achievement:${data.body.achievement}`, }]; + case 'exportCompleted': { + const entityName = { + antenna: i18n.ts.antennas, + blocking: i18n.ts.blockedUsers, + clip: i18n.ts.clips, + customEmoji: i18n.ts.customEmojis, + favorite: i18n.ts.favorites, + following: i18n.ts.following, + muting: i18n.ts.mutedUsers, + note: i18n.ts.notes, + userList: i18n.ts.lists, + } as const satisfies Record<typeof data.body.exportedEntity, string>; + + return [i18n.tsx._notification.exportOfXCompleted({ x: entityName[data.body.exportedEntity] }), { + badge: iconUrl('circle-check'), + data, + }]; + } + case 'pollEnded': - return [t('_notification.pollEnded'), { + return [i18n.ts._notification.pollEnded, { body: data.body.note.text ?? '', badge: iconUrl('chart-arrows'), data, @@ -226,16 +244,16 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif }]; case 'test': - return [t('_notification.testNotification'), { - body: t('_notification.notificationWillBeDisplayedLikeThis'), + return [i18n.ts._notification.testNotification, { + body: i18n.ts._notification.notificationWillBeDisplayedLikeThis, badge: iconUrl('bell'), data, }]; case 'edited': - return [t('_notification.edited', { name: getUserName(data.body.user) }), { + return [i18n.ts._notification.edited, { body: data.body.note.text ?? '', - icon: data.body.user.avatarUrl, + icon: data.body.user.avatarUrl ?? undefined, badge: iconUrl('messages'), data, }]; @@ -244,9 +262,9 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif return null; } case 'unreadAntennaNote': - return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), { + return [i18n.tsx._notification.unreadAntennaNote({ name: data.body.antenna.name }), { body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`, - icon: data.body.note.user.avatarUrl, + icon: data.body.note.user.avatarUrl ?? undefined, badge: iconUrl('antenna'), tag: `antenna:${data.body.antenna.id}`, data, @@ -260,7 +278,6 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif export async function createEmptyNotification(): Promise<void> { return new Promise<void>(async res => { const i18n = await (swLang.i18n ?? swLang.fetchLocale()); - const { t } = i18n; await globalThis.registration.showNotification( (new URL(origin)).host, @@ -272,11 +289,11 @@ export async function createEmptyNotification(): Promise<void> { actions: [ { action: 'markAllAsRead', - title: t('markAllAsRead'), + title: i18n.ts.markAllAsRead, }, { action: 'settings', - title: t('notificationSettings'), + title: i18n.ts.notificationSettings, }, ], data: {}, diff --git a/packages/sw/src/scripts/get-account-from-id.ts b/packages/sw/src/scripts/get-account-from-id.ts index 19bfe052eea2b0456a1e1f7d48226e2878a34e5b..157dbd005e194982bf5fa9e1175a6c18b77842ed 100644 --- a/packages/sw/src/scripts/get-account-from-id.ts +++ b/packages/sw/src/scripts/get-account-from-id.ts @@ -4,9 +4,10 @@ */ import { get } from 'idb-keyval'; +import * as Misskey from 'misskey-js'; -export async function getAccountFromId(id: string): Promise<{ token: string; id: string } | void> { - const accounts = await get<{ token: string; id: string }[]>('accounts'); +export async function getAccountFromId(id: string): Promise<Pick<Misskey.entities.SignupResponse, 'id' | 'token'> | undefined> { + const accounts = await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts'); if (!accounts) { console.log('Accounts are not recorded'); return; diff --git a/packages/sw/src/scripts/i18n.ts b/packages/sw/src/scripts/i18n.ts deleted file mode 100644 index 77b955dbe837ef3c20ff332def463713702a115d..0000000000000000000000000000000000000000 --- a/packages/sw/src/scripts/i18n.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type Locale = { [key: string]: string | Locale }; - -export class I18n<T extends Locale = Locale> { - public ts: T; - - constructor(locale: T) { - this.ts = locale; - - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - // string ã«ã—ã¦ã„ã‚‹ã®ã¯ã€ãƒ‰ãƒƒãƒˆåŒºåˆ‡ã‚Šã§ã®ãƒ‘ス指定を許å¯ã™ã‚‹ãŸã‚ - // ãªã‚‹ã¹ãã“ã®ãƒ¡ã‚½ãƒƒãƒ‰ä½¿ã†ã‚ˆã‚Šã‚‚locale直接å‚ç…§ã®æ–¹ãŒvueã®ã‚ャッシュ効ã„ã¦ãƒ‘フォーマンスãŒè‰¯ã„ã‹ã‚‚ - public t(key: string, args?: Record<string, string>): string { - try { - let str = key.split('.').reduce<Locale | Locale[keyof Locale]>((o, i) => o[i], this.ts); - if (typeof str !== 'string') throw new Error(); - - if (args) { - for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v); - } - } - return str; - } catch (err) { - console.warn(`missing localization '${key}'`); - return key; - } - } -} diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts index 6fccedd7466ce37e1a3e4e09558c39d60c0a6a0c..41b06515f4fed48acec1cc225417d64401b8c0ac 100644 --- a/packages/sw/src/scripts/lang.ts +++ b/packages/sw/src/scripts/lang.ts @@ -7,10 +7,11 @@ * Language manager for SW */ import { get, set } from 'idb-keyval'; -import { I18n, type Locale } from '@/scripts/i18n.js'; +import { I18n } from '@@/js/i18n.js'; +import type { Locale } from '../../../../locales/index.js'; class SwLang { - public cacheName = `mk-cache-${_VERSION_}`; + public cacheName = `mk-cache-${_LANGS_VERSION_}`; public lang: Promise<string> = get('lang').then(async prelang => { if (!prelang) return 'en-US'; @@ -23,7 +24,7 @@ class SwLang { return this.fetchLocale(); } - public i18n: Promise<I18n> | null = null; + public i18n: Promise<I18n<Locale>> | null = null; public fetchLocale(): Promise<I18n<Locale>> { return (this.i18n = this._fetch()); @@ -31,7 +32,7 @@ class SwLang { private async _fetch(): Promise<I18n<Locale>> { // Service Workerã¯ä½•åº¦ã‚‚èµ·å‹•ã—ãã®ãŸã³ã«localeã‚’èªã¿è¾¼ã‚€ã®ã§ã€CacheStorageを使ㆠ- const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`; + const localeUrl = `/assets/locales/${await this.lang}.${_LANGS_VERSION_}.json`; let localeRes = await caches.match(localeUrl); // _DEV_ãŒtrueã®å ´åˆã¯å¸¸ã«æœ€æ–°åŒ– diff --git a/packages/sw/src/scripts/operations.ts b/packages/sw/src/scripts/operations.ts index 24eea0623146ea5136a554b86a7476e725198f78..8862c6faa5f2330cbb3ab2ca903c9eee438da095 100644 --- a/packages/sw/src/scripts/operations.ts +++ b/packages/sw/src/scripts/operations.ts @@ -14,15 +14,22 @@ import { getUrlWithLoginId } from '@/scripts/login-id.js'; export const cli = new Misskey.api.APIClient({ origin, fetch: (...args): Promise<Response> => fetch(...args) }); -export async function api<E extends keyof Misskey.Endpoints, O extends Misskey.Endpoints[E]['req']>(endpoint: E, userId?: string, options?: O): Promise<void | ReturnType<typeof cli.request<E, O>>> { - let account: { token: string; id: string } | void = undefined; +export async function api< + E extends keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] +>(endpoint: E, userId?: string, params?: P): Promise<Misskey.api.SwitchCaseResponseType<E, P> | undefined> { + let account: Pick<Misskey.entities.SignupResponse, 'id' | 'token'> | undefined; if (userId) { account = await getAccountFromId(userId); if (!account) return; } - return cli.request(endpoint, options, account?.token); + return (cli.request as <E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ) => Promise<Misskey.api.SwitchCaseResponseType<E, P>>)(endpoint, params, account?.token); } // mark-all-as-readé€å‡ºã‚’1秒間隔ã«åˆ¶é™ã™ã‚‹ @@ -33,7 +40,7 @@ export function sendMarkAllAsRead(userId: string): Promise<null | undefined | vo return new Promise(resolve => { setTimeout(() => { readBlockingStatus.set(userId, false); - api('notifications/mark-all-as-read', userId).then(resolve, resolve); + (api('notifications/mark-all-as-read', userId) as Promise<void>).then(resolve, resolve); }, 1000); }); } diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 33614429de6d3eef87f5f93aab7e520a4e5dd34d..a7b9f27e2c1f8d19270c0bb551c73c90d5aed188 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -6,7 +6,8 @@ import { get } from 'idb-keyval'; import * as Misskey from 'misskey-js'; import type { PushNotificationDataMap } from '@/types.js'; -import type { I18n, Locale } from '@/scripts/i18n.js'; +import type { I18n } from '@@/js/i18n.js'; +import type { Locale } from '../../../locales/index.js'; import { createEmptyNotification, createNotification } from '@/scripts/create-notification.js'; import { swLang } from '@/scripts/lang.js'; import * as swos from '@/scripts/operations.js'; @@ -30,8 +31,8 @@ globalThis.addEventListener('activate', ev => { async function offlineContentHTML() { const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial<I18n<Locale>>; const messages = { - title: i18n.ts?._offlineScreen?.title ?? 'Offline - Could not connect to server', - header: i18n.ts?._offlineScreen?.header ?? 'Could not connect to server', + title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server', + header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server', reload: i18n.ts?.reload ?? 'Reload', }; @@ -162,8 +163,8 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv case 'markAllAsRead': await globalThis.registration.getNotifications() .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - await get('accounts').then(accounts => { - return Promise.all(accounts.map(async account => { + await get<Pick<Misskey.entities.SignupResponse, 'id' | 'token'>[]>('accounts').then(accounts => { + return Promise.all((accounts ?? []).map(async account => { await swos.sendMarkAllAsRead(account.id); })); }); diff --git a/packages/sw/tsconfig.json b/packages/sw/tsconfig.json index 50d4aae19d3edf5f97b5b4ddf093068a34b8e927..112a932e58212c2fe19d502ccf08b2441757c52e 100644 --- a/packages/sw/tsconfig.json +++ b/packages/sw/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "allowJs": true, "noEmitOnError": false, + "noImplicitAny": false, "noImplicitReturns": true, "noUnusedParameters": false, "noUnusedLocals": true, @@ -18,9 +19,11 @@ "experimentalDecorators": true, "resolveJsonModule": true, "isolatedModules": true, + "skipLibCheck": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@@/*": ["../frontend-shared/*"] }, "typeRoots": [ "./node_modules/@types", @@ -34,5 +37,8 @@ "compileOnSave": false, "include": [ "./**/*.ts" + ], + "exclude": [ + "node_modules" ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31efc9a89afcab5fe476a8d620f64f82433c7d2a..bda23dfd328074bc7941162c89466c3a955c64a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,10 +14,10 @@ importers: dependencies: cssnano: specifier: 6.1.2 - version: 6.1.2(postcss@8.4.40) + version: 6.1.2(postcss@8.4.47) esbuild: - specifier: 0.23.0 - version: 0.23.0 + specifier: 0.23.1 + version: 0.23.1 execa: specifier: 8.0.1 version: 8.0.1 @@ -34,48 +34,48 @@ importers: specifier: 4.1.0 version: 4.1.0 postcss: - specifier: 8.4.40 - version: 8.4.40 + specifier: 8.4.47 + version: 8.4.47 tar: specifier: 6.2.1 version: 6.2.1 terser: - specifier: 5.31.3 - version: 5.31.3 + specifier: 5.33.0 + version: 5.33.0 typescript: - specifier: 5.5.4 - version: 5.5.4 + specifier: 5.6.2 + version: 5.6.2 devDependencies: '@misskey-dev/eslint-plugin': specifier: 2.0.3 - version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.0) + version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0) '@types/node': specifier: 20.14.12 version: 20.14.12 '@typescript-eslint/eslint-plugin': specifier: 7.17.0 - version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2) '@typescript-eslint/parser': specifier: 7.17.0 - version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(eslint@9.8.0)(typescript@5.6.2) cross-env: specifier: 7.0.3 version: 7.0.3 cypress: - specifier: 13.13.1 - version: 13.13.1 + specifier: 13.14.2 + version: 13.14.2 eslint: specifier: 9.8.0 version: 9.8.0 globals: - specifier: 15.8.0 - version: 15.8.0 + specifier: 15.9.0 + version: 15.9.0 ncp: specifier: 2.0.0 version: 2.0.0 start-server-and-test: - specifier: 2.0.4 - version: 2.0.4 + specifier: 2.0.8 + version: 2.0.8 packages/backend: dependencies: @@ -86,41 +86,41 @@ importers: specifier: 3.620.0 version: 3.620.0(@aws-sdk/client-s3@3.620.0) '@bull-board/api': - specifier: 5.21.1 - version: 5.21.1(@bull-board/ui@5.21.1) + specifier: 6.0.0 + version: 6.0.0(@bull-board/ui@6.0.0) '@bull-board/fastify': - specifier: 5.21.1 - version: 5.21.1 + specifier: 6.0.0 + version: 6.0.0 '@bull-board/ui': - specifier: 5.21.1 - version: 5.21.1 + specifier: 6.0.0 + version: 6.0.0 '@discordapp/twemoji': - specifier: 15.0.3 - version: 15.0.3 + specifier: 15.1.0 + version: 15.1.0 '@fastify/accepts': - specifier: 4.3.0 - version: 4.3.0 + specifier: 5.0.0 + version: 5.0.0 '@fastify/cookie': - specifier: 9.3.1 - version: 9.3.1 + specifier: 10.0.0 + version: 10.0.0 '@fastify/cors': - specifier: 9.0.1 - version: 9.0.1 + specifier: 10.0.0 + version: 10.0.0 '@fastify/express': - specifier: 3.0.0 - version: 3.0.0 + specifier: 4.0.0 + version: 4.0.0 '@fastify/http-proxy': - specifier: 9.5.0 - version: 9.5.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + specifier: 10.0.0 + version: 10.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) '@fastify/multipart': - specifier: 8.3.0 - version: 8.3.0 + specifier: 9.0.0 + version: 9.0.0 '@fastify/static': - specifier: 7.0.4 - version: 7.0.4 + specifier: 8.0.0 + version: 8.0.0 '@fastify/view': - specifier: 9.1.0 - version: 9.1.0 + specifier: 10.0.0 + version: 10.0.0 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -128,17 +128,17 @@ importers: specifier: 5.1.0 version: 5.1.0 '@napi-rs/canvas': - specifier: ^0.1.53 - version: 0.1.53 + specifier: 0.1.56 + version: 0.1.56 '@nestjs/common': - specifier: 10.3.10 - version: 10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1) + specifier: 10.4.3 + version: 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': - specifier: 10.3.10 - version: 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + specifier: 10.4.3 + version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/testing': - specifier: 10.3.10 - version: 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)) + specifier: 10.4.3 + version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 @@ -191,11 +191,11 @@ importers: specifier: 2.0.5 version: 2.0.5 body-parser: - specifier: 1.20.2 - version: 1.20.2 + specifier: 1.20.3 + version: 1.20.3 bullmq: - specifier: 5.10.4 - version: 5.10.4 + specifier: 5.13.2 + version: 5.13.2 cacheable-lookup: specifier: 7.0.0 version: 7.0.0 @@ -227,23 +227,23 @@ importers: specifier: 0.1.21 version: 0.1.21 fast-xml-parser: - specifier: ^4.4.0 - version: 4.4.0 + specifier: 4.4.1 + version: 4.4.1 fastify: - specifier: 4.28.1 - version: 4.28.1 + specifier: 5.0.0 + version: 5.0.0 fastify-multer: specifier: ^2.0.3 version: 2.0.3 fastify-raw-body: - specifier: 4.3.0 - version: 4.3.0 + specifier: 5.0.0 + version: 5.0.0 feed: specifier: 4.2.2 version: 4.2.2 file-type: - specifier: 19.3.0 - version: 19.3.0 + specifier: 19.5.0 + version: 19.5.0 fluent-ffmpeg: specifier: 2.1.3 version: 2.1.3 @@ -272,14 +272,14 @@ importers: specifier: 5.4.1 version: 5.4.1 ip-cidr: - specifier: 4.0.1 - version: 4.0.1 + specifier: 4.0.2 + version: 4.0.2 ipaddr.js: specifier: 2.2.0 version: 2.2.0 is-svg: - specifier: 5.0.1 - version: 5.0.1 + specifier: 5.1.0 + version: 5.1.0 js-yaml: specifier: 4.1.0 version: 4.1.0 @@ -295,12 +295,15 @@ importers: jsrsasign: specifier: 11.1.0 version: 11.1.0 + juice: + specifier: 11.0.0 + version: 11.0.0 megalodon: specifier: workspace:* version: link:../megalodon meilisearch: - specifier: 0.41.0 - version: 0.41.0(encoding@0.1.13) + specifier: 0.42.0 + version: 0.42.0(encoding@0.1.13) microformats-parser: specifier: 2.0.2 version: 2.0.2 @@ -326,8 +329,8 @@ importers: specifier: 3.3.2 version: 3.3.2 nodemailer: - specifier: 6.9.14 - version: 6.9.14 + specifier: 6.9.15 + version: 6.9.15 oauth: specifier: 0.10.0 version: 0.10.0 @@ -341,14 +344,14 @@ importers: specifier: 0.0.14 version: 0.0.14 otpauth: - specifier: 9.3.1 - version: 9.3.1 + specifier: 9.3.2 + version: 9.3.2 parse5: specifier: 7.1.2 version: 7.1.2 pg: - specifier: 8.12.0 - version: 8.12.0 + specifier: 8.13.0 + version: 8.13.0 pkce-challenge: specifier: 4.1.0 version: 4.1.0 @@ -368,8 +371,8 @@ importers: specifier: 2.3.1 version: 2.3.1 qrcode: - specifier: 1.5.3 - version: 1.5.3 + specifier: 1.5.4 + version: 1.5.4 random-seed: specifier: 0.3.0 version: 0.3.0 @@ -377,8 +380,8 @@ importers: specifier: 3.4.1 version: 3.4.1 re2: - specifier: 1.21.3 - version: 1.21.3 + specifier: 1.21.4 + version: 1.21.4 redis-lock: specifier: 0.1.4 version: 0.1.4 @@ -401,8 +404,8 @@ importers: specifier: 2.7.0 version: 2.7.0 sharp: - specifier: 0.33.4 - version: 0.33.4 + specifier: 0.33.5 + version: 0.33.5 slacc: specifier: 0.0.10 version: 0.0.10 @@ -413,8 +416,8 @@ importers: specifier: 2.1.0 version: 2.1.0 systeminformation: - specifier: 5.22.11 - version: 5.22.11 + specifier: 5.23.5 + version: 5.23.5 tinycolor2: specifier: 1.6.0 version: 1.6.0 @@ -429,10 +432,10 @@ importers: version: 4.2.0 typeorm: specifier: 0.3.20 - version: 0.3.20(ioredis@5.4.1)(pg@8.12.0) + version: 0.3.20(ioredis@5.4.1)(pg@8.13.0) typescript: - specifier: 5.5.4 - version: 5.5.4 + specifier: 5.6.2 + version: 5.6.2 ulid: specifier: 2.3.0 version: 2.3.0 @@ -538,8 +541,8 @@ importers: specifier: 29.7.0 version: 29.7.0 '@nestjs/platform-express': - specifier: 10.3.10 - version: 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10) + specifier: 10.4.3 + version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3) '@simplewebauthn/types': specifier: 10.0.0 version: 10.0.0 @@ -565,8 +568,8 @@ importers: specifier: 0.5.8 version: 0.5.8 '@types/fluent-ffmpeg': - specifier: 2.1.24 - version: 2.1.24 + specifier: 2.1.26 + version: 2.1.26 '@types/htmlescape': specifier: 1.1.3 version: 1.1.3 @@ -574,8 +577,8 @@ importers: specifier: 1.0.7 version: 1.0.7 '@types/jest': - specifier: 29.5.12 - version: 29.5.12 + specifier: 29.5.13 + version: 29.5.13 '@types/js-yaml': specifier: 4.0.9 version: 4.0.9 @@ -598,8 +601,8 @@ importers: specifier: 20.14.12 version: 20.14.12 '@types/nodemailer': - specifier: 6.4.15 - version: 6.4.15 + specifier: 6.4.16 + version: 6.4.16 '@types/oauth': specifier: 0.9.5 version: 0.9.5 @@ -610,8 +613,8 @@ importers: specifier: 0.1.2 version: 0.1.2 '@types/pg': - specifier: 8.11.6 - version: 8.11.6 + specifier: 8.11.10 + version: 8.11.10 '@types/proxy-addr': specifier: ^2.0.3 version: 2.0.3 @@ -634,8 +637,8 @@ importers: specifier: 1.0.7 version: 1.0.7 '@types/sanitize-html': - specifier: 2.11.0 - version: 2.11.0 + specifier: 2.13.0 + version: 2.13.0 '@types/semver': specifier: 7.5.8 version: 7.5.8 @@ -661,14 +664,14 @@ importers: specifier: 3.6.3 version: 3.6.3 '@types/ws': - specifier: 8.5.11 - version: 8.5.11 + specifier: 8.5.12 + version: 8.5.12 '@typescript-eslint/eslint-plugin': specifier: 7.17.0 - version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2) '@typescript-eslint/parser': specifier: 7.17.0 - version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(eslint@9.8.0)(typescript@5.6.2) aws-sdk-client-mock: specifier: 4.0.1 version: 4.0.1 @@ -676,11 +679,11 @@ importers: specifier: 7.0.3 version: 7.0.3 eslint-plugin-import: - specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) + specifier: 2.30.0 + version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0) execa: - specifier: 9.3.0 - version: 9.3.0 + specifier: 9.4.0 + version: 9.4.0 fkill: specifier: 9.0.0 version: 9.0.0 @@ -691,8 +694,8 @@ importers: specifier: 29.7.0 version: 29.7.0 nodemon: - specifier: 3.1.4 - version: 3.1.4 + specifier: 3.1.7 + version: 3.1.7 pid-port: specifier: 1.0.0 version: 1.0.0 @@ -703,8 +706,8 @@ importers: packages/frontend: dependencies: '@discordapp/twemoji': - specifier: 15.0.3 - version: 15.0.3 + specifier: 15.1.0 + version: 15.1.0 '@github/webauthn-json': specifier: 2.1.1 version: 2.1.1 @@ -719,13 +722,13 @@ importers: version: 2.1.1 '@rollup/plugin-json': specifier: 6.1.0 - version: 6.1.0(rollup@4.19.1) + version: 6.1.0(rollup@4.22.5) '@rollup/plugin-replace': specifier: 5.0.7 - version: 5.0.7(rollup@4.19.1) + version: 5.0.7(rollup@4.22.5) '@rollup/pluginutils': - specifier: 5.1.0 - version: 5.1.0(rollup@4.19.1) + specifier: 5.1.2 + version: 5.1.2(rollup@4.22.5) '@syuilo/aiscript': specifier: 0.19.0 version: 0.19.0 @@ -736,17 +739,17 @@ importers: specifier: 15.1.1 version: 15.1.1 '@vitejs/plugin-vue': - specifier: 5.1.0 - version: 5.1.0(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4)) + specifier: 5.1.4 + version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2)) '@vue/compiler-sfc': - specifier: 3.4.37 - version: 3.4.37 + specifier: 3.5.10 + version: 3.5.10 aiscript-vscode: specifier: github:aiscript-dev/aiscript-vscode#v0.1.11 version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9 astring: - specifier: 1.8.6 - version: 1.8.6 + specifier: 1.9.0 + version: 1.9.0 broadcast-channel: specifier: 7.0.0 version: 7.0.0 @@ -757,41 +760,41 @@ importers: specifier: 1.9.3 version: 1.9.3 chart.js: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.4.4 + version: 4.4.4 chartjs-adapter-date-fns: specifier: 3.0.0 - version: 3.0.0(chart.js@4.4.3)(date-fns@2.30.0) + version: 3.0.0(chart.js@4.4.4)(date-fns@2.30.0) chartjs-chart-matrix: specifier: 2.0.1 - version: 2.0.1(chart.js@4.4.3) + version: 2.0.1(chart.js@4.4.4) chartjs-plugin-gradient: specifier: 0.6.1 - version: 0.6.1(chart.js@4.4.3) + version: 0.6.1(chart.js@4.4.4) chartjs-plugin-zoom: specifier: 2.0.1 - version: 2.0.1(chart.js@4.4.3) + version: 2.0.1(chart.js@4.4.4) chromatic: - specifier: 11.5.6 - version: 11.5.6 + specifier: 11.10.4 + version: 11.10.4 compare-versions: specifier: 6.1.1 version: 6.1.1 cropperjs: - specifier: 2.0.0-rc.1 - version: 2.0.0-rc.1 + specifier: 2.0.0-rc.2 + version: 2.0.0-rc.2 date-fns: specifier: 2.30.0 version: 2.30.0 - escape-regexp: - specifier: 0.0.1 - version: 0.0.1 estree-walker: specifier: 3.0.3 version: 3.0.3 eventemitter3: specifier: 5.0.1 version: 5.0.1 + frontend-shared: + specifier: workspace:* + version: link:../frontend-shared idb-keyval: specifier: 6.2.1 version: 6.2.1 @@ -805,8 +808,8 @@ importers: specifier: 2.2.3 version: 2.2.3 katex: - specifier: 0.16.9 - version: 0.16.9 + specifier: 0.16.10 + version: 0.16.10 matter-js: specifier: 0.19.0 version: 0.19.0 @@ -826,14 +829,14 @@ importers: specifier: 2.3.1 version: 2.3.1 rollup: - specifier: 4.19.1 - version: 4.19.1 + specifier: 4.22.5 + version: 4.22.5 sanitize-html: specifier: 2.13.0 version: 2.13.0 sass: - specifier: 1.77.8 - version: 1.77.8 + specifier: 1.79.3 + version: 1.79.3 shiki: specifier: 1.12.0 version: 1.12.0 @@ -844,8 +847,8 @@ importers: specifier: 3.1.0 version: 3.1.0 three: - specifier: 0.167.0 - version: 0.167.0 + specifier: 0.169.0 + version: 0.169.0 throttle-debounce: specifier: 5.0.2 version: 5.0.2 @@ -859,90 +862,90 @@ importers: specifier: 4.2.0 version: 4.2.0 typescript: - specifier: 5.5.4 - version: 5.5.4 + specifier: 5.6.2 + version: 5.6.2 uuid: specifier: 10.0.0 version: 10.0.0 v-code-diff: - specifier: 1.12.0 - version: 1.12.0(vue@3.4.37(typescript@5.5.4)) + specifier: 1.13.1 + version: 1.13.1(vue@3.5.10(typescript@5.6.2)) vite: - specifier: 5.3.5 - version: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) + specifier: 5.4.8 + version: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) vue: - specifier: 3.4.37 - version: 3.4.37(typescript@5.5.4) + specifier: 3.5.10 + version: 3.5.10(typescript@5.6.2) vuedraggable: specifier: next - version: 4.1.0(vue@3.4.37(typescript@5.5.4)) + version: 4.1.0(vue@3.5.10(typescript@5.6.2)) devDependencies: '@misskey-dev/summaly': specifier: 5.1.0 version: 5.1.0 '@storybook/addon-actions': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-essentials': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-interactions': - specifier: 8.2.6 - version: 8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-links': - specifier: 8.2.6 - version: 8.2.6(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-mdx-gfm': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/addon-storysource': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/blocks': - specifier: 8.2.6 - version: 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/components': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/core-events': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/manager-api': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/preview-api': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/react': - specifier: 8.2.6 - version: 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) + specifier: 8.3.3 + version: 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2) '@storybook/react-vite': - specifier: 8.2.6 - version: 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.19.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)) + specifier: 8.3.3 + version: 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)) '@storybook/test': - specifier: 8.2.6 - version: 8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/theming': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/types': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/vue3': - specifier: 8.2.6 - version: 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.4.37(typescript@5.5.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2)) '@storybook/vue3-vite': - specifier: 8.1.11 - version: 8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4)) + specifier: 8.3.3 + version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2)) '@testing-library/vue': specifier: 8.1.0 - version: 8.1.0(@vue/compiler-sfc@3.4.37)(@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4)) - '@types/escape-regexp': - specifier: 0.0.3 - version: 0.0.3 + version: 8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2)) '@types/estree': - specifier: 1.0.5 - version: 1.0.5 + specifier: 1.0.6 + version: 1.0.6 + '@types/katex': + specifier: ^0.16.7 + version: 0.16.7 '@types/matter-js': specifier: 0.19.7 version: 0.19.7 @@ -956,8 +959,8 @@ importers: specifier: 2.1.4 version: 2.1.4 '@types/sanitize-html': - specifier: 2.11.0 - version: 2.11.0 + specifier: 2.13.0 + version: 2.13.0 '@types/seedrandom': specifier: 3.0.8 version: 3.0.8 @@ -971,20 +974,20 @@ importers: specifier: 10.0.0 version: 10.0.0 '@types/ws': - specifier: 8.5.11 - version: 8.5.11 + specifier: 8.5.12 + version: 8.5.12 '@typescript-eslint/eslint-plugin': specifier: 7.17.0 - version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2) '@typescript-eslint/parser': specifier: 7.17.0 - version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(eslint@9.8.0)(typescript@5.6.2) '@vitest/coverage-v8': specifier: 1.6.0 - version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3)) + version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)) '@vue/runtime-core': - specifier: 3.4.37 - version: 3.4.37 + specifier: 3.5.10 + version: 3.5.10 acorn: specifier: 8.12.1 version: 8.12.1 @@ -992,14 +995,14 @@ importers: specifier: 7.0.3 version: 7.0.3 cypress: - specifier: 13.13.1 - version: 13.13.1 + specifier: 13.15.0 + version: 13.15.0 eslint-plugin-import: - specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) + specifier: 2.30.0 + version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0) eslint-plugin-vue: - specifier: 9.27.0 - version: 9.27.0(eslint@9.8.0) + specifier: 9.28.0 + version: 9.28.0(eslint@9.8.0) fast-glob: specifier: 3.3.2 version: 3.3.2 @@ -1010,17 +1013,17 @@ importers: specifier: 0.12.2 version: 0.12.2 micromatch: - specifier: 4.0.7 - version: 4.0.7 + specifier: 4.0.8 + version: 4.0.8 msw: - specifier: 2.3.4 - version: 2.3.4(typescript@5.5.4) + specifier: 2.4.9 + version: 2.4.9(typescript@5.6.2) msw-storybook-addon: specifier: 2.0.3 - version: 2.0.3(msw@2.3.4(typescript@5.5.4)) + version: 2.0.3(msw@2.4.9(typescript@5.6.2)) nodemon: - specifier: 3.1.4 - version: 3.1.4 + specifier: 3.1.7 + version: 3.1.7 prettier: specifier: 3.3.3 version: 3.3.3 @@ -1034,32 +1037,232 @@ importers: specifier: 3.0.5 version: 3.0.5 start-server-and-test: - specifier: 2.0.4 - version: 2.0.4 + specifier: 2.0.8 + version: 2.0.8 storybook: - specifier: 8.2.6 - version: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + specifier: 8.3.3 + version: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) storybook-addon-misskey-theme: specifier: github:misskey-dev/storybook-addon-misskey-theme - version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(3rvqj7p7l43ansgshs3zbslm7u) + version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vite-plugin-turbosnap: specifier: 1.0.3 version: 1.0.3 vitest: specifier: 1.6.0 - version: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3) + version: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0) vitest-fetch-mock: specifier: 0.2.2 - version: 0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3)) + version: 0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)) + vue-component-type-helpers: + specifier: 2.1.6 + version: 2.1.6 + vue-eslint-parser: + specifier: 9.4.3 + version: 9.4.3(eslint@9.8.0) + vue-tsc: + specifier: 2.1.6 + version: 2.1.6(typescript@5.6.2) + + packages/frontend-embed: + dependencies: + '@discordapp/twemoji': + specifier: 15.1.0 + version: 15.1.0 + '@phosphor-icons/web': + specifier: ^2.0.3 + version: 2.1.1 + '@rollup/plugin-json': + specifier: 6.1.0 + version: 6.1.0(rollup@4.22.5) + '@rollup/plugin-replace': + specifier: 5.0.7 + version: 5.0.7(rollup@4.22.5) + '@rollup/pluginutils': + specifier: 5.1.2 + version: 5.1.2(rollup@4.22.5) + '@transfem-org/sfm-js': + specifier: 0.24.5 + version: 0.24.5 + '@twemoji/parser': + specifier: 15.1.1 + version: 15.1.1 + '@vitejs/plugin-vue': + specifier: 5.1.4 + version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2)) + '@vue/compiler-sfc': + specifier: 3.5.10 + version: 3.5.10 + astring: + specifier: 1.9.0 + version: 1.9.0 + buraha: + specifier: 0.0.1 + version: 0.0.1 + estree-walker: + specifier: 3.0.3 + version: 3.0.3 + frontend-shared: + specifier: workspace:* + version: link:../frontend-shared + json5: + specifier: 2.2.3 + version: 2.2.3 + misskey-js: + specifier: workspace:* + version: link:../misskey-js + punycode: + specifier: 2.3.1 + version: 2.3.1 + rollup: + specifier: 4.22.5 + version: 4.22.5 + sass: + specifier: 1.79.3 + version: 1.79.3 + shiki: + specifier: 1.12.0 + version: 1.12.0 + tinycolor2: + specifier: 1.6.0 + version: 1.6.0 + tsc-alias: + specifier: 1.8.10 + version: 1.8.10 + tsconfig-paths: + specifier: 4.2.0 + version: 4.2.0 + typescript: + specifier: 5.6.2 + version: 5.6.2 + uuid: + specifier: 10.0.0 + version: 10.0.0 + vite: + specifier: 5.4.8 + version: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) + vue: + specifier: 3.5.10 + version: 3.5.10(typescript@5.6.2) + devDependencies: + '@misskey-dev/summaly': + specifier: 5.1.0 + version: 5.1.0 + '@testing-library/vue': + specifier: 8.1.0 + version: 8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2)) + '@types/estree': + specifier: 1.0.6 + version: 1.0.6 + '@types/micromatch': + specifier: 4.0.9 + version: 4.0.9 + '@types/node': + specifier: 20.14.12 + version: 20.14.12 + '@types/punycode': + specifier: 2.1.4 + version: 2.1.4 + '@types/tinycolor2': + specifier: 1.4.6 + version: 1.4.6 + '@types/uuid': + specifier: 10.0.0 + version: 10.0.0 + '@types/ws': + specifier: 8.5.12 + version: 8.5.12 + '@typescript-eslint/eslint-plugin': + specifier: 7.17.0 + version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2) + '@typescript-eslint/parser': + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.6.2) + '@vitest/coverage-v8': + specifier: 1.6.0 + version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0)) + '@vue/runtime-core': + specifier: 3.5.10 + version: 3.5.10 + acorn: + specifier: 8.12.1 + version: 8.12.1 + cross-env: + specifier: 7.0.3 + version: 7.0.3 + eslint-plugin-import: + specifier: 2.30.0 + version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0) + eslint-plugin-vue: + specifier: 9.28.0 + version: 9.28.0(eslint@9.8.0) + fast-glob: + specifier: 3.3.2 + version: 3.3.2 + happy-dom: + specifier: 10.0.3 + version: 10.0.3 + intersection-observer: + specifier: 0.12.2 + version: 0.12.2 + micromatch: + specifier: 4.0.8 + version: 4.0.8 + msw: + specifier: 2.3.4 + version: 2.3.4(typescript@5.6.2) + nodemon: + specifier: 3.1.7 + version: 3.1.7 + prettier: + specifier: 3.3.3 + version: 3.3.3 + start-server-and-test: + specifier: 2.0.8 + version: 2.0.8 + vite-plugin-turbosnap: + specifier: 1.0.3 + version: 1.0.3 vue-component-type-helpers: - specifier: 2.0.29 - version: 2.0.29 + specifier: 2.1.6 + version: 2.1.6 vue-eslint-parser: specifier: 9.4.3 version: 9.4.3(eslint@9.8.0) vue-tsc: - specifier: 2.0.29 - version: 2.0.29(typescript@5.5.4) + specifier: 2.1.6 + version: 2.1.6(typescript@5.6.2) + + packages/frontend-shared: + dependencies: + misskey-js: + specifier: workspace:* + version: link:../misskey-js + vue: + specifier: 3.4.37 + version: 3.4.37(typescript@5.5.4) + devDependencies: + '@types/node': + specifier: 20.14.12 + version: 20.14.12 + '@typescript-eslint/eslint-plugin': + specifier: 7.17.0 + version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) + esbuild: + specifier: 0.23.0 + version: 0.23.0 + eslint-plugin-vue: + specifier: 9.27.0 + version: 9.27.0(eslint@9.8.0) + typescript: + specifier: 5.5.4 + version: 5.5.4 + vue-eslint-parser: + specifier: 9.4.3 + version: 9.4.3(eslint@9.8.0) packages/megalodon: dependencies: @@ -1088,8 +1291,8 @@ importers: specifier: ^8.5.10 version: 8.5.11 axios: - specifier: 1.6.0 - version: 1.6.0 + specifier: 1.7.4 + version: 1.7.4 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -1118,8 +1321,8 @@ importers: specifier: ^9.0.1 version: 9.0.1 ws: - specifier: 8.14.2 - version: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) + specifier: 8.17.1 + version: 8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) devDependencies: '@typescript-eslint/eslint-plugin': specifier: ^6.12.0 @@ -1147,7 +1350,7 @@ importers: version: 3.3.3 ts-jest: specifier: ^29.1.1 - version: 29.1.2(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.23.0)(jest@29.7.0(@types/node@20.14.12))(typescript@5.1.6) + version: 29.1.2(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.23.1)(jest@29.7.0(@types/node@20.14.12))(typescript@5.1.6) typedoc: specifier: ^0.25.3 version: 0.25.13(typescript@5.1.6) @@ -1205,29 +1408,29 @@ importers: version: 4.4.0 devDependencies: '@microsoft/api-extractor': - specifier: 7.47.4 - version: 7.47.4(@types/node@20.14.12) + specifier: 7.47.9 + version: 7.47.9(@types/node@20.14.12) '@swc/jest': specifier: 0.2.36 version: 0.2.36(@swc/core@1.6.13) '@types/jest': - specifier: 29.5.12 - version: 29.5.12 + specifier: 29.5.13 + version: 29.5.13 '@types/node': specifier: 20.14.12 version: 20.14.12 '@typescript-eslint/eslint-plugin': specifier: 7.17.0 - version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2) '@typescript-eslint/parser': specifier: 7.17.0 - version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(eslint@9.8.0)(typescript@5.6.2) esbuild: - specifier: 0.23.0 - version: 0.23.0 + specifier: 0.23.1 + version: 0.23.1 execa: - specifier: 9.3.0 - version: 9.3.0 + specifier: 9.4.0 + version: 9.4.0 glob: specifier: 11.0.0 version: 11.0.0 @@ -1247,29 +1450,29 @@ importers: specifier: 2.0.0 version: 2.0.0 nodemon: - specifier: 3.1.4 - version: 3.1.4 + specifier: 3.1.7 + version: 3.1.7 tsd: - specifier: 0.31.1 - version: 0.31.1 + specifier: 0.31.2 + version: 0.31.2 typescript: - specifier: 5.5.4 - version: 5.5.4 + specifier: 5.6.2 + version: 5.6.2 packages/misskey-js/generator: devDependencies: '@readme/openapi-parser': - specifier: 2.5.0 - version: 2.5.0(openapi-types@12.1.3) + specifier: 2.6.0 + version: 2.6.0(openapi-types@12.1.3) '@types/node': specifier: 20.9.1 version: 20.9.1 '@typescript-eslint/eslint-plugin': - specifier: 6.11.0 - version: 6.11.0(@typescript-eslint/parser@6.11.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3) + specifier: 7.17.0 + version: 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2) '@typescript-eslint/parser': - specifier: 6.11.0 - version: 6.11.0(eslint@9.8.0)(typescript@5.3.3) + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.6.2) openapi-types: specifier: 12.1.3 version: 12.1.3 @@ -1277,14 +1480,14 @@ importers: specifier: 6.7.3 version: 6.7.3 ts-case-convert: - specifier: 2.0.2 - version: 2.0.2 + specifier: 2.0.7 + version: 2.0.7 tsx: specifier: 4.4.0 version: 4.4.0 typescript: - specifier: 5.3.3 - version: 5.3.3 + specifier: 5.6.2 + version: 5.6.2 packages/misskey-reversi: dependencies: @@ -1320,8 +1523,8 @@ importers: packages/sw: dependencies: esbuild: - specifier: 0.23.0 - version: 0.23.0 + specifier: 0.23.1 + version: 0.23.1 idb-keyval: specifier: 6.2.1 version: 6.2.1 @@ -1331,24 +1534,24 @@ importers: devDependencies: '@typescript-eslint/parser': specifier: 7.17.0 - version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) + version: 7.17.0(eslint@9.8.0)(typescript@5.6.2) '@typescript/lib-webworker': specifier: npm:@types/serviceworker@0.0.67 version: '@types/serviceworker@0.0.67' eslint-plugin-import: - specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) + specifier: 2.30.0 + version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0) nodemon: - specifier: 3.1.4 - version: 3.1.4 + specifier: 3.1.7 + version: 3.1.7 typescript: - specifier: 5.5.4 - version: 5.5.4 + specifier: 5.6.2 + version: 5.6.2 packages: - '@adobe/css-tools@4.3.3': - resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} + '@adobe/css-tools@4.4.0': + resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz': resolution: {tarball: https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz} @@ -1359,17 +1562,9 @@ packages: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} - '@apidevtools/openapi-schemas@2.1.0': - resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} - engines: {node: '>=10'} - '@apidevtools/swagger-methods@3.0.2': resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} - '@aw-web-design/x-default-browser@1.4.126': - resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==} - hasBin: true - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -1571,14 +1766,6 @@ packages: resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.24.7': - resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': - resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.22.15': resolution: {integrity: sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==} engines: {node: '>=6.9.0'} @@ -1587,51 +1774,18 @@ packages: resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.24.7': - resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.24.7': - resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-define-polyfill-provider@0.6.2': - resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - '@babel/helper-environment-visitor@7.22.20': - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-environment-visitor@7.24.7': - resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.23.0': - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-function-name@7.24.7': - resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-hoist-variables@7.22.5': - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + '@babel/helper-function-name@7.24.7': + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} engines: {node: '>=6.9.0'} '@babel/helper-hoist-variables@7.24.7': resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.24.7': - resolution: {integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.22.15': resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} engines: {node: '>=6.9.0'} @@ -1652,30 +1806,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.24.7': - resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} - engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.22.5': resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.24.7': - resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-remap-async-to-generator@7.24.7': - resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-replace-supers@7.24.7': - resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-simple-access@7.22.5': resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} @@ -1684,14 +1818,6 @@ packages: resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} engines: {node: '>=6.9.0'} - '@babel/helper-skip-transparent-expression-wrappers@7.24.7': - resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-split-export-declaration@7.22.6': - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} - '@babel/helper-split-export-declaration@7.24.7': resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} engines: {node: '>=6.9.0'} @@ -1700,10 +1826,18 @@ packages: resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.7': + resolution: {integrity: sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.24.7': resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.7': + resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.23.5': resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} @@ -1712,10 +1846,6 @@ packages: resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.24.7': - resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} - engines: {node: '>=6.9.0'} - '@babel/helpers@7.23.5': resolution: {integrity: sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==} engines: {node: '>=6.9.0'} @@ -1724,10 +1854,6 @@ packages: resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.23.4': - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} - engines: {node: '>=6.9.0'} - '@babel/highlight@7.24.7': resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} engines: {node: '>=6.9.0'} @@ -1737,35 +1863,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7': - resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7': - resolution: {integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7': - resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7': - resolution: {integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': - resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/parser@7.25.7': + resolution: {integrity: sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==} + engines: {node: '>=6.0.0'} + hasBin: true '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} @@ -1782,40 +1883,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-export-namespace-from@7.8.3': - resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-flow@7.23.3': - resolution: {integrity: sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-assertions@7.24.7': - resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.24.7': - resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-meta@7.10.4': resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -1862,12 +1929,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-top-level-await@7.14.5': resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -1880,487 +1941,150 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6': - resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + '@babel/runtime@7.23.4': + resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/plugin-transform-arrow-functions@7.24.7': - resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} + '@babel/template@7.22.15': + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.24.7': - resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} + '@babel/template@7.24.0': + resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.24.7': - resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} + '@babel/template@7.24.7': + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.24.7': - resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} + '@babel/traverse@7.23.5': + resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.24.7': - resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} + '@babel/traverse@7.24.7': + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.24.7': - resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} + '@babel/types@7.24.7': + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.24.7': - resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} + '@babel/types@7.25.7': + resolution: {integrity: sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.24.7': - resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@base2/pretty-print-object@1.0.1': + resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} - '@babel/plugin-transform-computed-properties@7.24.7': - resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@babel/plugin-transform-destructuring@7.24.7': - resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} - engines: {node: '>=6.9.0'} + '@bull-board/api@6.0.0': + resolution: {integrity: sha512-O0IsIwAOU47bPTJnqRO7RtKFQToMvwRebbuPi6M+SG1gXyiqixLg9pycnfXgSeroaT9E7QQ2PsCPW1HO8VORvw==} peerDependencies: - '@babel/core': ^7.0.0-0 + '@bull-board/ui': 6.0.0 - '@babel/plugin-transform-dotall-regex@7.24.7': - resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@bull-board/fastify@6.0.0': + resolution: {integrity: sha512-VrKa5BdxYmXh5fJvlSPSm71b+QA9VVXHyGk6xmI/qAefUQbwd2cWJo+ppqaWSaweXa9ymJc+V4l/un0K4oomVA==} - '@babel/plugin-transform-duplicate-keys@7.24.7': - resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@bull-board/ui@6.0.0': + resolution: {integrity: sha512-wAFTlBTJbq5DSWxCzTV+FOyZDVwrXP+G1CQ2BpLG9o9+dpwYxUESx/VxNEDHnyPcy13gm29kB4fSRY+nkelkcQ==} - '@babel/plugin-transform-dynamic-import@7.24.7': - resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@bundled-es-modules/cookie@2.0.0': + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} - '@babel/plugin-transform-exponentiation-operator@7.24.7': - resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - '@babel/plugin-transform-export-namespace-from@7.24.7': - resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} - '@babel/plugin-transform-flow-strip-types@7.23.3': - resolution: {integrity: sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@canvas/image-data@1.0.0': + resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==} - '@babel/plugin-transform-for-of@7.24.7': - resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} - '@babel/plugin-transform-function-name@7.24.7': - resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-canvas@2.0.0-rc.2': + resolution: {integrity: sha512-0aqbJ3ycQM6/yn4T03vw8K/OeTB8C6+Z/jimuavy4UM2CENH9ucSLM4hAG0yYCgghIyv9Zd0unaBmtgW+I5+SQ==} - '@babel/plugin-transform-json-strings@7.24.7': - resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-crosshair@2.0.0-rc.2': + resolution: {integrity: sha512-yopINLvaZhL3E2GNienju1zeQ1Cifkn5f/0R7ZabXcAgUI0s2sLzNqL8+2XV2J3DzEzYEIYc+49KmMle04nVWQ==} - '@babel/plugin-transform-literals@7.24.7': - resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-grid@2.0.0-rc.2': + resolution: {integrity: sha512-PzAfEya6CmIc/o/lcA/NZ1rohszz42wjq2z3E2zq2jMfNDxY/EIoFnGI6+hJrxCAaoKD8UlKOEHQdRQbtnjcMg==} - '@babel/plugin-transform-logical-assignment-operators@7.24.7': - resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-handle@2.0.0-rc.2': + resolution: {integrity: sha512-wOWX4xpryxKcrhnJC2mHebqQQ622UN2oyQoDZcaMzvlwt7nnX3bInF+SFrIj9/aCxtCUYY0oD2gaJkfd6aNJ0g==} - '@babel/plugin-transform-member-expression-literals@7.24.7': - resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-image@2.0.0-rc.2': + resolution: {integrity: sha512-RTKnuJrqn1K8FscS11auit2W57AG04mxRNOxBldYs3lKTkwZjzJdQFkZ/Nxu+cwVXT+c6IeEiayNKvu4B7CAQg==} - '@babel/plugin-transform-modules-amd@7.24.7': - resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-selection@2.0.0-rc.2': + resolution: {integrity: sha512-UIgIHKHz4qNKlm5YRnC/Pu9+VrInm5TSOzkmU8kPt2swUk0WHNRv3ZcOjCQZ2ccTQnAH3FVM3FYDZ8HjRwLcBg==} - '@babel/plugin-transform-modules-commonjs@7.24.7': - resolution: {integrity: sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-shade@2.0.0-rc.2': + resolution: {integrity: sha512-vHAGFxlqgflGZWkRYNWNHUY0zsV72YZGmCgtUu4sMrnWLZL/jMGhxmm8zZCe/aB94F829XcQ6uf3BoiApB+7Ng==} - '@babel/plugin-transform-modules-systemjs@7.24.7': - resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element-viewer@2.0.0-rc.2': + resolution: {integrity: sha512-2z9mIA7ic3enNS4xvq9Gq6hnRZ1tPr0h+lCrOHP55NL4he63lE9oTVJfDx19rL95wUS4VxL2ANvr2BVLNiBM7A==} - '@babel/plugin-transform-modules-umd@7.24.7': - resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/element@2.0.0-rc.2': + resolution: {integrity: sha512-4G6lTJblndwzpsb43YKeHiKcocOkDIWystGzbHNbqRysE0U0lYHuRyvV7FW6a9S63wtMFSYuwFxcdUdUcmkF8w==} - '@babel/plugin-transform-named-capturing-groups-regex@7.24.7': - resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 + '@cropper/elements@2.0.0-rc.2': + resolution: {integrity: sha512-NG5kdqpv7/tGvUfNjJiIHr2Ip431v5t/P5cIXTcYAgt8PRyFJmjx3fatC7NLnP/FUlv+bbzd8PMRI4LY4Gaw3Q==} - '@babel/plugin-transform-new-target@7.24.7': - resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cropper/utils@2.0.0-rc.2': + resolution: {integrity: sha512-EEivNsyV6BtL496m4Q/IeAC6FGlyKjKIT1qMtwaxtkR+2ZlKnf9O7AdcGpClemIBA+TbwWAzp0UyIvYFtKUZ1Q==} - '@babel/plugin-transform-nullish-coalescing-operator@7.24.7': - resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cypress/request@3.0.5': + resolution: {integrity: sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==} + engines: {node: '>= 6'} - '@babel/plugin-transform-numeric-separator@7.24.7': - resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@cypress/xvfb@1.2.4': + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} - '@babel/plugin-transform-object-rest-spread@7.24.7': - resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@digitalbazaar/http-client@3.4.1': + resolution: {integrity: sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==} + engines: {node: '>=14.0'} - '@babel/plugin-transform-object-super@7.24.7': - resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@discordapp/twemoji@15.1.0': + resolution: {integrity: sha512-QdpV4ifTONAXvDjRrMohausZeGrQ1ac/Ox6togUh6Xl3XKJ/KAaMMuAEi0qsb0wDwoVTSZBll5Y6+N3hB2ktBw==} - '@babel/plugin-transform-optional-catch-binding@7.24.7': - resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@emnapi/runtime@1.3.0': + resolution: {integrity: sha512-XMBySMuNZs3DM96xcJmLW4EfGnf+uGmFNjzpehMjuX5PLB5j87ar2Zc4e3PVeZ3I5g3tYtAqskB28manlF69Zw==} - '@babel/plugin-transform-optional-chaining@7.24.7': - resolution: {integrity: sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@esbuild/aix-ppc64@0.19.11': + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] - '@babel/plugin-transform-parameters@7.24.7': - resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] - '@babel/plugin-transform-private-methods@7.24.7': - resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@esbuild/aix-ppc64@0.23.0': + resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - '@babel/plugin-transform-private-property-in-object@7.24.7': - resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - '@babel/plugin-transform-property-literals@7.24.7': - resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regenerator@7.24.7': - resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-reserved-words@7.24.7': - resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.24.7': - resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.24.7': - resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.24.7': - resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.24.7': - resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.24.7': - resolution: {integrity: sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.23.5': - resolution: {integrity: sha512-2fMkXEJkrmwgu2Bsv1Saxgj30IXZdJ+84lQcKKI7sm719oXs0BBw2ZENKdJdR1PjWndgLCEBNXJOri0fk7RYQA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.24.7': - resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-property-regex@7.24.7': - resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.24.7': - resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-sets-regex@7.24.7': - resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/preset-env@7.24.7': - resolution: {integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-flow@7.23.3': - resolution: {integrity: sha512-7yn6hl8RIv+KNk6iIrGZ+D06VhVY35wLVf23Cz/mMu1zOr7u4MMP4j0nZ9tLf8+4ZFpnib8cFYgB/oYg9hfswA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-modules@0.1.6-no-external-plugins': - resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - - '@babel/preset-typescript@7.23.3': - resolution: {integrity: sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/register@7.22.15': - resolution: {integrity: sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/regjsgen@0.8.0': - resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - - '@babel/runtime@7.23.4': - resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.22.15': - resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.24.0': - resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.24.7': - resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.23.5': - resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.24.7': - resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.24.7': - resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} - engines: {node: '>=6.9.0'} - - '@base2/pretty-print-object@1.0.1': - resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - - '@bull-board/api@5.21.1': - resolution: {integrity: sha512-anzTfhOJ93eraT/GYeyxWpxRMarHwuevn6pPBfdOj0LC2eg98OPnkfdMSjcrpL3qrqsxON0RslS7kuPfCEnX6A==} - peerDependencies: - '@bull-board/ui': 5.21.1 - - '@bull-board/fastify@5.21.1': - resolution: {integrity: sha512-We33yolc70SALjDdF3cjEaLn1L/vrw85eFCsrviESaW3dFVIdB+xn0fdqMFK6NnaC0JjBa3Ypfev4Co+eaZ+1A==} - - '@bull-board/ui@5.21.1': - resolution: {integrity: sha512-JBDeCqG7j/c3WE0uGMN9snPkRJz9/D6MpTZzyVj7KOxIJwNKPOICNFZbCrCNi7bcJYHDJ2xGTN9OO1mw7i43BQ==} - - '@bundled-es-modules/cookie@2.0.0': - resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} - - '@bundled-es-modules/statuses@1.0.1': - resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - - '@bundled-es-modules/tough-cookie@0.1.6': - resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} - - '@canvas/image-data@1.0.0': - resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==} - - '@colors/colors@1.5.0': - resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} - engines: {node: '>=0.1.90'} - - '@cropper/element-canvas@2.0.0-rc.1': - resolution: {integrity: sha512-jRt9OM7cls+zch8U2m7pA9wp8dNOz0EtedGKkqH+DInUYw1+UtonEJirrxyl1YHRgOme5M5DTsDmkUSrUiZN6g==} - - '@cropper/element-crosshair@2.0.0-rc.1': - resolution: {integrity: sha512-xfLelqM8EnRZUf7xEE88RWQQx5erUv7jrzni52bAw3/Ua8HXIz3uAMnkrGKOTBj8K4Rv/mNJY8k1DVAEfHY6Lg==} - - '@cropper/element-grid@2.0.0-rc.1': - resolution: {integrity: sha512-U/BYPl76upd9sXT+pZTFoQzUqWyNxdGs4YR2UtaVCfTMHLDTrssPAedmqEEnHgbqVcr325sIEfVmwWVA+v+8Dg==} - - '@cropper/element-handle@2.0.0-rc.1': - resolution: {integrity: sha512-GuOHbjkg5CP1+oFzWQeD7VZffUE86dp4gKv5egLxkBEwnQp1VQxjO7L1Wkgj+KsQymoDczsl+x4bF12KDyDg2g==} - - '@cropper/element-image@2.0.0-rc.1': - resolution: {integrity: sha512-ttzawKbUkR2A9U3bc2AN/jbNdszBP/yb83PIc5jekjOs+Z7kUBVdOo1SLIewpQ0DjUzhfCRXWUowP1McVQUXZw==} - - '@cropper/element-selection@2.0.0-rc.1': - resolution: {integrity: sha512-AcRHRbsyt9xRfBD1QRyNDTS+vaYg6uAeuqhk/Ra58pqxlhtoimAV3oQ7uc/edwOlK60f/DxtKCc8rSOYFQ85bQ==} - - '@cropper/element-shade@2.0.0-rc.1': - resolution: {integrity: sha512-nHv2WujETENoIfxWQn7TYiOnXm5YUnZsoG4r6njK5cxj0gIUfPudUSbjWCQSuB2oxxpeEK8oyTdfOZtP9cxK4g==} - - '@cropper/element-viewer@2.0.0-rc.1': - resolution: {integrity: sha512-xTj0BObCygbVWXc7t7FYZ9k2eFyWN360it5uGeAkImXcwINRQGTFcLLOjs6i3SwedI7F1a1yNcTBfoT1B/sNAg==} - - '@cropper/element@2.0.0-rc.1': - resolution: {integrity: sha512-OPKgjUgYC2Xmv77vEqtAR6bdfKOW+v9FrSjr4re3u95rcVj6NJ0JidIta41Ipp8KydHTXSmLetq4XDrA+vuIJQ==} - - '@cropper/elements@2.0.0-rc.1': - resolution: {integrity: sha512-6qbtCq3iL3dETVav2XA03a8iLkHXWMIqHFxViMjlLr9CSuDjjaS5wp0JDuGtPv5FHxjsjyQ8Yayt8Ak5p09Zxg==} - - '@cropper/utils@2.0.0-rc.1': - resolution: {integrity: sha512-kreB3wdrAhmTEscfB8/j7ksGBgYSKN+28t37CAI0Vb5DvX/aUDPDH+3e2kyD7YE+DIZgdnuY2FsMYJAQ9sTThg==} - - '@cypress/request@3.0.0': - resolution: {integrity: sha512-GKFCqwZwMYmL3IBoNeR2MM1SnxRIGERsQOTWeQKoYBt2JLqcqiy7JXqO894FLrpjZYqGxW92MNwRH2BN56obdQ==} - engines: {node: '>= 6'} - - '@cypress/xvfb@1.2.4': - resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} - - '@digitalbazaar/http-client@3.4.1': - resolution: {integrity: sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==} - engines: {node: '>=14.0'} - - '@discordapp/twemoji@15.0.3': - resolution: {integrity: sha512-5t0LLrNaSqViG0cSaomWwfR0+3fWqok+xLq40M8hJHxNX7s8gIoyNZYybQJo+s5/rGMjgdldpt8Ox8MapGvBUA==} - - '@discoveryjs/json-ext@0.5.7': - resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} - engines: {node: '>=10.0.0'} - - '@emnapi/runtime@1.1.1': - resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==} - - '@emotion/use-insertion-effect-with-fallbacks@1.0.1': - resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} - peerDependencies: - react: '>=16.8.0' - - '@esbuild/aix-ppc64@0.19.11': - resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.23.0': - resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] '@esbuild/android-arm64@0.19.11': resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} @@ -2380,6 +2104,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -2404,6 +2134,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -2428,6 +2164,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -2452,6 +2194,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -2476,6 +2224,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -2500,6 +2254,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -2524,6 +2284,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -2548,6 +2314,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -2572,6 +2344,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -2596,6 +2374,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -2620,6 +2404,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -2644,6 +2434,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -2668,6 +2464,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -2692,6 +2494,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -2716,6 +2524,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -2740,6 +2554,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -2764,12 +2584,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.23.0': resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -2794,6 +2626,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -2818,6 +2656,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -2842,6 +2686,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -2866,7 +2716,13 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} cpu: [x64] @@ -2890,6 +2746,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2932,18 +2794,14 @@ packages: resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@fal-works/esbuild-plugin-global-externals@2.1.2': - resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} - - '@fastify/accept-negotiator@1.0.0': - resolution: {integrity: sha512-4R/N2KfYeld7A5LGkai+iUFMahXcxxYbDp+XS2B1yuL3cdmZLJ9TlCnNzT3q5xFTqsYm0GPpinLUwfSwjcVjyA==} - engines: {node: '>=14'} + '@fastify/accept-negotiator@2.0.0': + resolution: {integrity: sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==} - '@fastify/accepts@4.3.0': - resolution: {integrity: sha512-QK4FoqXdwwPmaPOLL6NrxsyaXVvdviYVoS6ltHyOLdFlUyREIaMykHQIp+x0aJz9hB3B3n/Ht6QRdvBeGkptGQ==} + '@fastify/accepts@5.0.0': + resolution: {integrity: sha512-5wpgycrn+DXPkATGqUbXY9tyqLNgxo9S8f0EHUyIWvUacor2cXa3liYZggsqoyMXgpIqUbGLPBl+dN2hRcU9jQ==} - '@fastify/ajv-compiler@3.5.0': - resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + '@fastify/ajv-compiler@4.0.1': + resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==} '@fastify/busboy@1.2.1': resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} @@ -2953,47 +2811,53 @@ packages: resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} engines: {node: '>=14'} - '@fastify/cookie@9.3.1': - resolution: {integrity: sha512-h1NAEhB266+ZbZ0e9qUE6NnNR07i7DnNXWG9VbbZ8uC6O/hxHpl+Zoe5sw1yfdZ2U6XhToUGDnzQtWJdCaPwfg==} + '@fastify/busboy@3.0.0': + resolution: {integrity: sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==} + + '@fastify/cookie@10.0.0': + resolution: {integrity: sha512-S43spazwAfzm5nKlqq/spAGW+O6r+WQzg5vXXI1ArCXXFa8KBA/tiU3XRVQUehSNtbN5PA6+g183hzh5/dZ6Iw==} - '@fastify/cors@9.0.1': - resolution: {integrity: sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==} + '@fastify/cors@10.0.0': + resolution: {integrity: sha512-kb9fkc/LVbLTQ3lhA+ZZjC/Styzysodo/MTCdVCvTtgHa/gBwxrEEkcp3fuoKIfAQt85wksrpXjUGbw5NQffEQ==} - '@fastify/deepmerge@1.3.0': - resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + '@fastify/deepmerge@2.0.0': + resolution: {integrity: sha512-fsaybTGDyQ5KpPsplQqb9yKdCf2x/pbNpMNk8Tvp3rRz7lVcupKysH4b2ELMN2P4Hak1+UqTYdTj/u4FNV2p0g==} - '@fastify/error@3.4.0': - resolution: {integrity: sha512-e/mafFwbK3MNqxUcFBLgHhgxsF8UT1m8aj0dAlqEa2nJEgPsRtpHTZ3ObgrgkZ2M1eJHPTwgyUl/tXkvabsZdQ==} + '@fastify/error@4.0.0': + resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==} - '@fastify/express@3.0.0': - resolution: {integrity: sha512-Ug6aulXCUiHgMyrHVYQqnQbGdsAV0aTad6nZxbOr6w3QjKn1mdQS3Kyzvc+I0xMjZ9yIyMUWHSooHgZ0l7nOng==} + '@fastify/express@4.0.0': + resolution: {integrity: sha512-e+IMKKV9+HRCVm7LVW8PaMrpEerHfqNLpRkbiVHYfVm0xeOphiwyNEoge4VA3Sh8gubtDfo9yKkpRzx6gx63kg==} - '@fastify/fast-json-stringify-compiler@4.3.0': - resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + '@fastify/fast-json-stringify-compiler@5.0.1': + resolution: {integrity: sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==} - '@fastify/http-proxy@9.5.0': - resolution: {integrity: sha512-1iqIdV10d5k9YtfHq9ylX5zt1NiM50fG+rIX40qt00R694sqWso3ukyTFZVk33SDoSiBW8roB7n11RUVUoN+Ag==} + '@fastify/http-proxy@10.0.0': + resolution: {integrity: sha512-n5/EPspNKtzpCUavuDflYtvtB+aEkablb2sZM83gDKbxM9GF+93maJYQrGozJ2HNRqpt7wfzsDeUuGVFFkYzMQ==} - '@fastify/multipart@8.3.0': - resolution: {integrity: sha512-A8h80TTyqUzaMVH0Cr9Qcm6RxSkVqmhK/MVBYHYeRRSUbUYv08WecjWKSlG2aSnD4aGI841pVxAjC+G1GafUeQ==} + '@fastify/merge-json-schemas@0.1.1': + resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==} - '@fastify/reply-from@9.0.1': - resolution: {integrity: sha512-q9vFNUiXZTY1x8omDPe59os2MYq+3y7KgO/kZoXpZlnud+45Nd8Ot/svEvrUATzjkizIggfS4K8LR9zXDyZZKg==} + '@fastify/multipart@9.0.0': + resolution: {integrity: sha512-B/rzOl1wmkj4LddH2i+zR8Gke8ZX1J8D7n4uJeis5VdIa7OR9Ys/TzUxI0/h1SF9ubHlNhBP+eO/FwnftarP9w==} - '@fastify/send@2.0.1': - resolution: {integrity: sha512-8jdouu0o5d0FMq1+zCKeKXc1tmOQ5tTGYdQP3MpyF9+WWrZT1KCBdh6hvoEYxOm3oJG/akdE9BpehLiJgYRvGw==} + '@fastify/reply-from@11.0.1': + resolution: {integrity: sha512-F2Qk88gcqIIiug9V+4I6WeeV1faj1Wu798JyOnwbJcjQhm4LYrHdkpFSVwJE0g1cVjYCFFmH3OVh1HHaninttQ==} - '@fastify/static@6.12.0': - resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==} + '@fastify/send@3.1.1': + resolution: {integrity: sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==} - '@fastify/static@7.0.4': - resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} + '@fastify/static@8.0.0': + resolution: {integrity: sha512-VKGn1PQslB2VqzspyMKPu9xasF9vj+YuyGhVLb1ih6V60VVcRvcf0fFRcl3opt6c6YWwhKKdTUTfVE6COnpw6A==} - '@fastify/view@8.2.0': - resolution: {integrity: sha512-hBSiBofCnJNlPHEMZWpO1SL84eqOaqujJ1hR3jntFyZZCkweH5jMs12DKYyGesjVll7SJFRRxPUBB8kmUmneRQ==} + '@fastify/static@8.0.1': + resolution: {integrity: sha512-7idyhbcgf14v4bjWzUeHEFvnVxvNJ1n5cyGPgFtwTZjnjUQ1wgC7a2FQai7OGKqCKywDEjzbPhAZRW+uEK1LMg==} - '@fastify/view@9.1.0': - resolution: {integrity: sha512-jRTGDljs/uB2p8bf6c1x4stGjP7H84VQkhbtDgCx55Mxf9Fplud5UZIHubvL4BTTX8jNYEzP1FpNAOBi7vibxg==} + '@fastify/view@10.0.0': + resolution: {integrity: sha512-2KnfgpSbAImKV5kKdNAkSyjV+9kYUYLvgDLx/wlzgqel92bN9Z520cwG3g3bAkr0yVnEJu62dIm2qAL9FASS1w==} + + '@fastify/view@10.0.1': + resolution: {integrity: sha512-rXtBN0oVDmoRZAS7lelrCIahf+qFtlMOOas8VPdA7JvrJ9ChcF7e36pIUPU0Vbs3KmHxESUb7XatavUZEe/k5Q==} '@github/webauthn-json@2.1.1': resolution: {integrity: sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==} @@ -3041,116 +2905,108 @@ packages: resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.33.4': - resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.4': - resolution: {integrity: sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.2': - resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} - engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.0.2': - resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} - engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.0.2': - resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} - engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.0.2': - resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} - engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-s390x@1.0.2': - resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} - engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.0.2': - resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} - engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.0.2': - resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} - engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.0.2': - resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} - engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.4': - resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.4': - resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==} - engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.33.4': - resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==} - engines: {glibc: '>=2.31', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.4': - resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.4': - resolution: {integrity: sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==} - engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.4': - resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==} - engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.4': - resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-ia32@0.33.4': - resolution: {integrity: sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.4': - resolution: {integrity: sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -3170,18 +3026,6 @@ packages: resolution: {integrity: sha512-Pe3PFccjPVJV1vtlfVvm9OnlbxqdnP5QcscFEFEnK5quChf1ufZtM0r8mR5ToWHMxZOh0s8o/qp9ANGRTo/DAw==} engines: {node: '>=18'} - '@intlify/core-base@9.13.1': - resolution: {integrity: sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==} - engines: {node: '>= 16'} - - '@intlify/message-compiler@9.13.1': - resolution: {integrity: sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==} - engines: {node: '>= 16'} - - '@intlify/shared@9.13.1': - resolution: {integrity: sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==} - engines: {node: '>= 16'} - '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} @@ -3267,8 +3111,8 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1': - resolution: {integrity: sha512-pdoMZ9QaPnVlSM+SdU/wgg0nyD/8wQ7y90ttO2CMCyrrm7RxveYIJ5eNfjPaoMFqW41LZra7QO9j+xV4Y18Glw==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0': + resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} peerDependencies: typescript: '>= 4.3.x' vite: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -3276,10 +3120,6 @@ packages: typescript: optional: true - '@jridgewell/gen-mapping@0.3.2': - resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -3288,10 +3128,6 @@ packages: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.1.2': - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} @@ -3305,6 +3141,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.18': resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} @@ -3324,8 +3163,8 @@ packages: resolution: {integrity: sha512-uSvJdwQU5nK+Vdf6zxcWAY2A8r7uqe+gePwLWzJ+fsQehq18pc0I2hJKwypZ2aLM90+Er9u1xn4iLJPZ+xlL4g==} engines: {node: '>=8'} - '@lukeed/ms@2.0.1': - resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==} + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} '@mcaptcha/core-glue@0.1.0-alpha-5': @@ -3340,11 +3179,11 @@ packages: '@types/react': '>=16' react: '>=16' - '@microsoft/api-extractor-model@7.29.4': - resolution: {integrity: sha512-LHOMxmT8/tU1IiiiHOdHFF83Qsi+V8d0kLfscG4EvQE9cafiR8blOYr8SfkQKWB1wgEilQgXJX3MIA4vetDLZw==} + '@microsoft/api-extractor-model@7.29.8': + resolution: {integrity: sha512-t3Z/xcO6TRbMcnKGVMs4uMzv/gd5j0NhMiJIGjD4cJMeFJ1Hf8wnLSx37vxlRlL0GWlGJhnFgxvnaL6JlS+73g==} - '@microsoft/api-extractor@7.47.4': - resolution: {integrity: sha512-HKm+P4VNzWwvq1Ey+Jfhhj/3MjsD+ka2hbt8L5AcRM95lu1MFOYnz3XlU7Gr79Q/ZhOb7W/imAKeYrOI0bFydg==} + '@microsoft/api-extractor@7.47.9': + resolution: {integrity: sha512-TTq30M1rikVsO5wZVToQT/dGyJY7UXJmjiRtkHPLb74Prx3Etw8+bX7Bv7iLuby6ysb7fuu1NFWqma+csym8Jw==} hasBin: true '@microsoft/tsdoc-config@0.17.0': @@ -3410,66 +3249,70 @@ packages: resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} engines: {node: '>=18'} - '@napi-rs/canvas-android-arm64@0.1.53': - resolution: {integrity: sha512-2YhxfVsZguATlRWE0fZdTx35SE9+r5D7HV5GPNDataZOKmHf+zZ5//dspuuBSbOriQdoicaFrgXKCUqI0pK3WQ==} + '@mswjs/interceptors@0.35.9': + resolution: {integrity: sha512-SSnyl/4ni/2ViHKkiZb8eajA/eN1DNFaHjhGiLUdZvDz6PKF4COSf/17xqSz64nOo2Ia29SA6B2KNCsyCbVmaQ==} + engines: {node: '>=18'} + + '@napi-rs/canvas-android-arm64@0.1.56': + resolution: {integrity: sha512-xBGqW2RZMAupkzar9t3gpbok9r524f3Wlk4PG2qnQdxbsiEND06OB8VxVtTcql6R02uJpXJGnyIhN02Te+GMVQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@0.1.53': - resolution: {integrity: sha512-ls+CWLMusf4RAGo5BvIIzA6dNcc0elwVp6LKjHfQECHA8KKmvdB58YuE5BQcTlb2rzk0SEKtBC/Th3NI2oNdfg==} + '@napi-rs/canvas-darwin-arm64@0.1.56': + resolution: {integrity: sha512-Pvuz6Ib9YZTB5MlGL9WSu9a2asUC0DZ1zBHozDiBXr/6Zurs9l/ZH5NxFYTM829BpkdkO8kuI8b8Rz7ek30zzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.53': - resolution: {integrity: sha512-ZAgcoCH5+5OKS2P8Lxx+jbkAPKkyLD2x6OvSrHg1U6ppdxmLA+CkJlRl8w45HCXwuyIiP7OeymECRtiNYTwznQ==} + '@napi-rs/canvas-darwin-x64@0.1.56': + resolution: {integrity: sha512-O393jWt7G6rg0X1ralbsbBeskSG0iwlkD7mEHhMLJxqRqe+eQn0/xnwhs9l6dUNFC+5dM8LOvfFca4o9Vs2Vww==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.53': - resolution: {integrity: sha512-p9km/3C/loDxu3AvA8/vtpIS1BGMd/Ehkl2Iu/v/Gw8N/KUIt3HUvTS7AKApyVE28bxTfq96wJQjtcT8jzDncw==} + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56': + resolution: {integrity: sha512-30NFb5lrF3YEwAO5XuATxpWDSXaBAgaFVswPJ+hYcAUyE3IkPPIFRY4ijQEh4frcSBvrzFGGYdNSoC18oLLWaQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@0.1.53': - resolution: {integrity: sha512-QKK+sykEiYwjwd+ogyLcpcnH38DNZ8KViBlnfEpoGA2Wa+21/cWQKfMxnbgb/rbvm5tazJinZcihFvH577WQ5g==} + '@napi-rs/canvas-linux-arm64-gnu@0.1.56': + resolution: {integrity: sha512-ODbWH9TLvba+39UxFwPn2Hm1ImALmWOZ0pEv5do/pz0439326Oz49hlfGot4KmkSBeKK81knWxRj9EXMSPwXPg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-arm64-musl@0.1.53': - resolution: {integrity: sha512-2N41U0X8RnrTKzpTtPv1ozlYkJtPsUdbfF3uP/KEd/BsULGd8Y8ghkGMS6CM+821au4ex0dPrWOOdT9wC1rSqQ==} + '@napi-rs/canvas-linux-arm64-musl@0.1.56': + resolution: {integrity: sha512-zqE4nz8CWiJJ0q5By7q9CDPicNkc0oyErgavK3ZV279zJL7Aapd3cIqayT6ynECArg7GgBl2WYSvr5AaRFmYgg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-x64-gnu@0.1.53': - resolution: {integrity: sha512-7XjuTvDKCODtf/vMwF43VGDrjfgwYKgS91ggdcX3UrJaBYWyWu/+eqNvNj+zdXSe/0x+YOjf5jG4m8xIXdBMQA==} + '@napi-rs/canvas-linux-x64-gnu@0.1.56': + resolution: {integrity: sha512-JTnGAtJBQMhfSpN8/rbMnf5oxuO/juUNa0n4LA0LlW0JS9UBpmsS2BwFNCakFqOeAPaqIM6sFFsK3M4hve+Esw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-linux-x64-musl@0.1.53': - resolution: {integrity: sha512-970WEvB8vmj+uxvgdBZ+AGFV7uq9GJhXrqG5PGQ5lWciHX0P0d/OhS2F7TITgFR0LsKDQZ7XQgzMxsYOfwZ0FQ==} + '@napi-rs/canvas-linux-x64-musl@0.1.56': + resolution: {integrity: sha512-mpws7DhVDIj8ZKa/qcnUVLAm0fxD9RK5ojfNNSI9TOzn2E0f+GUXx8sGsCxDpMVMtN+mtyrMwRqH3F3rTUMWXw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-win32-x64-msvc@0.1.53': - resolution: {integrity: sha512-rLFQCSJaWg/sv54Aap9nAhaodi4Vyb4un50EgW+PNkk8icMziU6KLRKirGBdQr9ZdxnshAPeQXD1g2ArStujKA==} + '@napi-rs/canvas-win32-x64-msvc@0.1.56': + resolution: {integrity: sha512-VKAAkgXF+lbFvRFawPOtkfV/P7ogAgWTu5FMCIiBn0Gc3vnkKFG2cLo/IHIJ7FuriToKEidkJGT88iAh7W7GDA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/canvas@0.1.53': - resolution: {integrity: sha512-XsEZi97+kKykmAiPpY+IpZoHxJY1srqFZp8jDt1/RySzC0kB0iZYt/VMIFqQKpLCARZjD7SOAz2AULtwYlesCA==} + '@napi-rs/canvas@0.1.56': + resolution: {integrity: sha512-SujSchzG6lLc/wT+Mwxam/w30Kk2sFTiU6bLFcidecKSmlhenAhGMQhZh2iGFfKoh2+8iit0jrt99n6TqReICQ==} engines: {node: '>= 10'} - '@nestjs/common@10.3.10': - resolution: {integrity: sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==} + '@nestjs/common@10.4.3': + resolution: {integrity: sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -3481,8 +3324,8 @@ packages: class-validator: optional: true - '@nestjs/core@10.3.10': - resolution: {integrity: sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==} + '@nestjs/core@10.4.3': + resolution: {integrity: sha512-6OQz+5C8mT8yRtfvE5pPCq+p6w5jDot+oQku1KzQ24ABn+lay1KGuJwcKZhdVNuselx+8xhdMxknZTA8wrGLIg==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/microservices': ^10.0.0 @@ -3498,14 +3341,14 @@ packages: '@nestjs/websockets': optional: true - '@nestjs/platform-express@10.3.10': - resolution: {integrity: sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==} + '@nestjs/platform-express@10.4.3': + resolution: {integrity: sha512-ss7gkofVm3eO+1P9iRhmGq6Xcjg+mIN3dWisKJZYelSV+msb0QpJmqChLvWjLkWtlqDnx915FKUk0IzCa0TVzw==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 - '@nestjs/testing@10.3.10': - resolution: {integrity: sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==} + '@nestjs/testing@10.4.3': + resolution: {integrity: sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 @@ -3770,12 +3613,16 @@ packages: '@readme/json-schema-ref-parser@1.2.0': resolution: {integrity: sha512-Bt3QVovFSua4QmHa65EHUmh2xS0XJ3rgTEUPH998f4OW4VVJke3BuS16f+kM0ZLOGdvIrzrPRqwihuv5BAjtrA==} - '@readme/openapi-parser@2.5.0': - resolution: {integrity: sha512-IbymbOqRuUzoIgxfAAR7XJt2FWl6n2yqN09fF5adacGm7W03siA3bj1Emql0X9D2T+RpBYz3x9zDsMhuoMP62A==} - engines: {node: '>=14'} + '@readme/openapi-parser@2.6.0': + resolution: {integrity: sha512-pyFJXezWj9WI1O+gdp95CoxfY+i+Uq3kKk4zXIFuRAZi9YnHpHOpjumWWr67wkmRTw19Hskh9spyY0Iyikf3fA==} + engines: {node: '>=18'} peerDependencies: openapi-types: '>=7' + '@readme/openapi-schemas@3.1.0': + resolution: {integrity: sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==} + engines: {node: '>=18'} + '@rollup/plugin-json@6.1.0': resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} engines: {node: '>=14.0.0'} @@ -3794,8 +3641,8 @@ packages: rollup: optional: true - '@rollup/pluginutils@5.1.0': - resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + '@rollup/pluginutils@5.1.2': + resolution: {integrity: sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -3803,88 +3650,91 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.19.1': - resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==} + '@rollup/rollup-android-arm-eabi@4.22.5': + resolution: {integrity: sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.19.1': - resolution: {integrity: sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==} + '@rollup/rollup-android-arm64@4.22.5': + resolution: {integrity: sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.19.1': - resolution: {integrity: sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==} + '@rollup/rollup-darwin-arm64@4.22.5': + resolution: {integrity: sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.19.1': - resolution: {integrity: sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==} + '@rollup/rollup-darwin-x64@4.22.5': + resolution: {integrity: sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.19.1': - resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==} + '@rollup/rollup-linux-arm-gnueabihf@4.22.5': + resolution: {integrity: sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.19.1': - resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==} + '@rollup/rollup-linux-arm-musleabihf@4.22.5': + resolution: {integrity: sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.19.1': - resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==} + '@rollup/rollup-linux-arm64-gnu@4.22.5': + resolution: {integrity: sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.19.1': - resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==} + '@rollup/rollup-linux-arm64-musl@4.22.5': + resolution: {integrity: sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': - resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.22.5': + resolution: {integrity: sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.19.1': - resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==} + '@rollup/rollup-linux-riscv64-gnu@4.22.5': + resolution: {integrity: sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.19.1': - resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==} + '@rollup/rollup-linux-s390x-gnu@4.22.5': + resolution: {integrity: sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.19.1': - resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==} + '@rollup/rollup-linux-x64-gnu@4.22.5': + resolution: {integrity: sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.19.1': - resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==} + '@rollup/rollup-linux-x64-musl@4.22.5': + resolution: {integrity: sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.19.1': - resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==} + '@rollup/rollup-win32-arm64-msvc@4.22.5': + resolution: {integrity: sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.19.1': - resolution: {integrity: sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==} + '@rollup/rollup-win32-ia32-msvc@4.22.5': + resolution: {integrity: sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.19.1': - resolution: {integrity: sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==} + '@rollup/rollup-win32-x64-msvc@4.22.5': + resolution: {integrity: sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==} cpu: [x64] os: [win32] - '@rushstack/node-core-library@5.5.1': - resolution: {integrity: sha512-ZutW56qIzH8xIOlfyaLQJFx+8IBqdbVCZdnj+XT1MorQ1JqqxHse8vbCpEM+2MjsrqcbxcgDIbfggB1ZSQ2A3g==} + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/node-core-library@5.9.0': + resolution: {integrity: sha512-MMsshEWkTbXqxqFxD4gcIUWQOCeBChlGczdZbHfqmNZQFLHB3yWxDFSMHFUdu2/OB9NUk7Awn5qRL+rws4HQNg==} peerDependencies: '@types/node': '*' peerDependenciesMeta: @@ -3894,16 +3744,16 @@ packages: '@rushstack/rig-package@0.5.3': resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} - '@rushstack/terminal@0.13.3': - resolution: {integrity: sha512-fc3zjXOw8E0pXS5t9vTiIPx9gHA0fIdTXsu9mT4WbH+P3mYvnrX0iAQ5a6NvyK1+CqYWBTw/wVNx7SDJkI+WYQ==} + '@rushstack/terminal@0.14.2': + resolution: {integrity: sha512-2fC1wqu1VCExKC0/L+0noVcFQEXEnoBOtCIex1TOjBzEDWcw8KzJjjj7aTP6mLxepG0XIyn9OufeFb6SFsa+sg==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/ts-command-line@4.22.3': - resolution: {integrity: sha512-edMpWB3QhFFZ4KtSzS8WNjBgR4PXPPOVrOHMbb7kNpmQ1UFS9HdVtjCXg1H5fG+xYAbeE+TMPcVPUyX2p84STA==} + '@rushstack/ts-command-line@4.22.8': + resolution: {integrity: sha512-XbFjOoV7qZHJnSuFUHv0pKaFA4ixyCuki+xMjsMfDwfvQjs5MYG0IK5COal3tRnG7KCDe2l/G+9LrzYE/RJhgg==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -3945,6 +3795,9 @@ packages: '@sideway/address@4.1.4': resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + '@sideway/formula@3.0.1': resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} @@ -3973,10 +3826,6 @@ packages: resolution: {integrity: sha512-WDTlVTyvFivSOuyvMeedzg2hdoBLZ3f1uNVuEida2Rl9BrfjrIRjWA/VZIrMRLvSwJYCAlCRA3usDt1THytxWQ==} engines: {node: '>=18'} - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -4229,115 +4078,97 @@ packages: '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} - '@storybook/addon-actions@8.2.6': - resolution: {integrity: sha512-iCsf3V28/jJ95w2zd8aSvR4denoA2UYV3fpNCTGOURqICyKOG3cyVxvqKp8Hhcwn7trNOsK+HlL6q5gpv56ViA==} + '@storybook/addon-actions@8.3.3': + resolution: {integrity: sha512-cbpksmld7iADwDGXgojZ4r8LGI3YA3NP68duAHg2n1dtnx1oUaFK5wd6dbNuz7GdjyhIOIy3OKU1dAuylYNGOQ==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-backgrounds@8.2.6': - resolution: {integrity: sha512-61NFowA6EmCw+Eyzp0U4fat9MlPDdnT7aoDyzqSImLwWLITY9IvmWuTeo7XKJZN3fe22z1r7cZseKdYrtaHcKw==} + '@storybook/addon-backgrounds@8.3.3': + resolution: {integrity: sha512-aX0OIrtjIB7UgSaiv20SFkfC1iWwJIGMPsPSJ5ZPhXIIOWIEBtSujh8YXwjDEXSC4DOHalmeT4bitRRe5KrVKA==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-controls@8.2.6': - resolution: {integrity: sha512-EHUwHy+oZZv3pXzN7fuXWrS/meHFjqcELY3RBvOyEkGf21agl6co6R1tnf6d5N5QoYAGfIbDO7dkauSL2RfNAw==} + '@storybook/addon-controls@8.3.3': + resolution: {integrity: sha512-78xRtVpY7eX/Lti00JLgwYCBRB6ZcvzY3SWk0uQjEqcTnQGoQkVg2L7oWFDlDoA1LBY18P5ei2vu8MYT9GXU4g==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-docs@8.2.6': - resolution: {integrity: sha512-qe7hxntaezqjKdU9QS+Q9NFL6i/uNdBxdvOnCKgPhBAY/zY6yhk5t3sOvonynPK5nkaNAowfSNPIzNxAXlJ1sA==} + '@storybook/addon-docs@8.3.3': + resolution: {integrity: sha512-REUandqq1RnMNOhsocRwx5q2fdlBAYPTDFlKASYfEn4Ln5NgbQRGxOAWl7yXAAFzbDmUDU7K20hkauecF0tyMw==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-essentials@8.2.6': - resolution: {integrity: sha512-diGjGZcZNov+RCAVQBTm8JKP2kUtMRuJIQFBeXdPWpu6hYBk6lw1FlAf2GywWGCvdny1pJT90hfoD33qUMNuDg==} + '@storybook/addon-essentials@8.3.3': + resolution: {integrity: sha512-E/uXoUYcg8ulG3lVbsEKb4v5hnMeGkq9YJqiZYKgVK7iRFa6p4HeVB1wU1adnm7RgjWvh+p0vQRo4KL2CTNXqw==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-highlight@8.2.6': - resolution: {integrity: sha512-03cV9USsfP3bS4wYV06DYcIaGPfoheQe53Q0Jr1B2yJUVyIPKvmO2nGjLBsqzeL3Wl7vSfLQn0/dUdxCcbqLsw==} + '@storybook/addon-highlight@8.3.3': + resolution: {integrity: sha512-MB084xJM66rLU+iFFk34kjLUiAWzDiy6Kz4uZRa1CnNqEK0sdI8HaoQGgOxTIa2xgJor05/8/mlYlMkP/0INsQ==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-interactions@8.2.6': - resolution: {integrity: sha512-YXpHf8jWPz9HJV+Fw4GaunaCWeE6uqF24aLXdAd8xuhN1UfWJeNV6AwAvFQ0hTLqvmz0yMhX/5JXDKeKESoYDA==} + '@storybook/addon-interactions@8.3.3': + resolution: {integrity: sha512-3w5tpCGYdF33wF44xEhTS3Zmcwd6nITtwy5q+PJvHCJAm3fpjzL3xrjtlHKDvXNwYacJPRCbWKn2QwtxZIdN0g==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-links@8.2.6': - resolution: {integrity: sha512-CUuU3nk8wyZ3bljCmOG/OCKazan+bPuNbCph8N763zyzdEx5M/CbBxV9d3pi3zjYpix7txlqrl2/YdMCejfyFw==} + '@storybook/addon-links@8.3.3': + resolution: {integrity: sha512-rz4KEbzr1ca4zZEZwbOnhKiaEsokCl1KkngxT/C1YIkpW908j/kg2nnIb5MrtlAW1nirXguAR74t6CGntvdU9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.2.6 + storybook: ^8.3.3 peerDependenciesMeta: react: optional: true - '@storybook/addon-mdx-gfm@8.2.6': - resolution: {integrity: sha512-PFVfJeuydxlV1VmxEuKNQ7z2vCDvQtHa2GB0ANM11ahxDSUz8QxsO0Y/L3LOn2JjJGYiVFrsHAaC+8NW43iArQ==} + '@storybook/addon-mdx-gfm@8.3.3': + resolution: {integrity: sha512-jdwVXoBSEdmuw8L4MxUeJ/qIInADfCwdtShnfTQIJBBRucOl8ykgfTKKNjllT79TFiK0gsWoiZmE05P4wuBofw==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-measure@8.2.6': - resolution: {integrity: sha512-neI8YeSOAtOmzasLxo6O8ZLr2ebMaD7XVF+kYatl5+SpyuwwvUGcP9NkKe5S+mB8V2zxFUIsXS74XrhmQhRoaQ==} + '@storybook/addon-measure@8.3.3': + resolution: {integrity: sha512-R20Z83gnxDRrocES344dw1Of/zDhe3XHSM6TLq80UQTJ9PhnMI+wYHQlK9DsdP3KiRkI+pQA6GCOp0s2ZRy5dg==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-outline@8.2.6': - resolution: {integrity: sha512-uAlPtqDWlq7MQQ4zJT80qdjbSdLF/zsvtPhidX6h9cjLKNPWAv79xJQ14AJHaMv+Hzy5xKnM4wdEhgPbzKabQg==} + '@storybook/addon-outline@8.3.3': + resolution: {integrity: sha512-OwqYfieNuqSqWNtUZLu3UmsfQNnwA2UaSMBZyeC2Dte9Jd59PPYggcWmH+b0S6OTbYXWNAUK5U6WdK+X9Ypzdw==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-storysource@8.2.6': - resolution: {integrity: sha512-8H2kvRIM12oXN4kO/oowABu88IOY9Je7PphCUUs/nIfTIB+Ck1GrLxx5fCNNCSwUrTBEZY8bDOfxmkf9d4qngw==} + '@storybook/addon-storysource@8.3.3': + resolution: {integrity: sha512-yPYQH9NepSNxoSsV9E7OV3/EVFrbU/r2B3E5WP/mCfqTXPg/5noce7iRi+rWqcVM1tsN1qPnSjfQQc7noF0h0Q==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-toolbars@8.2.6': - resolution: {integrity: sha512-0JmRirMpxHS6VZzBk0kY871xWTpkk3TN4S1sxoFf5fcnCfVTHDjEJ5Ws/QWru1RJlIZHuJKRdQIA6Vuq5X+KfQ==} + '@storybook/addon-toolbars@8.3.3': + resolution: {integrity: sha512-4WyiVqDm4hlJdENIVQg9pLNLdfhnNKa+haerYYSzTVjzYrUx0X6Bxafshq+sud6aRtSYU14abwP56lfW8hgTlA==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/addon-viewport@8.2.6': - resolution: {integrity: sha512-IAxH9H8tVFzSmZhKf5E+EALiAdkp19RzGqP/rWluD8LH7oW5HumQE/4oN0ZhVMy1RxYsCKFYjWyAp7AuxeMRSw==} + '@storybook/addon-viewport@8.3.3': + resolution: {integrity: sha512-2S+UpbKAL+z1ppzUCkixjaem2UDMkfmm/kyJ1wm3A/ofGLYi4fjMSKNRckk+7NdolXGQJjBo0RcaotUTxFIFwQ==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/blocks@8.2.6': - resolution: {integrity: sha512-nMlZJjVTyfOJ6xwORptsNuS1AZZlDbJUVXc2R8uukGd5GIXxxCdrPk4NvUsjfQslMT9LhYuFld3z62FATsM2rw==} + '@storybook/blocks@8.3.3': + resolution: {integrity: sha512-8Vsvxqstop3xfbsx3Dn1nEjyxvQUcOYd8vpxyp2YumxYO8FlXIRuYL6HAkYbcX8JexsKvCZYxor52D2vUGIKZg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.2.6 + storybook: ^8.3.3 peerDependenciesMeta: react: optional: true react-dom: optional: true - '@storybook/builder-manager@8.1.11': - resolution: {integrity: sha512-U7bmed4Ayg+OlJ8HPmLeGxLTHzDY7rxmxM4aAs4YL01fufYfBcjkIP9kFhJm+GJOvGm+YJEUAPe5mbM1P/bn0Q==} - - '@storybook/builder-vite@8.1.11': - resolution: {integrity: sha512-hG4eoNMCPgjZ2Ai+zSmk69zjsyEihe75XbJXtYfGRqjMWtz2+SAUFO54fLc2BD5svcUiTeN+ukWcTrwApyPsKg==} - peerDependencies: - '@preact/preset-vite': '*' - typescript: '>= 4.3.x' - vite: ^4.0.0 || ^5.0.0 - vite-plugin-glimmerx: '*' - peerDependenciesMeta: - '@preact/preset-vite': - optional: true - typescript: - optional: true - vite-plugin-glimmerx: - optional: true - - '@storybook/builder-vite@8.2.6': - resolution: {integrity: sha512-3PrsPZAedpQUbzRBEl23Fi1zG5bkQD76JsygVwmfiSm4Est4K8kW2AIB2ht9cIfKXh3mfQkyQlxXKHeQEHeQwQ==} + '@storybook/builder-vite@8.3.3': + resolution: {integrity: sha512-3yTXCLaB6bzhoPH3PqtacKkcaC1uV4L+IHTf1Zypx1NO1pLZHyhYf0T7dIOxTh2JZfqu1Pm9hTvOmWfR12m+9w==} peerDependencies: '@preact/preset-vite': '*' - storybook: ^8.2.6 + storybook: ^8.3.3 typescript: '>= 4.3.x' vite: ^4.0.0 || ^5.0.0 vite-plugin-glimmerx: '*' @@ -4349,190 +4180,115 @@ packages: vite-plugin-glimmerx: optional: true - '@storybook/channels@8.1.11': - resolution: {integrity: sha512-fu5FTqo6duOqtJFa6gFzKbiSLJoia+8Tibn3xFfB6BeifWrH81hc+AZq0lTmHo5qax2G5t8ZN8JooHjMw6k2RA==} - - '@storybook/client-logger@8.1.11': - resolution: {integrity: sha512-DVMh2usz3yYmlqCLCiCKy5fT8/UR9aTh+gSqwyNFkGZrIM4otC5A8eMXajXifzotQLT5SaOEnM3WzHwmpvMIEA==} - - '@storybook/codemod@8.2.6': - resolution: {integrity: sha512-+mFJ6R+JhJLpU7VPDlXU5Yn6nqIBq745GaEosnIiFOdNo3jaxJ58wq/sGhbQvoCHPUxMA+sDQvR7pS62YFoLRQ==} - - '@storybook/components@8.2.6': - resolution: {integrity: sha512-H8ckH1AnLkHtMtvJ3J8LxnmDtHxkJ7NJacGctHMRrsBIvdKTVwlT4su5nAVVJlan/PrEou+jESfw+OjjBYE5PA==} - peerDependencies: - storybook: ^8.2.6 - - '@storybook/core-common@8.1.11': - resolution: {integrity: sha512-Ix0nplD4I4DrV2t9B+62jaw1baKES9UbR/Jz9LVKFF9nsua3ON0aVe73dOjMxFWBngpzBYWe+zYBTZ7aQtDH4Q==} + '@storybook/components@8.3.3': + resolution: {integrity: sha512-i2JYtesFGkdu+Hwuj+o9fLuO3yo+LPT1/8o5xBVYtEqsgDtEAyuRUWjSz8d8NPtzloGPOv5kvR6MokWDfbeMfw==} peerDependencies: - prettier: ^2 || ^3 - peerDependenciesMeta: - prettier: - optional: true + storybook: ^8.3.3 - '@storybook/core-events@8.1.11': - resolution: {integrity: sha512-vXaNe2KEW9BGlLrg0lzmf5cJ0xt+suPjWmEODH5JqBbrdZ67X6ApA2nb6WcxDQhykesWCuFN5gp1l+JuDOBi7A==} - - '@storybook/core-events@8.2.6': - resolution: {integrity: sha512-bmtm7sHBExKCSGiCIyhwfHKFIsdrRQqd8ZEb/iNWsR93AxHszcf/adYAVynencdWKipw1haIWBNaiDhnsOBVPA==} + '@storybook/core-events@8.3.3': + resolution: {integrity: sha512-YL+gBuCS81qktzTkvw0MXUJW0bYAXfRzMoiLfDBTrEKZfcJOB4JAlMGmvRRar0+jygK3icD42Rl5BwWoZY6KFQ==} peerDependencies: - storybook: ^8.2.6 - - '@storybook/core-server@8.1.11': - resolution: {integrity: sha512-L6dzQTmR0np/kagNONvvlm6lSvF1FNc9js3vxsEEPnEypLbhx8bDZaHmuhmBpYUzKyUMpRVQTE/WgjHLuBBuxA==} + storybook: ^8.3.3 - '@storybook/core@8.2.6': - resolution: {integrity: sha512-XY71g3AcpD6IiER9k9Lt+vlUMYfPIYgWekd7e0Ggzz2gJkPuLunKEdQccLGDSHf5OFAobHhrTJc7ZsvWhmDMag==} + '@storybook/core@8.3.3': + resolution: {integrity: sha512-pmf2bP3fzh45e56gqOuBT8sDX05hGdUKIZ/hcI84d5xmd6MeHiPW8th2v946wCHcxHzxib2/UU9vQUh+mB4VNw==} - '@storybook/csf-plugin@8.1.11': - resolution: {integrity: sha512-hkA8gjFtSN/tabG0cuvmEqanMXtxPr3qTkp4UNSt1R6jBEgFHRG2y/KYLl367kDwOSFTT987ZgRfJJruU66Fvw==} - - '@storybook/csf-plugin@8.2.6': - resolution: {integrity: sha512-USn7E/bMQYVqvFBuW6d9rKoSuCImjk0BAmc/0wIOuMQ/yQNp2Xze0m8eVkNHUIUDokyx0TXDjRjwq10Xxk16ag==} + '@storybook/csf-plugin@8.3.3': + resolution: {integrity: sha512-7AD7ojpXr3THqpTcEI4K7oKUfSwt1hummgL/cASuQvEPOwAZCVZl2gpGtKxcXhtJXTkn3GMCAvlYMoe7O/1YWw==} peerDependencies: - storybook: ^8.2.6 - - '@storybook/csf-tools@8.1.11': - resolution: {integrity: sha512-6qMWAg/dBwCVIHzANM9lSHoirwqSS+wWmv+NwAs0t9S94M75IttHYxD3IyzwaSYCC5llp0EQFvtXXAuSfFbibg==} + storybook: ^8.3.3 '@storybook/csf@0.1.11': resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==} - '@storybook/csf@0.1.9': - resolution: {integrity: sha512-JlZ6v/iFn+iKohKGpYXnMeNeTiiAMeFoDhYnPLIC8GnyyIWqEI9wJYrOK9i9rxlJ8NZAH/ojGC/u/xVC41qSgQ==} - - '@storybook/docs-mdx@3.1.0-next.0': - resolution: {integrity: sha512-t4syFIeSyufieNovZbLruPt2DmRKpbwL4fERCZ1MifWDRIORCKLc4NCEHy+IqvIqd71/SJV2k4B51nF7vlJfmQ==} - - '@storybook/docs-tools@8.1.11': - resolution: {integrity: sha512-mEXtR9rS7Y+OdKtT/QG6JBGYR1L41mcDhIqhnk7RmYl9qJstVAegrCKWR53sPKFdTVOHU7dmu6k+BD+TqHpyyw==} - '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} - '@storybook/icons@1.2.5': - resolution: {integrity: sha512-m3jnuE+zmkZy6K+cdUDzAoUuCJyl0fWCAXPCji7VZCH1TzFohyvnPqhc9JMkQpanej2TOW3wWXaplPzHghcBSg==} + '@storybook/icons@1.2.12': + resolution: {integrity: sha512-UxgyK5W3/UV4VrI3dl6ajGfHM4aOqMAkFLWe2KibeQudLf6NJpDrDMSHwZj+3iKC4jFU7dkKbbtH2h/al4sW3Q==} engines: {node: '>=14.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@storybook/instrumenter@8.2.6': - resolution: {integrity: sha512-RxtpcMTUSq8/wPM6cR6EXVrPEiNuRbC71cIFVFZagOFYvnnOKwSPV+GOLPK0wxMbGB4c5/+Xe8ADefmZTvxOsA==} + '@storybook/instrumenter@8.3.3': + resolution: {integrity: sha512-ZiODB9EwCQkl4PBxGJjBHXRTLxcNs68ZZvR+xeMr0eMFzzlJG+trXoX5kK95oA4BFhGN+3uM0Zl3MoRjBtJTNA==} peerDependencies: - storybook: ^8.2.6 - - '@storybook/manager-api@8.1.11': - resolution: {integrity: sha512-QSgwKfAw01K9YvvZj30iGBMgQ4YaCT3vojmttuqdH5ukyXkiO7pENLJj4Y+alwUeSi0g+SJeadCI3PXySBHOGg==} + storybook: ^8.3.3 - '@storybook/manager-api@8.2.6': - resolution: {integrity: sha512-uv36h/b5RhlajWtEg4cVPBYV8gZs6juux0nIE+6G9i7vt8Ild6gM9tW1KNabgZcaHFiyWJYCNWxJZoKjgUmXDg==} + '@storybook/manager-api@8.3.3': + resolution: {integrity: sha512-Na4U+McOeVUJAR6qzJfQ6y2Qt0kUgEDUriNoAn+curpoKPTmIaZ79RAXBzIqBl31VyQKknKpZbozoRGf861YaQ==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/manager@8.1.11': - resolution: {integrity: sha512-e02y9dmxowo7cTKYm9am7UO6NOHoHy6Xi7xZf/UA932qLwFZUtk5pnwIEFaZWI3OQsRUCGhP+FL5zizU7uVZeg==} - - '@storybook/node-logger@8.1.11': - resolution: {integrity: sha512-wdzFo7B2naGhS52L3n1qBkt5BfvQjs8uax6B741yKRpiGgeAN8nz8+qelkD25MbSukxvbPgDot7WJvsMU/iCzg==} - - '@storybook/preview-api@8.1.11': - resolution: {integrity: sha512-8ZChmFV56GKppCJ0hnBd/kNTfGn2gWVq1242kuet13pbJtBpvOhyq4W01e/Yo14tAPXvgz8dSnMvWLbJx4QfhQ==} - - '@storybook/preview-api@8.2.6': - resolution: {integrity: sha512-5vTj2ndX5ng4nDntZYe+r8UwLjCIGFymhq5/r2adAvRKL+Bo4zQDWGO7bhvGJk16do2THb2JvPz49ComW9LLZw==} + '@storybook/preview-api@8.3.3': + resolution: {integrity: sha512-GP2QlaF3BBQGAyo248N7549YkTQjCentsc1hUvqPnFWU4xfjkejbnFk8yLaIw0VbYbL7jfd7npBtjZ+6AnphMQ==} peerDependencies: - storybook: ^8.2.6 - - '@storybook/preview@8.1.11': - resolution: {integrity: sha512-K/9NZmjnL0D1BROkTNWNoPqgL2UaocALRSqCARmkBLgU2Rn/FuZgEclHkWlYo6pUrmLNK+bZ+XzpNMu12iTbpg==} + storybook: ^8.3.3 - '@storybook/react-dom-shim@8.2.6': - resolution: {integrity: sha512-B+x8UAEQPDp1yhN3tMh09NvSL38QNfJB7PAyLgKrfE7xIAzvewq+RLW2DfGkoZCy+Zr7QSHm1p7NOgud8+sQCg==} + '@storybook/react-dom-shim@8.3.3': + resolution: {integrity: sha512-0dPC9K7+K5+X/bt3GwYmh+pCpisUyKVjWsI+PkzqGnWqaXFakzFakjswowIAIO1rf7wYZR591x3ehUAyL2bJiQ==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/react-vite@8.2.6': - resolution: {integrity: sha512-BpbteaIzsJZL1QN3iR7uuslrPfdtbZYXPhcU9awpfl5pW5MOQThuvl7728mwT8V7KdANeikJPgsnlETOb/afDA==} + '@storybook/react-vite@8.3.3': + resolution: {integrity: sha512-vzOqVaA/rv+X5J17eWKxdZztMKEKfsCSP8pNNmrqXWxK3pSlW0fAPxtn1kw3UNxGtAv71pcqvaCUtTJKqI1PYA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.2.6 + storybook: ^8.3.3 vite: ^4.0.0 || ^5.0.0 - '@storybook/react@8.2.6': - resolution: {integrity: sha512-awJlzfiAMrf8l9AgiLhjXEJ+HvS3VKPxNNQaRwBELGq/vigjJe656tMrhvg4OIlJXtlS+6XPshd2knLwjIWNLw==} + '@storybook/react@8.3.3': + resolution: {integrity: sha512-fHOW/mNqI+sZWttGOE32Q+rAIbN7/Oib091cmE8usOM0z0vPNpywUBtqC2cCQH39vp19bhTsQaSsTcoBSweAHw==} engines: {node: '>=18.0.0'} peerDependencies: + '@storybook/test': 8.3.3 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.2.6 + storybook: ^8.3.3 typescript: '>= 4.2.x' peerDependenciesMeta: + '@storybook/test': + optional: true typescript: optional: true - '@storybook/router@8.1.11': - resolution: {integrity: sha512-nU5lsBvy0L8wBYOkjagh29ztZicDATpZNYrHuavlhQ2jznmmHdJvXKYk+VrMAbthjQ6ZBqfeeMNPR1UlnqR5Rw==} - - '@storybook/source-loader@8.2.6': - resolution: {integrity: sha512-mOVf+TJhlQywCymFMs7l604CxEZRKZRKVQojrrgU6CH6EhhLx/q6BT8tf1CakY9JO3Ey+PhUMBBCerYiDaHLcQ==} - peerDependencies: - storybook: ^8.2.6 - - '@storybook/telemetry@8.1.11': - resolution: {integrity: sha512-Jqvm7HcZismKzPuebhyLECO6KjGiSk4ycbca1WUM/TUvifxCXqgoUPlHHQEEfaRdHS63/MSqtMNjLsQRLC/vNQ==} - - '@storybook/test@8.2.6': - resolution: {integrity: sha512-nTzNxReBcMRlX1+8PNU/MuA9ArFbeQhfZXMBIwJJoHOhnNe1knYpyn1++xINxAHKOh0BBhQ0NIMoKdcGmW3V6w==} + '@storybook/source-loader@8.3.3': + resolution: {integrity: sha512-NeP7l53mvnnfwi+91vtRaibZer+UJi6gkoaGRCpphL3L+3qVIXN3p41uXhAy+TahdFI2dbrWvLSNgtsvdXVaFg==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/theming@8.1.11': - resolution: {integrity: sha512-Chn/opjO6Rl1isNobutYqAH2PjKNkj09YBw/8noomk6gElSa3JbUTyaG/+JCHA6OG/9kUsqoKDb5cZmAKNq/jA==} + '@storybook/test@8.3.3': + resolution: {integrity: sha512-uZ8nMIovfI2ry989K2+cYAeEVD/3dpjj2+Rbmy7DiZWWVhFALfmqaTRkzZfShLmlH0TFv+rfcBPihGccBtw0FQ==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true + storybook: ^8.3.3 - '@storybook/theming@8.2.6': - resolution: {integrity: sha512-ICnYuLIVsYifVCMQljdHgrp+5vAquNybHxDGWiPeOxBicotwHF8rLhTckD2CdVQbMp0jk6r6jetvjXbFJ2MbvQ==} + '@storybook/theming@8.3.3': + resolution: {integrity: sha512-gWJKetI6XJQgkrvvry4ez10+jLaGNCQKi5ygRPM9N+qrjA3BB8F2LCuFUTBuisa4l64TILDNjfwP/YTWV5+u5A==} peerDependencies: - storybook: ^8.2.6 - - '@storybook/types@8.1.11': - resolution: {integrity: sha512-k9N5iRuY2+t7lVRL6xeu6diNsxO3YI3lS4Juv3RZ2K4QsE/b3yG5ElfJB8DjHDSHwRH4ORyrU71KkOCUVfvtnw==} + storybook: ^8.3.3 - '@storybook/types@8.2.6': - resolution: {integrity: sha512-9Kb5+nui8M7TP/EDGwiuOAHYQPg9U6iQl0OWwgbDIYGBpldwlCwVKAoQWzXz/LlhQijULXIpe1cLvEvJN2Uwhg==} + '@storybook/types@8.3.3': + resolution: {integrity: sha512-wV1kupG1tfTMOXaBrtVHXuqp19vURVDqWTQX6nqkoUFD7Xb1lz/YNVeGP1uT/zJdJy42/HIyoib9JPx9h0Vx9w==} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 - '@storybook/vue3-vite@8.1.11': - resolution: {integrity: sha512-q0bqh8XEEunaTmp4YiDqM2+YZLwEIevTb5PnNe7G7f2qOiSCE1ncBDnBK717UlCd+iYr34NTztgV2/jIhz1i5w==} + '@storybook/vue3-vite@8.3.3': + resolution: {integrity: sha512-IFcoOGlUGuUkL3rpm9UFs8FK9JX1ZdfGpLXRObVOVRhW3t+MsNLpx4Fqp3a/re6WcCC3yvHzbLXgvGcjpapkbw==} engines: {node: '>=18.0.0'} peerDependencies: + storybook: ^8.3.3 vite: ^4.0.0 || ^5.0.0 - '@storybook/vue3@8.1.11': - resolution: {integrity: sha512-xJtvfLiCOY3UqwDMd0hZdsadPm1q8dwjfM1UN2Q2ssRWNfXzww1oi+Msj902wz9zFZMYVZypfTfgrdRgWmfEjA==} - engines: {node: '>=18.0.0'} - peerDependencies: - vue: ^3.0.0 - - '@storybook/vue3@8.2.6': - resolution: {integrity: sha512-j4gMuWc1ZDzqWSdf79YswcZmcbhmbByq/6upqxwqXtjv1mHAiBnEs8bbnnylDrzg4GOvBC8w+FjArkzlFA7uXg==} + '@storybook/vue3@8.3.3': + resolution: {integrity: sha512-peu8MFGwmhpXoD3n42qG6TxeVHRhfHZ0/HW4+A6FXSB1c9w0CC4AzHs5f1w3yUvshtexNN5bkw9Q4nSVKtfU7A==} engines: {node: '>=18.0.0'} peerDependencies: - storybook: ^8.2.6 + storybook: ^8.3.3 vue: ^3.0.0 '@swc/cli@0.3.12': @@ -4782,34 +4538,17 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@testing-library/dom@10.1.0': - resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} '@testing-library/dom@9.3.4': resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} engines: {node: '>=14'} - '@testing-library/jest-dom@6.4.5': - resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} + '@testing-library/jest-dom@6.5.0': + resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - peerDependencies: - '@jest/globals': '>= 28' - '@types/bun': latest - '@types/jest': '>= 28' - jest: '>= 28' - vitest: '>= 0.32' - peerDependenciesMeta: - '@jest/globals': - optional: true - '@types/bun': - optional: true - '@types/jest': - optional: true - jest: - optional: true - vitest: - optional: true '@testing-library/user-event@14.5.2': resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} @@ -4844,6 +4583,9 @@ packages: '@twemoji/parser@15.0.0': resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==} + '@twemoji/parser@15.1.0': + resolution: {integrity: sha512-3HTiSxPvkWUJ4kZeCvwyKlIwkpTUfBOk6igpBBRQni58ceQMv5YK4smkc8vX/eqOlMMNER/9qobv+Q6Q8LVrqA==} + '@twemoji/parser@15.1.1': resolution: {integrity: sha512-CChRzIu6ngkCJOmURBlYEdX5DZSu+bBTtqR60XjBkFrmvplKW7OQsea+i8XwF4bLVlUXBO7ZmHhRPDzfQyLwwg==} @@ -4904,36 +4646,15 @@ packages: '@types/core-js@2.5.8': resolution: {integrity: sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg==} - '@types/cross-spawn@6.0.2': - resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@types/detect-port@1.3.2': - resolution: {integrity: sha512-xxgAGA2SAU4111QefXPSp5eGbDm/hW6zhvYl9IeEPZEry9F4d66QAHm5qpUXjb6IsevZV/7emAEx5MhP6O192g==} - - '@types/diff@5.2.1': - resolution: {integrity: sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==} - '@types/disposable-email-domains@1.0.2': resolution: {integrity: sha512-SDKwyYTjk3y5aZBxxc38yRecpJPjsqn57STz1bNxYYlv4k11bBe7QB8w4llXDTmQXKT1mFvgGmJv+8Zdu3YmJw==} - '@types/doctrine@0.0.3': - resolution: {integrity: sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==} - '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} - '@types/ejs@3.1.2': - resolution: {integrity: sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g==} - - '@types/emscripten@1.39.7': - resolution: {integrity: sha512-tLqYV94vuqDrXh515F/FOGtBcRMTPGvVV1LzLbtYDcQmmhtpf/gLYf+hikBbQk8MzOHNz37wpFfJbYAuSn8HqA==} - - '@types/escape-regexp@0.0.3': - resolution: {integrity: sha512-FQMYUxaf1dVeWLUzJFSvfdDugfOpDyM13p67QfyMdagxSkBa689opkr/q9uR/VWyrWrl0jAyQaSPKxX9MpAXFw==} - '@types/escodegen@0.0.6': resolution: {integrity: sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==} @@ -4943,8 +4664,8 @@ packages: '@types/estree@0.0.51': resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} '@types/express-serve-static-core@4.17.33': resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} @@ -4958,8 +4679,8 @@ packages: '@types/find-cache-dir@3.2.1': resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} - '@types/fluent-ffmpeg@2.1.24': - resolution: {integrity: sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==} + '@types/fluent-ffmpeg@2.1.26': + resolution: {integrity: sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==} '@types/form-data@2.5.0': resolution: {integrity: sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg==} @@ -4995,6 +4716,9 @@ packages: '@types/jest@29.5.12': resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + '@types/jest@29.5.13': + resolution: {integrity: sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -5016,6 +4740,9 @@ packages: '@types/jsrsasign@10.5.14': resolution: {integrity: sha512-lppSlfK6etu+cuKs40K4rg8As79PH6hzIB+v55zSqImbSH3SE6Fm8MBHCiI91cWlAP3Z4igtJK1VL3fSN09blQ==} + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -5058,9 +4785,6 @@ packages: '@types/mysql@2.15.22': resolution: {integrity: sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==} - '@types/node@18.17.15': - resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==} - '@types/node@20.11.5': resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} @@ -5070,8 +4794,11 @@ packages: '@types/node@20.9.1': resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} - '@types/nodemailer@6.4.15': - resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + + '@types/nodemailer@6.4.16': + resolution: {integrity: sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==} '@types/normalize-package-data@2.4.1': resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -5094,15 +4821,12 @@ packages: '@types/pg-pool@2.0.4': resolution: {integrity: sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==} - '@types/pg@8.11.6': - resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + '@types/pg@8.11.10': + resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==} '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} - '@types/pretty-hrtime@1.0.1': - resolution: {integrity: sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==} - '@types/prop-types@15.7.5': resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -5145,8 +4869,8 @@ packages: '@types/responselike@1.0.0': resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} - '@types/sanitize-html@2.11.0': - resolution: {integrity: sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==} + '@types/sanitize-html@2.13.0': + resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==} '@types/scheduler@0.16.2': resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} @@ -5223,6 +4947,9 @@ packages: '@types/ws@8.5.11': resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==} + '@types/ws@8.5.12': + resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} + '@types/yargs-parser@21.0.0': resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -5232,17 +4959,6 @@ packages: '@types/yauzl@2.10.0': resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} - '@typescript-eslint/eslint-plugin@6.11.0': - resolution: {integrity: sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5276,16 +4992,6 @@ packages: typescript: optional: true - '@typescript-eslint/parser@6.11.0': - resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/parser@6.21.0': resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5316,10 +5022,6 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@6.11.0': - resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/scope-manager@6.21.0': resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5332,16 +5034,6 @@ packages: resolution: {integrity: sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/type-utils@6.11.0': - resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/type-utils@6.21.0': resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5372,10 +5064,6 @@ packages: typescript: optional: true - '@typescript-eslint/types@6.11.0': - resolution: {integrity: sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/types@6.21.0': resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5388,15 +5076,6 @@ packages: resolution: {integrity: sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/typescript-estree@6.11.0': - resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@6.21.0': resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5424,12 +5103,6 @@ packages: typescript: optional: true - '@typescript-eslint/utils@6.11.0': - resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - '@typescript-eslint/utils@6.21.0': resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5448,10 +5121,6 @@ packages: peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/visitor-keys@6.11.0': - resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} - engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/visitor-keys@6.21.0': resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -5467,8 +5136,8 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitejs/plugin-vue@5.1.0': - resolution: {integrity: sha512-QMRxARyrdiwi1mj3AW4fLByoHTavreXq0itdEW696EihXglf1MB3D4C2gBvE0jMPH29ZjC3iK8aIaUMLf4EOGA==} + '@vitejs/plugin-vue@5.1.4': + resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 @@ -5482,6 +5151,15 @@ packages: '@vitest/expect@1.6.0': resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/pretty-format@2.1.2': + resolution: {integrity: sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==} + '@vitest/runner@1.6.0': resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} @@ -5491,54 +5169,63 @@ packages: '@vitest/spy@1.6.0': resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + '@vitest/utils@2.1.2': + resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==} + '@volar/language-core@2.2.0': resolution: {integrity: sha512-a8WG9+4OdeNDW4ywABZIM6S6UN7em8uIlM/BZ2pWQUYrVmX+m8sj/X+QadvO+Li/t/LjAqbWJQtVgxdpEWLALQ==} - '@volar/language-core@2.4.0-alpha.18': - resolution: {integrity: sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==} + '@volar/language-core@2.4.6': + resolution: {integrity: sha512-FxUfxaB8sCqvY46YjyAAV6c3mMIq/NWQMVvJ+uS4yxr1KzOvyg61gAuOnNvgCvO4TZ7HcLExBEsWcDu4+K4E8A==} '@volar/source-map@2.2.0': resolution: {integrity: sha512-HQlPRlHOVqCCHK8wI76ZldHkEwKsjp7E6idUc36Ekni+KJDNrqgSqPvyHQixybXPHNU7CI9Uxd9/IkxO7LuNBw==} - '@volar/source-map@2.4.0-alpha.18': - resolution: {integrity: sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==} + '@volar/source-map@2.4.6': + resolution: {integrity: sha512-Nsh7UW2ruK+uURIPzjJgF0YRGP5CX9nQHypA2OMqdM2FKy7rh+uv3XgPnWPw30JADbKvZ5HuBzG4gSbVDYVtiw==} '@volar/typescript@2.2.0': resolution: {integrity: sha512-wC6l4zLiiCLxF+FGaHCbWlQYf4vMsnRxYhcI6WgvaNppOD6r1g+Ef1RKRJUApALWU46Yy/JDU/TbdV6w/X6Liw==} - '@volar/typescript@2.4.0-alpha.18': - resolution: {integrity: sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==} - - '@vue/compiler-core@3.4.31': - resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==} - - '@vue/compiler-core@3.4.34': - resolution: {integrity: sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==} + '@volar/typescript@2.4.6': + resolution: {integrity: sha512-NMIrA7y5OOqddL9VtngPWYmdQU03htNKFtAYidbYfWA0TOhyGVd9tfcP4TsLWQ+RBWDZCbBqsr8xzU0ZOxYTCQ==} '@vue/compiler-core@3.4.37': resolution: {integrity: sha512-ZDDT/KiLKuCRXyzWecNzC5vTcubGz4LECAtfGPENpo0nrmqJHwuWtRLxk/Sb9RAKtR9iFflFycbkjkY+W/PZUQ==} - '@vue/compiler-dom@3.4.34': - resolution: {integrity: sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==} + '@vue/compiler-core@3.5.10': + resolution: {integrity: sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==} '@vue/compiler-dom@3.4.37': resolution: {integrity: sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==} + '@vue/compiler-dom@3.5.10': + resolution: {integrity: sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==} + '@vue/compiler-sfc@3.4.37': resolution: {integrity: sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==} + '@vue/compiler-sfc@3.5.10': + resolution: {integrity: sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==} + '@vue/compiler-ssr@3.4.37': resolution: {integrity: sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==} + '@vue/compiler-ssr@3.5.10': + resolution: {integrity: sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==} + '@vue/compiler-vue2@2.7.16': resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} - '@vue/devtools-api@6.6.1': - resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==} - '@vue/language-core@2.0.16': resolution: {integrity: sha512-Bc2sexRH99pznOph8mLw2BlRZ9edm7tW51kcBXgx8adAoOcZUWJj3UNSsdQ6H9Y8meGz7BoazVrVo/jUukIsPw==} peerDependencies: @@ -5547,8 +5234,8 @@ packages: typescript: optional: true - '@vue/language-core@2.0.29': - resolution: {integrity: sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==} + '@vue/language-core@2.1.6': + resolution: {integrity: sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -5558,26 +5245,37 @@ packages: '@vue/reactivity@3.4.37': resolution: {integrity: sha512-UmdKXGx0BZ5kkxPqQr3PK3tElz6adTey4307NzZ3whZu19i5VavYal7u2FfOmAzlcDVgE8+X0HZ2LxLb/jgbYw==} + '@vue/reactivity@3.5.10': + resolution: {integrity: sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==} + '@vue/runtime-core@3.4.37': resolution: {integrity: sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==} + '@vue/runtime-core@3.5.10': + resolution: {integrity: sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==} + '@vue/runtime-dom@3.4.37': resolution: {integrity: sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==} + '@vue/runtime-dom@3.5.10': + resolution: {integrity: sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==} + '@vue/server-renderer@3.4.37': resolution: {integrity: sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==} peerDependencies: vue: 3.4.37 - '@vue/shared@3.4.31': - resolution: {integrity: sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==} - - '@vue/shared@3.4.34': - resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==} + '@vue/server-renderer@3.5.10': + resolution: {integrity: sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==} + peerDependencies: + vue: 3.5.10 '@vue/shared@3.4.37': resolution: {integrity: sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==} + '@vue/shared@3.5.10': + resolution: {integrity: sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==} + '@vue/test-utils@2.4.1': resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==} peerDependencies: @@ -5587,20 +5285,6 @@ packages: '@vue/server-renderer': optional: true - '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15': - resolution: {integrity: sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==} - engines: {node: '>=14.15.0'} - peerDependencies: - esbuild: '>=0.10.0' - - '@yarnpkg/fslib@2.10.3': - resolution: {integrity: sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==} - engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} - - '@yarnpkg/libzip@2.3.0': - resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==} - engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} - abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -5652,14 +5336,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - address@1.2.2: - resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} - engines: {node: '>= 10.0.0'} - - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - agent-base@7.1.0: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} @@ -5685,14 +5361,6 @@ packages: ajv: optional: true - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -5755,9 +5423,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - app-root-dir@1.0.2: - resolution: {integrity: sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==} - app-root-path@3.1.0: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} engines: {node: '>= 6.0.0'} @@ -5776,9 +5441,6 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} - archy@1.0.0: - resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} - arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -5801,19 +5463,23 @@ packages: array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-includes@3.1.7: - resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.3: - resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} engines: {node: '>= 0.4'} array.prototype.flat@1.3.2: @@ -5828,6 +5494,10 @@ packages: resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==} engines: {node: '>= 0.4'} + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + arrify@1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} @@ -5852,12 +5522,13 @@ packages: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} - assert@2.1.0: - resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} - assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -5866,8 +5537,8 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} - astring@1.8.6: - resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true async-mutex@0.5.0: @@ -5894,8 +5565,12 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - avvio@8.3.0: - resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + avvio@9.0.0: + resolution: {integrity: sha512-UbYrOXgE/I+knFG+3kJr9AgC7uNo8DG+FGGODpH9Bj1O1kL/QDjBXnTem9leD3VdQKtaHjV3O85DQ7hHh4IIHw==} aws-sdk-client-mock@4.0.1: resolution: {integrity: sha512-yD2mmgy73Xce097G5hIpr1k7j50qzvJ49/+6osGZiCyk4m6cwhb+2x7kKFY1gEMwTzaS8+m8fXv9SB29SkRYyQ==} @@ -5909,20 +5584,15 @@ packages: axios@0.24.0: resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} - axios@1.6.0: - resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} + axios@1.7.4: + resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==} - axios@1.6.2: - resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} b4a@1.6.4: resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} - babel-core@7.0.0-bridge.0: - resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5937,21 +5607,6 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-plugin-polyfill-corejs2@0.4.11: - resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-corejs3@0.10.4: - resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-regenerator@0.6.2: - resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-preset-current-node-syntax@1.0.1: resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: @@ -5986,10 +5641,6 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} - big-integer@1.6.51: - resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} - engines: {node: '>=0.6'} - bin-check@4.1.0: resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==} engines: {node: '>=4'} @@ -6006,9 +5657,6 @@ packages: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} @@ -6025,16 +5673,16 @@ packages: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} - bplist-parser@0.2.0: - resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} - engines: {node: '>= 5.10.0'} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -6102,8 +5750,8 @@ packages: resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} engines: {node: '>=6.14.2'} - bullmq@5.10.4: - resolution: {integrity: sha512-YEssEbWBbPXvSW2YMjIBKZdkIPZsOaTGWo1y2wpCFv/wUY+tRLKiSVuHgv09x0QEieybx844f9//UWuarG1JHg==} + bullmq@5.13.2: + resolution: {integrity: sha512-McGE8k3mrCvdUHdU0sHkTKDS1xr4pff+hbEKBY51wk5S6Za0gkuejYA620VQTo3Zz37E/NVWMgumwiXPQ3yZcA==} buraha@0.0.1: resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==} @@ -6112,10 +5760,6 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} - bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -6155,6 +5799,10 @@ packages: call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -6203,6 +5851,10 @@ packages: resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} + chai@5.1.1: + resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==} + engines: {node: '>=12'} + chalk-template@1.1.0: resolution: {integrity: sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==} engines: {node: '>=14.16'} @@ -6233,8 +5885,8 @@ packages: character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} - chart.js@4.4.3: - resolution: {integrity: sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==} + chart.js@4.4.4: + resolution: {integrity: sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==} engines: {pnpm: '>=8'} chartjs-adapter-date-fns@3.0.0: @@ -6261,6 +5913,10 @@ packages: check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + check-more-types@2.24.0: resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} engines: {node: '>= 0.8.0'} @@ -6268,6 +5924,10 @@ packages: cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + cheerio@1.0.0-rc.12: resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} engines: {node: '>= 6'} @@ -6280,8 +5940,8 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - chromatic@11.5.6: - resolution: {integrity: sha512-ycX/hlZLs69BltwwBNsEXr+As6x5/0rlwp6W/CiHMZ3tpm7dmkd+hQCsb8JGHb1h49W3qPOKQ/Lh9evqcJ1yeQ==} + chromatic@11.10.4: + resolution: {integrity: sha512-nfgDpW5gQ4FtgV1lZXXfqLjONKDCh2K4vwI3dbZrtU1ObOL9THyAzpIdnK9LRcNSeisDLX+XFCryfMg1Ql2U2g==} hasBin: true peerDependencies: '@chromatic-com/cypress': ^0.*.* || ^1.0.0 @@ -6342,17 +6002,9 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} - clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -6402,6 +6054,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -6435,14 +6091,6 @@ packages: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} engines: {node: '>= 14'} - compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} - - compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} - computeds@0.0.1: resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} @@ -6492,8 +6140,9 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - core-js-compat@3.37.1: - resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -6523,8 +6172,8 @@ packages: resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==} engines: {node: '>=12.0.0'} - cropperjs@2.0.0-rc.1: - resolution: {integrity: sha512-Y9ciurIuK6G1vy0ErHC8Gt6wHWvsHWJ5fgE60GL6vsuF2WzHwDpH7F1yof40XAEheeSN4v3rD09D1VZ7kiiSOA==} + cropperjs@2.0.0-rc.2: + resolution: {integrity: sha512-BTuz+UeZphGOEnBCuQiNT4rk1uFfKJaKmTgoH9XU7Q8IMkLdodW7YPWINmXJXwWMt1nXiKze5qKADVbz9xtVFg==} cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} @@ -6544,10 +6193,6 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - crypto-random-string@4.0.0: - resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} - engines: {node: '>=12'} - css-declaration-sorter@7.2.0: resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} engines: {node: ^14 || ^16 || >=18} @@ -6606,8 +6251,13 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - cypress@13.13.1: - resolution: {integrity: sha512-8F9UjL5MDUdgC/S5hr8CGLHbS5gGht5UOV184qc2pFny43fnkoaKxlzH/U6//zmGu/xRTaKimNfjknLT8+UDFg==} + cypress@13.14.2: + resolution: {integrity: sha512-lsiQrN17vHMB2fnvxIrKLAjOr9bPwsNbPZNrWf99s4u+DVmCY6U+w7O3GGG9FvP4EUVYaDu+guWeNLiUzBrqvA==} + engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + hasBin: true + + cypress@13.15.0: + resolution: {integrity: sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -6623,6 +6273,18 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -6667,6 +6329,15 @@ packages: supports-color: optional: true + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -6708,6 +6379,10 @@ packages: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-equal@2.2.0: resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==} @@ -6718,17 +6393,14 @@ packages: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} - default-browser-id@3.0.0: - resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} - engines: {node: '>=12'} - - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - defer-to-connect@2.0.1: resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} engines: {node: '>=10'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} engines: {node: '>=8'} @@ -6737,8 +6409,9 @@ packages: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -6760,10 +6433,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -6772,14 +6441,6 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - detect-package-manager@2.0.1: - resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==} - engines: {node: '>=12'} - - detect-port@1.5.1: - resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} - hasBin: true - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -6825,22 +6486,35 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domhandler@3.3.0: + resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} + engines: {node: '>= 4'} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + domutils@3.0.1: resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} - dotenv-expand@10.0.0: - resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} - engines: {node: '>=12'} + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} @@ -6887,13 +6561,17 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encode-utf8@1.0.3: - resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} - encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} @@ -6919,11 +6597,6 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - envinfo@7.8.1: - resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} - engines: {node: '>=4'} - hasBin: true - err-code@2.0.3: resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} @@ -6934,26 +6607,46 @@ packages: resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} engines: {node: '>= 0.4'} + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + es-shim-unscopables@1.0.0: resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + es-to-primitive@1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} - esbuild-plugin-alias@0.2.1: - resolution: {integrity: sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==} - esbuild-register@3.5.0: resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==} peerDependencies: @@ -6979,10 +6672,19 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} + escape-goat@3.0.0: + resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} + engines: {node: '>=10'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -7023,8 +6725,8 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-module-utils@2.8.0: - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -7044,8 +6746,8 @@ packages: eslint-import-resolver-webpack: optional: true - eslint-plugin-import@2.29.1: - resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + eslint-plugin-import@2.30.0: + resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -7060,6 +6762,12 @@ packages: peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint-plugin-vue@9.28.0: + resolution: {integrity: sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint-rule-docs@1.1.235: resolution: {integrity: sha512-+TQ+x4JdTnDoFEXXb3fDvfGOwnyNV7duH8fXWTPD1ieaBmB8omj7Gw/pMBBu4uI2uJCCU8APDaQJzWuXnTsH4A==} @@ -7082,6 +6790,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true eslint@9.8.0: @@ -7172,8 +6881,8 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} - execa@9.3.0: - resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + execa@9.4.0: + resolution: {integrity: sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw==} engines: {node: ^18.19.0 || >=20.5.0} executable@4.1.1: @@ -7195,6 +6904,10 @@ packages: resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} + express@4.21.0: + resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==} + engines: {node: '>= 0.10.0'} + ext-list@2.2.2: resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} engines: {node: '>=0.10.0'} @@ -7215,8 +6928,8 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} - fast-content-type-parse@1.1.0: - resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + fast-content-type-parse@2.0.0: + resolution: {integrity: sha512-fCqg/6Sps8tqk8p+kqyKqYfOF0VjPNYrqpLiqNl0RBKmD80B080AJWVV6EkSkscjToNExcXg1+Mfzftrx6+iSA==} fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -7234,8 +6947,8 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-json-stringify@5.8.0: - resolution: {integrity: sha512-VVwK8CFMSALIvt14U8AvrSzQAwN/0vaVRiFFUVlpnXSnDGrSkOAO5MtzyN8oQNjLd5AqTW5OZRgyjoNuAuR3jQ==} + fast-json-stringify@6.0.0: + resolution: {integrity: sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -7250,8 +6963,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@2.2.0: - resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} + fast-uri@2.4.0: + resolution: {integrity: sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==} fast-uri@3.0.1: resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==} @@ -7260,8 +6973,8 @@ packages: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} hasBin: true - fast-xml-parser@4.4.0: - resolution: {integrity: sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==} + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true fastify-multer@2.0.3: @@ -7271,15 +6984,18 @@ packages: fastify-plugin@2.3.4: resolution: {integrity: sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ==} - fastify-plugin@4.5.0: - resolution: {integrity: sha512-79ak0JxddO0utAXAQ5ccKhvs6vX2MGyHHMMsmZkBANrq3hXc1CHzvNPHOcvTsVMEPl5I+NT+RO4YKMGehOfSIg==} + fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} - fastify-raw-body@4.3.0: - resolution: {integrity: sha512-F4o8ZIMVx4YoxGfwrZys6wyjl40gF3Yv6AWWRy62ozFAyZBSS831/uyyCAqKYw3tR73g180ryG98yih6To1PUQ==} + fastify-plugin@5.0.1: + resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + + fastify-raw-body@5.0.0: + resolution: {integrity: sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA==} engines: {node: '>= 10'} - fastify@4.28.1: - resolution: {integrity: sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==} + fastify@5.0.0: + resolution: {integrity: sha512-Qe4dU+zGOzg7vXjw4EvcuyIbNnMwTmcuOhlOrOJsgwzvjEZmsM/IeHulgJk+r46STjdJS/ZJbxO8N70ODXDMEQ==} fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -7287,9 +7003,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fd-package-json@1.2.0: - resolution: {integrity: sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==} - fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -7301,9 +7014,6 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} - fetch-retry@5.0.4: - resolution: {integrity: sha512-LXcdgpdcVedccGg0AZqg+S8lX/FCdwXD92WNZ5k5qsb0irRhSFsBOpcJt7oevyqT2/C2nEE0zSFNdBEpj3YOSw==} - figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -7320,15 +7030,12 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - file-system-cache@2.3.0: - resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} - file-type@17.1.6: resolution: {integrity: sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - file-type@19.3.0: - resolution: {integrity: sha512-mROwiKLZf/Kwa/2Rol+OOZQn1eyTkPB3ZTwC0ExY6OLFCbgxHYZvBm7xI77NvfZFMKBsmuXfmLJnD4eEftEhrA==} + file-type@19.5.0: + resolution: {integrity: sha512-dMuq6WWnP6BpQY0zYJNpTtQWgeCImSMG0BTIzUBXvxbwc1HWP/E7AE4UWU9XSCOPGJuOHda0HpDnwM2FW+d90A==} engines: {node: '>=18'} filelist@1.0.4: @@ -7354,25 +7061,21 @@ packages: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} - find-cache-dir@2.1.0: - resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} - engines: {node: '>=6'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} find-cache-dir@3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} - find-my-way@8.2.0: - resolution: {integrity: sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==} + find-my-way@9.1.0: + resolution: {integrity: sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==} engines: {node: '>=14'} find-package-json@1.2.0: resolution: {integrity: sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==} - find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -7400,10 +7103,6 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - flow-parser@0.202.0: - resolution: {integrity: sha512-ZiXxSIXK3zPmY3zrzCofFonM2T+/3Jz5QZKJyPVtUERQEJUnYkXBQ+0H3FzyqiyJs+VXqb/UNU6/K6sziVYdxw==} - engines: {node: '>=0.4.0'} - fluent-ffmpeg@2.1.3: resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} engines: {node: '>=18'} @@ -7417,6 +7116,15 @@ packages: debug: optional: true + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -7435,10 +7143,6 @@ packages: resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==} engines: {node: '>= 18'} - form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} - form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -7497,6 +7201,10 @@ packages: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} engines: {node: '>= 0.4'} + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} @@ -7514,6 +7222,10 @@ packages: get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -7542,6 +7254,10 @@ packages: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + get-tsconfig@4.7.2: resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} @@ -7551,10 +7267,6 @@ packages: getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} - giget@1.1.2: - resolution: {integrity: sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==} - hasBin: true - github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -7572,9 +7284,6 @@ packages: peerDependencies: glob: ^7.1.6 - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} @@ -7610,8 +7319,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@15.8.0: - resolution: {integrity: sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==} + globals@15.9.0: + resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} engines: {node: '>=18'} globalthis@1.0.3: @@ -7622,10 +7331,6 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} - globby@14.0.1: - resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==} - engines: {node: '>=18'} - gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -7658,11 +7363,6 @@ packages: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} - handlebars@4.7.7: - resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} - engines: {node: '>=0.4.7'} - hasBin: true - happy-dom@10.0.3: resolution: {integrity: sha512-WkCP+Z5fX6U5PY+yHP3ElV5D9PoxRAHRWPFq3pG9rg/6Hjf5ak7dozAgSCywsTRUq2qfa8vV8OQvUy5pRXy8EQ==} @@ -7688,10 +7388,17 @@ packages: has-property-descriptors@1.0.0: resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -7700,6 +7407,10 @@ packages: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -7707,13 +7418,14 @@ packages: hash-sum@2.0.0: resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - hashlru@2.3.0: - resolution: {integrity: sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==} - hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + hast-util-heading-rank@3.0.0: resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} @@ -7733,8 +7445,8 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - highlight.js@11.9.0: - resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} + highlight.js@11.10.0: + resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==} engines: {node: '>=12.0.0'} hosted-git-info@2.8.9: @@ -7766,9 +7478,15 @@ packages: resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==} engines: {node: '>=0.10'} + htmlparser2@5.0.1: + resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} + htmlparser2@8.0.1: resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==} + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-cache-semantics@4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -7784,8 +7502,8 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} - http-signature@1.3.6: - resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} engines: {node: '>=0.10'} http2-wrapper@1.0.3: @@ -7800,18 +7518,10 @@ packages: resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} engines: {node: '>=16'} - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - https-proxy-agent@7.0.2: resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} engines: {node: '>= 14'} - https-proxy-agent@7.0.4: - resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} - engines: {node: '>= 14'} - https-proxy-agent@7.0.5: resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} @@ -7832,8 +7542,8 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - human-signals@7.0.0: - resolution: {integrity: sha512-74kytxOUSvNbjrT9KisAbaTZ/eJwD/LrbM/kh5j0IhPuJzwuA19dWvniFGwBzN9rVjg+O/e+F310PjObDXS+9Q==} + human-signals@8.0.0: + resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} engines: {node: '>=18.18.0'} iconv-lite@0.4.24: @@ -7920,6 +7630,10 @@ packages: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} + internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + intersection-observer@0.12.2: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} @@ -7931,8 +7645,8 @@ packages: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} - ip-cidr@4.0.1: - resolution: {integrity: sha512-V5Nce94SVJ7NtyT/UKUeTM7sY3V7TEk48hURhtBgTiGduOa5t6p9Hd+zBOGvr4Gu7iWPxFVYNl017p0akQA84w==} + ip-cidr@4.0.2: + resolution: {integrity: sha512-KifhLKBjdS/hB3TD4UUOalVp1BpzPFvRpgJvXcP0Ya98tuSQTUQ71iI7EW7CKddkBJTYB3GfTWl5eJwpLOXj2A==} engines: {node: '>=16.14.0'} ip-regex@4.3.0: @@ -7965,6 +7679,10 @@ packages: is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -7993,6 +7711,14 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} @@ -8032,10 +7758,6 @@ packages: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} - is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - is-ip@3.1.0: resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==} engines: {node: '>=8'} @@ -8046,14 +7768,14 @@ packages: is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} - is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - is-negative-zero@2.0.2: resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} engines: {node: '>= 0.4'} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} @@ -8077,10 +7799,6 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -8101,6 +7819,10 @@ packages: is-shared-array-buffer@1.0.2: resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + is-stream@1.1.0: resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} engines: {node: '>=0.10.0'} @@ -8121,8 +7843,8 @@ packages: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} - is-svg@5.0.1: - resolution: {integrity: sha512-mLYxDsfisQWdS4+gSblAwhATDoNMS/tx8G7BKA+aBIf7F0m1iUwMvuKAo6mW4WMleQAEE50I1Zqef9yMMfHk3w==} + is-svg@5.1.0: + resolution: {integrity: sha512-uVg5yifaTxHoefNf5Jcx+i9RZe2OBYd/UStp1umx+EERa4xGRa3LLGXjoEph43qUORC0qkafUgrXZ6zzK89yGA==} engines: {node: '>=14.16'} is-symbol@1.0.4: @@ -8133,6 +7855,10 @@ packages: resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} engines: {node: '>= 0.4'} + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + is-typedarray@1.0.0: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} @@ -8173,10 +7899,6 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} @@ -8366,6 +8088,9 @@ packages: joi@17.11.0: resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==} + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + js-beautify@1.14.9: resolution: {integrity: sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==} engines: {node: '>=12'} @@ -8398,14 +8123,9 @@ packages: resolution: {integrity: sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ==} engines: {node: '>=0.1.90'} - jscodeshift@0.15.1: - resolution: {integrity: sha512-hIJfxUy8Rt4HkJn/zZPU9ChKfKZM1342waJ1QC2e2YsPcWhM+3BJ4dcfQCzArTrk1jJeNLB341H+qOcEHRxJZg==} - hasBin: true - peerDependencies: - '@babel/preset-env': ^7.1.6 - peerDependenciesMeta: - '@babel/preset-env': - optional: true + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} jsdom@24.1.1: resolution: {integrity: sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==} @@ -8416,10 +8136,6 @@ packages: canvas: optional: true - jsesc@0.5.0: - resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} - hasBin: true - jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -8431,6 +8147,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -8493,6 +8212,11 @@ packages: jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + juice@11.0.0: + resolution: {integrity: sha512-sGF8hPz9/Wg+YXbaNDqc1Iuoaw+J/P9lBHNQKXAGc9pPNjCd4fyPai0Zxj7MRtdjMr0lcgk5PjEIkP2b8R9F3w==} + engines: {node: '>=18.17'} + hasBin: true + just-extend@4.2.1: resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==} @@ -8502,8 +8226,8 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} - katex@0.16.9: - resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} + katex@0.16.10: + resolution: {integrity: sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==} hasBin: true keyv@4.5.4: @@ -8535,10 +8259,6 @@ packages: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} - lazy-universal-dotenv@4.0.0: - resolution: {integrity: sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==} - engines: {node: '>=14.0.0'} - lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -8551,8 +8271,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - light-my-request@5.11.0: - resolution: {integrity: sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==} + light-my-request@6.1.0: + resolution: {integrity: sha512-+NFuhlOGoEwxeQfJ/pobkVFxcnKyDtiX847hLjuB/IzBxIl3q4VJeFI8uRCgb3AlTWL1lgOr+u5+8QdUcr33ng==} lilconfig@3.1.1: resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} @@ -8574,10 +8294,6 @@ packages: resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} - locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -8586,9 +8302,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -8631,6 +8344,9 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -8679,16 +8395,15 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magicast@0.3.4: resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} mailcheck@1.1.1: resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==} - make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} - engines: {node: '>=6'} - make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -8781,12 +8496,15 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - meilisearch@0.41.0: - resolution: {integrity: sha512-5KcGLxEXD7E+uNO7R68rCbGSHgCqeM3Q3RFFLSsN7ZrIgr8HPDXVAIlP4LHggAZfk0FkSzo8VSXifHCwa2k80g==} + meilisearch@0.42.0: + resolution: {integrity: sha512-pXaOPx/uhVGYVpejNuOcXifQVJlRVSxtvpgrGKb7ygmYo4qSNXkQXPxq1p0Tv+4/RsPJug3W04pcNnYXiqungA==} memoizerific@1.11.3: resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} + mensch@0.3.4: + resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + meow@9.0.0: resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} engines: {node: '>=10'} @@ -8794,6 +8512,9 @@ packages: merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -8893,8 +8614,8 @@ packages: micromark@4.0.0: resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} - micromatch@4.0.7: - resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} mime-db@1.52.0: @@ -8910,6 +8631,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -9032,8 +8758,8 @@ packages: mlly@1.5.0: resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==} - mnemonist@0.39.6: - resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==} + mnemonist@0.39.8: + resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} mock-socket@9.3.1: resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==} @@ -9042,10 +8768,6 @@ packages: module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -9081,6 +8803,16 @@ packages: typescript: optional: true + msw@2.4.9: + resolution: {integrity: sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -9128,9 +8860,6 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nested-property@4.0.0: resolution: {integrity: sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==} @@ -9159,17 +8888,10 @@ packages: resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} engines: {node: ^16 || ^18 || >= 20} - node-dir@0.1.17: - resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} - engines: {node: '>= 0.10.5'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} - node-fetch-native@1.0.2: - resolution: {integrity: sha512-KIkvH1jl6b3O7es/0ShyCgWLcfXxlBrLBbP3rOr23WArC66IMcU4DeZEeYEOwnopYhawLTn7/y+YtmASe8DFVQ==} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -9195,8 +8917,8 @@ packages: resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} hasBin: true - node-gyp@10.1.0: - resolution: {integrity: sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==} + node-gyp@10.2.0: + resolution: {integrity: sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==} engines: {node: ^16.14.0 || >=18.0.0} hasBin: true @@ -9206,8 +8928,8 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - nodemailer@6.9.14: - resolution: {integrity: sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==} + nodemailer@6.9.15: + resolution: {integrity: sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==} engines: {node: '>=6.0.0'} nodemon@3.0.2: @@ -9215,8 +8937,8 @@ packages: engines: {node: '>=10'} hasBin: true - nodemon@3.1.4: - resolution: {integrity: sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==} + nodemon@3.1.7: + resolution: {integrity: sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==} engines: {node: '>=10'} hasBin: true @@ -9253,10 +8975,6 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} - normalize-url@8.0.0: - resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} - engines: {node: '>=14.16'} - normalize-url@8.0.1: resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} engines: {node: '>=14.16'} @@ -9277,6 +8995,10 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -9304,6 +9026,10 @@ packages: object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + object-is@1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} engines: {node: '>= 0.4'} @@ -9316,15 +9042,20 @@ packages: resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} engines: {node: '>= 0.4'} - object.fromentries@2.0.7: - resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} engines: {node: '>= 0.4'} - object.groupby@1.0.1: - resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} - object.values@1.1.7: - resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} obliterator@2.0.4: @@ -9344,10 +9075,6 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -9380,10 +9107,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - os-filter-obj@2.0.0: resolution: {integrity: sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==} engines: {node: '>=4'} @@ -9394,12 +9117,15 @@ packages: ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} - otpauth@9.3.1: - resolution: {integrity: sha512-E6d2tMxPofHNk4sRFp+kqW7vQ+WJGO9VLI2N/W00DnI+ThskU12Qa10kyNSGklrzhN5c+wRUsN4GijVgCU2N9w==} + otpauth@9.3.2: + resolution: {integrity: sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==} outvariant@1.4.2: resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -9428,10 +9154,6 @@ packages: resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} engines: {node: '>=18'} - p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -9483,6 +9205,9 @@ packages: parse5-htmlparser2-tree-adapter@7.0.0: resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + parse5@5.1.1: resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} @@ -9499,10 +9224,6 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -9534,43 +9255,49 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} path-to-regexp@1.8.0: resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} - path-to-regexp@3.2.0: - resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} path-to-regexp@6.2.1: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - path-type@5.0.0: - resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} - engines: {node: '>=12'} - pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + pause-stream@0.0.11: resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} - peek-readable@5.0.0: - resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==} - engines: {node: '>=14.16'} - peek-readable@5.1.3: resolution: {integrity: sha512-kCsc9HwH5RgVA3H3VqkWFyGQwsxUxLdiSX1d5nqAm7hnMFjNFX1VhBLmJoUY0hZNc8gmDNgBkLjfhiWPsziXWA==} engines: {node: '>=14.16'} + peek-readable@5.2.0: + resolution: {integrity: sha512-U94a+eXHzct7vAd19GH3UQ2dH4Satbng0MyYTMaQatL0pvYYL5CTPR25HBhKtecl+4bfu1/i3vC6k0hydO5Vcw==} + engines: {node: '>=14.16'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -9580,8 +9307,8 @@ packages: pg-cloudflare@1.1.1: resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} - pg-connection-string@2.6.4: - resolution: {integrity: sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==} + pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} @@ -9591,14 +9318,17 @@ packages: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} - pg-pool@3.6.2: - resolution: {integrity: sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==} + pg-pool@3.7.0: + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} peerDependencies: pg: '>=8.0' pg-protocol@1.6.1: resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} + pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -9607,8 +9337,8 @@ packages: resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==} engines: {node: '>=10'} - pg@8.12.0: - resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==} + pg@8.13.0: + resolution: {integrity: sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -9629,6 +9359,9 @@ packages: picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picocolors@1.1.0: + resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -9641,10 +9374,6 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - pino-abstract-transport@1.2.0: resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} @@ -9666,18 +9395,10 @@ packages: resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==} engines: {node: '>=16.20.0'} - pkg-dir@3.0.0: - resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} - engines: {node: '>=6'} - pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - pkg-dir@5.0.0: - resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} - engines: {node: '>=10'} - pkg-types@1.0.3: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} @@ -9696,6 +9417,10 @@ packages: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} engines: {node: '>=10'} + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + postcss-calc@9.0.1: resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} engines: {node: ^14 || ^16 || >=18.0} @@ -9873,8 +9598,8 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.40: - resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} + postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -9933,10 +9658,6 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-hrtime@1.0.3: - resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} - engines: {node: '>= 0.8'} - pretty-ms@9.0.0: resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==} engines: {node: '>=18'} @@ -9947,8 +9668,8 @@ packages: probe-image-size@7.2.3: resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==} - proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} process-exists@5.0.0: @@ -9958,12 +9679,12 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - process-warning@2.2.0: - resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==} - process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} + process-warning@4.0.0: + resolution: {integrity: sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -10075,21 +9796,17 @@ packages: resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} engines: {node: '>=6.0.0'} - qrcode@1.5.3: - resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} engines: {node: '>=10.13.0'} hasBin: true - qs@6.10.4: - resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} - engines: {node: '>=0.6'} - qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} - qs@6.11.1: - resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} querystringify@2.2.0: @@ -10115,9 +9832,6 @@ packages: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} - ramda@0.29.0: - resolution: {integrity: sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==} - random-seed@0.3.0: resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==} engines: {node: '>= 0.6.0'} @@ -10133,12 +9847,16 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rdf-canonize@3.4.0: resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==} engines: {node: '>=12'} - re2@1.21.3: - resolution: {integrity: sha512-GI+KoGkHT4kxTaX+9p0FgNB1XUnCndO9slG5qqeEoZ7kbf6Dk6ohQVpmwKVeSp7LPLn+g6Q3BaCopz4oHuBDuQ==} + re2@1.21.4: + resolution: {integrity: sha512-MVIfXWJmsP28mRsSt8HeL750ifb8H5+oF2UDIxGaiJCr8fkMqhLZ7kcX9ADRk2dC8qeGKedB7UVYRfBVpEiLfA==} react-colorful@5.6.1: resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} @@ -10245,30 +9963,16 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - regenerate-unicode-properties@10.1.0: - resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} - engines: {node: '>=4'} - - regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - regenerator-transform@0.15.2: - resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} - regexpu-core@5.3.2: - resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} - engines: {node: '>=4'} - - regjsparser@0.9.1: - resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} - hasBin: true + regexp.prototype.flags@1.5.3: + resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} + engines: {node: '>= 0.4'} rehype-external-links@3.0.0: resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} @@ -10346,8 +10050,8 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - ret@0.4.3: - resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} retry@0.12.0: @@ -10361,18 +10065,16 @@ packages: rfdc@1.3.0: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - rimraf@2.6.3: - resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup@4.19.1: - resolution: {integrity: sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==} + rollup@4.22.5: + resolution: {integrity: sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -10395,6 +10097,10 @@ packages: resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} engines: {node: '>=0.4'} + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -10404,8 +10110,12 @@ packages: safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} - safe-regex2@3.1.0: - resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + + safe-regex2@4.0.0: + resolution: {integrity: sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==} safe-stable-stringify@2.4.2: resolution: {integrity: sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==} @@ -10417,8 +10127,8 @@ packages: sanitize-html@2.13.0: resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==} - sass@1.77.8: - resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==} + sass@1.79.3: + resolution: {integrity: sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==} engines: {node: '>=14.0.0'} hasBin: true @@ -10435,6 +10145,9 @@ packages: secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + secure-json-parse@3.0.0: + resolution: {integrity: sha512-YO+gVWyp97H+nCG/qdC8X819iKx5g+BpnO9nYT4uFq4uyI0rSxwtx5qD9rGfScg7FGLYu/YBf8uOtwQKv+gq8g==} + seedrandom@3.0.5: resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} @@ -10464,20 +10177,41 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + serve-static@1.15.0: resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} engines: {node: '>= 0.8.0'} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} set-cookie-parser@2.6.0: resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -10488,13 +10222,9 @@ packages: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true - shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} - - sharp@0.33.4: - resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==} - engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} @@ -10524,6 +10254,10 @@ packages: side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -10635,10 +10369,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -10647,6 +10377,9 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} + slick@1.12.2: + resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} + smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -10677,6 +10410,10 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -10724,6 +10461,11 @@ packages: engines: {node: '>=0.10.0'} hasBin: true + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + ssri@10.0.4: resolution: {integrity: sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -10738,8 +10480,8 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - start-server-and-test@2.0.4: - resolution: {integrity: sha512-CKNeBTcP0hVqIlNismHMudb9q3lLdAjcVPO13/7gfI66fcJpeIb/o4NzQd1JK/CD+lfWVqr10ZH9Y14+OwlJuw==} + start-server-and-test@2.0.8: + resolution: {integrity: sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==} engines: {node: '>=16'} hasBin: true @@ -10754,9 +10496,6 @@ packages: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} - store2@2.14.2: - resolution: {integrity: sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==} - storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640: resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640} version: 0.0.0 @@ -10776,8 +10515,8 @@ packages: react-dom: optional: true - storybook@8.2.6: - resolution: {integrity: sha512-8j30wDxQmkcqI0fWcSYFsUCjErsY1yTWbTW+yjbwM8DyW18Cud6CwbFRCxjFsH+2M0CjP6Pqs/m1PGI0vcQscQ==} + storybook@8.3.3: + resolution: {integrity: sha512-FG2KAVQN54T9R6voudiEftehtkXtLO+YVGP2gBPfacEdDQjY++ld7kTbHzpTT/bpCDx7Yq3dqOegLm9arVJfYw==} hasBin: true stream-browserify@3.0.0: @@ -10789,10 +10528,6 @@ packages: stream-parser@0.3.1: resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} - stream-wormhole@1.1.0: - resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} - engines: {node: '>=4.0.0'} - streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -10826,12 +10561,23 @@ packages: resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} engines: {node: '>= 0.4'} + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + string.prototype.trimend@1.0.6: resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + string.prototype.trimstart@1.0.6: resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -10899,8 +10645,8 @@ packages: resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==} engines: {node: '>=14.16'} - strtok3@8.0.1: - resolution: {integrity: sha512-HNkTAnNWQj2YBzfTtoC5OQyu1QwPsMwiB7VyQmNvQKCrmEDSvFB857Vh97UY9InGLNRAB91sdS1ztifRo/3hdA==} + strtok3@8.1.0: + resolution: {integrity: sha512-ExzDvHYPj6F6QkSNe/JxSlBxTh3OrI6wrAIz53ulxo1c4hBJ1bT9C/JrAthEKHWG9riVH3Xzg7B03Oxty6S2Lw==} engines: {node: '>=16'} stylehacks@6.1.1: @@ -10941,8 +10687,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - systeminformation@5.22.11: - resolution: {integrity: sha512-aLws5yi4KCHTb0BVvbodQY5bY8eW4asMRDTxTW46hqw9lGjACX6TlLdJrkdoHYRB0qs+MekqEq1zG7WDnWE8Ug==} + systeminformation@5.23.5: + resolution: {integrity: sha512-PEpJwhRYxZgBCAlWZhWIgfMTjXLqfcaZ1pJsJn9snWNfBW/Z1YQg1mbIUSWrEV3ErAHF7l/OoVLQeaZDlPzkpA==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -10961,20 +10707,8 @@ packages: telejson@7.2.0: resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} - temp-dir@3.0.0: - resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} - engines: {node: '>=14.16'} - - temp@0.8.4: - resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} - engines: {node: '>=6.0.0'} - - tempy@3.1.0: - resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} - engines: {node: '>=14.16'} - - terser@5.31.3: - resolution: {integrity: sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==} + terser@5.33.0: + resolution: {integrity: sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g==} engines: {node: '>=10'} hasBin: true @@ -11001,8 +10735,8 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - three@0.167.0: - resolution: {integrity: sha512-9Y1a66fpjqF3rhq7ivKTaKtjQLZ97Hj/lZ00DmZWaKHaQFH4uzYT1znwRDWQOcgMmCcOloQzo61gDmqO8l9xmA==} + three@0.169.0: + resolution: {integrity: sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==} throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} @@ -11017,10 +10751,6 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tiny-lru@10.0.1: - resolution: {integrity: sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA==} - engines: {node: '>=6'} - tinybench@2.6.0: resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} @@ -11031,10 +10761,18 @@ packages: resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + tinyspy@2.2.0: resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} engines: {node: '>=14.0.0'} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tmp@0.2.3: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} @@ -11107,8 +10845,8 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-case-convert@2.0.2: - resolution: {integrity: sha512-vdKfx1VAdpvEBOBv5OpVu5ZFqRg9HdTI4sYt6qqMeICBeNyXvitrarCnFWNDAki51IKwCyx+ZssY46Q9jH5otA==} + ts-case-convert@2.0.7: + resolution: {integrity: sha512-Kqj8wrkuduWsKUOUNRczrkdHCDt4ZNNd6HKjVw42EnMIGHQUABS4pqfy0acETVLwUTppc1fzo/yi11+uMTaqzw==} ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} @@ -11149,20 +10887,20 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} - tsd@0.31.1: - resolution: {integrity: sha512-sSL84A0SFwx2xGMWrxlGaarKFSQszWjJS2vgNDDLwatytzg2aq6ShlwHsBYxRNmjzXISODwMva5ZOdAg/4AoOA==} + tsd@0.31.2: + resolution: {integrity: sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==} engines: {node: '>=14.16'} hasBin: true - tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tsx@4.4.0: resolution: {integrity: sha512-4fwcEjRUxW20ciSaMB8zkpGwCPxuRGnadDuj/pBk5S9uT29zvWz15PK36GrKJo45mSJomDxVejZ73c6lr3811Q==} engines: {node: '>=18.0.0'} @@ -11202,10 +10940,6 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} - type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -11222,17 +10956,33 @@ packages: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + typed-array-byte-length@1.0.0: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + typed-array-byte-offset@1.0.0: resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} engines: {node: '>= 0.4'} + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -11321,14 +11071,14 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.6.2: + resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + engines: {node: '>=14.17'} + hasBin: true + ufo@1.3.2: resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} - uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} - engines: {node: '>=0.8.0'} - hasBin: true - uid2@0.0.4: resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} @@ -11353,28 +11103,19 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici@5.28.2: resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} engines: {node: '>=14.0'} - unicode-canonical-property-names-ecmascript@2.0.0: - resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} - engines: {node: '>=4'} - - unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - - unicode-match-property-value-ecmascript@2.1.0: - resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} - engines: {node: '>=4'} - - unicode-property-aliases-ecmascript@2.1.0: - resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} - engines: {node: '>=4'} + undici@6.20.0: + resolution: {integrity: sha512-AITZfPuxubm31Sx0vr8bteSalEbs9wQb/BOBi9FPlD9Qpd6HxZ4Q0+hI742jBhkPb4RT2v5MQzaW5VhRVyj+9A==} + engines: {node: '>=18.17'} - unicorn-magic@0.1.0: - resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} unified@11.0.4: @@ -11388,10 +11129,6 @@ packages: resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - unique-string@3.0.0: - resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} - engines: {node: '>=12'} - unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -11472,8 +11209,8 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - v-code-diff@1.12.0: - resolution: {integrity: sha512-vvdCBG02mIIiW6Gx6jF119hzxELt+6TlJIwchglR1JYzboHePNxIkVBjR/aoAOVlsGa+5Vtb77cd/N84nrXWPA==} + v-code-diff@1.13.1: + resolution: {integrity: sha512-9LTV1dZhC1oYTntyB94vfumGgsfIX5u0fEDSI2Txx4vCE5sI5LkgeLJRRy2SsTVZmDcV+R73sBr0GpPn0TJxMw==} peerDependencies: '@vue/composition-api': ^1.4.9 vue: ^2.6.0 || >=3.0.0 @@ -11485,6 +11222,10 @@ packages: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} + valid-data-url@3.0.1: + resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} + engines: {node: '>=10'} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -11510,8 +11251,8 @@ packages: vite-plugin-turbosnap@1.0.3: resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==} - vite@5.3.5: - resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} + vite@5.4.8: + resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -11519,6 +11260,7 @@ packages: less: '*' lightningcss: ^1.21.0 sass: '*' + sass-embedded: '*' stylus: '*' sugarss: '*' terser: ^5.4.0 @@ -11531,6 +11273,8 @@ packages: optional: true sass: optional: true + sass-embedded: + optional: true stylus: optional: true sugarss: @@ -11617,9 +11361,6 @@ packages: vue-component-type-helpers@2.0.16: resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==} - vue-component-type-helpers@2.0.29: - resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==} - vue-component-type-helpers@2.1.6: resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==} @@ -11645,12 +11386,6 @@ packages: peerDependencies: eslint: '>=6.0.0' - vue-i18n@9.13.1: - resolution: {integrity: sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==} - engines: {node: '>= 16'} - peerDependencies: - vue: ^3.0.0 - vue-inbrowser-compiler-independent-utils@4.71.1: resolution: {integrity: sha512-K3wt3iVmNGaFEOUR4JIThQRWfqokxLfnPslD41FDZB2ajXp789+wCqJyGYlIFsvEQ2P61PInw6/ph5iiqg51gg==} peerDependencies: @@ -11659,8 +11394,8 @@ packages: vue-template-compiler@2.7.14: resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} - vue-tsc@2.0.29: - resolution: {integrity: sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==} + vue-tsc@2.1.6: + resolution: {integrity: sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -11673,6 +11408,14 @@ packages: typescript: optional: true + vue@3.5.10: + resolution: {integrity: sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + vuedraggable@4.1.0: resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} peerDependencies: @@ -11682,29 +11425,23 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - wait-on@7.2.0: - resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} + wait-on@8.0.1: + resolution: {integrity: sha512-1wWQOyR2LVVtaqrcIL2+OM+x7bkpmzVROa0Nf6FryXkS+er5Sa1kzFGjzZRqLnHa3n1rACFLeTwUqE1ETL9Mig==} engines: {node: '>=12.0.0'} hasBin: true - walk-up-path@3.0.1: - resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} - walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} - - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - web-push@3.6.7: resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} engines: {node: '>= 16'} hasBin: true + web-resource-inliner@7.0.0: + resolution: {integrity: sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==} + engines: {node: '>=10.0.0'} + web-streams-polyfill@3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} @@ -11763,6 +11500,10 @@ packages: resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} engines: {node: '>= 0.4'} + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -11790,9 +11531,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -11808,15 +11546,12 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@2.4.3: - resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} - write-file-atomic@4.0.2: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@8.14.2: - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -11933,7 +11668,7 @@ packages: snapshots: - '@adobe/css-tools@4.3.3': {} + '@adobe/css-tools@4.4.0': {} '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz': dependencies: @@ -11945,17 +11680,11 @@ snapshots: '@ampproject/remapping@2.2.1': dependencies: - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 - - '@apidevtools/openapi-schemas@2.1.0': {} + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 '@apidevtools/swagger-methods@3.0.2': {} - '@aw-web-design/x-default-browser@1.4.126': - dependencies: - default-browser-id: 3.0.0 - '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -12465,13 +12194,13 @@ snapshots: '@babel/code-frame@7.23.5': dependencies: - '@babel/highlight': 7.23.4 + '@babel/highlight': 7.24.7 chalk: 2.4.2 '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 - picocolors: 1.0.0 + picocolors: 1.0.1 '@babel/compat-data@7.23.5': {} @@ -12524,17 +12253,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 - '@babel/helper-annotate-as-pure@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': - dependencies: - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-compilation-targets@7.22.15': dependencies: '@babel/compat-data': 7.23.5 @@ -12551,70 +12269,19 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.7 - '@babel/helper-optimise-call-expression': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - regexpu-core: 5.3.2 - semver: 6.3.1 - - '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - debug: 4.3.5(supports-color@8.1.1) - lodash.debounce: 4.0.8 - resolve: 1.22.8 - transitivePeerDependencies: - - supports-color - - '@babel/helper-environment-visitor@7.22.20': {} - '@babel/helper-environment-visitor@7.24.7': dependencies: '@babel/types': 7.24.7 - '@babel/helper-function-name@7.23.0': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.7 - '@babel/helper-function-name@7.24.7': dependencies: '@babel/template': 7.24.7 '@babel/types': 7.24.7 - '@babel/helper-hoist-variables@7.22.5': - dependencies: - '@babel/types': 7.24.7 - '@babel/helper-hoist-variables@7.24.7': dependencies: '@babel/types': 7.24.7 - '@babel/helper-member-expression-to-functions@7.24.7': - dependencies: - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-imports@7.22.15': dependencies: '@babel/types': 7.24.7 @@ -12628,796 +12295,222 @@ snapshots: '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.23.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.24.7 - - '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-optimise-call-expression@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-plugin-utils@7.22.5': {} - - '@babel/helper-plugin-utils@7.24.7': {} - - '@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-wrap-function': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-replace-supers@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.7 - '@babel/helper-optimise-call-expression': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-simple-access@7.22.5': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-simple-access@7.24.7': - dependencies: - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-skip-transparent-expression-wrappers@7.24.7': - dependencies: - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-split-export-declaration@7.22.6': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-split-export-declaration@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/helper-string-parser@7.24.7': {} - - '@babel/helper-validator-identifier@7.24.7': {} - - '@babel/helper-validator-option@7.23.5': {} - - '@babel/helper-validator-option@7.24.7': {} - - '@babel/helper-wrap-function@7.24.7': - dependencies: - '@babel/helper-function-name': 7.24.7 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helpers@7.23.5': - dependencies: - '@babel/template': 7.22.15 - '@babel/traverse': 7.23.5 - '@babel/types': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/helpers@7.24.7': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.7 - - '@babel/highlight@7.23.4': - dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - - '@babel/highlight@7.24.7': - dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.0 - - '@babel/parser@7.24.7': - dependencies: - '@babel/types': 7.24.7 - - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - optional: true - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-flow@7.23.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)': - dependencies: - '@babel/core': 7.23.5 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-classes@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) - '@babel/helper-split-export-declaration': 7.24.7 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/template': 7.24.7 - - '@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - - '@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) + '@babel/core': 7.23.5 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 - '@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.24.7)': + '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) + '@babel/helper-plugin-utils@7.22.5': {} - '@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.24.7)': + '@babel/helper-simple-access@7.22.5': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.24.7) + '@babel/types': 7.24.7 - '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.7)': + '@babel/helper-simple-access@7.24.7': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.24.7(@babel/core@7.24.7)': + '@babel/helper-split-export-declaration@7.24.7': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/types': 7.24.7 - '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) + '@babel/helper-string-parser@7.24.7': {} - '@babel/plugin-transform-literals@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-string-parser@7.25.7': {} - '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) + '@babel/helper-validator-identifier@7.24.7': {} - '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-identifier@7.25.7': {} - '@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - transitivePeerDependencies: - - supports-color + '@babel/helper-validator-option@7.23.5': {} - '@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.24.7)': - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - transitivePeerDependencies: - - supports-color + '@babel/helper-validator-option@7.24.7': {} - '@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.7)': + '@babel/helpers@7.23.5': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 + '@babel/template': 7.22.15 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.24.7)': + '@babel/helpers@7.24.7': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - transitivePeerDependencies: - - supports-color + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 - '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.7)': + '@babel/highlight@7.24.7': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 - '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.7)': + '@babel/parser@7.24.7': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/types': 7.24.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.24.7)': + '@babel/parser@7.25.7': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) + '@babel/types': 7.25.7 - '@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-optional-chaining@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - regenerator-transform: 0.15.2 + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - transitivePeerDependencies: - - supports-color + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-typeof-symbol@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-typescript@7.23.5(@babel/core@7.24.7)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.7) - '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/preset-env@7.24.7(@babel/core@7.24.7)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.5)': dependencies: - '@babel/compat-data': 7.24.7 - '@babel/core': 7.24.7 - '@babel/helper-compilation-targets': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-validator-option': 7.24.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.7) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.7) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.7) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.7) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.7) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.7) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.7) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.7) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.7) - '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-async-generator-functions': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-modules-systemjs': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-typeof-symbol': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-unicode-sets-regex': 7.24.7(@babel/core@7.24.7) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.7) - babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.7) - babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.7) - babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.7) - core-js-compat: 3.37.1 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/preset-flow@7.23.3(@babel/core@7.24.7)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-validator-option': 7.24.7 - '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.24.7) + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.7)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/types': 7.24.7 - esutils: 2.0.3 + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 - '@babel/preset-typescript@7.23.3(@babel/core@7.24.7)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7)': dependencies: '@babel/core': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-validator-option': 7.24.7 - '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.24.7) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-typescript': 7.23.5(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color + '@babel/helper-plugin-utils': 7.22.5 + optional: true - '@babel/register@7.22.15(@babel/core@7.24.7)': + '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)': dependencies: - '@babel/core': 7.24.7 - clone-deep: 4.0.1 - find-cache-dir: 2.1.0 - make-dir: 2.1.0 - pirates: 4.0.5 - source-map-support: 0.5.21 - - '@babel/regjsgen@0.8.0': {} + '@babel/core': 7.23.5 + '@babel/helper-plugin-utils': 7.22.5 '@babel/runtime@7.23.4': dependencies: @@ -13425,7 +12518,7 @@ snapshots: '@babel/template@7.22.15': dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 @@ -13443,12 +12536,12 @@ snapshots: '@babel/traverse@7.23.5': dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/generator': 7.24.7 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 '@babel/parser': 7.24.7 '@babel/types': 7.24.7 debug: 4.3.5(supports-color@8.1.1) @@ -13477,26 +12570,32 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@babel/types@7.25.7': + dependencies: + '@babel/helper-string-parser': 7.25.7 + '@babel/helper-validator-identifier': 7.25.7 + to-fast-properties: 2.0.0 + '@base2/pretty-print-object@1.0.1': {} '@bcoe/v8-coverage@0.2.3': {} - '@bull-board/api@5.21.1(@bull-board/ui@5.21.1)': + '@bull-board/api@6.0.0(@bull-board/ui@6.0.0)': dependencies: - '@bull-board/ui': 5.21.1 + '@bull-board/ui': 6.0.0 redis-info: 3.1.0 - '@bull-board/fastify@5.21.1': + '@bull-board/fastify@6.0.0': dependencies: - '@bull-board/api': 5.21.1(@bull-board/ui@5.21.1) - '@bull-board/ui': 5.21.1 - '@fastify/static': 6.12.0 - '@fastify/view': 8.2.0 + '@bull-board/api': 6.0.0(@bull-board/ui@6.0.0) + '@bull-board/ui': 6.0.0 + '@fastify/static': 8.0.1 + '@fastify/view': 10.0.1 ejs: 3.1.10 - '@bull-board/ui@5.21.1': + '@bull-board/ui@6.0.0': dependencies: - '@bull-board/api': 5.21.1(@bull-board/ui@5.21.1) + '@bull-board/api': 6.0.0(@bull-board/ui@6.0.0) '@bundled-es-modules/cookie@2.0.0': dependencies: @@ -13516,73 +12615,73 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@cropper/element-canvas@2.0.0-rc.1': + '@cropper/element-canvas@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element-crosshair@2.0.0-rc.1': + '@cropper/element-crosshair@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element-grid@2.0.0-rc.1': + '@cropper/element-grid@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element-handle@2.0.0-rc.1': + '@cropper/element-handle@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element-image@2.0.0-rc.1': + '@cropper/element-image@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/element-canvas': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/element-canvas': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element-selection@2.0.0-rc.1': + '@cropper/element-selection@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/element-canvas': 2.0.0-rc.1 - '@cropper/element-image': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/element-canvas': 2.0.0-rc.2 + '@cropper/element-image': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element-shade@2.0.0-rc.1': + '@cropper/element-shade@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/element-canvas': 2.0.0-rc.1 - '@cropper/element-selection': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/element-canvas': 2.0.0-rc.2 + '@cropper/element-selection': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element-viewer@2.0.0-rc.1': + '@cropper/element-viewer@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/element-canvas': 2.0.0-rc.1 - '@cropper/element-image': 2.0.0-rc.1 - '@cropper/element-selection': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/element-canvas': 2.0.0-rc.2 + '@cropper/element-image': 2.0.0-rc.2 + '@cropper/element-selection': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/element@2.0.0-rc.1': + '@cropper/element@2.0.0-rc.2': dependencies: - '@cropper/utils': 2.0.0-rc.1 + '@cropper/utils': 2.0.0-rc.2 - '@cropper/elements@2.0.0-rc.1': + '@cropper/elements@2.0.0-rc.2': dependencies: - '@cropper/element': 2.0.0-rc.1 - '@cropper/element-canvas': 2.0.0-rc.1 - '@cropper/element-crosshair': 2.0.0-rc.1 - '@cropper/element-grid': 2.0.0-rc.1 - '@cropper/element-handle': 2.0.0-rc.1 - '@cropper/element-image': 2.0.0-rc.1 - '@cropper/element-selection': 2.0.0-rc.1 - '@cropper/element-shade': 2.0.0-rc.1 - '@cropper/element-viewer': 2.0.0-rc.1 + '@cropper/element': 2.0.0-rc.2 + '@cropper/element-canvas': 2.0.0-rc.2 + '@cropper/element-crosshair': 2.0.0-rc.2 + '@cropper/element-grid': 2.0.0-rc.2 + '@cropper/element-handle': 2.0.0-rc.2 + '@cropper/element-image': 2.0.0-rc.2 + '@cropper/element-selection': 2.0.0-rc.2 + '@cropper/element-shade': 2.0.0-rc.2 + '@cropper/element-viewer': 2.0.0-rc.2 - '@cropper/utils@2.0.0-rc.1': {} + '@cropper/utils@2.0.0-rc.2': {} - '@cypress/request@3.0.0': + '@cypress/request@3.0.5': dependencies: aws-sign2: 0.7.0 aws4: 1.12.0 @@ -13590,14 +12689,14 @@ snapshots: combined-stream: 1.0.8 extend: 3.0.2 forever-agent: 0.6.1 - form-data: 2.3.3 - http-signature: 1.3.6 + form-data: 4.0.0 + http-signature: 1.4.0 is-typedarray: 1.0.0 isstream: 0.1.2 json-stringify-safe: 5.0.1 mime-types: 2.1.35 performance-now: 2.1.0 - qs: 6.10.4 + qs: 6.13.0 safe-buffer: 5.2.1 tough-cookie: 4.1.4 tunnel-agent: 0.6.0 @@ -13618,24 +12717,18 @@ snapshots: transitivePeerDependencies: - web-streams-polyfill - '@discordapp/twemoji@15.0.3': + '@discordapp/twemoji@15.1.0': dependencies: - '@twemoji/parser': 15.0.0 + '@twemoji/parser': 15.1.0 fs-extra: 8.1.0 jsonfile: 5.0.0 universalify: 0.1.2 - '@discoveryjs/json-ext@0.5.7': {} - - '@emnapi/runtime@1.1.1': + '@emnapi/runtime@1.3.0': dependencies: tslib: 2.6.3 optional: true - '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1)': - dependencies: - react: 18.3.1 - '@esbuild/aix-ppc64@0.19.11': optional: true @@ -13645,6 +12738,9 @@ snapshots: '@esbuild/aix-ppc64@0.23.0': optional: true + '@esbuild/aix-ppc64@0.23.1': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true @@ -13657,6 +12753,9 @@ snapshots: '@esbuild/android-arm64@0.23.0': optional: true + '@esbuild/android-arm64@0.23.1': + optional: true + '@esbuild/android-arm@0.18.20': optional: true @@ -13669,6 +12768,9 @@ snapshots: '@esbuild/android-arm@0.23.0': optional: true + '@esbuild/android-arm@0.23.1': + optional: true + '@esbuild/android-x64@0.18.20': optional: true @@ -13681,6 +12783,9 @@ snapshots: '@esbuild/android-x64@0.23.0': optional: true + '@esbuild/android-x64@0.23.1': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true @@ -13693,6 +12798,9 @@ snapshots: '@esbuild/darwin-arm64@0.23.0': optional: true + '@esbuild/darwin-arm64@0.23.1': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true @@ -13705,6 +12813,9 @@ snapshots: '@esbuild/darwin-x64@0.23.0': optional: true + '@esbuild/darwin-x64@0.23.1': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true @@ -13717,6 +12828,9 @@ snapshots: '@esbuild/freebsd-arm64@0.23.0': optional: true + '@esbuild/freebsd-arm64@0.23.1': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true @@ -13729,6 +12843,9 @@ snapshots: '@esbuild/freebsd-x64@0.23.0': optional: true + '@esbuild/freebsd-x64@0.23.1': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true @@ -13741,6 +12858,9 @@ snapshots: '@esbuild/linux-arm64@0.23.0': optional: true + '@esbuild/linux-arm64@0.23.1': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true @@ -13753,6 +12873,9 @@ snapshots: '@esbuild/linux-arm@0.23.0': optional: true + '@esbuild/linux-arm@0.23.1': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true @@ -13765,6 +12888,9 @@ snapshots: '@esbuild/linux-ia32@0.23.0': optional: true + '@esbuild/linux-ia32@0.23.1': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true @@ -13777,6 +12903,9 @@ snapshots: '@esbuild/linux-loong64@0.23.0': optional: true + '@esbuild/linux-loong64@0.23.1': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true @@ -13789,6 +12918,9 @@ snapshots: '@esbuild/linux-mips64el@0.23.0': optional: true + '@esbuild/linux-mips64el@0.23.1': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true @@ -13801,6 +12933,9 @@ snapshots: '@esbuild/linux-ppc64@0.23.0': optional: true + '@esbuild/linux-ppc64@0.23.1': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true @@ -13813,6 +12948,9 @@ snapshots: '@esbuild/linux-riscv64@0.23.0': optional: true + '@esbuild/linux-riscv64@0.23.1': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true @@ -13825,6 +12963,9 @@ snapshots: '@esbuild/linux-s390x@0.23.0': optional: true + '@esbuild/linux-s390x@0.23.1': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true @@ -13837,6 +12978,9 @@ snapshots: '@esbuild/linux-x64@0.23.0': optional: true + '@esbuild/linux-x64@0.23.1': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true @@ -13849,9 +12993,15 @@ snapshots: '@esbuild/netbsd-x64@0.23.0': optional: true + '@esbuild/netbsd-x64@0.23.1': + optional: true + '@esbuild/openbsd-arm64@0.23.0': optional: true + '@esbuild/openbsd-arm64@0.23.1': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true @@ -13864,6 +13014,9 @@ snapshots: '@esbuild/openbsd-x64@0.23.0': optional: true + '@esbuild/openbsd-x64@0.23.1': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true @@ -13876,6 +13029,9 @@ snapshots: '@esbuild/sunos-x64@0.23.0': optional: true + '@esbuild/sunos-x64@0.23.1': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true @@ -13888,6 +13044,9 @@ snapshots: '@esbuild/win32-arm64@0.23.0': optional: true + '@esbuild/win32-arm64@0.23.1': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true @@ -13900,6 +13059,9 @@ snapshots: '@esbuild/win32-ia32@0.23.0': optional: true + '@esbuild/win32-ia32@0.23.1': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true @@ -13912,6 +13074,9 @@ snapshots: '@esbuild/win32-x64@0.23.0': optional: true + '@esbuild/win32-x64@0.23.1': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -13970,20 +13135,18 @@ snapshots: '@eslint/object-schema@2.1.4': {} - '@fal-works/esbuild-plugin-global-externals@2.1.2': {} + '@fastify/accept-negotiator@2.0.0': {} - '@fastify/accept-negotiator@1.0.0': {} - - '@fastify/accepts@4.3.0': + '@fastify/accepts@5.0.0': dependencies: accepts: 1.3.8 - fastify-plugin: 4.5.0 + fastify-plugin: 5.0.1 - '@fastify/ajv-compiler@3.5.0': + '@fastify/ajv-compiler@4.0.1': dependencies: ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - fast-uri: 2.2.0 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.1 '@fastify/busboy@1.2.1': dependencies: @@ -13991,94 +13154,99 @@ snapshots: '@fastify/busboy@2.1.0': {} - '@fastify/cookie@9.3.1': + '@fastify/busboy@3.0.0': {} + + '@fastify/cookie@10.0.0': dependencies: cookie-signature: 1.2.1 - fastify-plugin: 4.5.0 + fastify-plugin: 5.0.1 - '@fastify/cors@9.0.1': + '@fastify/cors@10.0.0': dependencies: - fastify-plugin: 4.5.0 - mnemonist: 0.39.6 + fastify-plugin: 5.0.1 + mnemonist: 0.39.8 - '@fastify/deepmerge@1.3.0': {} + '@fastify/deepmerge@2.0.0': {} - '@fastify/error@3.4.0': {} + '@fastify/error@4.0.0': {} - '@fastify/express@3.0.0': + '@fastify/express@4.0.0': dependencies: express: 4.19.2 - fastify-plugin: 4.5.0 + fastify-plugin: 5.0.1 transitivePeerDependencies: - supports-color - '@fastify/fast-json-stringify-compiler@4.3.0': + '@fastify/fast-json-stringify-compiler@5.0.1': dependencies: - fast-json-stringify: 5.8.0 + fast-json-stringify: 6.0.0 - '@fastify/http-proxy@9.5.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)': + '@fastify/http-proxy@10.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)': dependencies: - '@fastify/reply-from': 9.0.1 + '@fastify/reply-from': 11.0.1 fast-querystring: 1.1.2 - fastify-plugin: 4.5.0 + fastify-plugin: 5.0.1 ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - utf-8-validate - '@fastify/multipart@8.3.0': + '@fastify/merge-json-schemas@0.1.1': dependencies: - '@fastify/busboy': 2.1.0 - '@fastify/deepmerge': 1.3.0 - '@fastify/error': 3.4.0 - fastify-plugin: 4.5.0 - secure-json-parse: 2.7.0 - stream-wormhole: 1.1.0 + fast-deep-equal: 3.1.3 - '@fastify/reply-from@9.0.1': + '@fastify/multipart@9.0.0': dependencies: - '@fastify/error': 3.4.0 + '@fastify/busboy': 3.0.0 + '@fastify/deepmerge': 2.0.0 + '@fastify/error': 4.0.0 + fastify-plugin: 5.0.1 + secure-json-parse: 3.0.0 + + '@fastify/reply-from@11.0.1': + dependencies: + '@fastify/error': 4.0.0 end-of-stream: 1.4.4 + fast-content-type-parse: 2.0.0 fast-querystring: 1.1.2 - fastify-plugin: 4.5.0 - pump: 3.0.0 - tiny-lru: 10.0.1 - undici: 5.28.2 + fastify-plugin: 4.5.1 + toad-cache: 3.7.0 + undici: 6.20.0 - '@fastify/send@2.0.1': + '@fastify/send@3.1.1': dependencies: - '@lukeed/ms': 2.0.1 + '@lukeed/ms': 2.0.2 escape-html: 1.0.3 fast-decode-uri-component: 1.0.1 http-errors: 2.0.0 mime: 3.0.0 - '@fastify/static@6.12.0': + '@fastify/static@8.0.0': dependencies: - '@fastify/accept-negotiator': 1.0.0 - '@fastify/send': 2.0.1 + '@fastify/accept-negotiator': 2.0.0 + '@fastify/send': 3.1.1 content-disposition: 0.5.4 - fastify-plugin: 4.5.0 - glob: 8.1.0 - p-limit: 3.1.0 + fastify-plugin: 5.0.1 + fastq: 1.17.1 + glob: 11.0.0 - '@fastify/static@7.0.4': + '@fastify/static@8.0.1': dependencies: - '@fastify/accept-negotiator': 1.0.0 - '@fastify/send': 2.0.1 + '@fastify/accept-negotiator': 2.0.0 + '@fastify/send': 3.1.1 content-disposition: 0.5.4 - fastify-plugin: 4.5.0 + fastify-plugin: 5.0.1 fastq: 1.17.1 - glob: 10.3.10 + glob: 11.0.0 - '@fastify/view@8.2.0': + '@fastify/view@10.0.0': dependencies: - fastify-plugin: 4.5.0 - hashlru: 2.3.0 + fastify-plugin: 5.0.1 + toad-cache: 3.7.0 - '@fastify/view@9.1.0': + '@fastify/view@10.0.1': dependencies: - fastify-plugin: 4.5.0 + fastify-plugin: 5.0.1 toad-cache: 3.7.0 '@github/webauthn-json@2.1.1': {} @@ -14121,79 +13289,79 @@ snapshots: '@humanwhocodes/retry@0.3.0': {} - '@img/sharp-darwin-arm64@0.33.4': + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.2 + '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true - '@img/sharp-darwin-x64@0.33.4': + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.2 + '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.0.2': + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true - '@img/sharp-libvips-darwin-x64@1.0.2': + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true - '@img/sharp-libvips-linux-arm64@1.0.2': + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true - '@img/sharp-libvips-linux-arm@1.0.2': + '@img/sharp-libvips-linux-arm@1.0.5': optional: true - '@img/sharp-libvips-linux-s390x@1.0.2': + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true - '@img/sharp-libvips-linux-x64@1.0.2': + '@img/sharp-libvips-linux-x64@1.0.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.0.2': + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true - '@img/sharp-linux-arm64@0.33.4': + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.2 + '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true - '@img/sharp-linux-arm@0.33.4': + '@img/sharp-linux-arm@0.33.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.2 + '@img/sharp-libvips-linux-arm': 1.0.5 optional: true - '@img/sharp-linux-s390x@0.33.4': + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.2 + '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true - '@img/sharp-linux-x64@0.33.4': + '@img/sharp-linux-x64@0.33.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.2 + '@img/sharp-libvips-linux-x64': 1.0.4 optional: true - '@img/sharp-linuxmusl-arm64@0.33.4': + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true - '@img/sharp-linuxmusl-x64@0.33.4': + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true - '@img/sharp-wasm32@0.33.4': + '@img/sharp-wasm32@0.33.5': dependencies: - '@emnapi/runtime': 1.1.1 + '@emnapi/runtime': 1.3.0 optional: true - '@img/sharp-win32-ia32@0.33.4': + '@img/sharp-win32-ia32@0.33.5': optional: true - '@img/sharp-win32-x64@0.33.4': + '@img/sharp-win32-x64@0.33.5': optional: true '@inquirer/confirm@3.1.6': @@ -14221,18 +13389,6 @@ snapshots: '@inquirer/type@1.3.1': {} - '@intlify/core-base@9.13.1': - dependencies: - '@intlify/message-compiler': 9.13.1 - '@intlify/shared': 9.13.1 - - '@intlify/message-compiler@9.13.1': - dependencies: - '@intlify/shared': 9.13.1 - source-map-js: 1.2.0 - - '@intlify/shared@9.13.1': {} - '@ioredis/commands@1.2.0': {} '@isaacs/cliui@8.0.2': @@ -14289,7 +13445,7 @@ snapshots: jest-util: 29.7.0 jest-validate: 29.7.0 jest-watcher: 29.7.0 - micromatch: 4.0.7 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 strip-ansi: 6.0.1 @@ -14373,7 +13529,7 @@ snapshots: '@jest/source-map@29.6.3': dependencies: - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.25 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -14404,7 +13560,7 @@ snapshots: jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 - micromatch: 4.0.7 + micromatch: 4.0.8 pirates: 4.0.5 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -14420,21 +13576,15 @@ snapshots: '@types/yargs': 17.0.19 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))': dependencies: glob: 7.2.3 glob-promise: 4.2.2(glob@7.2.3) magic-string: 0.27.0 - react-docgen-typescript: 2.2.2(typescript@5.5.4) - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) + react-docgen-typescript: 2.2.2(typescript@5.6.2) + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) optionalDependencies: - typescript: 5.5.4 - - '@jridgewell/gen-mapping@0.3.2': - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 + typescript: 5.6.2 '@jridgewell/gen-mapping@0.3.5': dependencies: @@ -14444,8 +13594,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.0': {} - '@jridgewell/set-array@1.1.2': {} - '@jridgewell/set-array@1.2.1': {} '@jridgewell/source-map@0.3.6': @@ -14457,6 +13605,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/trace-mapping@0.3.18': dependencies: '@jridgewell/resolve-uri': 3.1.0 @@ -14475,7 +13625,7 @@ snapshots: '@lukeed/csprng@1.0.1': {} - '@lukeed/ms@2.0.1': {} + '@lukeed/ms@2.0.2': {} '@mcaptcha/core-glue@0.1.0-alpha-5': {} @@ -14489,23 +13639,23 @@ snapshots: '@types/react': 18.0.28 react: 18.3.1 - '@microsoft/api-extractor-model@7.29.4(@types/node@20.14.12)': + '@microsoft/api-extractor-model@7.29.8(@types/node@20.14.12)': dependencies: '@microsoft/tsdoc': 0.15.0 '@microsoft/tsdoc-config': 0.17.0 - '@rushstack/node-core-library': 5.5.1(@types/node@20.14.12) + '@rushstack/node-core-library': 5.9.0(@types/node@20.14.12) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.47.4(@types/node@20.14.12)': + '@microsoft/api-extractor@7.47.9(@types/node@20.14.12)': dependencies: - '@microsoft/api-extractor-model': 7.29.4(@types/node@20.14.12) + '@microsoft/api-extractor-model': 7.29.8(@types/node@20.14.12) '@microsoft/tsdoc': 0.15.0 '@microsoft/tsdoc-config': 0.17.0 - '@rushstack/node-core-library': 5.5.1(@types/node@20.14.12) + '@rushstack/node-core-library': 5.9.0(@types/node@20.14.12) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.13.3(@types/node@20.14.12) - '@rushstack/ts-command-line': 4.22.3(@types/node@20.14.12) + '@rushstack/terminal': 0.14.2(@types/node@20.14.12) + '@rushstack/ts-command-line': 4.22.8(@types/node@20.14.12) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -14526,20 +13676,20 @@ snapshots: '@misskey-dev/browser-image-resizer@2024.1.0': {} - '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0))(eslint@9.8.0)(globals@15.8.0)': + '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)': dependencies: '@eslint/compat': 1.1.1 - '@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) - '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2) + '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2) eslint: 9.8.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) - globals: 15.8.0 + eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0) + globals: 15.9.0 '@misskey-dev/sharp-read-bmp@1.2.0': dependencies: decode-bmp: 0.2.1 decode-ico: 0.4.1 - sharp: 0.33.4 + sharp: 0.33.5 '@misskey-dev/summaly@5.1.0': dependencies: @@ -14590,88 +13740,97 @@ snapshots: outvariant: 1.4.2 strict-event-emitter: 0.5.1 - '@napi-rs/canvas-android-arm64@0.1.53': + '@mswjs/interceptors@0.35.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@napi-rs/canvas-android-arm64@0.1.56': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.53': + '@napi-rs/canvas-darwin-arm64@0.1.56': optional: true - '@napi-rs/canvas-darwin-x64@0.1.53': + '@napi-rs/canvas-darwin-x64@0.1.56': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.53': + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.56': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.53': + '@napi-rs/canvas-linux-arm64-gnu@0.1.56': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.53': + '@napi-rs/canvas-linux-arm64-musl@0.1.56': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.53': + '@napi-rs/canvas-linux-x64-gnu@0.1.56': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.53': + '@napi-rs/canvas-linux-x64-musl@0.1.56': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.53': + '@napi-rs/canvas-win32-x64-msvc@0.1.56': optional: true - '@napi-rs/canvas@0.1.53': + '@napi-rs/canvas@0.1.56': optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.53 - '@napi-rs/canvas-darwin-arm64': 0.1.53 - '@napi-rs/canvas-darwin-x64': 0.1.53 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.53 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.53 - '@napi-rs/canvas-linux-arm64-musl': 0.1.53 - '@napi-rs/canvas-linux-x64-gnu': 0.1.53 - '@napi-rs/canvas-linux-x64-musl': 0.1.53 - '@napi-rs/canvas-win32-x64-msvc': 0.1.53 - - '@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@napi-rs/canvas-android-arm64': 0.1.56 + '@napi-rs/canvas-darwin-arm64': 0.1.56 + '@napi-rs/canvas-darwin-x64': 0.1.56 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.56 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.56 + '@napi-rs/canvas-linux-arm64-musl': 0.1.56 + '@napi-rs/canvas-linux-x64-gnu': 0.1.56 + '@napi-rs/canvas-linux-x64-musl': 0.1.56 + '@napi-rs/canvas-win32-x64-msvc': 0.1.56 + + '@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: iterare: 1.2.1 reflect-metadata: 0.2.2 rxjs: 7.8.1 - tslib: 2.6.3 + tslib: 2.7.0 uid: 2.0.2 - '@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: - '@nestjs/common': 10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13) fast-safe-stringify: 2.1.1 iterare: 1.2.1 - path-to-regexp: 3.2.0 + path-to-regexp: 3.3.0 reflect-metadata: 0.2.2 rxjs: 7.8.1 - tslib: 2.6.3 + tslib: 2.7.0 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10) + '@nestjs/platform-express': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3) transitivePeerDependencies: - encoding - '@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10)': + '@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)': dependencies: - '@nestjs/common': 10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) - body-parser: 1.20.2 + '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + body-parser: 1.20.3 cors: 2.8.5 - express: 4.19.2 + express: 4.21.0 multer: 1.4.4-lts.1 - tslib: 2.6.3 + tslib: 2.7.0 transitivePeerDependencies: - supports-color - '@nestjs/testing@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10))': + '@nestjs/testing@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3))': dependencies: - '@nestjs/common': 10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.10)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) - tslib: 2.6.3 + '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + tslib: 2.7.0 optionalDependencies: - '@nestjs/platform-express': 10.3.10(@nestjs/common@10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.10) + '@nestjs/platform-express': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3) '@noble/hashes@1.4.0': {} @@ -14691,7 +13850,7 @@ snapshots: dependencies: agent-base: 7.1.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 + https-proxy-agent: 7.0.5 lru-cache: 10.2.2 socks-proxy-agent: 8.0.2 transitivePeerDependencies: @@ -14994,7 +14153,7 @@ snapshots: '@readme/better-ajv-errors@1.6.0(ajv@8.17.1)': dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/runtime': 7.23.4 '@humanwhocodes/momoa': 2.0.4 ajv: 8.17.1 @@ -15010,88 +14169,92 @@ snapshots: call-me-maybe: 1.0.2 js-yaml: 4.1.0 - '@readme/openapi-parser@2.5.0(openapi-types@12.1.3)': + '@readme/openapi-parser@2.6.0(openapi-types@12.1.3)': dependencies: - '@apidevtools/openapi-schemas': 2.1.0 '@apidevtools/swagger-methods': 3.0.2 '@jsdevtools/ono': 7.1.3 '@readme/better-ajv-errors': 1.6.0(ajv@8.17.1) '@readme/json-schema-ref-parser': 1.2.0 + '@readme/openapi-schemas': 3.1.0 ajv: 8.17.1 ajv-draft-04: 1.0.0(ajv@8.17.1) call-me-maybe: 1.0.2 openapi-types: 12.1.3 - '@rollup/plugin-json@6.1.0(rollup@4.19.1)': + '@readme/openapi-schemas@3.1.0': {} + + '@rollup/plugin-json@6.1.0(rollup@4.22.5)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.19.1) + '@rollup/pluginutils': 5.1.2(rollup@4.22.5) optionalDependencies: - rollup: 4.19.1 + rollup: 4.22.5 - '@rollup/plugin-replace@5.0.7(rollup@4.19.1)': + '@rollup/plugin-replace@5.0.7(rollup@4.22.5)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.19.1) + '@rollup/pluginutils': 5.1.2(rollup@4.22.5) magic-string: 0.30.10 optionalDependencies: - rollup: 4.19.1 + rollup: 4.22.5 - '@rollup/pluginutils@5.1.0(rollup@4.19.1)': + '@rollup/pluginutils@5.1.2(rollup@4.22.5)': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 4.19.1 + rollup: 4.22.5 - '@rollup/rollup-android-arm-eabi@4.19.1': + '@rollup/rollup-android-arm-eabi@4.22.5': optional: true - '@rollup/rollup-android-arm64@4.19.1': + '@rollup/rollup-android-arm64@4.22.5': optional: true - '@rollup/rollup-darwin-arm64@4.19.1': + '@rollup/rollup-darwin-arm64@4.22.5': optional: true - '@rollup/rollup-darwin-x64@4.19.1': + '@rollup/rollup-darwin-x64@4.22.5': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.19.1': + '@rollup/rollup-linux-arm-gnueabihf@4.22.5': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.19.1': + '@rollup/rollup-linux-arm-musleabihf@4.22.5': optional: true - '@rollup/rollup-linux-arm64-gnu@4.19.1': + '@rollup/rollup-linux-arm64-gnu@4.22.5': optional: true - '@rollup/rollup-linux-arm64-musl@4.19.1': + '@rollup/rollup-linux-arm64-musl@4.22.5': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.22.5': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.19.1': + '@rollup/rollup-linux-riscv64-gnu@4.22.5': optional: true - '@rollup/rollup-linux-s390x-gnu@4.19.1': + '@rollup/rollup-linux-s390x-gnu@4.22.5': optional: true - '@rollup/rollup-linux-x64-gnu@4.19.1': + '@rollup/rollup-linux-x64-gnu@4.22.5': optional: true - '@rollup/rollup-linux-x64-musl@4.19.1': + '@rollup/rollup-linux-x64-musl@4.22.5': optional: true - '@rollup/rollup-win32-arm64-msvc@4.19.1': + '@rollup/rollup-win32-arm64-msvc@4.22.5': optional: true - '@rollup/rollup-win32-ia32-msvc@4.19.1': + '@rollup/rollup-win32-ia32-msvc@4.22.5': optional: true - '@rollup/rollup-win32-x64-msvc@4.19.1': + '@rollup/rollup-win32-x64-msvc@4.22.5': optional: true - '@rushstack/node-core-library@5.5.1(@types/node@20.14.12)': + '@rtsao/scc@1.1.0': {} + + '@rushstack/node-core-library@5.9.0(@types/node@20.14.12)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -15109,16 +14272,16 @@ snapshots: resolve: 1.22.8 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.13.3(@types/node@20.14.12)': + '@rushstack/terminal@0.14.2(@types/node@20.14.12)': dependencies: - '@rushstack/node-core-library': 5.5.1(@types/node@20.14.12) + '@rushstack/node-core-library': 5.9.0(@types/node@20.14.12) supports-color: 8.1.1 optionalDependencies: '@types/node': 20.14.12 - '@rushstack/ts-command-line@4.22.3(@types/node@20.14.12)': + '@rushstack/ts-command-line@4.22.8(@types/node@20.14.12)': dependencies: - '@rushstack/terminal': 0.13.3(@types/node@20.14.12) + '@rushstack/terminal': 0.14.2(@types/node@20.14.12) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.1 @@ -15203,6 +14366,10 @@ snapshots: dependencies: '@hapi/hoek': 9.3.0 + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + '@sideway/formula@3.0.1': {} '@sideway/pinpoint@2.0.0': {} @@ -15231,8 +14398,6 @@ snapshots: '@sindresorhus/is@7.0.0': {} - '@sindresorhus/merge-streams@2.3.0': {} - '@sindresorhus/merge-streams@4.0.0': {} '@sinonjs/commons@2.0.0': @@ -15622,134 +14787,124 @@ snapshots: '@sqltools/formatter@1.2.5': {} - '@storybook/addon-actions@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-actions@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 polished: 4.2.2 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-backgrounds@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-controls@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: + '@storybook/global': 5.0.0 dequal: 2.0.3 lodash: 4.17.21 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-docs@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@babel/core': 7.24.7 '@mdx-js/react': 3.0.1(@types/react@18.0.28)(react@18.3.1) - '@storybook/blocks': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/csf-plugin': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/blocks': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/csf-plugin': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/react-dom-shim': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/react': 18.0.28 fs-extra: 11.1.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) rehype-external-links: 3.0.0 rehype-slug: 6.0.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color - '@storybook/addon-essentials@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': - dependencies: - '@storybook/addon-actions': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-backgrounds': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-controls': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-docs': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-highlight': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-measure': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-outline': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-toolbars': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/addon-viewport': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@storybook/addon-essentials@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + dependencies: + '@storybook/addon-actions': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-backgrounds': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-controls': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-docs': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-highlight': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-measure': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-outline': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-toolbars': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/addon-viewport': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color - '@storybook/addon-highlight@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-highlight@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/addon-interactions@8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3))': + '@storybook/addon-interactions@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/test': 8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3)) + '@storybook/instrumenter': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/test': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) polished: 4.2.2 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@jest/globals' - - '@types/bun' - - '@types/jest' - - jest - - vitest - '@storybook/addon-links@8.2.6(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-links@8.3.3(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 - '@storybook/addon-mdx-gfm@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-mdx-gfm@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: remark-gfm: 4.0.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-measure@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-measure@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-outline@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - '@storybook/addon-storysource@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-storysource@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@storybook/source-loader': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/source-loader': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) estraverse: 5.3.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) tiny-invariant: 1.3.3 - '@storybook/addon-toolbars@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-toolbars@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/addon-viewport@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/addon-viewport@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: memoizerific: 1.11.3 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/blocks@8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/lodash': 4.14.191 color-convert: 2.0.1 dequal: 2.0.3 @@ -15758,7 +14913,7 @@ snapshots: memoizerific: 1.11.3 polished: 4.2.2 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) telejson: 7.2.0 ts-dedent: 2.2.0 util-deprecate: 1.0.2 @@ -15766,57 +14921,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-manager@8.1.11(encoding@0.1.13)(prettier@3.3.3)': - dependencies: - '@fal-works/esbuild-plugin-global-externals': 2.1.2 - '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/manager': 8.1.11 - '@storybook/node-logger': 8.1.11 - '@types/ejs': 3.1.2 - '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.19.11) - browser-assert: 1.2.1 - ejs: 3.1.10 - esbuild: 0.19.11 - esbuild-plugin-alias: 0.2.1 - express: 4.19.2 - fs-extra: 11.1.1 - process: 0.11.10 - util: 0.12.5 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - - '@storybook/builder-vite@8.1.11(encoding@0.1.13)(prettier@3.3.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))': - dependencies: - '@storybook/channels': 8.1.11 - '@storybook/client-logger': 8.1.11 - '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/core-events': 8.1.11 - '@storybook/csf-plugin': 8.1.11 - '@storybook/node-logger': 8.1.11 - '@storybook/preview': 8.1.11 - '@storybook/preview-api': 8.1.11 - '@storybook/types': 8.1.11 - '@types/find-cache-dir': 3.2.1 - browser-assert: 1.2.1 - es-module-lexer: 1.5.4 - express: 4.19.2 - find-cache-dir: 3.3.2 - fs-extra: 11.1.1 - magic-string: 0.30.10 - ts-dedent: 2.2.0 - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - - '@storybook/builder-vite@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))': + '@storybook/builder-vite@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))': dependencies: - '@storybook/csf-plugin': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/csf-plugin': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 1.5.4 @@ -15824,163 +14931,35 @@ snapshots: find-cache-dir: 3.3.2 fs-extra: 11.1.1 magic-string: 0.30.10 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - ts-dedent: 2.2.0 - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color - - '@storybook/channels@8.1.11': - dependencies: - '@storybook/client-logger': 8.1.11 - '@storybook/core-events': 8.1.11 - '@storybook/global': 5.0.0 - telejson: 7.2.0 - tiny-invariant: 1.3.3 - - '@storybook/client-logger@8.1.11': - dependencies: - '@storybook/global': 5.0.0 - - '@storybook/codemod@8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)': - dependencies: - '@babel/core': 7.24.7 - '@babel/preset-env': 7.24.7(@babel/core@7.24.7) - '@babel/types': 7.24.7 - '@storybook/core': 8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/csf': 0.1.11 - '@types/cross-spawn': 6.0.2 - cross-spawn: 7.0.3 - globby: 14.0.1 - jscodeshift: 0.15.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)) - lodash: 4.17.21 - prettier: 3.3.3 - recast: 0.23.6 - tiny-invariant: 1.3.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@storybook/components@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': - dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - - '@storybook/core-common@8.1.11(encoding@0.1.13)(prettier@3.3.3)': - dependencies: - '@storybook/core-events': 8.1.11 - '@storybook/csf-tools': 8.1.11 - '@storybook/node-logger': 8.1.11 - '@storybook/types': 8.1.11 - '@yarnpkg/fslib': 2.10.3 - '@yarnpkg/libzip': 2.3.0 - chalk: 4.1.2 - cross-spawn: 7.0.3 - esbuild: 0.19.11 - esbuild-register: 3.5.0(esbuild@0.19.11) - execa: 5.1.1 - file-system-cache: 2.3.0 - find-cache-dir: 3.3.2 - find-up: 5.0.0 - fs-extra: 11.1.1 - glob: 10.3.10 - handlebars: 4.7.7 - lazy-universal-dotenv: 4.0.0 - node-fetch: 2.7.0(encoding@0.1.13) - picomatch: 2.3.1 - pkg-dir: 5.0.0 - prettier-fallback: prettier@3.3.3 - pretty-hrtime: 1.0.3 - resolve-from: 5.0.0 - semver: 7.6.0 - tempy: 3.1.0 - tiny-invariant: 1.3.3 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 - util: 0.12.5 + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) optionalDependencies: - prettier: 3.3.3 + typescript: 5.6.2 transitivePeerDependencies: - - encoding - supports-color - '@storybook/core-events@8.1.11': - dependencies: - '@storybook/csf': 0.1.9 - ts-dedent: 2.2.0 - - '@storybook/core-events@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/core-server@8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4)': + '@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@aw-web-design/x-default-browser': 1.4.126 - '@babel/core': 7.24.7 - '@babel/parser': 7.24.7 - '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/channels': 8.1.11 - '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/core-events': 8.1.11 - '@storybook/csf': 0.1.9 - '@storybook/csf-tools': 8.1.11 - '@storybook/docs-mdx': 3.1.0-next.0 - '@storybook/global': 5.0.0 - '@storybook/manager': 8.1.11 - '@storybook/manager-api': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/node-logger': 8.1.11 - '@storybook/preview-api': 8.1.11 - '@storybook/telemetry': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/types': 8.1.11 - '@types/detect-port': 1.3.2 - '@types/diff': 5.2.1 - '@types/node': 18.17.15 - '@types/pretty-hrtime': 1.0.1 - '@types/semver': 7.5.8 - better-opn: 3.0.2 - chalk: 4.1.2 - cli-table3: 0.6.3 - compression: 1.7.4 - detect-port: 1.5.1 - diff: 5.2.0 - express: 4.19.2 - fs-extra: 11.1.1 - globby: 14.0.1 - lodash: 4.17.21 - open: 8.4.2 - pretty-hrtime: 1.0.3 - prompts: 2.4.2 - read-pkg-up: 7.0.1 - semver: 7.6.0 - telejson: 7.2.0 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 - util: 0.12.5 - util-deprecate: 1.0.2 - watchpack: 2.4.0 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) - transitivePeerDependencies: - - bufferutil - - encoding - - prettier - - react - - react-dom - - supports-color - - utf-8-validate + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/core@8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)': + '@storybook/core@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: '@storybook/csf': 0.1.11 '@types/express': 4.17.21 - '@types/node': 18.17.15 + better-opn: 3.0.2 browser-assert: 1.2.1 - esbuild: 0.19.11 - esbuild-register: 3.5.0(esbuild@0.19.11) + esbuild: 0.23.1 + esbuild-register: 3.5.0(esbuild@0.23.1) express: 4.19.2 + jsdoc-type-pratt-parser: 4.1.0 process: 0.11.10 recast: 0.23.6 + semver: 7.6.3 util: 0.12.5 ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: @@ -15988,305 +14967,153 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@8.1.11': + '@storybook/csf-plugin@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@storybook/csf-tools': 8.1.11 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) unplugin: 1.4.0 - transitivePeerDependencies: - - supports-color - - '@storybook/csf-plugin@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': - dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - unplugin: 1.4.0 - - '@storybook/csf-tools@8.1.11': - dependencies: - '@babel/generator': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - '@storybook/csf': 0.1.9 - '@storybook/types': 8.1.11 - fs-extra: 11.1.1 - recast: 0.23.6 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color '@storybook/csf@0.1.11': dependencies: type-fest: 2.19.0 - '@storybook/csf@0.1.9': - dependencies: - type-fest: 2.19.0 - - '@storybook/docs-mdx@3.1.0-next.0': {} - - '@storybook/docs-tools@8.1.11(encoding@0.1.13)(prettier@3.3.3)': - dependencies: - '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/core-events': 8.1.11 - '@storybook/preview-api': 8.1.11 - '@storybook/types': 8.1.11 - '@types/doctrine': 0.0.3 - assert: 2.1.0 - doctrine: 3.0.0 - lodash: 4.17.21 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - '@storybook/global@5.0.0': {} - '@storybook/icons@1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/icons@1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/instrumenter@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/global': 5.0.0 - '@vitest/utils': 1.6.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@vitest/utils': 2.1.2 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) util: 0.12.5 - '@storybook/manager-api@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@storybook/channels': 8.1.11 - '@storybook/client-logger': 8.1.11 - '@storybook/core-events': 8.1.11 - '@storybook/csf': 0.1.9 - '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/router': 8.1.11 - '@storybook/theming': 8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.1.11 - dequal: 2.0.3 - lodash: 4.17.21 - memoizerific: 1.11.3 - store2: 2.14.2 - telejson: 7.2.0 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - react - - react-dom - - '@storybook/manager-api@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': - dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - - '@storybook/manager@8.1.11': {} - - '@storybook/node-logger@8.1.11': {} - - '@storybook/preview-api@8.1.11': + '@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@storybook/channels': 8.1.11 - '@storybook/client-logger': 8.1.11 - '@storybook/core-events': 8.1.11 - '@storybook/csf': 0.1.9 - '@storybook/global': 5.0.0 - '@storybook/types': 8.1.11 - '@types/qs': 6.9.7 - dequal: 2.0.3 - lodash: 4.17.21 - memoizerific: 1.11.3 - qs: 6.11.1 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 - util-deprecate: 1.0.2 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/preview-api@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - - '@storybook/preview@8.1.11': {} + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/react-dom-shim@8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/react-dom-shim@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/react-vite@8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.19.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))': + '@storybook/react-vite@8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)) - '@rollup/pluginutils': 5.1.0(rollup@4.19.1) - '@storybook/builder-vite': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)) - '@storybook/react': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)) + '@rollup/pluginutils': 5.1.2(rollup@4.22.5) + '@storybook/builder-vite': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)) + '@storybook/react': 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2) find-up: 5.0.0 magic-string: 0.30.10 react: 18.3.1 react-docgen: 7.0.1 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) tsconfig-paths: 4.2.0 - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) transitivePeerDependencies: - '@preact/preset-vite' + - '@storybook/test' - rollup - supports-color - typescript - vite-plugin-glimmerx - '@storybook/react@8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.5.4)': + '@storybook/react@8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)': dependencies: - '@storybook/components': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/preview-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/react-dom-shim': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/theming': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/react-dom-shim': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 - '@types/node': 18.17.15 + '@types/node': 22.7.5 acorn: 7.4.1 acorn-jsx: 5.3.2(acorn@7.4.1) acorn-walk: 7.2.0 escodegen: 2.1.0 html-tags: 3.2.0 - lodash: 4.17.21 prop-types: 15.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: 7.6.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 type-fest: 2.19.0 util-deprecate: 1.0.2 optionalDependencies: - typescript: 5.5.4 + '@storybook/test': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + typescript: 5.6.2 - '@storybook/router@8.1.11': - dependencies: - '@storybook/client-logger': 8.1.11 - memoizerific: 1.11.3 - qs: 6.11.1 - - '@storybook/source-loader@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/source-loader@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: '@storybook/csf': 0.1.11 estraverse: 5.3.0 - lodash: 4.17.21 - prettier: 3.3.3 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - - '@storybook/telemetry@8.1.11(encoding@0.1.13)(prettier@3.3.3)': - dependencies: - '@storybook/client-logger': 8.1.11 - '@storybook/core-common': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/csf-tools': 8.1.11 - chalk: 4.1.2 - detect-package-manager: 2.0.1 - fetch-retry: 5.0.4 - fs-extra: 11.1.1 - read-pkg-up: 7.0.1 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - - '@storybook/test@8.2.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3))': - dependencies: - '@storybook/csf': 0.1.11 - '@storybook/instrumenter': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3)) - '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) - '@vitest/expect': 1.6.0 - '@vitest/spy': 1.6.0 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) - util: 0.12.5 - transitivePeerDependencies: - - '@jest/globals' - - '@types/bun' - - '@types/jest' - - jest - - vitest + lodash: 4.17.21 + prettier: 3.3.3 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/theming@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) - '@storybook/client-logger': 8.1.11 + '@storybook/csf': 0.1.11 '@storybook/global': 5.0.0 - memoizerific: 1.11.3 - optionalDependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - - '@storybook/theming@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': - dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@storybook/instrumenter': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@testing-library/dom': 10.4.0 + '@testing-library/jest-dom': 6.5.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/expect': 2.0.5 + '@vitest/spy': 2.0.5 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) + util: 0.12.5 - '@storybook/types@8.1.11': + '@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - '@storybook/channels': 8.1.11 - '@types/express': 4.17.17 - file-system-cache: 2.3.0 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/types@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))': + '@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))': dependencies: - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/vue3-vite@8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4))': + '@storybook/vue3-vite@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))': dependencies: - '@storybook/builder-vite': 8.1.11(encoding@0.1.13)(prettier@3.3.3)(typescript@5.5.4)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)) - '@storybook/core-server': 8.1.11(bufferutil@4.0.8)(encoding@0.1.13)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.4) - '@storybook/types': 8.1.11 - '@storybook/vue3': 8.1.11(encoding@0.1.13)(prettier@3.3.3)(vue@3.4.37(typescript@5.5.4)) + '@storybook/builder-vite': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)) + '@storybook/vue3': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2)) find-package-json: 1.2.0 magic-string: 0.30.10 - typescript: 5.5.4 - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - vue-component-meta: 2.0.16(typescript@5.5.4) - vue-docgen-api: 4.75.1(vue@3.4.37(typescript@5.5.4)) + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) + typescript: 5.6.2 + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) + vue-component-meta: 2.0.16(typescript@5.6.2) + vue-docgen-api: 4.75.1(vue@3.5.10(typescript@5.6.2)) transitivePeerDependencies: - '@preact/preset-vite' - - bufferutil - - encoding - - prettier - - react - - react-dom - supports-color - - utf-8-validate - vite-plugin-glimmerx - vue - '@storybook/vue3@8.1.11(encoding@0.1.13)(prettier@3.3.3)(vue@3.4.37(typescript@5.5.4))': - dependencies: - '@storybook/docs-tools': 8.1.11(encoding@0.1.13)(prettier@3.3.3) - '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.1.11 - '@storybook/types': 8.1.11 - '@vue/compiler-core': 3.4.34 - lodash: 4.17.21 - ts-dedent: 2.2.0 - type-fest: 2.19.0 - vue: 3.4.37(typescript@5.5.4) - vue-component-type-helpers: 2.1.6 - transitivePeerDependencies: - - encoding - - prettier - - supports-color - - '@storybook/vue3@8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.4.37(typescript@5.5.4))': + '@storybook/vue3@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2))': dependencies: - '@storybook/components': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/preview-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/theming': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@vue/compiler-core': 3.4.31 - lodash: 4.17.21 - storybook: 8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4) + '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@vue/compiler-core': 3.4.37 + storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) ts-dedent: 2.2.0 type-fest: 2.19.0 - vue: 3.4.37(typescript@5.5.4) + vue: 3.5.10(typescript@5.6.2) vue-component-type-helpers: 2.1.6 '@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)': @@ -16473,7 +15300,7 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@testing-library/dom@10.1.0': + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.24.7 '@babel/runtime': 7.23.4 @@ -16486,7 +15313,7 @@ snapshots: '@testing-library/dom@9.3.4': dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 '@babel/runtime': 7.23.4 '@types/aria-query': 5.0.1 aria-query: 5.1.3 @@ -16495,34 +15322,28 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.14.12))(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3))': + '@testing-library/jest-dom@6.5.0': dependencies: - '@adobe/css-tools': 4.3.3 - '@babel/runtime': 7.23.4 + '@adobe/css-tools': 4.4.0 aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - optionalDependencies: - '@jest/globals': 29.7.0 - '@types/jest': 29.5.12 - jest: 29.7.0(@types/node@20.14.12) - vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3) - '@testing-library/user-event@14.5.2(@testing-library/dom@10.1.0)': + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: - '@testing-library/dom': 10.1.0 + '@testing-library/dom': 10.4.0 - '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.4.37)(@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4))': + '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))': dependencies: '@babel/runtime': 7.23.4 '@testing-library/dom': 9.3.4 - '@vue/test-utils': 2.4.1(@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4)) - vue: 3.4.37(typescript@5.5.4) + '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2)) + vue: 3.5.10(typescript@5.6.2) optionalDependencies: - '@vue/compiler-sfc': 3.4.37 + '@vue/compiler-sfc': 3.5.10 transitivePeerDependencies: - '@vue/server-renderer' @@ -16538,6 +15359,8 @@ snapshots: '@twemoji/parser@15.0.0': {} + '@twemoji/parser@15.1.0': {} + '@twemoji/parser@15.1.1': {} '@types/accepts@1.3.7': @@ -16609,40 +15432,24 @@ snapshots: '@types/core-js@2.5.8': {} - '@types/cross-spawn@6.0.2': - dependencies: - '@types/node': 20.14.12 - '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 - '@types/detect-port@1.3.2': {} - - '@types/diff@5.2.1': {} - '@types/disposable-email-domains@1.0.2': {} - '@types/doctrine@0.0.3': {} - '@types/doctrine@0.0.9': {} - '@types/ejs@3.1.2': {} - - '@types/emscripten@1.39.7': {} - - '@types/escape-regexp@0.0.3': {} - '@types/escodegen@0.0.6': {} '@types/eslint@7.29.0': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 '@types/estree@0.0.51': {} - '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} '@types/express-serve-static-core@4.17.33': dependencies: @@ -16666,7 +15473,7 @@ snapshots: '@types/find-cache-dir@3.2.1': {} - '@types/fluent-ffmpeg@2.1.24': + '@types/fluent-ffmpeg@2.1.26': dependencies: '@types/node': 20.14.12 @@ -16710,6 +15517,11 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/jest@29.5.13': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + '@types/js-yaml@4.0.9': {} '@types/jsdom@21.1.7': @@ -16728,6 +15540,8 @@ snapshots: '@types/jsrsasign@10.5.14': {} + '@types/katex@0.16.7': {} + '@types/keyv@3.1.4': dependencies: '@types/node': 20.14.12 @@ -16766,8 +15580,6 @@ snapshots: dependencies: '@types/node': 20.14.12 - '@types/node@18.17.15': {} - '@types/node@20.11.5': dependencies: undici-types: 5.26.5 @@ -16780,7 +15592,11 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/nodemailer@6.4.15': + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + + '@types/nodemailer@6.4.16': dependencies: '@types/node': 20.14.12 @@ -16805,9 +15621,9 @@ snapshots: '@types/pg-pool@2.0.4': dependencies: - '@types/pg': 8.11.6 + '@types/pg': 8.11.10 - '@types/pg@8.11.6': + '@types/pg@8.11.10': dependencies: '@types/node': 20.14.12 pg-protocol: 1.6.1 @@ -16819,8 +15635,6 @@ snapshots: pg-protocol: 1.6.1 pg-types: 2.2.0 - '@types/pretty-hrtime@1.0.1': {} - '@types/prop-types@15.7.5': {} '@types/proxy-addr@2.0.3': @@ -16861,7 +15675,7 @@ snapshots: dependencies: '@types/node': 20.14.12 - '@types/sanitize-html@2.11.0': + '@types/sanitize-html@2.13.0': dependencies: htmlparser2: 8.0.1 @@ -16926,6 +15740,10 @@ snapshots: dependencies: '@types/node': 20.14.12 + '@types/ws@8.5.12': + dependencies: + '@types/node': 20.14.12 + '@types/yargs-parser@21.0.0': {} '@types/yargs@17.0.19': @@ -16937,26 +15755,6 @@ snapshots: '@types/node': 20.14.12 optional: true - '@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3)': - dependencies: - '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 6.11.0(eslint@9.8.0)(typescript@5.3.3) - '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/type-utils': 6.11.0(eslint@9.8.0)(typescript@5.3.3) - '@typescript-eslint/utils': 6.11.0(eslint@9.8.0)(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@5.5.0) - eslint: 9.8.0 - graphemer: 1.4.0 - ignore: 5.3.1 - natural-compare: 1.4.0 - semver: 7.5.4 - ts-api-utils: 1.3.0(typescript@5.3.3) - optionalDependencies: - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.1.6))(eslint@8.57.0)(typescript@5.1.6)': dependencies: '@eslint-community/regexpp': 4.11.0 @@ -17015,16 +15813,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.11.0(eslint@9.8.0)(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2)': dependencies: - '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.4(supports-color@5.5.0) + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2) + '@typescript-eslint/scope-manager': 7.17.0 + '@typescript-eslint/type-utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2) + '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 7.17.0 eslint: 9.8.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.6.2) optionalDependencies: - typescript: 5.3.3 + typescript: 5.6.2 transitivePeerDependencies: - supports-color @@ -17067,10 +15870,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@6.11.0': + '@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2)': dependencies: - '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/visitor-keys': 6.11.0 + '@typescript-eslint/scope-manager': 7.17.0 + '@typescript-eslint/types': 7.17.0 + '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2) + '@typescript-eslint/visitor-keys': 7.17.0 + debug: 4.3.5(supports-color@8.1.1) + eslint: 9.8.0 + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color '@typescript-eslint/scope-manager@6.21.0': dependencies: @@ -17087,18 +15898,6 @@ snapshots: '@typescript-eslint/types': 7.17.0 '@typescript-eslint/visitor-keys': 7.17.0 - '@typescript-eslint/type-utils@6.11.0(eslint@9.8.0)(typescript@5.3.3)': - dependencies: - '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.11.0(eslint@9.8.0)(typescript@5.3.3) - debug: 4.3.5(supports-color@8.1.1) - eslint: 9.8.0 - ts-api-utils: 1.3.0(typescript@5.3.3) - optionalDependencies: - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.1.6)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) @@ -17135,7 +15934,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@6.11.0': {} + '@typescript-eslint/type-utils@7.17.0(eslint@9.8.0)(typescript@5.6.2)': + dependencies: + '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2) + '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.6.2) + debug: 4.3.5(supports-color@8.1.1) + eslint: 9.8.0 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 + transitivePeerDependencies: + - supports-color '@typescript-eslint/types@6.21.0': {} @@ -17143,20 +15952,6 @@ snapshots: '@typescript-eslint/types@7.17.0': {} - '@typescript-eslint/typescript-estree@6.11.0(typescript@5.3.3)': - dependencies: - '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/visitor-keys': 6.11.0 - debug: 4.3.5(supports-color@8.1.1) - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - ts-api-utils: 1.3.0(typescript@5.3.3) - optionalDependencies: - typescript: 5.3.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@6.21.0(typescript@5.1.6)': dependencies: '@typescript-eslint/types': 6.21.0 @@ -17202,19 +15997,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.11.0(eslint@9.8.0)(typescript@5.3.3)': + '@typescript-eslint/typescript-estree@7.17.0(typescript@5.6.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 6.11.0 - '@typescript-eslint/types': 6.11.0 - '@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3) - eslint: 9.8.0 + '@typescript-eslint/types': 7.17.0 + '@typescript-eslint/visitor-keys': 7.17.0 + debug: 4.3.5(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.4 semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@5.6.2) + optionalDependencies: + typescript: 5.6.2 transitivePeerDependencies: - supports-color - - typescript '@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.1.6)': dependencies: @@ -17255,10 +16051,16 @@ snapshots: - supports-color - typescript - '@typescript-eslint/visitor-keys@6.11.0': + '@typescript-eslint/utils@7.17.0(eslint@9.8.0)(typescript@5.6.2)': dependencies: - '@typescript-eslint/types': 6.11.0 - eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) + '@typescript-eslint/scope-manager': 7.17.0 + '@typescript-eslint/types': 7.17.0 + '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.6.2) + eslint: 9.8.0 + transitivePeerDependencies: + - supports-color + - typescript '@typescript-eslint/visitor-keys@6.21.0': dependencies: @@ -17277,27 +16079,46 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-vue@5.1.0(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.37(typescript@5.5.4))': + '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))': dependencies: - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - vue: 3.4.37(typescript@5.5.4) + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) + vue: 3.5.10(typescript@5.6.2) - '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3))': + '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))': dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.5(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.4 istanbul-reports: 3.1.6 magic-string: 0.30.10 magicast: 0.3.4 - picocolors: 1.0.0 + picocolors: 1.0.1 + std-env: 3.7.0 + strip-literal: 2.1.0 + test-exclude: 6.0.0 + vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0))': + dependencies: + '@ampproject/remapping': 2.2.1 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.5(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.4 + istanbul-reports: 3.1.6 + magic-string: 0.30.10 + magicast: 0.3.4 + picocolors: 1.0.1 std-env: 3.7.0 strip-literal: 2.1.0 test-exclude: 6.0.0 - vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3) + vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0) transitivePeerDependencies: - supports-color @@ -17307,6 +16128,21 @@ snapshots: '@vitest/utils': 1.6.0 chai: 4.3.10 + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.1.2': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/runner@1.6.0': dependencies: '@vitest/utils': 1.6.0 @@ -17323,6 +16159,10 @@ snapshots: dependencies: tinyspy: 2.2.0 + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.2 + '@vitest/utils@1.6.0': dependencies: diff-sequences: 29.6.3 @@ -17330,47 +16170,44 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + + '@vitest/utils@2.1.2': + dependencies: + '@vitest/pretty-format': 2.1.2 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + '@volar/language-core@2.2.0': dependencies: '@volar/source-map': 2.2.0 - '@volar/language-core@2.4.0-alpha.18': + '@volar/language-core@2.4.6': dependencies: - '@volar/source-map': 2.4.0-alpha.18 + '@volar/source-map': 2.4.6 '@volar/source-map@2.2.0': dependencies: muggle-string: 0.4.1 - '@volar/source-map@2.4.0-alpha.18': {} + '@volar/source-map@2.4.6': {} '@volar/typescript@2.2.0': dependencies: '@volar/language-core': 2.2.0 path-browserify: 1.0.1 - '@volar/typescript@2.4.0-alpha.18': + '@volar/typescript@2.4.6': dependencies: - '@volar/language-core': 2.4.0-alpha.18 + '@volar/language-core': 2.4.6 path-browserify: 1.0.1 vscode-uri: 3.0.8 - '@vue/compiler-core@3.4.31': - dependencies: - '@babel/parser': 7.24.7 - '@vue/shared': 3.4.31 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - - '@vue/compiler-core@3.4.34': - dependencies: - '@babel/parser': 7.24.7 - '@vue/shared': 3.4.34 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - '@vue/compiler-core@3.4.37': dependencies: '@babel/parser': 7.24.7 @@ -17379,16 +16216,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.0 - '@vue/compiler-dom@3.4.34': + '@vue/compiler-core@3.5.10': dependencies: - '@vue/compiler-core': 3.4.34 - '@vue/shared': 3.4.34 + '@babel/parser': 7.25.7 + '@vue/shared': 3.5.10 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 '@vue/compiler-dom@3.4.37': dependencies: '@vue/compiler-core': 3.4.37 '@vue/shared': 3.4.37 + '@vue/compiler-dom@3.5.10': + dependencies: + '@vue/compiler-core': 3.5.10 + '@vue/shared': 3.5.10 + '@vue/compiler-sfc@3.4.37': dependencies: '@babel/parser': 7.24.7 @@ -17398,7 +16243,19 @@ snapshots: '@vue/shared': 3.4.37 estree-walker: 2.0.2 magic-string: 0.30.10 - postcss: 8.4.40 + postcss: 8.4.47 + source-map-js: 1.2.0 + + '@vue/compiler-sfc@3.5.10': + dependencies: + '@babel/parser': 7.25.7 + '@vue/compiler-core': 3.5.10 + '@vue/compiler-dom': 3.5.10 + '@vue/compiler-ssr': 3.5.10 + '@vue/shared': 3.5.10 + estree-walker: 2.0.2 + magic-string: 0.30.11 + postcss: 8.4.47 source-map-js: 1.2.0 '@vue/compiler-ssr@3.4.37': @@ -17406,47 +16263,59 @@ snapshots: '@vue/compiler-dom': 3.4.37 '@vue/shared': 3.4.37 + '@vue/compiler-ssr@3.5.10': + dependencies: + '@vue/compiler-dom': 3.5.10 + '@vue/shared': 3.5.10 + '@vue/compiler-vue2@2.7.16': dependencies: de-indent: 1.0.2 he: 1.2.0 - '@vue/devtools-api@6.6.1': {} - - '@vue/language-core@2.0.16(typescript@5.5.4)': + '@vue/language-core@2.0.16(typescript@5.6.2)': dependencies: '@volar/language-core': 2.2.0 - '@vue/compiler-dom': 3.4.34 - '@vue/shared': 3.4.34 + '@vue/compiler-dom': 3.4.37 + '@vue/shared': 3.4.37 computeds: 0.0.1 minimatch: 9.0.4 path-browserify: 1.0.1 vue-template-compiler: 2.7.14 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 - '@vue/language-core@2.0.29(typescript@5.5.4)': + '@vue/language-core@2.1.6(typescript@5.6.2)': dependencies: - '@volar/language-core': 2.4.0-alpha.18 - '@vue/compiler-dom': 3.4.34 + '@volar/language-core': 2.4.6 + '@vue/compiler-dom': 3.4.37 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.4.34 + '@vue/shared': 3.4.37 computeds: 0.0.1 minimatch: 9.0.4 muggle-string: 0.4.1 path-browserify: 1.0.1 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 '@vue/reactivity@3.4.37': dependencies: '@vue/shared': 3.4.37 + '@vue/reactivity@3.5.10': + dependencies: + '@vue/shared': 3.5.10 + '@vue/runtime-core@3.4.37': dependencies: '@vue/reactivity': 3.4.37 '@vue/shared': 3.4.37 + '@vue/runtime-core@3.5.10': + dependencies: + '@vue/reactivity': 3.5.10 + '@vue/shared': 3.5.10 + '@vue/runtime-dom@3.4.37': dependencies: '@vue/reactivity': 3.4.37 @@ -17454,40 +16323,36 @@ snapshots: '@vue/shared': 3.4.37 csstype: 3.1.3 + '@vue/runtime-dom@3.5.10': + dependencies: + '@vue/reactivity': 3.5.10 + '@vue/runtime-core': 3.5.10 + '@vue/shared': 3.5.10 + csstype: 3.1.3 + '@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4))': dependencies: '@vue/compiler-ssr': 3.4.37 '@vue/shared': 3.4.37 vue: 3.4.37(typescript@5.5.4) - '@vue/shared@3.4.31': {} - - '@vue/shared@3.4.34': {} + '@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2))': + dependencies: + '@vue/compiler-ssr': 3.5.10 + '@vue/shared': 3.5.10 + vue: 3.5.10(typescript@5.6.2) '@vue/shared@3.4.37': {} - '@vue/test-utils@2.4.1(@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4)))(vue@3.4.37(typescript@5.5.4))': + '@vue/shared@3.5.10': {} + + '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))': dependencies: js-beautify: 1.14.9 - vue: 3.4.37(typescript@5.5.4) + vue: 3.5.10(typescript@5.6.2) vue-component-type-helpers: 1.8.4 optionalDependencies: - '@vue/server-renderer': 3.4.37(vue@3.4.37(typescript@5.5.4)) - - '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.19.11)': - dependencies: - esbuild: 0.19.11 - tslib: 2.6.3 - - '@yarnpkg/fslib@2.10.3': - dependencies: - '@yarnpkg/libzip': 2.3.0 - tslib: 1.14.1 - - '@yarnpkg/libzip@2.3.0': - dependencies: - '@types/emscripten': 1.39.7 - tslib: 1.14.1 + '@vue/server-renderer': 3.5.10(vue@3.5.10(typescript@5.6.2)) abbrev@1.1.1: {} @@ -17529,14 +16394,6 @@ snapshots: acorn@8.12.1: {} - address@1.2.2: {} - - agent-base@6.0.2: - dependencies: - debug: 4.3.5(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - agent-base@7.1.0: dependencies: debug: 4.3.5(supports-color@8.1.1) @@ -17566,14 +16423,14 @@ snapshots: optionalDependencies: ajv: 8.17.1 - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv-formats@3.0.1(ajv@8.13.0): optionalDependencies: ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -17633,8 +16490,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - app-root-dir@1.0.2: {} - app-root-path@3.1.0: {} append-field@1.0.0: {} @@ -17661,8 +16516,6 @@ snapshots: tar-stream: 3.1.6 zip-stream: 6.0.1 - archy@1.0.0: {} - arg@5.0.2: {} argon2@0.40.1: @@ -17690,25 +16543,32 @@ snapshots: call-bind: 1.0.2 is-array-buffer: 3.0.2 + array-buffer-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + array-flatten@1.1.1: {} - array-includes@3.1.7: + array-includes@3.1.8: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - get-intrinsic: 1.2.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 is-string: 1.0.7 array-union@2.1.0: {} - array.prototype.findlastindex@1.2.3: + array.prototype.findlastindex@1.2.5: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - es-shim-unscopables: 1.0.0 - get-intrinsic: 1.2.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-shim-unscopables: 1.0.2 array.prototype.flat@1.3.2: dependencies: @@ -17733,6 +16593,17 @@ snapshots: is-array-buffer: 3.0.2 is-shared-array-buffer: 1.0.2 + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + arrify@1.0.1: {} asap@2.0.6: {} @@ -17754,27 +16625,21 @@ snapshots: pvutils: 1.1.3 tslib: 2.6.3 - assert-never@1.2.1: {} - - assert-plus@1.0.0: {} - - assert@2.1.0: - dependencies: - call-bind: 1.0.2 - is-nan: 1.3.2 - object-is: 1.1.5 - object.assign: 4.1.4 - util: 0.12.5 - + assert-never@1.2.1: {} + + assert-plus@1.0.0: {} + assertion-error@1.1.0: {} + assertion-error@2.0.1: {} + ast-types@0.16.1: dependencies: tslib: 2.6.3 astral-regex@2.0.0: {} - astring@1.8.6: {} + astring@1.9.0: {} async-mutex@0.5.0: dependencies: @@ -17792,14 +16657,14 @@ snapshots: available-typed-arrays@1.0.5: {} - avvio@8.3.0: + available-typed-arrays@1.0.7: dependencies: - '@fastify/error': 3.4.0 - archy: 1.0.0 - debug: 4.3.5(supports-color@8.1.1) + possible-typed-array-names: 1.0.0 + + avvio@9.0.0: + dependencies: + '@fastify/error': 4.0.0 fastq: 1.17.1 - transitivePeerDependencies: - - supports-color aws-sdk-client-mock@4.0.1: dependencies: @@ -17813,21 +16678,21 @@ snapshots: axios@0.24.0: dependencies: - follow-redirects: 1.15.2(debug@4.3.5) + follow-redirects: 1.15.2 transitivePeerDependencies: - debug - axios@1.6.0: + axios@1.7.4: dependencies: - follow-redirects: 1.15.2(debug@4.3.5) + follow-redirects: 1.15.9(debug@4.3.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - axios@1.6.2(debug@4.3.5): + axios@1.7.7(debug@4.3.7): dependencies: - follow-redirects: 1.15.2(debug@4.3.5) + follow-redirects: 1.15.9(debug@4.3.7) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -17835,10 +16700,6 @@ snapshots: b4a@1.6.4: {} - babel-core@7.0.0-bridge.0(@babel/core@7.24.7): - dependencies: - '@babel/core': 7.24.7 - babel-jest@29.7.0(@babel/core@7.23.5): dependencies: '@babel/core': 7.23.5 @@ -17883,30 +16744,6 @@ snapshots: '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.20.0 - babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.7): - dependencies: - '@babel/compat-data': 7.24.7 - '@babel/core': 7.24.7 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.7): - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) - core-js-compat: 3.37.1 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.7): - dependencies: - '@babel/core': 7.24.7 - '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) - transitivePeerDependencies: - - supports-color - babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.5): dependencies: '@babel/core': 7.23.5 @@ -17973,8 +16810,6 @@ snapshots: dependencies: open: 8.4.2 - big-integer@1.6.51: {} - bin-check@4.1.0: dependencies: execa: 0.7.0 @@ -17993,12 +16828,6 @@ snapshots: binary-extensions@2.2.0: {} - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.0 - blob-util@2.0.2: {} bluebird@3.7.2: {} @@ -18024,14 +16853,27 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} bowser@2.11.0: {} - bplist-parser@0.2.0: - dependencies: - big-integer: 1.6.51 - brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -18113,7 +16955,7 @@ snapshots: node-gyp-build: 4.8.1 optional: true - bullmq@5.10.4: + bullmq@5.13.2: dependencies: cron-parser: 4.8.1 ioredis: 5.4.1 @@ -18131,8 +16973,6 @@ snapshots: dependencies: streamsearch: 1.1.0 - bytes@3.0.0: {} - bytes@3.1.2: {} cac@6.7.14: {} @@ -18163,7 +17003,7 @@ snapshots: http-cache-semantics: 4.1.1 keyv: 4.5.4 mimic-response: 4.0.0 - normalize-url: 8.0.0 + normalize-url: 8.0.1 responselike: 3.0.0 cacheable-request@12.0.1: @@ -18193,6 +17033,14 @@ snapshots: function-bind: 1.1.2 get-intrinsic: 1.2.1 + call-bind@1.0.7: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + call-me-maybe@1.0.2: {} callsites@3.1.0: {} @@ -18240,6 +17088,14 @@ snapshots: pathval: 1.1.1 type-detect: 4.0.8 + chai@5.1.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + chalk-template@1.1.0: dependencies: chalk: 5.3.0 @@ -18270,32 +17126,34 @@ snapshots: dependencies: is-regex: 1.1.4 - chart.js@4.4.3: + chart.js@4.4.4: dependencies: '@kurkle/color': 0.3.2 - chartjs-adapter-date-fns@3.0.0(chart.js@4.4.3)(date-fns@2.30.0): + chartjs-adapter-date-fns@3.0.0(chart.js@4.4.4)(date-fns@2.30.0): dependencies: - chart.js: 4.4.3 + chart.js: 4.4.4 date-fns: 2.30.0 - chartjs-chart-matrix@2.0.1(chart.js@4.4.3): + chartjs-chart-matrix@2.0.1(chart.js@4.4.4): dependencies: - chart.js: 4.4.3 + chart.js: 4.4.4 - chartjs-plugin-gradient@0.6.1(chart.js@4.4.3): + chartjs-plugin-gradient@0.6.1(chart.js@4.4.4): dependencies: - chart.js: 4.4.3 + chart.js: 4.4.4 - chartjs-plugin-zoom@2.0.1(chart.js@4.4.3): + chartjs-plugin-zoom@2.0.1(chart.js@4.4.4): dependencies: - chart.js: 4.4.3 + chart.js: 4.4.4 hammerjs: 2.0.8 check-error@1.0.3: dependencies: get-func-name: 2.0.2 + check-error@2.1.1: {} + check-more-types@2.24.0: {} cheerio-select@2.1.0: @@ -18307,6 +17165,20 @@ snapshots: domhandler: 5.0.3 domutils: 3.0.1 + cheerio@1.0.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + parse5-parser-stream: 7.1.2 + undici: 6.20.0 + whatwg-mimetype: 4.0.0 + cheerio@1.0.0-rc.12: dependencies: cheerio-select: 2.1.0 @@ -18331,7 +17203,7 @@ snapshots: chownr@2.0.0: {} - chromatic@11.5.6: {} + chromatic@11.10.4: {} ci-info@3.7.1: {} @@ -18389,18 +17261,10 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone-deep@4.0.1: - dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - clone-response@1.0.3: dependencies: mimic-response: 1.0.1 - clone@1.0.4: {} - cluster-key-slot@1.1.2: {} co@4.6.0: {} @@ -18441,6 +17305,8 @@ snapshots: commander@10.0.1: {} + commander@12.1.0: {} + commander@2.20.3: {} commander@6.2.1: {} @@ -18465,22 +17331,6 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.3.0 - compressible@2.0.18: - dependencies: - mime-db: 1.52.0 - - compression@1.7.4: - dependencies: - accepts: 1.3.8 - bytes: 3.0.0 - compressible: 2.0.18 - debug: 2.6.9 - on-headers: 1.0.2 - safe-buffer: 5.1.2 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - computeds@0.0.1: {} concat-map@0.0.1: {} @@ -18527,9 +17377,7 @@ snapshots: cookie@0.6.0: {} - core-js-compat@3.37.1: - dependencies: - browserslist: 4.23.0 + cookie@0.7.2: {} core-util-is@1.0.2: {} @@ -18566,10 +17414,10 @@ snapshots: dependencies: luxon: 3.3.0 - cropperjs@2.0.0-rc.1: + cropperjs@2.0.0-rc.2: dependencies: - '@cropper/elements': 2.0.0-rc.1 - '@cropper/utils': 2.0.0-rc.1 + '@cropper/elements': 2.0.0-rc.2 + '@cropper/utils': 2.0.0-rc.2 cross-env@7.0.3: dependencies: @@ -18599,13 +17447,9 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crypto-random-string@4.0.0: - dependencies: - type-fest: 1.4.0 - - css-declaration-sorter@7.2.0(postcss@8.4.40): + css-declaration-sorter@7.2.0(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 css-select@5.1.0: dependencies: @@ -18631,49 +17475,49 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@6.1.2(postcss@8.4.40): + cssnano-preset-default@6.1.2(postcss@8.4.47): dependencies: browserslist: 4.23.0 - css-declaration-sorter: 7.2.0(postcss@8.4.40) - cssnano-utils: 4.0.2(postcss@8.4.40) - postcss: 8.4.40 - postcss-calc: 9.0.1(postcss@8.4.40) - postcss-colormin: 6.1.0(postcss@8.4.40) - postcss-convert-values: 6.1.0(postcss@8.4.40) - postcss-discard-comments: 6.0.2(postcss@8.4.40) - postcss-discard-duplicates: 6.0.3(postcss@8.4.40) - postcss-discard-empty: 6.0.3(postcss@8.4.40) - postcss-discard-overridden: 6.0.2(postcss@8.4.40) - postcss-merge-longhand: 6.0.5(postcss@8.4.40) - postcss-merge-rules: 6.1.1(postcss@8.4.40) - postcss-minify-font-values: 6.1.0(postcss@8.4.40) - postcss-minify-gradients: 6.0.3(postcss@8.4.40) - postcss-minify-params: 6.1.0(postcss@8.4.40) - postcss-minify-selectors: 6.0.4(postcss@8.4.40) - postcss-normalize-charset: 6.0.2(postcss@8.4.40) - postcss-normalize-display-values: 6.0.2(postcss@8.4.40) - postcss-normalize-positions: 6.0.2(postcss@8.4.40) - postcss-normalize-repeat-style: 6.0.2(postcss@8.4.40) - postcss-normalize-string: 6.0.2(postcss@8.4.40) - postcss-normalize-timing-functions: 6.0.2(postcss@8.4.40) - postcss-normalize-unicode: 6.1.0(postcss@8.4.40) - postcss-normalize-url: 6.0.2(postcss@8.4.40) - postcss-normalize-whitespace: 6.0.2(postcss@8.4.40) - postcss-ordered-values: 6.0.2(postcss@8.4.40) - postcss-reduce-initial: 6.1.0(postcss@8.4.40) - postcss-reduce-transforms: 6.0.2(postcss@8.4.40) - postcss-svgo: 6.0.3(postcss@8.4.40) - postcss-unique-selectors: 6.0.4(postcss@8.4.40) - - cssnano-utils@4.0.2(postcss@8.4.40): - dependencies: - postcss: 8.4.40 - - cssnano@6.1.2(postcss@8.4.40): - dependencies: - cssnano-preset-default: 6.1.2(postcss@8.4.40) + css-declaration-sorter: 7.2.0(postcss@8.4.47) + cssnano-utils: 4.0.2(postcss@8.4.47) + postcss: 8.4.47 + postcss-calc: 9.0.1(postcss@8.4.47) + postcss-colormin: 6.1.0(postcss@8.4.47) + postcss-convert-values: 6.1.0(postcss@8.4.47) + postcss-discard-comments: 6.0.2(postcss@8.4.47) + postcss-discard-duplicates: 6.0.3(postcss@8.4.47) + postcss-discard-empty: 6.0.3(postcss@8.4.47) + postcss-discard-overridden: 6.0.2(postcss@8.4.47) + postcss-merge-longhand: 6.0.5(postcss@8.4.47) + postcss-merge-rules: 6.1.1(postcss@8.4.47) + postcss-minify-font-values: 6.1.0(postcss@8.4.47) + postcss-minify-gradients: 6.0.3(postcss@8.4.47) + postcss-minify-params: 6.1.0(postcss@8.4.47) + postcss-minify-selectors: 6.0.4(postcss@8.4.47) + postcss-normalize-charset: 6.0.2(postcss@8.4.47) + postcss-normalize-display-values: 6.0.2(postcss@8.4.47) + postcss-normalize-positions: 6.0.2(postcss@8.4.47) + postcss-normalize-repeat-style: 6.0.2(postcss@8.4.47) + postcss-normalize-string: 6.0.2(postcss@8.4.47) + postcss-normalize-timing-functions: 6.0.2(postcss@8.4.47) + postcss-normalize-unicode: 6.1.0(postcss@8.4.47) + postcss-normalize-url: 6.0.2(postcss@8.4.47) + postcss-normalize-whitespace: 6.0.2(postcss@8.4.47) + postcss-ordered-values: 6.0.2(postcss@8.4.47) + postcss-reduce-initial: 6.1.0(postcss@8.4.47) + postcss-reduce-transforms: 6.0.2(postcss@8.4.47) + postcss-svgo: 6.0.3(postcss@8.4.47) + postcss-unique-selectors: 6.0.4(postcss@8.4.47) + + cssnano-utils@4.0.2(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + + cssnano@6.1.2(postcss@8.4.47): + dependencies: + cssnano-preset-default: 6.1.2(postcss@8.4.47) lilconfig: 3.1.1 - postcss: 8.4.40 + postcss: 8.4.47 csso@5.0.5: dependencies: @@ -18685,9 +17529,54 @@ snapshots: csstype@3.1.3: {} - cypress@13.13.1: + cypress@13.14.2: + dependencies: + '@cypress/request': 3.0.5 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.3 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.3.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.3 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.10 + debug: 4.3.5(supports-color@8.1.1) + enquirer: 2.3.6 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.3.6) + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.6.0 + supports-color: 8.1.1 + tmp: 0.2.3 + untildify: 4.0.0 + yauzl: 2.10.0 + + cypress@13.15.0: dependencies: - '@cypress/request': 3.0.0 + '@cypress/request': 3.0.5 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) '@types/sinonjs__fake-timers': 8.1.1 '@types/sizzle': 2.3.3 @@ -18741,6 +17630,24 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + date-fns@2.30.0: dependencies: '@babel/runtime': 7.23.4 @@ -18765,12 +17672,22 @@ snapshots: optionalDependencies: supports-color: 5.5.0 + debug@4.3.5(supports-color@5.5.0): + dependencies: + ms: 2.1.2 + optionalDependencies: + supports-color: 5.5.0 + debug@4.3.5(supports-color@8.1.1): dependencies: ms: 2.1.2 optionalDependencies: supports-color: 8.1.1 + debug@4.3.7: + dependencies: + ms: 2.1.3 + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 @@ -18814,6 +17731,8 @@ snapshots: dependencies: type-detect: 4.0.8 + deep-eql@5.0.2: {} + deep-equal@2.2.0: dependencies: call-bind: 1.0.2 @@ -18838,16 +17757,13 @@ snapshots: deepmerge@4.2.2: {} - default-browser-id@3.0.0: - dependencies: - bplist-parser: 0.2.0 - untildify: 4.0.0 + defer-to-connect@2.0.1: {} - defaults@1.0.4: + define-data-property@1.1.4: dependencies: - clone: 1.0.4 - - defer-to-connect@2.0.1: {} + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 define-lazy-prop@2.0.0: {} @@ -18856,7 +17772,11 @@ snapshots: has-property-descriptors: 1.0.0 object-keys: 1.1.1 - defu@6.1.4: {} + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 delayed-stream@1.0.0: {} @@ -18868,23 +17788,10 @@ snapshots: destroy@1.2.0: {} - detect-indent@6.1.0: {} - detect-libc@2.0.3: {} detect-newline@3.1.0: {} - detect-package-manager@2.0.1: - dependencies: - execa: 5.1.1 - - detect-port@1.5.1: - dependencies: - address: 1.2.2 - debug: 4.3.5(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -18919,6 +17826,12 @@ snapshots: dom-accessibility-api@0.6.3: {} + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -18927,17 +17840,35 @@ snapshots: domelementtype@2.3.0: {} + domhandler@3.3.0: + dependencies: + domelementtype: 2.3.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils@3.0.1: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - dotenv-expand@10.0.0: {} + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 dotenv@16.0.3: {} @@ -18977,10 +17908,15 @@ snapshots: emoji-regex@9.2.2: {} - encode-utf8@1.0.3: {} - encodeurl@1.0.2: {} + encodeurl@2.0.0: {} + + encoding-sniffer@0.2.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + encoding@0.1.13: dependencies: iconv-lite: 0.6.3 @@ -19002,8 +17938,6 @@ snapshots: env-paths@2.2.1: {} - envinfo@7.8.1: {} - err-code@2.0.3: {} error-ex@1.3.2: @@ -19052,6 +17986,61 @@ snapshots: unbox-primitive: 1.0.2 which-typed-array: 1.1.11 + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.2 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.3 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + es-get-iterator@1.1.3: dependencies: call-bind: 1.0.2 @@ -19066,28 +18055,40 @@ snapshots: es-module-lexer@1.5.4: {} + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.1: dependencies: get-intrinsic: 1.2.1 has: 1.0.3 has-tostringtag: 1.0.0 + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-shim-unscopables@1.0.0: dependencies: has: 1.0.3 + es-shim-unscopables@1.0.2: + dependencies: + hasown: 2.0.2 + es-to-primitive@1.2.1: dependencies: is-callable: 1.2.7 is-date-object: 1.0.5 is-symbol: 1.0.4 - esbuild-plugin-alias@0.2.1: {} - - esbuild-register@3.5.0(esbuild@0.19.11): + esbuild-register@3.5.0(esbuild@0.23.1): dependencies: debug: 4.3.5(supports-color@8.1.1) - esbuild: 0.19.11 + esbuild: 0.23.1 transitivePeerDependencies: - supports-color @@ -19195,8 +18196,37 @@ snapshots: '@esbuild/win32-ia32': 0.23.0 '@esbuild/win32-x64': 0.23.0 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.1.1: {} + escape-goat@3.0.0: {} + escape-html@1.0.3: {} escape-regexp@0.0.1: {} @@ -19235,43 +18265,44 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7(supports-color@8.1.1) - is-core-module: 2.13.1 + is-core-module: 2.15.1 resolve: 1.22.8 transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2) eslint: 9.8.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0): dependencies: - array-includes: 3.1.7 - array.prototype.findlastindex: 1.2.3 + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 eslint: 9.8.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0) - hasown: 2.0.0 - is-core-module: 2.13.1 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0) + hasown: 2.0.2 + is-core-module: 2.15.1 is-glob: 4.0.3 minimatch: 3.1.2 - object.fromentries: 2.0.7 - object.groupby: 1.0.1 - object.values: 1.1.7 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.0 semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -19291,6 +18322,20 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-vue@9.28.0(eslint@9.8.0): + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) + eslint: 9.8.0 + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.0.16 + semver: 7.6.3 + vue-eslint-parser: 9.4.3(eslint@9.8.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + eslint-rule-docs@1.1.235: {} eslint-scope@7.2.2: @@ -19421,7 +18466,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 esutils@2.0.3: {} @@ -19505,16 +18550,16 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 - execa@9.3.0: + execa@9.4.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 cross-spawn: 7.0.3 figures: 6.1.0 get-stream: 9.0.1 - human-signals: 7.0.0 + human-signals: 8.0.0 is-plain-obj: 4.1.0 is-stream: 4.0.1 - npm-run-path: 5.3.0 + npm-run-path: 6.0.0 pretty-ms: 9.0.0 signal-exit: 4.1.0 strip-final-newline: 4.0.0 @@ -19536,34 +18581,70 @@ snapshots: exponential-backoff@3.1.1: {} - express@4.19.2: + express@4.19.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.2 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.6.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@4.21.0: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.2 + body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.6.0 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 - encodeurl: 1.0.2 + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.2.0 + finalhandler: 1.3.1 fresh: 0.5.2 http-errors: 2.0.0 - merge-descriptors: 1.0.1 + merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.7 + path-to-regexp: 0.1.10 proxy-addr: 2.0.7 - qs: 6.11.0 + qs: 6.13.0 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 + send: 0.19.0 + serve-static: 1.16.2 setprototypeof: 1.2.0 statuses: 2.0.1 type-is: 1.6.18 @@ -19595,7 +18676,7 @@ snapshots: extsprintf@1.3.0: {} - fast-content-type-parse@1.1.0: {} + fast-content-type-parse@2.0.0: {} fast-decode-uri-component@1.0.1: {} @@ -19609,18 +18690,19 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.7 + micromatch: 4.0.8 fast-json-stable-stringify@2.1.0: {} - fast-json-stringify@5.8.0: + fast-json-stringify@6.0.0: dependencies: - '@fastify/deepmerge': 1.3.0 + '@fastify/merge-json-schemas': 0.1.1 ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) + ajv-formats: 3.0.1(ajv@8.17.1) fast-deep-equal: 3.1.3 - fast-uri: 2.2.0 - rfdc: 1.3.0 + fast-uri: 2.4.0 + json-schema-ref-resolver: 1.0.1 + rfdc: 1.4.1 fast-levenshtein@2.0.6: {} @@ -19632,7 +18714,7 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@2.2.0: {} + fast-uri@2.4.0: {} fast-uri@3.0.1: {} @@ -19640,7 +18722,7 @@ snapshots: dependencies: strnum: 1.0.5 - fast-xml-parser@4.4.0: + fast-xml-parser@4.4.1: dependencies: strnum: 1.0.5 @@ -19659,34 +18741,33 @@ snapshots: dependencies: semver: 7.6.0 - fastify-plugin@4.5.0: {} + fastify-plugin@4.5.1: {} + + fastify-plugin@5.0.1: {} - fastify-raw-body@4.3.0: + fastify-raw-body@5.0.0: dependencies: - fastify-plugin: 4.5.0 - raw-body: 2.5.2 + fastify-plugin: 5.0.1 + raw-body: 3.0.0 secure-json-parse: 2.7.0 - fastify@4.28.1: + fastify@5.0.0: dependencies: - '@fastify/ajv-compiler': 3.5.0 - '@fastify/error': 3.4.0 - '@fastify/fast-json-stringify-compiler': 4.3.0 + '@fastify/ajv-compiler': 4.0.1 + '@fastify/error': 4.0.0 + '@fastify/fast-json-stringify-compiler': 5.0.1 abstract-logging: 2.0.1 - avvio: 8.3.0 - fast-content-type-parse: 1.1.0 - fast-json-stringify: 5.8.0 - find-my-way: 8.2.0 - light-my-request: 5.11.0 + avvio: 9.0.0 + fast-json-stringify: 6.0.0 + find-my-way: 9.1.0 + light-my-request: 6.1.0 pino: 9.2.0 - process-warning: 3.0.0 + process-warning: 4.0.0 proxy-addr: 2.0.7 - rfdc: 1.3.0 + rfdc: 1.4.1 secure-json-parse: 2.7.0 semver: 7.6.0 toad-cache: 3.7.0 - transitivePeerDependencies: - - supports-color fastq@1.17.1: dependencies: @@ -19696,10 +18777,6 @@ snapshots: dependencies: bser: 2.1.1 - fd-package-json@1.2.0: - dependencies: - walk-up-path: 3.0.1 - fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -19713,8 +18790,6 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.2.1 - fetch-retry@5.0.4: {} - figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -19731,20 +18806,16 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-system-cache@2.3.0: - dependencies: - fs-extra: 11.1.1 - ramda: 0.29.0 - file-type@17.1.6: dependencies: readable-web-to-node-stream: 3.0.2 strtok3: 7.0.0 token-types: 5.0.1 - file-type@19.3.0: + file-type@19.5.0: dependencies: - strtok3: 8.0.1 + get-stream: 9.0.1 + strtok3: 8.1.0 token-types: 6.0.0 uint8array-extras: 1.4.0 @@ -19780,11 +18851,17 @@ snapshots: transitivePeerDependencies: - supports-color - find-cache-dir@2.1.0: + finalhandler@1.3.1: dependencies: - commondir: 1.0.1 - make-dir: 2.1.0 - pkg-dir: 3.0.0 + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color find-cache-dir@3.3.2: dependencies: @@ -19792,18 +18869,14 @@ snapshots: make-dir: 3.1.0 pkg-dir: 4.2.0 - find-my-way@8.2.0: + find-my-way@9.1.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 - safe-regex2: 3.1.0 + safe-regex2: 4.0.0 find-package-json@1.2.0: {} - find-up@3.0.0: - dependencies: - locate-path: 3.0.0 - find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -19840,16 +18913,16 @@ snapshots: flatted@3.3.1: {} - flow-parser@0.202.0: {} - fluent-ffmpeg@2.1.3: dependencies: async: 0.2.10 which: 1.3.1 - follow-redirects@1.15.2(debug@4.3.5): + follow-redirects@1.15.2: {} + + follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 for-each@0.3.3: dependencies: @@ -19866,12 +18939,6 @@ snapshots: form-data-encoder@4.0.2: {} - form-data@2.3.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -19935,6 +19002,13 @@ snapshots: es-abstract: 1.22.1 functions-have-names: 1.2.3 + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + functions-have-names@1.2.3: {} gensync@1.0.0-beta.2: {} @@ -19950,6 +19024,14 @@ snapshots: has-proto: 1.0.1 has-symbols: 1.0.3 + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.2 + get-package-type@0.1.0: {} get-stream@3.0.0: {} @@ -19972,6 +19054,12 @@ snapshots: call-bind: 1.0.2 get-intrinsic: 1.2.1 + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + get-tsconfig@4.7.2: dependencies: resolve-pkg-maps: 1.0.0 @@ -19984,18 +19072,6 @@ snapshots: dependencies: assert-plus: 1.0.0 - giget@1.1.2: - dependencies: - colorette: 2.0.19 - defu: 6.1.4 - https-proxy-agent: 5.0.1 - mri: 1.2.0 - node-fetch-native: 1.0.2 - pathe: 1.1.2 - tar: 6.2.1 - transitivePeerDependencies: - - supports-color - github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -20011,8 +19087,6 @@ snapshots: '@types/glob': 7.2.0 glob: 7.2.3 - glob-to-regexp@0.4.1: {} - glob@10.3.10: dependencies: foreground-child: 3.1.1 @@ -20059,11 +19133,11 @@ snapshots: globals@14.0.0: {} - globals@15.8.0: {} + globals@15.9.0: {} globalthis@1.0.3: dependencies: - define-properties: 1.2.0 + define-properties: 1.2.1 globby@11.1.0: dependencies: @@ -20074,18 +19148,9 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 - globby@14.0.1: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.2 - ignore: 5.3.1 - path-type: 5.0.0 - slash: 5.1.0 - unicorn-magic: 0.1.0 - gopd@1.0.1: dependencies: - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 got@11.8.5: dependencies: @@ -20139,15 +19204,6 @@ snapshots: hammerjs@2.0.8: {} - handlebars@4.7.7: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.17.4 - happy-dom@10.0.3: dependencies: css.escape: 1.5.1 @@ -20173,28 +19229,40 @@ snapshots: has-property-descriptors@1.0.0: dependencies: - get-intrinsic: 1.2.1 + get-intrinsic: 1.2.4 + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 has-proto@1.0.1: {} + has-proto@1.0.3: {} + has-symbols@1.0.3: {} has-tostringtag@1.0.0: dependencies: has-symbols: 1.0.3 + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + has@1.0.3: dependencies: function-bind: 1.1.2 hash-sum@2.0.0: {} - hashlru@2.3.0: {} - hasown@2.0.0: dependencies: function-bind: 1.1.2 + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + hast-util-heading-rank@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -20213,7 +19281,7 @@ snapshots: highlight.js@10.7.3: {} - highlight.js@11.9.0: {} + highlight.js@11.10.0: {} hosted-git-info@2.8.9: {} @@ -20235,6 +19303,13 @@ snapshots: htmlescape@1.1.1: {} + htmlparser2@5.0.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 3.3.0 + domutils: 2.8.0 + entities: 2.2.0 + htmlparser2@8.0.1: dependencies: domelementtype: 2.3.0 @@ -20242,6 +19317,13 @@ snapshots: domutils: 3.0.1 entities: 4.5.0 + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + http-cache-semantics@4.1.1: {} http-errors@2.0.0: @@ -20261,11 +19343,11 @@ snapshots: transitivePeerDependencies: - supports-color - http-signature@1.3.6: + http-signature@1.4.0: dependencies: assert-plus: 1.0.0 jsprim: 2.0.2 - sshpk: 1.17.0 + sshpk: 1.18.0 http2-wrapper@1.0.3: dependencies: @@ -20279,13 +19361,6 @@ snapshots: http_ece@1.2.0: {} - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.3.5(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - https-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 @@ -20293,13 +19368,6 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.4: - dependencies: - agent-base: 7.1.0 - debug: 4.3.5(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.0 @@ -20315,7 +19383,7 @@ snapshots: human-signals@5.0.0: {} - human-signals@7.0.0: {} + human-signals@8.0.0: {} iconv-lite@0.4.24: dependencies: @@ -20393,6 +19461,12 @@ snapshots: has: 1.0.3 side-channel: 1.0.4 + internal-slot@1.0.7: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.4 + intersection-observer@0.12.2: {} ioredis@5.4.1: @@ -20414,7 +19488,7 @@ snapshots: jsbn: 1.1.0 sprintf-js: 1.1.3 - ip-cidr@4.0.1: + ip-cidr@4.0.2: dependencies: ip-address: 9.0.5 @@ -20441,6 +19515,11 @@ snapshots: get-intrinsic: 1.2.1 is-typed-array: 1.1.10 + is-array-buffer@3.0.4: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + is-arrayish@0.2.1: {} is-arrayish@0.3.2: {} @@ -20468,6 +19547,14 @@ snapshots: dependencies: hasown: 2.0.0 + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + is-date-object@1.0.5: dependencies: has-tostringtag: 1.0.0 @@ -20500,8 +19587,6 @@ snapshots: global-dirs: 3.0.1 is-path-inside: 3.0.3 - is-interactive@1.0.0: {} - is-ip@3.1.0: dependencies: ip-regex: 4.3.0 @@ -20510,13 +19595,10 @@ snapshots: is-map@2.0.2: {} - is-nan@1.3.2: - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - is-negative-zero@2.0.2: {} + is-negative-zero@2.0.3: {} + is-node-process@1.2.0: {} is-number-object@1.0.7: @@ -20531,10 +19613,6 @@ snapshots: is-plain-obj@4.1.0: {} - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -20552,6 +19630,10 @@ snapshots: dependencies: call-bind: 1.0.2 + is-shared-array-buffer@1.0.3: + dependencies: + call-bind: 1.0.7 + is-stream@1.1.0: {} is-stream@2.0.1: {} @@ -20564,9 +19646,9 @@ snapshots: dependencies: has-tostringtag: 1.0.0 - is-svg@5.0.1: + is-svg@5.1.0: dependencies: - fast-xml-parser: 4.4.0 + fast-xml-parser: 4.4.1 is-symbol@1.0.4: dependencies: @@ -20580,6 +19662,10 @@ snapshots: gopd: 1.0.1 has-tostringtag: 1.0.0 + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + is-typedarray@1.0.0: {} is-unicode-supported@0.1.0: {} @@ -20590,7 +19676,7 @@ snapshots: is-weakref@1.0.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 is-weakset@2.0.2: dependencies: @@ -20611,8 +19697,6 @@ snapshots: isexe@3.1.1: {} - isobject@3.0.1: {} - isstream@0.1.2: {} istanbul-lib-coverage@3.2.2: {} @@ -20755,7 +19839,7 @@ snapshots: jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.7 + micromatch: 4.0.8 parse-json: 5.2.0 pretty-format: 29.7.0 slash: 3.0.0 @@ -20814,7 +19898,7 @@ snapshots: jest-regex-util: 29.6.3 jest-util: 29.7.0 jest-worker: 29.7.0 - micromatch: 4.0.7 + micromatch: 4.0.8 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 @@ -20838,7 +19922,7 @@ snapshots: '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.7 + micromatch: 4.0.8 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -21015,6 +20099,14 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + js-beautify@1.14.9: dependencies: config-chain: 1.1.13 @@ -21043,32 +20135,36 @@ snapshots: jschardet@3.0.0: {} - jscodeshift@0.15.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)): + jsdoc-type-pratt-parser@4.1.0: {} + + jsdom@24.1.1: dependencies: - '@babel/core': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.7) - '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.7) - '@babel/preset-flow': 7.23.3(@babel/core@7.24.7) - '@babel/preset-typescript': 7.23.3(@babel/core@7.24.7) - '@babel/register': 7.22.15(@babel/core@7.24.7) - babel-core: 7.0.0-bridge.0(@babel/core@7.24.7) - chalk: 4.1.2 - flow-parser: 0.202.0 - graceful-fs: 4.2.11 - micromatch: 4.0.7 - neo-async: 2.6.2 - node-dir: 0.1.17 - recast: 0.23.6 - temp: 0.8.4 - write-file-atomic: 2.4.3 - optionalDependencies: - '@babel/preset-env': 7.24.7(@babel/core@7.24.7) + cssstyle: 4.0.1 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.12 + parse5: 7.1.2 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + xml-name-validator: 5.0.0 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate + optional: true jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3): dependencies: @@ -21127,14 +20223,16 @@ snapshots: - utf-8-validate optional: true - jsesc@0.5.0: {} - jsesc@2.5.2: {} json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} + json-schema-ref-resolver@1.0.1: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -21206,6 +20304,14 @@ snapshots: is-promise: 2.2.2 promise: 7.3.1 + juice@11.0.0: + dependencies: + cheerio: 1.0.0 + commander: 12.1.0 + mensch: 0.3.4 + slick: 1.12.2 + web-resource-inliner: 7.0.0 + just-extend@4.2.1: {} jwa@2.0.0: @@ -21219,7 +20325,7 @@ snapshots: jwa: 2.0.0 safe-buffer: 5.2.1 - katex@0.16.9: + katex@0.16.10: dependencies: commander: 8.3.0 @@ -21243,12 +20349,6 @@ snapshots: lazy-ass@1.6.0: {} - lazy-universal-dotenv@4.0.0: - dependencies: - app-root-dir: 1.0.2 - dotenv: 16.0.3 - dotenv-expand: 10.0.0 - lazystream@1.0.1: dependencies: readable-stream: 2.3.7 @@ -21260,10 +20360,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - light-my-request@5.11.0: + light-my-request@6.1.0: dependencies: - cookie: 0.5.0 - process-warning: 2.2.0 + cookie: 0.7.2 + process-warning: 4.0.0 set-cookie-parser: 2.6.0 lilconfig@3.1.1: {} @@ -21288,11 +20388,6 @@ snapshots: mlly: 1.5.0 pkg-types: 1.0.3 - locate-path@3.0.0: - dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 - locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -21301,8 +20396,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.debounce@4.0.8: {} - lodash.defaults@4.2.0: {} lodash.get@4.4.2: {} @@ -21341,6 +20434,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + loupe@3.1.2: {} + lowercase-keys@2.0.0: {} lowercase-keys@3.0.0: {} @@ -21378,6 +20473,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + magic-string@0.30.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + magicast@0.3.4: dependencies: '@babel/parser': 7.24.7 @@ -21386,11 +20485,6 @@ snapshots: mailcheck@1.1.1: {} - make-dir@2.1.0: - dependencies: - pify: 4.0.1 - semver: 5.7.1 - make-dir@3.1.0: dependencies: semver: 6.3.1 @@ -21546,7 +20640,7 @@ snapshots: media-typer@0.3.0: {} - meilisearch@0.41.0(encoding@0.1.13): + meilisearch@0.42.0(encoding@0.1.13): dependencies: cross-fetch: 3.1.6(encoding@0.1.13) transitivePeerDependencies: @@ -21556,6 +20650,8 @@ snapshots: dependencies: map-or-similar: 1.5.0 + mensch@0.3.4: {} + meow@9.0.0: dependencies: '@types/minimist': 1.2.2 @@ -21573,6 +20669,8 @@ snapshots: merge-descriptors@1.0.1: {} + merge-descriptors@1.0.3: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -21774,7 +20872,7 @@ snapshots: transitivePeerDependencies: - supports-color - micromatch@4.0.7: + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 @@ -21787,6 +20885,8 @@ snapshots: mime@1.6.0: {} + mime@2.6.0: {} + mime@3.0.0: {} mimic-fn@2.1.0: {} @@ -21893,7 +20993,7 @@ snapshots: pkg-types: 1.0.3 ufo: 1.3.2 - mnemonist@0.39.6: + mnemonist@0.39.8: dependencies: obliterator: 2.0.4 @@ -21901,8 +21001,6 @@ snapshots: module-details-from-path@1.0.3: {} - mri@1.2.0: {} - ms@2.0.0: {} ms@2.1.2: {} @@ -21927,12 +21025,12 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.2 - msw-storybook-addon@2.0.3(msw@2.3.4(typescript@5.5.4)): + msw-storybook-addon@2.0.3(msw@2.4.9(typescript@5.6.2)): dependencies: is-node-process: 1.2.0 - msw: 2.3.4(typescript@5.5.4) + msw: 2.4.9(typescript@5.6.2) - msw@2.3.4(typescript@5.5.4): + msw@2.3.4(typescript@5.6.2): dependencies: '@bundled-es-modules/cookie': 2.0.0 '@bundled-es-modules/statuses': 1.0.1 @@ -21952,7 +21050,29 @@ snapshots: type-fest: 4.20.1 yargs: 17.7.2 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 + + msw@2.4.9(typescript@5.6.2): + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 3.1.6 + '@mswjs/interceptors': 0.35.9 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.4 + chalk: 4.1.2 + graphql: 16.8.1 + headers-polyfill: 4.0.2 + is-node-process: 1.2.0 + outvariant: 1.4.2 + path-to-regexp: 6.3.0 + strict-event-emitter: 0.5.1 + type-fest: 4.20.1 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.6.2 muggle-string@0.4.1: {} @@ -21996,8 +21116,6 @@ snapshots: negotiator@0.6.3: {} - neo-async@2.6.2: {} - nested-property@4.0.0: {} netmask@2.0.2: {} @@ -22027,14 +21145,8 @@ snapshots: node-addon-api@7.1.0: {} - node-dir@0.1.17: - dependencies: - minimatch: 3.1.2 - node-domexception@1.0.0: {} - node-fetch-native@1.0.2: {} - node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -22055,7 +21167,7 @@ snapshots: node-gyp-build@4.8.1: {} - node-gyp@10.1.0: + node-gyp@10.2.0: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.1 @@ -22063,7 +21175,7 @@ snapshots: graceful-fs: 4.2.11 make-fetch-happen: 13.0.0 nopt: 7.2.0 - proc-log: 3.0.0 + proc-log: 4.2.0 semver: 7.6.0 tar: 6.2.1 which: 4.0.0 @@ -22074,7 +21186,7 @@ snapshots: node-releases@2.0.14: {} - nodemailer@6.9.14: {} + nodemailer@6.9.15: {} nodemon@3.0.2: dependencies: @@ -22089,10 +21201,10 @@ snapshots: touch: 3.1.0 undefsafe: 2.0.5 - nodemon@3.1.4: + nodemon@3.1.7: dependencies: chokidar: 3.5.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.5(supports-color@5.5.0) ignore-by-default: 1.0.1 minimatch: 3.1.2 pstree.remy: 1.1.8 @@ -22134,8 +21246,6 @@ snapshots: normalize-url@6.1.0: {} - normalize-url@8.0.0: {} - normalize-url@8.0.1: {} npm-run-path@2.0.2: @@ -22154,6 +21264,11 @@ snapshots: dependencies: path-key: 4.0.0 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -22178,6 +21293,8 @@ snapshots: object-inspect@1.12.3: {} + object-inspect@1.13.2: {} + object-is@1.1.5: dependencies: call-bind: 1.0.2 @@ -22192,24 +21309,31 @@ snapshots: has-symbols: 1.0.3 object-keys: 1.1.1 - object.fromentries@2.0.7: + object.assign@4.1.5: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 - object.groupby@1.0.1: + object.fromentries@2.0.8: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - get-intrinsic: 1.2.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 - object.values@1.1.7: + object.groupby@1.0.3: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + + object.values@1.2.0: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 obliterator@2.0.4: {} @@ -22223,8 +21347,6 @@ snapshots: dependencies: ee-first: 1.1.1 - on-headers@1.0.2: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -22266,23 +21388,11 @@ snapshots: optionator@0.9.4: dependencies: deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - ora@5.4.1: - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 os-filter-obj@2.0.0: dependencies: @@ -22292,12 +21402,14 @@ snapshots: ospath@1.2.2: {} - otpauth@9.3.1: + otpauth@9.3.2: dependencies: '@noble/hashes': 1.4.0 outvariant@1.4.2: {} + outvariant@1.4.3: {} + p-cancelable@2.1.1: {} p-cancelable@3.0.0: {} @@ -22318,10 +21430,6 @@ snapshots: dependencies: yocto-queue: 1.0.0 - p-locate@3.0.0: - dependencies: - p-limit: 2.3.0 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -22375,6 +21483,10 @@ snapshots: domhandler: 5.0.3 parse5: 7.1.2 + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.1.2 + parse5@5.1.1: {} parse5@6.0.1: {} @@ -22387,8 +21499,6 @@ snapshots: path-browserify@1.0.1: {} - path-exists@3.0.0: {} - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -22411,32 +21521,36 @@ snapshots: lru-cache: 11.0.0 minipass: 7.1.2 + path-to-regexp@0.1.10: {} + path-to-regexp@0.1.7: {} path-to-regexp@1.8.0: dependencies: isarray: 0.0.1 - path-to-regexp@3.2.0: {} + path-to-regexp@3.3.0: {} path-to-regexp@6.2.1: {} - path-type@4.0.0: {} + path-to-regexp@6.3.0: {} - path-type@5.0.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} pathval@1.1.1: {} + pathval@2.0.0: {} + pause-stream@0.0.11: dependencies: through: 2.3.8 - peek-readable@5.0.0: {} - peek-readable@5.1.3: {} + peek-readable@5.2.0: {} + pend@1.2.0: {} performance-now@2.1.0: {} @@ -22444,18 +21558,20 @@ snapshots: pg-cloudflare@1.1.1: optional: true - pg-connection-string@2.6.4: {} + pg-connection-string@2.7.0: {} pg-int8@1.0.1: {} pg-numeric@1.0.2: {} - pg-pool@3.6.2(pg@8.12.0): + pg-pool@3.7.0(pg@8.13.0): dependencies: - pg: 8.12.0 + pg: 8.13.0 pg-protocol@1.6.1: {} + pg-protocol@1.7.0: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -22474,11 +21590,11 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.3 - pg@8.12.0: + pg@8.13.0: dependencies: - pg-connection-string: 2.6.4 - pg-pool: 3.6.2(pg@8.12.0) - pg-protocol: 1.6.1 + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.13.0) + pg-protocol: 1.7.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -22494,6 +21610,8 @@ snapshots: picocolors@1.0.1: {} + picocolors@1.1.0: {} + picomatch@2.3.1: {} pid-port@1.0.0: @@ -22502,8 +21620,6 @@ snapshots: pify@2.3.0: {} - pify@4.0.1: {} - pino-abstract-transport@1.2.0: dependencies: readable-stream: 4.3.0 @@ -22533,18 +21649,10 @@ snapshots: pkce-challenge@4.1.0: {} - pkg-dir@3.0.0: - dependencies: - find-up: 3.0.0 - pkg-dir@4.2.0: dependencies: find-up: 4.1.0 - pkg-dir@5.0.0: - dependencies: - find-up: 5.0.0 - pkg-types@1.0.3: dependencies: jsonc-parser: 3.2.0 @@ -22565,140 +21673,142 @@ snapshots: dependencies: '@babel/runtime': 7.23.4 - postcss-calc@9.0.1(postcss@8.4.40): + possible-typed-array-names@1.0.0: {} + + postcss-calc@9.0.1(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-selector-parser: 6.0.15 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.4.40): + postcss-colormin@6.1.0(postcss@8.4.47): dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.4.40): + postcss-convert-values@6.1.0(postcss@8.4.47): dependencies: browserslist: 4.23.0 - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-discard-comments@6.0.2(postcss@8.4.40): + postcss-discard-comments@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 - postcss-discard-duplicates@6.0.3(postcss@8.4.40): + postcss-discard-duplicates@6.0.3(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 - postcss-discard-empty@6.0.3(postcss@8.4.40): + postcss-discard-empty@6.0.3(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 - postcss-discard-overridden@6.0.2(postcss@8.4.40): + postcss-discard-overridden@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 - postcss-merge-longhand@6.0.5(postcss@8.4.40): + postcss-merge-longhand@6.0.5(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.4.40) + stylehacks: 6.1.1(postcss@8.4.47) - postcss-merge-rules@6.1.1(postcss@8.4.40): + postcss-merge-rules@6.1.1(postcss@8.4.47): dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.4.40) - postcss: 8.4.40 + cssnano-utils: 4.0.2(postcss@8.4.47) + postcss: 8.4.47 postcss-selector-parser: 6.0.16 - postcss-minify-font-values@6.1.0(postcss@8.4.40): + postcss-minify-font-values@6.1.0(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.4.40): + postcss-minify-gradients@6.0.3(postcss@8.4.47): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.4.40) - postcss: 8.4.40 + cssnano-utils: 4.0.2(postcss@8.4.47) + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.4.40): + postcss-minify-params@6.1.0(postcss@8.4.47): dependencies: browserslist: 4.23.0 - cssnano-utils: 4.0.2(postcss@8.4.40) - postcss: 8.4.40 + cssnano-utils: 4.0.2(postcss@8.4.47) + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.4.40): + postcss-minify-selectors@6.0.4(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-selector-parser: 6.0.16 - postcss-normalize-charset@6.0.2(postcss@8.4.40): + postcss-normalize-charset@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 - postcss-normalize-display-values@6.0.2(postcss@8.4.40): + postcss-normalize-display-values@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.4.40): + postcss-normalize-positions@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.4.40): + postcss-normalize-repeat-style@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.4.40): + postcss-normalize-string@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.4.40): + postcss-normalize-timing-functions@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.4.40): + postcss-normalize-unicode@6.1.0(postcss@8.4.47): dependencies: browserslist: 4.23.0 - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.4.40): + postcss-normalize-url@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.4.40): + postcss-normalize-whitespace@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-ordered-values@6.0.2(postcss@8.4.40): + postcss-ordered-values@6.0.2(postcss@8.4.47): dependencies: - cssnano-utils: 4.0.2(postcss@8.4.40) - postcss: 8.4.40 + cssnano-utils: 4.0.2(postcss@8.4.47) + postcss: 8.4.47 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.4.40): + postcss-reduce-initial@6.1.0(postcss@8.4.47): dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 - postcss: 8.4.40 + postcss: 8.4.47 - postcss-reduce-transforms@6.0.2(postcss@8.4.40): + postcss-reduce-transforms@6.0.2(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 postcss-selector-parser@6.0.15: @@ -22711,15 +21821,15 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@6.0.3(postcss@8.4.40): + postcss-svgo@6.0.3(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-value-parser: 4.2.0 svgo: 3.2.0 - postcss-unique-selectors@6.0.4(postcss@8.4.40): + postcss-unique-selectors@6.0.4(postcss@8.4.47): dependencies: - postcss: 8.4.40 + postcss: 8.4.47 postcss-selector-parser: 6.0.16 postcss-value-parser@4.2.0: {} @@ -22730,11 +21840,11 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.2.0 - postcss@8.4.40: + postcss@8.4.47: dependencies: nanoid: 3.3.7 - picocolors: 1.0.1 - source-map-js: 1.2.0 + picocolors: 1.1.0 + source-map-js: 1.2.1 postgres-array@2.0.0: {} @@ -22776,8 +21886,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 - pretty-hrtime@1.0.3: {} - pretty-ms@9.0.0: dependencies: parse-ms: 4.0.0 @@ -22797,7 +21905,7 @@ snapshots: transitivePeerDependencies: - supports-color - proc-log@3.0.0: {} + proc-log@4.2.0: {} process-exists@5.0.0: dependencies: @@ -22805,10 +21913,10 @@ snapshots: process-nextick-args@2.0.1: {} - process-warning@2.2.0: {} - process-warning@3.0.0: {} + process-warning@4.0.0: {} + process@0.11.10: {} promise-limit@2.7.0: {} @@ -22942,24 +22050,19 @@ snapshots: pvutils@1.1.3: {} - qrcode@1.5.3: + qrcode@1.5.4: dependencies: dijkstrajs: 1.0.2 - encode-utf8: 1.0.3 pngjs: 5.0.0 yargs: 15.4.1 - qs@6.10.4: - dependencies: - side-channel: 1.0.4 - qs@6.11.0: dependencies: side-channel: 1.0.4 - qs@6.11.1: + qs@6.13.0: dependencies: - side-channel: 1.0.4 + side-channel: 1.0.6 querystringify@2.2.0: {} @@ -22975,8 +22078,6 @@ snapshots: quick-lru@5.1.1: {} - ramda@0.29.0: {} - random-seed@0.3.0: dependencies: json-stringify-safe: 5.0.1 @@ -22992,15 +22093,22 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rdf-canonize@3.4.0: dependencies: setimmediate: 1.0.5 - re2@1.21.3: + re2@1.21.4: dependencies: install-artifact-from-github: 1.3.5 nan: 2.20.0 - node-gyp: 10.1.0 + node-gyp: 10.2.0 transitivePeerDependencies: - supports-color @@ -23009,9 +22117,9 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-docgen-typescript@2.2.2(typescript@5.5.4): + react-docgen-typescript@2.2.2(typescript@5.6.2): dependencies: - typescript: 5.5.4 + typescript: 5.6.2 react-docgen@7.0.1: dependencies: @@ -23133,36 +22241,20 @@ snapshots: reflect-metadata@0.2.2: {} - regenerate-unicode-properties@10.1.0: - dependencies: - regenerate: 1.4.2 - - regenerate@1.4.2: {} - regenerator-runtime@0.14.0: {} - regenerator-transform@0.15.2: - dependencies: - '@babel/runtime': 7.23.4 - regexp.prototype.flags@1.5.0: dependencies: call-bind: 1.0.2 define-properties: 1.2.0 functions-have-names: 1.2.3 - regexpu-core@5.3.2: + regexp.prototype.flags@1.5.3: dependencies: - '@babel/regjsgen': 0.8.0 - regenerate: 1.4.2 - regenerate-unicode-properties: 10.1.0 - regjsparser: 0.9.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.1.0 - - regjsparser@0.9.1: - dependencies: - jsesc: 0.5.0 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 rehype-external-links@3.0.0: dependencies: @@ -23266,7 +22358,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - ret@0.4.3: {} + ret@0.5.0: {} retry@0.12.0: {} @@ -23274,34 +22366,32 @@ snapshots: rfdc@1.3.0: {} - rimraf@2.6.3: - dependencies: - glob: 7.2.3 + rfdc@1.4.1: {} rimraf@3.0.2: dependencies: glob: 7.2.3 - rollup@4.19.1: + rollup@4.22.5: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.19.1 - '@rollup/rollup-android-arm64': 4.19.1 - '@rollup/rollup-darwin-arm64': 4.19.1 - '@rollup/rollup-darwin-x64': 4.19.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.19.1 - '@rollup/rollup-linux-arm-musleabihf': 4.19.1 - '@rollup/rollup-linux-arm64-gnu': 4.19.1 - '@rollup/rollup-linux-arm64-musl': 4.19.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.19.1 - '@rollup/rollup-linux-riscv64-gnu': 4.19.1 - '@rollup/rollup-linux-s390x-gnu': 4.19.1 - '@rollup/rollup-linux-x64-gnu': 4.19.1 - '@rollup/rollup-linux-x64-musl': 4.19.1 - '@rollup/rollup-win32-arm64-msvc': 4.19.1 - '@rollup/rollup-win32-ia32-msvc': 4.19.1 - '@rollup/rollup-win32-x64-msvc': 4.19.1 + '@rollup/rollup-android-arm-eabi': 4.22.5 + '@rollup/rollup-android-arm64': 4.22.5 + '@rollup/rollup-darwin-arm64': 4.22.5 + '@rollup/rollup-darwin-x64': 4.22.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.22.5 + '@rollup/rollup-linux-arm-musleabihf': 4.22.5 + '@rollup/rollup-linux-arm64-gnu': 4.22.5 + '@rollup/rollup-linux-arm64-musl': 4.22.5 + '@rollup/rollup-linux-powerpc64le-gnu': 4.22.5 + '@rollup/rollup-linux-riscv64-gnu': 4.22.5 + '@rollup/rollup-linux-s390x-gnu': 4.22.5 + '@rollup/rollup-linux-x64-gnu': 4.22.5 + '@rollup/rollup-linux-x64-musl': 4.22.5 + '@rollup/rollup-win32-arm64-msvc': 4.22.5 + '@rollup/rollup-win32-ia32-msvc': 4.22.5 + '@rollup/rollup-win32-x64-msvc': 4.22.5 fsevents: 2.3.3 rrweb-cssom@0.6.0: {} @@ -23328,6 +22418,13 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -23338,9 +22435,15 @@ snapshots: get-intrinsic: 1.2.1 is-regex: 1.1.4 - safe-regex2@3.1.0: + safe-regex-test@1.0.3: dependencies: - ret: 0.4.3 + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + + safe-regex2@4.0.0: + dependencies: + ret: 0.5.0 safe-stable-stringify@2.4.2: {} @@ -23355,7 +22458,7 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.4.38 - sass@1.77.8: + sass@1.79.3: dependencies: chokidar: 3.5.3 immutable: 4.2.2 @@ -23373,6 +22476,8 @@ snapshots: secure-json-parse@2.7.0: {} + secure-json-parse@3.0.0: {} + seedrandom@3.0.5: {} semver-regex@4.0.5: {} @@ -23393,6 +22498,8 @@ snapshots: dependencies: lru-cache: 6.0.0 + semver@7.6.3: {} + send@0.18.0: dependencies: debug: 2.6.9 @@ -23411,6 +22518,24 @@ snapshots: transitivePeerDependencies: - supports-color + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + serve-static@1.15.0: dependencies: encodeurl: 1.0.2 @@ -23420,10 +22545,35 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + set-blocking@2.0.0: {} set-cookie-parser@2.6.0: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + setimmediate@1.0.5: {} setprototypeof@1.2.0: {} @@ -23433,35 +22583,31 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 - shallow-clone@3.0.1: - dependencies: - kind-of: 6.0.3 - - sharp@0.33.4: + sharp@0.33.5: dependencies: color: 4.2.3 detect-libc: 2.0.3 - semver: 7.6.0 + semver: 7.6.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.4 - '@img/sharp-darwin-x64': 0.33.4 - '@img/sharp-libvips-darwin-arm64': 1.0.2 - '@img/sharp-libvips-darwin-x64': 1.0.2 - '@img/sharp-libvips-linux-arm': 1.0.2 - '@img/sharp-libvips-linux-arm64': 1.0.2 - '@img/sharp-libvips-linux-s390x': 1.0.2 - '@img/sharp-libvips-linux-x64': 1.0.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 - '@img/sharp-libvips-linuxmusl-x64': 1.0.2 - '@img/sharp-linux-arm': 0.33.4 - '@img/sharp-linux-arm64': 0.33.4 - '@img/sharp-linux-s390x': 0.33.4 - '@img/sharp-linux-x64': 0.33.4 - '@img/sharp-linuxmusl-arm64': 0.33.4 - '@img/sharp-linuxmusl-x64': 0.33.4 - '@img/sharp-wasm32': 0.33.4 - '@img/sharp-win32-ia32': 0.33.4 - '@img/sharp-win32-x64': 0.33.4 + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 shebang-command@1.2.0: dependencies: @@ -23495,6 +22641,13 @@ snapshots: get-intrinsic: 1.2.1 object-inspect: 1.12.3 + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -23586,8 +22739,6 @@ snapshots: slash@3.0.0: {} - slash@5.1.0: {} - slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -23600,6 +22751,8 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + slick@1.12.2: {} + smart-buffer@4.2.0: {} socks-proxy-agent@8.0.2: @@ -23631,6 +22784,8 @@ snapshots: source-map-js@1.2.0: {} + source-map-js@1.2.1: {} + source-map-support@0.5.13: dependencies: buffer-from: 1.1.2 @@ -23683,6 +22838,18 @@ snapshots: safer-buffer: 2.1.2 tweetnacl: 0.14.5 + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + ssri@10.0.4: dependencies: minipass: 5.0.0 @@ -23695,16 +22862,16 @@ snapshots: standard-as-callback@2.1.0: {} - start-server-and-test@2.0.4: + start-server-and-test@2.0.8: dependencies: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.5(supports-color@8.1.1) + debug: 4.3.7 execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 - wait-on: 7.2.0(debug@4.3.5) + wait-on: 8.0.1(debug@4.3.7) transitivePeerDependencies: - supports-color @@ -23716,53 +22883,23 @@ snapshots: dependencies: internal-slot: 1.0.5 - store2@2.14.2: {} - - storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(3rvqj7p7l43ansgshs3zbslm7u): + storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@storybook/blocks': 8.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/components': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/core-events': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/manager-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/preview-api': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/theming': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - '@storybook/types': 8.2.6(storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/blocks': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/core-events': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) + '@storybook/types': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook@8.2.6(@babel/preset-env@7.24.7(@babel/core@7.24.7))(bufferutil@4.0.8)(utf-8-validate@6.0.4): + storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4): dependencies: - '@babel/core': 7.24.7 - '@babel/types': 7.24.7 - '@storybook/codemod': 8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@storybook/core': 8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4) - '@types/semver': 7.5.8 - '@yarnpkg/fslib': 2.10.3 - '@yarnpkg/libzip': 2.3.0 - chalk: 4.1.2 - commander: 6.2.1 - cross-spawn: 7.0.3 - detect-indent: 6.1.0 - envinfo: 7.8.1 - execa: 5.1.1 - fd-package-json: 1.2.0 - find-up: 5.0.0 - fs-extra: 11.1.1 - giget: 1.1.2 - globby: 14.0.1 - jscodeshift: 0.15.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)) - leven: 3.1.0 - ora: 5.4.1 - prettier: 3.3.3 - prompts: 2.4.2 - semver: 7.6.0 - strip-json-comments: 3.1.1 - tempy: 3.1.0 - tiny-invariant: 1.3.3 - ts-dedent: 2.2.0 + '@storybook/core': 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - - '@babel/preset-env' - bufferutil - supports-color - utf-8-validate @@ -23782,8 +22919,6 @@ snapshots: transitivePeerDependencies: - supports-color - stream-wormhole@1.1.0: {} - streamsearch@1.1.0: {} streamx@2.15.0: @@ -23820,18 +22955,37 @@ snapshots: define-properties: 1.2.0 es-abstract: 1.22.1 + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + string.prototype.trimend@1.0.6: dependencies: call-bind: 1.0.2 define-properties: 1.2.0 es-abstract: 1.22.1 + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + string.prototype.trimstart@1.0.6: dependencies: call-bind: 1.0.2 define-properties: 1.2.0 es-abstract: 1.22.1 + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -23885,17 +23039,17 @@ snapshots: strtok3@7.0.0: dependencies: '@tokenizer/token': 0.3.0 - peek-readable: 5.0.0 + peek-readable: 5.1.3 - strtok3@8.0.1: + strtok3@8.1.0: dependencies: '@tokenizer/token': 0.3.0 - peek-readable: 5.1.3 + peek-readable: 5.2.0 - stylehacks@6.1.1(postcss@8.4.40): + stylehacks@6.1.1(postcss@8.4.47): dependencies: browserslist: 4.23.0 - postcss: 8.4.40 + postcss: 8.4.47 postcss-selector-parser: 6.0.16 supports-color@5.5.0: @@ -23931,7 +23085,7 @@ snapshots: symbol-tree@3.2.4: {} - systeminformation@5.22.11: {} + systeminformation@5.23.5: {} tar-stream@3.1.6: dependencies: @@ -23956,20 +23110,7 @@ snapshots: dependencies: memoizerific: 1.11.3 - temp-dir@3.0.0: {} - - temp@0.8.4: - dependencies: - rimraf: 2.6.3 - - tempy@3.1.0: - dependencies: - is-stream: 3.0.0 - temp-dir: 3.0.0 - type-fest: 2.19.0 - unique-string: 3.0.0 - - terser@5.31.3: + terser@5.33.0: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.12.1 @@ -24000,7 +23141,7 @@ snapshots: dependencies: real-require: 0.2.0 - three@0.167.0: {} + three@0.169.0: {} throttle-debounce@5.0.2: {} @@ -24010,16 +23151,18 @@ snapshots: tiny-invariant@1.3.3: {} - tiny-lru@10.0.1: {} - tinybench@2.6.0: {} tinycolor2@1.6.0: {} tinypool@0.8.4: {} + tinyrainbow@1.2.0: {} + tinyspy@2.2.0: {} + tinyspy@3.0.2: {} + tmp@0.2.3: {} tmpl@1.0.5: {} @@ -24087,11 +23230,15 @@ snapshots: dependencies: typescript: 5.5.4 - ts-case-convert@2.0.2: {} + ts-api-utils@1.3.0(typescript@5.6.2): + dependencies: + typescript: 5.6.2 + + ts-case-convert@2.0.7: {} ts-dedent@2.2.0: {} - ts-jest@29.1.2(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.23.0)(jest@29.7.0(@types/node@20.14.12))(typescript@5.1.6): + ts-jest@29.1.2(@babel/core@7.24.7)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.7))(esbuild@0.23.1)(jest@29.7.0(@types/node@20.14.12))(typescript@5.1.6): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -24107,7 +23254,7 @@ snapshots: '@babel/core': 7.24.7 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.24.7) - esbuild: 0.23.0 + esbuild: 0.23.1 ts-map@1.0.3: {} @@ -24133,7 +23280,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsd@0.31.1: + tsd@0.31.2: dependencies: '@tsd/typescript': 5.4.5 eslint-formatter-pretty: 4.1.0 @@ -24143,12 +23290,12 @@ snapshots: path-exists: 4.0.0 read-pkg-up: 7.0.1 - tslib@1.14.1: {} - tslib@2.6.2: {} tslib@2.6.3: {} + tslib@2.7.0: {} + tsx@4.4.0: dependencies: esbuild: 0.18.20 @@ -24178,8 +23325,6 @@ snapshots: type-fest@0.8.1: {} - type-fest@1.4.0: {} - type-fest@2.19.0: {} type-fest@4.20.1: {} @@ -24195,6 +23340,12 @@ snapshots: get-intrinsic: 1.2.1 is-typed-array: 1.1.10 + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + typed-array-byte-length@1.0.0: dependencies: call-bind: 1.0.2 @@ -24202,6 +23353,14 @@ snapshots: has-proto: 1.0.1 is-typed-array: 1.1.10 + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + typed-array-byte-offset@1.0.0: dependencies: available-typed-arrays: 1.0.5 @@ -24210,12 +23369,30 @@ snapshots: has-proto: 1.0.1 is-typed-array: 1.1.10 + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + typed-array-length@1.0.4: dependencies: call-bind: 1.0.2 for-each: 0.3.3 is-typed-array: 1.1.10 + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + typedarray@0.0.6: {} typedoc@0.25.13(typescript@5.1.6): @@ -24226,7 +23403,7 @@ snapshots: shiki: 0.14.7 typescript: 5.1.6 - typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0): + typeorm@0.3.20(ioredis@5.4.1)(pg@8.13.0): dependencies: '@sqltools/formatter': 1.2.5 app-root-path: 3.1.0 @@ -24245,7 +23422,7 @@ snapshots: yargs: 17.7.2 optionalDependencies: ioredis: 5.4.1 - pg: 8.12.0 + pg: 8.13.0 transitivePeerDependencies: - supports-color @@ -24257,10 +23434,9 @@ snapshots: typescript@5.5.4: {} - ufo@1.3.2: {} + typescript@5.6.2: {} - uglify-js@3.17.4: - optional: true + ufo@1.3.2: {} uid2@0.0.4: {} @@ -24274,7 +23450,7 @@ snapshots: unbox-primitive@1.0.2: dependencies: - call-bind: 1.0.2 + call-bind: 1.0.7 has-bigints: 1.0.2 has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 @@ -24283,22 +23459,15 @@ snapshots: undici-types@5.26.5: {} + undici-types@6.19.8: {} + undici@5.28.2: dependencies: '@fastify/busboy': 2.1.0 - unicode-canonical-property-names-ecmascript@2.0.0: {} + undici@6.20.0: {} - unicode-match-property-ecmascript@2.0.0: - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.0 - unicode-property-aliases-ecmascript: 2.1.0 - - unicode-match-property-value-ecmascript@2.1.0: {} - - unicode-property-aliases-ecmascript@2.1.0: {} - - unicorn-magic@0.1.0: {} + unicorn-magic@0.3.0: {} unified@11.0.4: dependencies: @@ -24318,10 +23487,6 @@ snapshots: dependencies: imurmurhash: 0.1.4 - unique-string@3.0.0: - dependencies: - crypto-random-string: 4.0.0 - unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.2 @@ -24409,21 +23574,22 @@ snapshots: uuid@9.0.1: {} - v-code-diff@1.12.0(vue@3.4.37(typescript@5.5.4)): + v-code-diff@1.13.1(vue@3.5.10(typescript@5.6.2)): dependencies: - diff: 5.1.0 + diff: 5.2.0 diff-match-patch: 1.0.5 - highlight.js: 11.9.0 - vue: 3.4.37(typescript@5.5.4) - vue-demi: 0.14.7(vue@3.4.37(typescript@5.5.4)) - vue-i18n: 9.13.1(vue@3.4.37(typescript@5.5.4)) + highlight.js: 11.10.0 + vue: 3.5.10(typescript@5.6.2) + vue-demi: 0.14.7(vue@3.5.10(typescript@5.6.2)) v8-to-istanbul@9.2.0: dependencies: - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/trace-mapping': 0.3.25 '@types/istanbul-lib-coverage': 2.0.4 convert-source-map: 2.0.0 + valid-data-url@3.0.1: {} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.1.1 @@ -24448,18 +23614,19 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-node@1.6.0(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3): + vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0): dependencies: cac: 6.7.14 debug: 4.3.5(supports-color@8.1.1) pathe: 1.1.2 - picocolors: 1.0.0 - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) + picocolors: 1.0.1 + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) transitivePeerDependencies: - '@types/node' - less - lightningcss - sass + - sass-embedded - stylus - sugarss - supports-color @@ -24467,25 +23634,25 @@ snapshots: vite-plugin-turbosnap@1.0.3: {} - vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3): + vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0): dependencies: esbuild: 0.21.5 - postcss: 8.4.40 - rollup: 4.19.1 + postcss: 8.4.47 + rollup: 4.22.5 optionalDependencies: '@types/node': 20.14.12 fsevents: 2.3.3 - sass: 1.77.8 - terser: 5.31.3 + sass: 1.79.3 + terser: 5.33.0 - vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3)): + vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)): dependencies: cross-fetch: 3.1.6(encoding@0.1.13) - vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3) + vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0) transitivePeerDependencies: - encoding - vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3): + vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -24504,8 +23671,8 @@ snapshots: strip-literal: 2.1.0 tinybench: 2.6.0 tinypool: 0.8.4 - vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) - vite-node: 1.6.0(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) + vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) why-is-node-running: 2.2.2 optionalDependencies: '@types/node': 20.14.12 @@ -24515,6 +23682,43 @@ snapshots: - less - lightningcss - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0): + dependencies: + '@vitest/expect': 1.6.0 + '@vitest/runner': 1.6.0 + '@vitest/snapshot': 1.6.0 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 + acorn-walk: 8.3.2 + chai: 4.3.10 + debug: 4.3.4(supports-color@5.5.0) + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.10 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.6.0 + tinypool: 0.8.4 + vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) + vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0) + why-is-node-running: 2.2.2 + optionalDependencies: + '@types/node': 20.14.12 + happy-dom: 10.0.3 + jsdom: 24.1.1 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded - stylus - sugarss - supports-color @@ -24549,41 +23753,39 @@ snapshots: vscode-uri@3.0.8: {} - vue-component-meta@2.0.16(typescript@5.5.4): + vue-component-meta@2.0.16(typescript@5.6.2): dependencies: '@volar/typescript': 2.2.0 - '@vue/language-core': 2.0.16(typescript@5.5.4) + '@vue/language-core': 2.0.16(typescript@5.6.2) path-browserify: 1.0.1 vue-component-type-helpers: 2.0.16 optionalDependencies: - typescript: 5.5.4 + typescript: 5.6.2 vue-component-type-helpers@1.8.4: {} vue-component-type-helpers@2.0.16: {} - vue-component-type-helpers@2.0.29: {} - vue-component-type-helpers@2.1.6: {} - vue-demi@0.14.7(vue@3.4.37(typescript@5.5.4)): + vue-demi@0.14.7(vue@3.5.10(typescript@5.6.2)): dependencies: - vue: 3.4.37(typescript@5.5.4) + vue: 3.5.10(typescript@5.6.2) - vue-docgen-api@4.75.1(vue@3.4.37(typescript@5.5.4)): + vue-docgen-api@4.75.1(vue@3.5.10(typescript@5.6.2)): dependencies: '@babel/parser': 7.24.7 '@babel/types': 7.24.7 - '@vue/compiler-dom': 3.4.34 - '@vue/compiler-sfc': 3.4.37 + '@vue/compiler-dom': 3.4.37 + '@vue/compiler-sfc': 3.5.10 ast-types: 0.16.1 hash-sum: 2.0.0 lru-cache: 8.0.4 pug: 3.0.3 recast: 0.23.6 ts-map: 1.0.3 - vue: 3.4.37(typescript@5.5.4) - vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.4.37(typescript@5.5.4)) + vue: 3.5.10(typescript@5.6.2) + vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.10(typescript@5.6.2)) vue-eslint-parser@9.4.3(eslint@9.8.0): dependencies: @@ -24598,28 +23800,21 @@ snapshots: transitivePeerDependencies: - supports-color - vue-i18n@9.13.1(vue@3.4.37(typescript@5.5.4)): - dependencies: - '@intlify/core-base': 9.13.1 - '@intlify/shared': 9.13.1 - '@vue/devtools-api': 6.6.1 - vue: 3.4.37(typescript@5.5.4) - - vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.4.37(typescript@5.5.4)): + vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.10(typescript@5.6.2)): dependencies: - vue: 3.4.37(typescript@5.5.4) + vue: 3.5.10(typescript@5.6.2) vue-template-compiler@2.7.14: dependencies: de-indent: 1.0.2 he: 1.2.0 - vue-tsc@2.0.29(typescript@5.5.4): + vue-tsc@2.1.6(typescript@5.6.2): dependencies: - '@volar/typescript': 2.4.0-alpha.18 - '@vue/language-core': 2.0.29(typescript@5.5.4) + '@volar/typescript': 2.4.6 + '@vue/language-core': 2.1.6(typescript@5.6.2) semver: 7.6.0 - typescript: 5.5.4 + typescript: 5.6.2 vue@3.4.37(typescript@5.5.4): dependencies: @@ -24631,40 +23826,39 @@ snapshots: optionalDependencies: typescript: 5.5.4 - vuedraggable@4.1.0(vue@3.4.37(typescript@5.5.4)): + vue@3.5.10(typescript@5.6.2): + dependencies: + '@vue/compiler-dom': 3.5.10 + '@vue/compiler-sfc': 3.5.10 + '@vue/runtime-dom': 3.5.10 + '@vue/server-renderer': 3.5.10(vue@3.5.10(typescript@5.6.2)) + '@vue/shared': 3.5.10 + optionalDependencies: + typescript: 5.6.2 + + vuedraggable@4.1.0(vue@3.5.10(typescript@5.6.2)): dependencies: sortablejs: 1.14.0 - vue: 3.4.37(typescript@5.5.4) + vue: 3.5.10(typescript@5.6.2) w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 - wait-on@7.2.0(debug@4.3.5): + wait-on@8.0.1(debug@4.3.7): dependencies: - axios: 1.6.2(debug@4.3.5) - joi: 17.11.0 + axios: 1.7.7(debug@4.3.7) + joi: 17.13.3 lodash: 4.17.21 minimist: 1.2.8 rxjs: 7.8.1 transitivePeerDependencies: - debug - walk-up-path@3.0.1: {} - walker@1.0.8: dependencies: makeerror: 1.0.12 - watchpack@2.4.0: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - - wcwidth@1.0.1: - dependencies: - defaults: 1.0.4 - web-push@3.6.7: dependencies: asn1.js: 5.4.1 @@ -24675,6 +23869,14 @@ snapshots: transitivePeerDependencies: - supports-color + web-resource-inliner@7.0.0: + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 5.0.1 + mime: 2.6.0 + valid-data-url: 3.0.1 + web-streams-polyfill@3.2.1: {} web-streams-polyfill@4.0.0: @@ -24735,6 +23937,14 @@ snapshots: gopd: 1.0.1 has-tostringtag: 1.0.0 + which-typed-array@1.1.15: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 @@ -24761,8 +23971,6 @@ snapshots: word-wrap@1.2.5: {} - wordwrap@1.0.0: {} - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -24783,18 +23991,12 @@ snapshots: wrappy@1.0.2: {} - write-file-atomic@2.4.3: - dependencies: - graceful-fs: 4.2.11 - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - write-file-atomic@4.0.2: dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.4): + ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@6.0.4): optionalDependencies: bufferutil: 4.0.8 utf-8-validate: 6.0.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6b19c64e82b9177897e9b402a101b7e1f8dccdb1..c1e062a78908949860260c59d2983523170bbfda 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,8 @@ packages: - 'packages/backend' + - 'packages/frontend-shared' - 'packages/frontend' + - 'packages/frontend-embed' - 'packages/sw' - 'packages/misskey-js' - 'packages/misskey-js/generator' diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs index fcf29cef2209bf60d176dceb2be71a1d0cb2d327..66380a0e17fca34e9bca35ee527fdf05b5396321 100644 --- a/scripts/build-assets.mjs +++ b/scripts/build-assets.mjs @@ -15,6 +15,7 @@ import { build as buildLocales } from '../locales/index.js'; import generateDTS from '../locales/generateDTS.js'; import meta from '../package.json' with { type: "json" }; import buildTarball from './tarball.mjs'; +import { localesVersion } from '../locales/version.js'; const configDir = fileURLToPath(new URL('../.config', import.meta.url)); const configPath = process.env.MISSKEY_CONFIG_YML @@ -56,10 +57,10 @@ async function copyFrontendLocales() { await fs.mkdir('./built/_frontend_dist_/locales', { recursive: true }); - const v = { '_version_': meta.version }; + const v = { '_version_': localesVersion }; for (const [lang, locale] of Object.entries(locales)) { - await fs.writeFile(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8'); + await fs.writeFile(`./built/_frontend_dist_/locales/${lang}.${localesVersion}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8'); } } @@ -72,11 +73,13 @@ async function buildBackendScript() { for (const file of [ './packages/backend/src/server/web/boot.js', + './packages/backend/src/server/web/boot.embed.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js' ]) { let source = await fs.readFile(file, { encoding: 'utf-8' }); - source = source.replaceAll('LANGS', JSON.stringify(Object.keys(locales))); + source = source.replaceAll(/\bLANGS\b/g, JSON.stringify(Object.keys(locales))); + source = source.replaceAll(/\bLANGS_VERSION\b/g, JSON.stringify(localesVersion)); const { code } = await terser.minify(source, { toplevel: true }); await fs.writeFile(`./packages/backend/built/server/web/${path.basename(file)}`, code); } @@ -87,6 +90,7 @@ async function buildBackendStyle() { for (const file of [ './packages/backend/src/server/web/style.css', + './packages/backend/src/server/web/style.embed.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css' diff --git a/scripts/clean-all.js b/scripts/clean-all.js index 3df2f2ceff694efdcac29bf764c3b98b4158d769..a35868e9e78807bf21d62a409cbeaa820efeec1f 100644 --- a/scripts/clean-all.js +++ b/scripts/clean-all.js @@ -10,9 +10,15 @@ const fs = require('fs'); fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/backend/node_modules', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/frontend-shared/node_modules', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/frontend/node_modules', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/frontend-embed/node_modules', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/sw/node_modules', { recursive: true, force: true }); diff --git a/scripts/clean.js b/scripts/clean.js index 6d8182fec2aaf4ea26ad59de2999082303ce36ac..ebff55dacddb071b9e8b03eb86c96d38a0f3e2bd 100644 --- a/scripts/clean.js +++ b/scripts/clean.js @@ -7,7 +7,9 @@ const fs = require('fs'); (async () => { fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/misskey-js/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/misskey-reversi/built', { recursive: true, force: true }); diff --git a/scripts/dev.mjs b/scripts/dev.mjs index a5c071fa7ef00fd51b71df115cd158ab3f333f9c..ea3b017ba5bb061e4c5825671e3e2fb3a675e907 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -70,12 +70,24 @@ execa('pnpm', ['--filter', 'backend', 'dev'], { stderr: process.stderr, }); +execa('pnpm', ['--filter', 'frontend-shared', 'watch'], { + cwd: _dirname + '/../', + stdout: process.stdout, + stderr: process.stderr, +}); + execa('pnpm', ['--filter', 'frontend', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], { cwd: _dirname + '/../', stdout: process.stdout, stderr: process.stderr, }); +execa('pnpm', ['--filter', 'frontend-embed', process.env.MK_DEV_PREFER === 'backend' ? 'watch' : 'dev'], { + cwd: _dirname + '/../', + stdout: process.stdout, + stderr: process.stderr, +}); + execa('pnpm', ['--filter', 'sw', 'watch'], { cwd: _dirname + '/../', stdout: process.stdout, diff --git a/sharkey-locales/ar-SA.yml b/sharkey-locales/ar-SA.yml new file mode 100644 index 0000000000000000000000000000000000000000..552ad8864f4f1cce8c4c3da10ab12e1956b140fb --- /dev/null +++ b/sharkey-locales/ar-SA.yml @@ -0,0 +1,4 @@ +--- +forwardReport: "وجّه البلاغ إلى المثيل البعيد" +forwardReportIsAnonymous: "ÙÙŠ المثيل البعيد سيظهر المبلّغ ÙƒØساب مجهول." +abuseMarkAsResolved: "علّم البلاغ كمØلول" diff --git a/sharkey-locales/bn-BD.yml b/sharkey-locales/bn-BD.yml new file mode 100644 index 0000000000000000000000000000000000000000..be6c2c401fc06b7cd791f91ea9eb26090a50c0c6 --- /dev/null +++ b/sharkey-locales/bn-BD.yml @@ -0,0 +1,5 @@ +--- +disableDrawer: "ডà§à¦°à¦¯à¦¼à¦¾à¦° মেনৠপà§à¦°à¦¦à¦°à§à¦¶à¦¨ করবেন না" +forwardReport: "রিমোট ইনà§à¦¸à¦¤à§à¦¯à¦¾à¦¨à§à¦¸à§‡ অà¦à¦¿à¦¯à§‹à¦—টি পাঠান" +forwardReportIsAnonymous: "আপনার তথà§à¦¯ রিমোট ইনà§à¦¸à¦¤à§à¦¯à¦¾à¦¨à§à¦¸à§‡ পাঠানো হবে না à¦à¦¬à¦‚ à¦à¦•à¦Ÿà¦¿ বেনামী সিসà§à¦Ÿà§‡à¦® অà§à¦¯à¦¾à¦•à¦¾à¦‰à¦¨à§à¦Ÿ হিসাবে পà§à¦°à¦¦à¦°à§à¦¶à¦¿à¦¤ হবে।" +abuseMarkAsResolved: "অà¦à¦¿à¦¯à§‹à¦—টিকে সমাধাকৃত হিসাবে চিহà§à¦¨à¦¿à¦¤ করà§à¦¨" diff --git a/sharkey-locales/ca-ES.yml b/sharkey-locales/ca-ES.yml new file mode 100644 index 0000000000000000000000000000000000000000..1eff1cd2c1742168d0a4eb3284a7068f5b6fb935 --- /dev/null +++ b/sharkey-locales/ca-ES.yml @@ -0,0 +1,10 @@ +--- +disableDrawer: "No mostrar els menús en calaixos" +forwardReport: "Transferir la denúncia a una instà ncia remota" +forwardReportIsAnonymous: "En lloc del teu compte, es farà servir un compte anònim com a denunciant al servidor remot." +abuseMarkAsResolved: "Marca la denúncia com a resolta" +replies: "Respostes" +_initialTutorial: + _reaction: + letsTryReacting: "Es poden afegir reaccions fent clic al botó '{reaction}'. Prova reaccionant a aquesta nota!" + reactDone: "Pots desfer una reacció fent clic al botó '{undo}'." diff --git a/sharkey-locales/cs-CZ.yml b/sharkey-locales/cs-CZ.yml new file mode 100644 index 0000000000000000000000000000000000000000..b1bee8efec106c0bc7dc69dcdfc9cc9bd56e9d1e --- /dev/null +++ b/sharkey-locales/cs-CZ.yml @@ -0,0 +1,6 @@ +--- +disableDrawer: "NepoužÃvat Å¡uplÃkové menu" +forwardReport: "PÅ™eposlat nahlášenà do vzdálené instance" +forwardReportIsAnonymous: "MÃsto vaÅ¡eho úÄtu se ve vzdálené instanci zobrazà anonymnà systémový úÄet jako nahlaÅ¡ovaÄ." +abuseMarkAsResolved: "OznaÄit nahlášenà jako vyÅ™eÅ¡ené" +replies: "OdpovÄ›di" diff --git a/sharkey-locales/da-DK.yml b/sharkey-locales/da-DK.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/da-DK.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/de-DE.yml b/sharkey-locales/de-DE.yml new file mode 100644 index 0000000000000000000000000000000000000000..6f52905083183cc81a3999984fba841129bdec50 --- /dev/null +++ b/sharkey-locales/de-DE.yml @@ -0,0 +1,53 @@ +--- +introMisskey: "Willkommen! Sharkey ist eine dezentralisierte Open-Source Microblogging-Platform.\nVerfasse „Notizen“ um mitzuteilen, was gerade passiert oder um Ereignisse mit anderen zu teilen. 📡\nMit „Reaktionen“ kannst du außerdem schnell deine Gefühle über Notizen anderer Benutzer zum Ausdruck bringen. ðŸ‘\nEine neue Welt wartet auf dich! 🚀" +poweredByMisskeyDescription: "{name} ist einer der durch die Open-Source-Plattform <b>Sharkey</b> betriebenen Dienste die auf Misskey basiert ist (meist als \"Misskey-Instanz\" bezeichnet)." +renotedBy: "Geboostet von {user}" +renote: "Boost" +unrenote: "Boost zurücknehmen" +renoted: "Boost getätigt." +cantRenote: "Boosten dieses Beitrags nicht möglich." +cantReRenote: "Boosten von einen Boost nicht möglich." +inChannelRenote: "Kanal-interner Boost" +flagAsBotDescription: "Aktiviere diese Option, falls dieses Benutzerkonto durch ein Programm gesteuert wird. Falls aktiviert, agiert es als Flag für andere Entwickler zur Verhinderung von endlosen Kettenreaktionen mit anderen Bots und lässt Sharkeys interne Systeme dieses Benutzerkonto als Bot behandeln." +intro: "Sharkey ist installiert! Lass uns nun ein Administratorkonto einrichten." +aboutMisskey: "Ãœber Sharkey" +disableDrawer: "Keine ausfahrbaren Menüs verwenden" +scratchpadDescription: "Die Testumgebung bietet einen Bereich für AiScript-Experimente. Dort kannst du AiScript schreiben, ausführen sowie dessen Auswirkungen auf Sharkey überprüfen." +forwardReport: "Meldung an fremde Instanz weiterleiten" +forwardReportIsAnonymous: "Anstatt deines Benutzerkontos wird bei der fremden Instanz ein anonymes Systemkonto als Melder angezeigt." +abuseMarkAsResolved: "Meldung als gelöst markieren" +renotedCount: "Anzahl erhaltener Boosts" +sendErrorReportsDescription: "Ist diese Option aktiviert, so werden beim Auftreten von Fehlern detaillierte Fehlerinformationen an Sharkey weitergegeben, was zur Verbesserung der Qualität von Sharkey beiträgt.\nEnthalten in diesen Informationen sind u.a. die Version deines Betriebssystems, welchen Browser du verwendest und ein Verlauf deiner Aktivitäten innerhalb Sharkey." +misskeyUpdated: "Sharkey wurde aktualisiert!" +didYouLikeMisskey: "Gefällt dir Sharkey?" +pleaseDonate: "Sharkey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" +pleaseDonateInstance: "Du kannst {host} auch direkt unterstützen, indem du an deine Instanz Administration spendest." +goToMisskey: "Zu Sharkey" +donation: "Spenden" +donationUrl: "Spenden-URL" +_accountMigration: + moveAccountDescription: "Hierdurch wird dein Konto zu einem anderen migriert.\n ・Follower von diesem Konto werden automatisch auf das neue Konto migriert\n ・Dieses Konto wird allen Nutzern, denen es derzeit folgt, nicht mehr folgen\n ・Mit diesem Konto können keine neuen Notizen usw. erstellt werden\n\nWährend die Migration der Follower automatisch erfolgt, muss die Migration der Konten, denen du folgst, manuell vorbereitet werden. Exportiere hierzu die Liste der gefolgten Nutzer über das Einstellungsmenu, und importiere diese Liste im neuen Konto. Das gleiche Verfahren gilt für erstellte Listen und stummgeschaltete oder blockierte Nutzer.\n\n(Diese Erklärung gilt für Sharkey v13.12.0 oder später. Die Funktionsweise andere ActivityPub-Software, beispielsweise Mastodon, kann hiervon abweichen.)" +_achievements: + _types: + _notes1: + title: "Hallo Sharkey!" + flavor: "Hab eine schöne Zeit mit Sharkey!" + _login1000: + flavor: "Danke, dass du Sharkey nutzt!" + _iLoveMisskey: + title: "I Love Sharkey" + description: "Sende \"I ⤠#Sharkey\"" + flavor: "Danke, dass du Sharkey verwendest! - vom Entwicklerteam" + _client30min: + description: "Habe Sharkey für mindestens 30 Minuten geöffnet" + _client60min: + title: "Munter mit Sharkey" + description: "Habe Sharkey für mindestens 60 Minuten geöffnet" + _brainDiver: + flavor: "Sharkey-Sharkey La-Tu-Ma" +_aboutMisskey: + about: "Sharkey ist Open-Source-Software basiert auf Misskey welche von syuilo seit 2014 entwickelt wird." + translation: "Sharkey übersetzen" + donate: "An Sharkey spenden" +_notification: + youRenoted: "Boost deiner Notiz von {name}" diff --git a/sharkey-locales/el-GR.yml b/sharkey-locales/el-GR.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/el-GR.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml new file mode 100644 index 0000000000000000000000000000000000000000..163fd0b0ae8ba90879280aeba31212c3dc72a4e2 --- /dev/null +++ b/sharkey-locales/en-US.yml @@ -0,0 +1,399 @@ +--- +introMisskey: "Welcome! Sharkey is an open source, decentralized microblogging service.\nCreate \"notes\" to share your thoughts with everyone around you. 📡\nWith \"reactions\", you can also quickly express your feelings about everyone's notes. ðŸ‘\nLet's explore a new world! 🚀" +poweredByMisskeyDescription: "{name} is one of the services powered by the open source platform <b>Sharkey</b> which is based on Misskey (referred to as a \"Misskey instance\")." +renotedBy: "Boosted by {user}" +approvals: "Approvals" +copyLinkRenote: "Copy boost link" +deleteAndEditConfirm: "Are you sure you want to redraft this note? This means you will lose all reactions, boosts, and replies to it." +openRemoteProfile: "Open remote profile" +trustedLinkUrlPatterns: "Link to external site warning exclusion list" +trustedLinkUrlPatternsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition. Using surrounding keywords with slashes will turn them into a regular expression. If you write only the domain name, it will be a backward match." +mutuals: "Mutuals" +isLocked: "Private account" +isAdmin: "Administrator" +isBot: "Bot user" +open: "Open" +emailDestination: "Destination address" +date: "Date" +renote: "Boost" +unrenote: "Remove boost" +renoted: "Boosted." +quoted: "Quoted." +rmboost: "Unboosted." +renotedToX: "Boosted to {name}" +cantRenote: "This post can't be boosted." +cantReRenote: "A boost can't be boosted." +inChannelRenote: "Channel-only Boost" +muted: "Muted" +renoteMute: "Mute Boosts" +renoteMuted: "Boosts muted" +renoteUnmute: "Unmute Boosts" +markAsNSFW: "Mark all media from user as NSFW" +markInstanceAsNSFW: "Mark as NSFW" +nsfwConfirm: "Are you sure that you want to mark all media from this account as NSFW?" +unNsfwConfirm: "Are you sure that you want to unmark all media from this account as NSFW?" +approveConfirm: "Are you sure that you want to approve this account?" +flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Sharkey's internal systems to treat this account as a bot." +flagSpeakAsCat: "Speak as a cat" +flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode. If this isn't working, then please check that you dont have 'Disable cat speak' on under General/Note Display" +continueOnRemote: "Continue on remote instance" +chooseServerOnMisskeyHub: "Choose a instance from Misskey Hub" +mediaSilenceThisInstance: "Silence media from this instance" +rejectReports: "Reject reports from this instance" +silencedInstancesDescription: "List the host names of the instances that you want to silence, separated by a new line. All accounts belonging to the listed instances will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances." +mediaSilencedInstances: "Media-silenced instances" +mediaSilencedInstancesDescription: "List the host names of the instances that you want to media-silence, separated by a new line. All accounts belonging to the listed instances will be treated as sensitive, and can't use custom emojis. This will not affect the blocked instances." +intro: "Installation of Sharkey has been finished! Please create an admin user." +blockedByBase: "This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s)." +silencedByBase: "This host is silenced implicitly because a base domain is silenced. To un-silence this host, first un-silence the base domain(s)." +mediaSilencedByBase: "This host's media is silenced implicitly because a base domain's media is silenced. To un-silence this host, first un-silence the base domain(s)." +driveSearchbarPlaceholder: "Search drive" +fileNotSelected: "No file selected" +background: "Background" +antennaUsersDescription: "List one username per line. Use \"*@instance.com\" to specify all users of an instance" +aboutMisskey: "About Sharkey" +expandAllCws: "Show content for all replies" +collapseAllCws: "Hide content for all replies" +attachAsFileQuestion: "The text in clipboard is long. Would you like to attach it as a text file?" +signinOrContinueOnRemote: "To continue, you need to go to your instance to perform this action or sign up / log in to the instance you are trying to interact with." +disableDrawer: "Don't use drawer-style menus" +showReactionsCount: "Show the number of reactions in notes" +cornerRadius: "Corner roundness" +warnForMissingAltText: "Warn you when you forget to put alt text" +deeplFreeMode: "Use DeepLX-JS (No Auth Key)" +deeplFreeModeDescription: "Need Help? Check our documentation to know how to setup DeepLX-JS." +useSoundOnlyWhenActive: "Output sounds only if Sharkey is active." +scratchpadDescription: "The Scratchpad provides an environment for AiScript experiments. You can write, execute, and check the results of it interacting with Sharkey in it." +deleteAllFilesQueued: "Deletion of all files queued" +systemAccountTitle: "This is a system account" +systemAccountDescription: "This account is created and managed automatically by the system, and cannot be logged into." +postFiltered: "post is hidden by a filter" +enableFaviconNotificationDot: "Enable favicon notification dot" +verifyNotificationDotWorkingButton: "Check if the notification dot works on your instance" +notificationDotNotWorking: "Unfortunately, this instance does not support the notification dot feature at this time." +notificationDotWorking: "The notification dot is functioning properly on this instance." +notificationDotNotWorkingAdvice: "If the notification dot doesn't work, ask an admin to check our documentation {link}" +reportAbuseRenote: "Report boost" +forwardReport: "Forward report to remote instance" +forwardReportIsAnonymous: "Instead of your account, an anonymous system account will be displayed as reporter at the remote instance." +abuseMarkAsResolved: "Mark report as resolved" +i18nInfoSharkey: "Sharkey specific changes are translated in its own {link}." +renotesCount: "Number of boosts sent" +renotedCount: "Number of boosts received" +showTickerOnReplies: "Show instance ticker on replies" +disableCatSpeak: "Disable cat speak" +searchEngine: "Search Engine For Search MFM" +searchEngineOther: "Other" +searchEngineCustomURIDescription: "The custom URI must be input in the format like \"https://www.google.com/search?q=\\{query}\" or \"https://www.google.com/search?q=%s\"." +searchEngineCusomURI: "Custom URI" +makeIndexable: "Make public notes not indexable" +makeIndexableDescription: "Stop note search from indexing your public notes." +sendErrorReportsDescription: "When turned on, detailed error information will be shared with Sharkey when a problem occurs, helping to improve the quality of Sharkey.\nThis will include information such the version of your OS, what browser you're using, your activity in Sharkey, etc." +noInquiryUrlWarning: "Contact URL is not set." +misskeyUpdated: "Sharkey has been updated!" +usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames cannot be changed later." +approvalRequiredForSignup: "Require approval for new users" +voteConfirmMulti: "Confirm your vote for \"{choice}\"?\n You can choose more options after confirmation." +pendingUserApprovals: "There are users awaiting approval." +approveAccount: "Approve" +denyAccount: "Deny & Delete" +approved: "Approved" +notApproved: "Not Approved" +approvalStatus: "Approval Status" +numberOfReplies: "Number of replies in a thread" +numberOfRepliesDescription: "Increasing this number will display more replies. Setting this too high can cause replies to be cramped and unreadable." +boostSettings: "Boost Settings" +showVisibilitySelectorOnBoost: "Show Visibility Selector" +showVisibilitySelectorOnBoostDescription: "Shows the visiblity selector if enabled when clicking boost, if disabled it will use the default visiblity defined below and the selector will not show up." +visibilityOnBoost: "Default boost visibility" +defaultLike: "Default like emoji" +didYouLikeMisskey: "Have you taken a liking to Sharkey?" +pleaseDonate: "{host} uses the free software, Sharkey. We would highly appreciate your donations so development of Sharkey can continue!" +pleaseDonateInstance: "You can also support {host} directly by donating to your instance administration." +thisPostIsMissingAltTextCancel: "Cancel" +thisPostIsMissingAltTextIgnore: "Post anyway" +thisPostIsMissingAltText: "One of the files attached to this post is missing alt text. Please ensure all the attachments have alt text." +collapseRenotes: "Collapse boosts you've already seen" +collapseRenotesDescription: "Collapse boosts that you have boosted or reacted to" +collapseNotesRepliedTo: "Collapse notes replied to" +collapseFiles: "Collapse files" +uncollapseCW: "Uncollapse CWs on notes" +expandLongNote: "Always expand long notes" +autoloadConversation: "Load conversation on replies" +approvalRequiredToRegister: "This instance is only accepting users who specify a reason for registration." +limitWidthOfReaction: "Limits the maximum width of reactions and display them in reduced size." +oneko: "Cat friend :3" +renotesList: "Boosts" +lookupConfirm: "Are you sure that you want to look this up?" +openTagPageConfirm: "Are you sure you want to open this hashtags page?" +specifyHost: "Specify a host" +goToMisskey: "To Sharkey" +enableAchievements: "Enable Achievements" +turnOffAchievements: "Turning this off will disable the achievement system" +enableBotTrending: "Populate Hashtags with Bots" +turnOffBotTrending: "Turning this off will stop Bots from populating Hashtags" +replies: "Replies" +renotes: "Boosts" +clickToOpen: "Click to open notes" +showBots: "Show bots in timeline" +showRenotes: "Show boosts" +sourceCodeIsNotYetProvided: "The source code is not yet available. Please contact your administrator to fix this problem." +repositoryUrlDescription: "If there is a repository where the source code is publicly available, enter its URL. If you are using Sharkey as-is (without any changes to the source code), enter https://activitypub.software/TransFem-org/Sharkey/." +donation: "Donate" +donationUrl: "Donation URL" +showBelowAvatar: "Show Below Avatar" +seasonalScreenEffect: "Seasonal screen effects" +noDescription: "No description" +sensitiveMediaRevealConfirm: "This media might be sensitive. Are you sure you want to reveal it?" +severAllFollowRelations: "Break following relationships" +severAllFollowRelationsConfirm: "Really break all follow relationships? This is irreversible! This will break {followingCount} following and {followersCount} follower relations on {instanceName}!" +severAllFollowRelationsQueued: "Severing all follow relations with {host} queued." +pendingFollowRequests: "Pending follow requests" +showQuotes: "Show quotes" +showReplies: "Show replies" +showNonPublicNotes: "Show non-public" +allowClickingNotifications: "Allow clicking on pop-up notifications" +pinnedOnly: "Pinned" +blockingYou: "Blocking you" +warnExternalUrl: "Show warning when opening external URLs" +_delivery: + stop: "Suspend delivery" + resume: "Resume delivery" +_initialAccountSetting: + youCanContinueTutorial: "You can proceed to a tutorial on how to use {name} (Sharkey) or you can exit the setup here and start using it immediately." +_initialTutorial: + _landing: + description: "Here, you can learn the basics of using Sharkey and its features." + _note: + description: "Posts on Sharkey are called 'Notes.' Notes are arranged chronologically on the timeline and are updated in real-time." + _reaction: + letsTryReacting: "Reactions can be added by clicking the '{reaction}' button on the note. Try reacting to this sample note!" + reactDone: "You can undo a reaction by pressing the '{undo}' button." + _timeline: + description1: "Sharkey provides multiple timelines based on usage (some may not be available depending on the server's policies)." + bubble: "You can view notes from connected servers picked by your admins." + _postNote: + description1: "When posting a note on Sharkey, various options are available. The posting form looks like this." + _visibility: + home: "Public only on the Home timeline. People visiting your profile, via followers, and through boosts can see it." + followers: "Visible to followers only. Only followers can see it and no one else, and it cannot be boosted by others." + _done: + title: "The tutorial is complete! 🎉" + description: "The functions introduced here are just a small part. For a more detailed understanding of using Sharkey, please refer to {link}." +_timelineDescription: + bubble: "In the Bubble timeline, you can see notes from connected servers picked by your admins." +_serverSettings: + sidebarLogoUrl: "Logo URL" + sidebarLogoDescription: "Specifies the logo to use instead of the regular icon in high definition, dynamic-width scenarios." + sidebarLogoUsageExample: "E.g. In the sidebar, to visitors and in the \"About\" page." + inquiryUrl: "Contact URL" + inquiryUrlDescription: "Specify the URL of a web page that contains a contact form or the instance operators' contact information." +_accountMigration: + moveAccountDescription: "This will migrate your account to a different one.\n ・Followers from this account will automatically be migrated to the new account\n ・This account will unfollow all users it is currently following\n ・You will be unable to create new notes etc. on this account\n\nWhile migration of followers is automatic, you must manually prepare some steps to migrate the list of users you are following. To do so, carry out a follows export that you will later import on the new account in the settings menu. The same procedure applies to your lists as well as your muted and blocked users.\n\n(This explanation applies to Sharkey v13.12.0 and later. Other ActivityPub software, such as Mastodon, might function differently.)" +_achievements: + _types: + _notes1: + title: "just setting up my shonk" + flavor: "Have a good time with Sharkey!" + _login1000: + flavor: "Thank you for using Sharkey!" + _iLoveMisskey: + title: "I Love Sharkey" + description: "Post \"I ⤠#Sharkey\"" + flavor: "Sharkey's development team greatly appreciates your support!" + _client30min: + description: "Keep Sharkey opened for at least 30 minutes" + _client60min: + title: "No \"Miss\" in Sharkey" + description: "Keep Sharkey opened for at least 60 minutes" + _brainDiver: + flavor: "Sharkey-Sharkey La-Tu-Ma" + _tutorialCompleted: + title: "Sharkey Elementary Course Diploma" +_role: + _options: + btlAvailable: "Can view the bubble timeline" + canImportNotes: "Can import notes" + canUpdateBioMedia: "Allow users to edit their avatar or banner" + _condition: + isLocked: "Private account" + isExplorable: "Account is discoverable" +_emailUnavailable: + banned: "This email address is banned" +_signup: + approvalPending: "Your account has been created and is awaiting approval." + reasonInfo: "Please enter a reason as to why you want to join the instance." +_aboutMisskey: + about: "Sharkey is open-source software based on Misskey which has been in development by syuilo since 2014." + original: "Misskey original" + original_sharkey: "Sharkey original" + thisIsModifiedVersion: "{name} uses a modified version of the original Sharkey." + translation: "Translate Sharkey" + donate_sharkey: "Donate to Sharkey" + testers: "Testers" +_serverDisconnectedBehavior: + disabled: "Disable warning" +_channel: + allowRenoteToExternal: "Allow boosts and quote outside the channel" +_instanceMute: + instanceMuteDescription: "This will mute any notes/boosts from the listed instances, including those of users replying to a user from a muted instance." +_theme: + keys: + renote: "Boost" +_soundSettings: + driveFileDurationWarnDescription: "Long audio may disrupt using Sharkey. Still continue?" + driveFileError: "The audio couldn't be loaded. Please make sure you selected an audio file." +_2fa: + moreDetailedGuideHere: "Click here for a detailed guide" +_widgets: + search: "Search" +_poll: + multiple: "Multiple choices" +_profile: + updateBanner: "Update banner" + removeBanner: "Remove banner" + changeBackground: "Change background" + updateBackground: "Update background" + removeBackground: "Remove background" +_timelines: + bubble: "Bubble" +_pages: + blocks: + dynamicDescription: "This block type has been removed. Please use {play} from now on." +_notification: + youRenoted: "Boost from {name}" + renotedBySomeUsers: "Boosted by {n} users" + edited: "Note got edited" + _types: + renote: "Boosts" + edited: "Edits" + _actions: + renote: "Boost" +_webhookSettings: + _events: + renote: "When boosted" + _systemEvents: + abuseReportResolved: "When resolved abuse reports" +_abuseReport: + _notificationRecipient: + _recipientType: + _captions: + mail: "Send an email to the moderators when an abuse report is received." + webhook: "Send a notification to the SystemWebhook when an abuse report is received or resolved." +_moderationLogTypes: + approve: "Approved" + setRemoteInstanceNSFW: "Set remote instance as NSFW" + unsetRemoteInstanceNSFW: "Set remote instance as NSFW" + rejectRemoteInstanceReports: "Rejected reports from remote instance" + acceptRemoteInstanceReports: "Accepted reports from remote instance" +_mfm: + uncommonFeature: "This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks" + intro: "MFM is a markup language used on Misskey, Sharkey, Firefish, Akkoma, and more that can be used in many places. Here you can view a list of all available MFM syntax." + dummy: "Sharkey expands the world of the Fediverse" + mention: "Mention" + mentionDescription: "You can specify a user by using an At-Symbol and a username." + hashtag: "Hashtag" + hashtagDescription: "You can specify a hashtag using a number sign and text." + url: "URL" + urlDescription: "URLs can be displayed." + link: "Link" + linkDescription: "Specific parts of text can be displayed as a URL." + bold: "Bold" + boldDescription: "Highlights letters by making them thicker." + small: "Small" + smallDescription: "Displays content small and thin." + center: "Center" + centerDescription: "Displays content centered." + inlineCode: "Code (Inline)" + inlineCodeDescription: "Displays inline syntax highlighting for (program) code." + blockCode: "Code (Block)" + blockCodeDescription: "Displays syntax highlighting for multi-line (program) code in a block." + inlineMath: "Math (Inline)" + inlineMathDescription: "Display math formulas (KaTeX) in-line" + blockMath: "Math (Block)" + blockMathDescription: "Display math formulas (KaTeX) in a block" + quote: "Quote" + quoteDescription: "Displays content as a quote." + emoji: "Custom Emoji" + emojiDescription: "By surrounding a custom emoji name with colons, custom emoji can be displayed." + search: "Search" + searchDescription: "Displays a search box with pre-entered text." + flip: "Flip" + flipDescription: "Flips content horizontally or vertically." + jelly: "Animation (Jelly)" + jellyDescription: "Gives content a jelly-like animation." + tada: "Animation (Tada)" + tadaDescription: "Gives content a \"Tada!\"-like animation." + jump: "Animation (Jump)" + jumpDescription: "Gives content a jumping animation." + bounce: "Animation (Bounce)" + bounceDescription: "Gives content a bouncy animation." + shake: "Animation (Shake)" + shakeDescription: "Gives content a shaking animation." + twitch: "Animation (Twitch)" + twitchDescription: "Gives content a strongly twitching animation." + spin: "Animation (Spin)" + spinDescription: "Gives content a spinning animation." + x2: "Big" + x2Description: "Displays content bigger." + x3: "Very big" + x3Description: "Displays content even bigger." + x4: "Unbelievably big" + x4Description: "Displays content even bigger than bigger than big." + blur: "Blur" + blurDescription: "Blurs content. It will be displayed clearly when hovered over." + font: "Font" + fontDescription: "Sets the font to display content in." + rainbow: "Rainbow" + rainbowDescription: "Makes the content appear in rainbow colors." + sparkle: "Sparkle" + sparkleDescription: "Gives content a sparkling particle effect." + rotate: "Rotate" + rotateDescription: "Turns content by a specified angle." + position: "Position" + positionDescription: "Move content by a specified amount." + crop: "Crop" + cropDescription: "Crop content." + followMouse: "Follow Mouse" + followMouseDescription: "Content will follow the mouse. On mobile it will follow wherever the user taps." + scale: "Scale" + scaleDescription: "Scale content by a specified amount." + foreground: "Foreground color" + foregroundDescription: "Change the foreground color of text." + fade: 'Fade' + fadeDescription: 'Fade text in and out.' + background: "Background color" + backgroundDescription: "Change the background color of text." + plain: "Plain" + plainDescription: "Deactivates the effects of all MFM contained within this MFM effect." +_animatedMFM: + play: "Play MFM Animation" + stop: "Stop MFM Animation" + _alert: + text: "Animated MFMs could include flashing lights and fast moving text/emojis." + confirm: "Animate" +_dataRequest: + title: "Request Data" + warn: "Data requests are only possible every 3 days." + text: "Once the data is ready to download, an email will be sent to the email address registered to this account." + button: "Request" +_dataSaver: + _avatar: + description: "Stop avatar image animation. Animated images can be larger in file size than normal images, potentially leading to further reductions in data traffic." +_urlPreviewSetting: + timeoutDescription: "If it takes longer than this value to get the preview, the preview won't be generated." + requireContentLength: "Generate the preview only if we can get Content-Length" + summaryProxy: "Endpoint for proxy to generate previews" + summaryProxyDescription: "Generate previews using Summaly Proxy, instead of Sharkey itself." + summaryProxyDescription2: "The following parameters are sent to the proxy as a query string. If the proxy does not support them, the values are ignored." +_externalNavigationWarning: + title: "Navigate to an external site" + description: "Leave {host} and go to an external site" + trustThisDomain: "Trust this domain on this device in the future" + +remoteFollowersWarning: "Remote followers may have incomplete or outdated activity" + +_auth: + allowed: "Allowed" +_announcement: + new: "New" diff --git a/sharkey-locales/es-ES.yml b/sharkey-locales/es-ES.yml new file mode 100644 index 0000000000000000000000000000000000000000..75aa932e449ec79714b7d9336941c4349dfce5a3 --- /dev/null +++ b/sharkey-locales/es-ES.yml @@ -0,0 +1,14 @@ +--- +antennaUsersDescription: "Elegir nombres de usuarios separados por una linea nueva. Utilice \"*@instance.com\" para especificar todos los usuarios de una instancia." +disableDrawer: "No mostrar los menús en cajones" +forwardReport: "Transferir un informe a una instancia remota" +forwardReportIsAnonymous: "No puede ver su información de la instancia remota y aparecerá como una cuenta anónima del sistema" +abuseMarkAsResolved: "Marcar reporte como resuelto" +replies: "Respuestas" +renotes: "Renotas" +_initialTutorial: + _reaction: + letsTryReacting: "Puedes añadir reacciones pulsando en el botón '{reaction}' de la nota. ¡Intenta reaccionar a esta nota de ejemplo!" + reactDone: "Puedes deshacer una reacción pulsando en el botón '{undo}'." +_poll: + multiple: "Opciones múltiples" diff --git a/sharkey-locales/fr-FR.yml b/sharkey-locales/fr-FR.yml new file mode 100644 index 0000000000000000000000000000000000000000..c823fca00f15f15646120de577841afd91f345bb --- /dev/null +++ b/sharkey-locales/fr-FR.yml @@ -0,0 +1,13 @@ +--- +disableDrawer: "Les menus ne s'affichent pas dans le tiroir" +forwardReport: "Transférer le signalement à l’instance distante" +forwardReportIsAnonymous: "L'instance distante ne sera pas en mesure de voir vos informations et apparaîtra comme un compte anonyme du système." +abuseMarkAsResolved: "Marquer le signalement comme résolu" +_initialTutorial: + _reaction: + letsTryReacting: "Des réactions peuvent être ajoutées en cliquant sur le bouton « {reaction} » de la note. Essayez d'ajouter une réaction à cet exemple de note !" + reactDone: "Vous pouvez annuler la réaction en cliquant sur le bouton « {undo} » ." +_achievements: + _types: + _notes1: + title: "Je viens tout juste de configurer mon shonk" diff --git a/sharkey-locales/hr-HR.yml b/sharkey-locales/hr-HR.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/hr-HR.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/ht-HT.yml b/sharkey-locales/ht-HT.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/ht-HT.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/hu-HU.yml b/sharkey-locales/hu-HU.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/hu-HU.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/id-ID.yml b/sharkey-locales/id-ID.yml new file mode 100644 index 0000000000000000000000000000000000000000..6e2c283d6fcd397df43f0fdb8f26fba531d4d561 --- /dev/null +++ b/sharkey-locales/id-ID.yml @@ -0,0 +1,10 @@ +--- +disableDrawer: "Jangan gunakan menu bergaya laci" +forwardReport: "Teruskan laporan ke instansi luar" +forwardReportIsAnonymous: "Untuk melindungi privasi akun kamu, akun anonim dari sistem akan digunakan sebagai pelapor pada instansi luar." +abuseMarkAsResolved: "Tandai laporan sebagai selesai" +replies: "Balasan" +_initialTutorial: + _reaction: + letsTryReacting: "Reaksi dapat ditambahkan dengan mengklik tombol '{reaction}' pada catatan. Coba lakukan mereaksi contoh catatan ini!" + reactDone: "Kamu dapat mengurungkan reaksi dengan menekan tombol '{undo}'." diff --git a/sharkey-locales/it-IT.yml b/sharkey-locales/it-IT.yml new file mode 100644 index 0000000000000000000000000000000000000000..0a340cf43507f787bb4bdcc4becde188f9b5c9da --- /dev/null +++ b/sharkey-locales/it-IT.yml @@ -0,0 +1,18 @@ +--- +openRemoteProfile: "Apri profilo remoto" +disableDrawer: "Non mostrare il menù sul drawer" +forwardReport: "Inoltro di un report a un'istanza remota." +forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo." +abuseMarkAsResolved: "Risolvi segnalazione" +makeIndexable: "Non indicizzare le note pubbliche" +makeIndexableDescription: "Le tue note pubbliche non saranno cercabili" +defaultLike: "Emoji predefinita per \"mi piace\"" +_delivery: + stop: "Sospendi la distribuzione di attività " + resume: "Riprendi la distribuzione di attività " +_initialTutorial: + _reaction: + letsTryReacting: "Puoi aggiungere una Reazione cliccando il bottone \"{reaction}\" della relativa Nota. Prova ad aggiungerne una a questa Nota di esempio!" + reactDone: "Annulla la tua Reazione premendo il bottone \"{undo}\"" +_serverDisconnectedBehavior: + disabled: "Non visualizzare l'avviso" diff --git a/sharkey-locales/ja-JP.yml b/sharkey-locales/ja-JP.yml new file mode 100644 index 0000000000000000000000000000000000000000..0977a2e5648fc6e73bd5fd9ff5b83a4c445bbf12 --- /dev/null +++ b/sharkey-locales/ja-JP.yml @@ -0,0 +1,377 @@ +--- +introMisskey: "よã†ã“ãï¼Sharkeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スã§ã™ã€‚\n「ノートã€ã‚’作æˆã—ã¦ã€ã„ã¾èµ·ã“ã£ã¦ã„ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ãªãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\n「リアクションã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ðŸ‘\næ–°ã—ã„世界を探検ã—よã†ðŸš€" +poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Sharkey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ã§ã™ã€‚" +renotedBy: "{user}ãŒãƒ–ースト" +approvals: "承èª" +copyLinkRenote: "ブーストã®ãƒªãƒ³ã‚¯ã‚’コピー" +deleteAndEditConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’削除ã—ã¦ã‚‚ã†ä¸€åº¦ç·¨é›†ã—ã¾ã™ã‹ï¼Ÿã“ã®ãƒŽãƒ¼ãƒˆã¸ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã€ãƒ–ーストã€è¿”ä¿¡ã‚‚å…¨ã¦å‰Šé™¤ã•ã‚Œã¾ã™ã€‚" +openRemoteProfile: "リモートプãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’é–‹ã" +trustedLinkUrlPatterns: "外部サイトã¸ã®ãƒªãƒ³ã‚¯è¦å‘Š 除外リスト" +trustedLinkUrlPatternsDescription: "スペースã§åŒºåˆ‡ã‚‹ã¨AND指定ã«ãªã‚Šã€æ”¹è¡Œã§åŒºåˆ‡ã‚‹ã¨OR指定ã«ãªã‚Šã¾ã™ã€‚スラッシュã§å›²ã‚€ã¨æ£è¦è¡¨ç¾ã«ãªã‚Šã¾ã™ã€‚ドメインåã ã‘書ãã¨å¾Œæ–¹ä¸€è‡´ã«ãªã‚Šã¾ã™ã€‚" +mutuals: "Mutuals" +renote: "ブースト" +unrenote: "ブースト解除" +renoted: "ブーストã—ã¾ã—ãŸã€‚" +quoted: "引用。" +rmboost: "ブースト解除ã—ã¾ã—ãŸã€‚" +cantRenote: "ã“ã®æŠ•ç¨¿ã¯ãƒ–ーストã§ãã¾ã›ã‚“。" +cantReRenote: "ブーストをブーストã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。" +inChannelRenote: "ãƒãƒ£ãƒ³ãƒãƒ«å†…ブースト" +muted: "Muted" +renoteMute: "ブーストをミュート" +renoteMuted: "Boosts muted" +renoteUnmute: "ブーストã®ãƒŸãƒ¥ãƒ¼ãƒˆã‚’解除" +markAsNSFW: "ユーザーã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯ã™ã‚‹" +markInstanceAsNSFW: "Mark as NSFW" +nsfwConfirm: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" +unNsfwConfirm: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã™ã¹ã¦ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’NSFWã¨ã—ã¦ãƒžãƒ¼ã‚¯è§£é™¤ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" +approveConfirm: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’承èªã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" +flagAsBotDescription: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒãƒ—ãƒã‚°ãƒ©ãƒ ã«ã‚ˆã£ã¦é‹ç”¨ã•ã‚Œã‚‹å ´åˆã¯ã€ã“ã®ãƒ•ãƒ©ã‚°ã‚’オンã«ã—ã¾ã™ã€‚オンã«ã™ã‚‹ã¨ã€åå¿œã®é€£éŽ–を防ããŸã‚ã®ãƒ•ãƒ©ã‚°ã¨ã—ã¦ä»–ã®é–‹ç™ºè€…ã«å½¹ç«‹ã£ãŸã‚Šã€Sharkeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã®ã«ãªã‚Šã¾ã™ã€‚" +flagSpeakAsCat: "猫語ã§è©±ã™" +flagSpeakAsCatDescription: "有効ã«ã™ã‚‹ã¨ã€ã‚ãªãŸã®æŠ•ç¨¿ã® 「ãªã€ã‚’「ã«ã‚ƒã€ã«ã—ã¾ã™ã€‚" +continueOnRemote: "リモートã§ç¶šè¡Œ" +chooseServerOnMisskeyHub: "Misskey Hubã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã‚’é¸æŠž" +mediaSilenceThisInstance: "サーãƒãƒ¼ã‚’メディアサイレンス" +rejectReports: "Reject reports from this instance" +silencedInstancesDescription: "サイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚サイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã™ã¹ã¦ã€Œã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã€ã¨ã—ã¦æ‰±ã‚ã‚Œã€ãƒ•ã‚©ãƒãƒ¼ãŒã™ã¹ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。" +mediaSilencedInstances: "メディアサイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼" +mediaSilencedInstancesDescription: "メディアサイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚メディアサイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã™ã¹ã¦ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã¨ã—ã¦æ‰±ã‚ã‚Œã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—ãŒä½¿ç”¨ã§ããªã„よã†ã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。" +intro: "Sharkeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ã¾ã—ãŸï¼ç®¡ç†è€…アカウントを作æˆã—ã¾ã—ょã†ã€‚" +blockedByBase: "This host is blocked implicitly because a base domain is blocked. To unblock this host, first unblock the base domain(s)." +silencedByBase: "This host is silenced implicitly because a base domain is silenced. To un-silence this host, first un-silence the base domain(s)." +mediaSilencedByBase: "This host's media is silenced implicitly because a base domain's media is silenced. To un-silence this host, first un-silence the base domain(s)." +driveSearchbarPlaceholder: "検索ドライブ" +fileNotSelected: "ファイルãŒé¸æŠžã•ã‚Œã¦ã„ã¾ã›ã‚“" +background: "背景" +antennaUsersDescription: "ユーザーåを改行ã§åŒºåˆ‡ã£ã¦æŒ‡å®šã—ã¾ã™" +aboutMisskey: "Sharkeyã«ã¤ã„ã¦" +expandAllCws: "ã™ã¹ã¦ã®è¿”ä¿¡ã®å†…容を表示ã™ã‚‹" +collapseAllCws: "ã™ã¹ã¦ã®è¿”ä¿¡ã®å†…å®¹ã‚’éš ã™" +attachAsFileQuestion: "クリップボードã®ãƒ†ã‚ストãŒé•·ã„ã§ã™ã€‚テã‚ストファイルã¨ã—ã¦æ·»ä»˜ã—ã¾ã™ã‹ï¼Ÿ" +signinOrContinueOnRemote: "続行ã™ã‚‹ã«ã¯ã€ãŠä½¿ã„ã®ã‚µãƒ¼ãƒãƒ¼ã«ç§»å‹•ã™ã‚‹ã‹ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«ç™»éŒ²ãƒ»ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™" +disableDrawer: "メニューをドãƒãƒ¯ãƒ¼ã§è¡¨ç¤ºã—ãªã„" +showReactionsCount: "ノートã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³æ•°ã‚’表示ã™ã‚‹" +cornerRadius: "コーナーã®ä¸¸ã¿" +warnForMissingAltText: "代替テã‚ストを入れ忘れãŸã¨ãã«è¦å‘Šã™ã‚‹" +deeplFreeMode: "DeepLX-JS を使用ã™ã‚‹ (èªè¨¼ã‚ーä¸è¦)" +deeplFreeModeDescription: "DeepLX-JSã®è¨å®šæ–¹æ³•ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ãã ã•ã„。" +useSoundOnlyWhenActive: "SharkeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹" +scratchpadDescription: "スクラッãƒãƒ‘ッドã¯ã€AiScriptã®å®Ÿé¨“環境をæä¾›ã—ã¾ã™ã€‚Sharkeyã¨å¯¾è©±ã™ã‚‹ã‚³ãƒ¼ãƒ‰ã®è¨˜è¿°ã€å®Ÿè¡Œã€çµæžœã®ç¢ºèªãŒã§ãã¾ã™ã€‚" +deleteAllFilesQueued: "ã‚ューã«å…¥ã‚Œã‚‰ã‚ŒãŸã™ã¹ã¦ã®ãƒ•ã‚¡ã‚¤ãƒ«ã®å‰Šé™¤" +systemAccountTitle: "This is a system account" +systemAccountDescription: "This account is created and managed automatically by the system, and cannot be logged into." +postFiltered: "post is hidden by a filter" +enableFaviconNotificationDot: "未èªã®é€šçŸ¥ãŒã‚ã‚‹ã¨ãã«ã‚¿ãƒ–ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’目立ãŸã›ã‚‹" +verifyNotificationDotWorkingButton: "タブアイコン強調機能ã®å‹•ä½œç¢ºèª" +notificationDotNotWorking: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ç¾æ™‚点ã§ã¯ã‚¿ãƒ–アイコン強調機能をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。" +notificationDotWorking: "タブアイコン強調機能ã¯ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§æ£ã—ã機能ã—ã¦ã„ã¾ã™ã€‚" +notificationDotNotWorkingAdvice: "タブアイコン強調機能ãŒæ©Ÿèƒ½ã—ãªã„å ´åˆã¯ã€ç®¡ç†è€…ã«ãƒ‰ã‚ュメントを確èªã™ã‚‹ã‚ˆã†ã«ä¾é ¼ã—ã¦ãã ã•ã„ {link}" +reportAbuseRenote: "ãƒ–ãƒ¼ã‚¹ãƒˆã‚’é€šå ±" +forwardReport: "リモートサーãƒãƒ¼ã«é€šå ±ã‚’転é€ã™ã‚‹" +forwardReportIsAnonymous: "リモートサーãƒãƒ¼ã‹ã‚‰ã¯ã‚ãªãŸã®æƒ…å ±ã¯è¦‹ã‚Œãšã€åŒ¿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚" +abuseMarkAsResolved: "対応済ã¿ã«ã™ã‚‹" +i18nInfo: "Sharkeyã¯æœ‰å¿—ã«ã‚ˆã£ã¦æ§˜ã€…ãªè¨€èªžã«ç¿»è¨³ã•ã‚Œã¦ã„ã¾ã™ã€‚{link}ã§ç¿»è¨³ã«å”力ã§ãã¾ã™ã€‚" +renotesCount: "ブーストã—ãŸæ•°" +renotedCount: "ブーストã•ã‚ŒãŸæ•°" +showTickerOnReplies: "返信ã«ã‚µãƒ¼ãƒãƒ¼æƒ…å ±ã‚’è¡¨ç¤ºã™ã‚‹" +disableCatSpeak: "猫ã®è©±ã—方を無効ã«ã™ã‚‹" +searchEngine: "検索MFMã®æ¤œç´¢ã‚¨ãƒ³ã‚¸ãƒ³" +searchEngineOther: "カスタム" +searchEngineCustomURIDescription: "カスタム検索エンジンã®URIã¯ã€\"https://www.google.com/search?q=\\{query}\" ã‚„ \"https://www.google.com/search?q=%s\" ã®ã‚ˆã†ãªå½¢å¼ã§å…¥åŠ›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" +searchEngineCusomURI: "カスタム検索エンジン URI" +makeIndexable: "公開ノートをインデックスä¸å¯ã«ã™ã‚‹" +makeIndexableDescription: "ノート検索ãŒã‚ãªãŸã®å…¬é–‹ãƒŽãƒ¼ãƒˆã‚’インデックス化ã—ãªã„よã†ã«ã—ã¾ã™ã€‚" +sendErrorReportsDescription: "オンã«ã™ã‚‹ã¨ã€å•é¡ŒãŒç™ºç”Ÿã—ãŸã¨ãã«ã‚¨ãƒ©ãƒ¼ã®è©³ç´°æƒ…å ±ãŒSharkeyã«å…±æœ‰ã•ã‚Œã€ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã®å“質å‘上ã«å½¹ç«‹ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã©ãŒå«ã¾ã‚Œã¾ã™ã€‚" +noInquiryUrlWarning: "å•ã„åˆã‚ã›å…ˆURLãŒè¨å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。" +misskeyUpdated: "SharkeyãŒæ›´æ–°ã•ã‚Œã¾ã—ãŸï¼" +usernameInfo: "サーãƒãƒ¼ä¸Šã§ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’一æ„ã«è˜åˆ¥ã™ã‚‹ãŸã‚ã®åå‰ã€‚アルファベット(a~z, A~Z)ã€æ•°å—(0~9)ã€ãŠã‚ˆã³ã‚¢ãƒ³ãƒ€ãƒ¼ãƒãƒ¼(_)ãŒä½¿ç”¨ã§ãã¾ã™ã€‚ユーザーåã¯å¾Œã‹ã‚‰å¤‰æ›´ã™ã‚‹ã“ã¨ã¯å‡ºæ¥ã¾ã›ã‚“。" +approvalRequiredForSignup: "アカウント登録を承èªåˆ¶ã«ã™ã‚‹" +voteConfirmMulti: "「{choice}ã€ã«æŠ•ç¥¨ã—ã¾ã™ã‹ï¼Ÿ\n 確èªå¾Œã€é¸æŠžè‚¢ã‚’増やã™ã“ã¨ãŒã§ãã¾ã™ã€‚" +pendingUserApprovals: "承èªå¾…ã¡ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„ã¾ã™ã€‚" +approveAccount: "承èªã™ã‚‹" +denyAccount: "æ‹’å¦ã¨å‰Šé™¤" +approved: "承èªæ¸ˆã¿" +notApproved: "承èªã•ã‚Œã¦ã„ãªã„" +approvalStatus: "承èªçŠ¶æ³" +numberOfReplies: "スレッド内ã®è¿”ä¿¡æ•°" +numberOfRepliesDescription: "ã“ã®æ•°å€¤ã‚’大ããã™ã‚‹ã¨ã€ã‚ˆã‚Šå¤šãã®è¿”ä¿¡ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®å€¤ã‚’大ããã—ã™ãŽã‚‹ã¨ã€UIãŒçª®å±ˆã«ãªã£ã¦èªã¿ã«ãããªã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" +boostSettings: "ブーストè¨å®š" +showVisibilitySelectorOnBoost: "公開範囲セレクターを表示" +showVisibilitySelectorOnBoostDescription: "無効ã®å ´åˆã€ä»¥ä¸‹ã§è¨å®šã—ãŸãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å…¬é–‹ç¯„囲ãŒä½¿ç”¨ã•ã‚Œã€ã‚»ãƒ¬ã‚¯ã‚¿ãƒ¼ã¯è¡¨ç¤ºã•ã‚Œã¾ã›ã‚“。" +visibilityOnBoost: "デフォルトã®ãƒ–ースト公開範囲" +defaultLike: "絵文å—ã®ã‚ˆã†ãªãƒ‡ãƒ•ã‚©ãƒ«ãƒˆ" +didYouLikeMisskey: "Sharkeyã‚’æ°—ã«å…¥ã£ã¦ã„ãŸã ã‘ã¾ã—ãŸã‹ï¼Ÿ" +pleaseDonate: "Sharkeyã¯{host}ãŒä½¿ç”¨ã—ã¦ã„ã‚‹ç„¡æ–™ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚ã“ã‚Œã‹ã‚‰ã‚‚開発を続ã‘られるよã†ã«ã€ãœã²å¯„付をãŠé¡˜ã„ã—ã¾ã™ï¼" +pleaseDonateInstance: "インスタンス管ç†è€…ã¸ã®å¯„付ã«ã‚ˆã£ã¦{host}を直接サãƒãƒ¼ãƒˆã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" +thisPostIsMissingAltTextCancel: "ã‚„ã‚ã‚‹" +thisPostIsMissingAltTextIgnore: "ã“ã®ã¾ã¾æŠ•ç¨¿" +thisPostIsMissingAltText: "代替テã‚ストãŒãªã„ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ã¾ã™ã€‚ã™ã¹ã¦ã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã«ä»£æ›¿ãƒ†ã‚ストをå«ã‚€ã‚ˆã†ã«ã—ã¦ãã ã•ã„。" +collapseRenotes: "ブーストã®ã‚¹ãƒžãƒ¼ãƒˆçœç•¥" +collapseRenotesDescription: "リアクションやブーストをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã—ã¾ã™ã€‚" +collapseNotesRepliedTo: "返信元ã®ãƒŽãƒ¼ãƒˆã‚’折りãŸãŸã‚€" +collapseFiles: "ファイルを折りãŸãŸã‚€" +uncollapseCW: "CWを展開ã™ã‚‹" +expandLongNote: "é•·ã„投稿を常ã«å±•é–‹ã™ã‚‹" +autoloadConversation: "会話スレッドを自動ã§èªã¿è¾¼ã‚€" +approvalRequiredToRegister: "ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‰¿èªåˆ¶ã§ã™ã€‚å‚åŠ ã—ãŸã„ç†ç”±ã‚’記入ã—ã€æ‰¿èªã•ã‚ŒãŸæ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚" +limitWidthOfReaction: "リアクションã®æœ€å¤§æ¨ªå¹…を制é™ã—ã€ç¸®å°ã—ã¦è¡¨ç¤ºã™ã‚‹" +oneko: "ã«ã‚ƒã‚“ã“フレンド :3" +renotesList: "ブースト一覧" +lookupConfirm: "照会ã—ã¾ã™ã‹ï¼Ÿ" +openTagPageConfirm: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒšãƒ¼ã‚¸ã‚’é–‹ãã¾ã™ã‹ï¼Ÿ" +specifyHost: "ホスト指定" +goToMisskey: "Sharkeyã¸" +enableAchievements: "実績を有効ã«ã™ã‚‹" +turnOffAchievements: "オフã«ã™ã‚‹ã¨å®Ÿç¸¾ã‚·ã‚¹ãƒ†ãƒ ã¯ç„¡åŠ¹ã«ãªã‚Šã¾ã™ã€‚" +enableBotTrending: "botã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°è¿½åŠ を許å¯ã™ã‚‹" +turnOffBotTrending: "オフã«ã™ã‚‹ã¨ãƒœãƒƒãƒˆãŒãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’入力ã—ãªããªã‚Šã¾ã™ã€‚" +replies: "返信" +renotes: "ブースト" +clickToOpen: "クリックã—ã¦ãƒŽãƒ¼ãƒˆã‚’é–‹ã" +showBots: "ボットをタイムラインã«è¡¨ç¤º" +showRenotes: "ブーストを表示" +sourceCodeIsNotYetProvided: "ソースコードã¯ã¾ã æä¾›ã•ã‚Œã¦ã„ã¾ã›ã‚“。ã“ã®å•é¡Œã®ä¿®æ£ã«ã¤ã„ã¦ç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。" +repositoryUrlDescription: "ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Sharkeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://activitypub.software/TransFem-org/Sharkey/ ã¨è¨˜å…¥ã—ã¾ã™ã€‚" +donation: "寄付ã™ã‚‹" +donationUrl: "寄付URL" +showBelowAvatar: "アイコンã®å¾Œã‚ã«è¡¨ç¤º" +allowClickingNotifications: "ãƒãƒƒãƒ—アップ通知ã®ã‚¯ãƒªãƒƒã‚¯ã‚’許å¯ã™ã‚‹" +seasonalScreenEffect: "å£ç¯€ã«å¿œã˜ãŸç”»é¢ã®æ¼”出" +noDescription: "説明文ã¯ã‚ã‚Šã¾ã›ã‚“" +sensitiveMediaRevealConfirm: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã§ã™ã€‚表示ã—ã¾ã™ã‹ï¼Ÿ" +severAllFollowRelations: "以下ã®é–¢ä¿‚ã‚’ã™ã¹ã¦æ–ã¡åˆ‡ã‚‹" +severAllFollowRelationsConfirm: "ã™ã¹ã¦ã®äººé–“関係を壊ã™ï¼Ÿã“ã‚Œã¯ä¸å¯é€†ã§ã™ï¼ã“ã‚Œã¯{instanceName}ã®{followingCount}フォãƒãƒ¼ã¨{followersCount}フォãƒãƒ¯ãƒ¼ã®é–¢ä¿‚を壊ã™ï¼" +severAllFollowRelationsQueued: "ã‚ューã«å…¥ã‚Œã‚‰ã‚ŒãŸ{host}ã¨ã®ã™ã¹ã¦ã®ãƒ•ã‚©ãƒãƒ¼é–¢ä¿‚を切æ–ã™ã‚‹ã€‚" +_delivery: + stop: "é…ä¿¡åœæ¢" + resume: "é…ä¿¡å†é–‹" +_initialAccountSetting: + youCanContinueTutorial: "ã“ã®ã¾ã¾{name}(Sharkey)ã®ä½¿ã„æ–¹ã«ã¤ã„ã¦ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«é€²ã‚€ã“ã¨ã‚‚ã§ãã¾ã™ãŒã€ã“ã“ã§ä¸æ–ã—ã¦ã™ãã«ä½¿ã„始ã‚ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" +_initialTutorial: + _landing: + description: "ã“ã“ã§ã¯ã€Sharkeyã®åŸºæœ¬çš„ãªä½¿ã„方や機能を確èªã§ãã¾ã™ã€‚" + _note: + description: "Sharkeyã§ã®æŠ•ç¨¿ã¯ã€ŒãƒŽãƒ¼ãƒˆã€ã¨å‘¼ã³ã¾ã™ã€‚ノートã¯ã‚¿ã‚¤ãƒ ラインã«æ™‚系列ã§ä¸¦ã‚“ã§ã„ã¦ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§æ›´æ–°ã•ã‚Œã¦ã„ãã¾ã™ã€‚" + _reaction: + letsTryReacting: "リアクションã¯ã€ãƒŽãƒ¼ãƒˆã®ã€Œ{reaction}ã€ãƒœã‚¿ãƒ³ã‚’クリックã™ã‚‹ã¨ã¤ã‘られã¾ã™ã€‚試ã—ã«ã“ã®ã‚µãƒ³ãƒ—ルã®ãƒŽãƒ¼ãƒˆã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’ã¤ã‘ã¦ã¿ã¦ãã ã•ã„ï¼" + reactDone: "「{undo}ã€ãƒœã‚¿ãƒ³ã‚’押ã™ã¨ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’å–り消ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" + _timeline: + description1: "Sharkeyã«ã¯ã€ä½¿ã„æ–¹ã«å¿œã˜ã¦è¤‡æ•°ã®ã‚¿ã‚¤ãƒ ラインãŒç”¨æ„ã•ã‚Œã¦ã„ã¾ã™ï¼ˆã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦ã¯ã„ãšã‚Œã‹ãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ï¼‰ã€‚" + bubble: "管ç†è€…ãŒé¸æŠžã—ãŸä»–ã®æŽ¥ç¶šã‚µãƒ¼ãƒãƒ¼ã®æŠ•ç¨¿ã‚’見るã“ã¨ãŒã§ãã¾ã™ã€‚" + _postNote: + description1: "Sharkeyã«ãƒŽãƒ¼ãƒˆã‚’投稿ã™ã‚‹éš›ã«ã¯ã€æ§˜ã€…ãªã‚ªãƒ—ションã®è¨å®šãŒå¯èƒ½ã§ã™ã€‚投稿フォームã¯ã“ã®ã‚ˆã†ã«ãªã£ã¦ã„ã¾ã™ã€‚" + _visibility: + home: "ホームタイムラインã®ã¿ã«å…¬é–‹ã€‚フォãƒãƒ¯ãƒ¼ãƒ»ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’見ã«æ¥ãŸäººãƒ»ãƒ–ーストã‹ã‚‰ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚‚見るã“ã¨ãŒã§ãã¾ã™ã€‚" + followers: "フォãƒãƒ¯ãƒ¼ã«ã®ã¿å…¬é–‹ã€‚本人以外ãŒãƒ–ーストã™ã‚‹ã“ã¨ã¯ã§ããšã€ã¾ãŸãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã¯é–²è¦§ã§ãã¾ã›ã‚“。" + _done: + title: "ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã¯çµ‚了ã§ã™ðŸŽ‰" + description: "ã“ã“ã§ç´¹ä»‹ã—ãŸæ©Ÿèƒ½ã¯ã»ã‚“ã®ä¸€éƒ¨ã«ã™ãŽã¾ã›ã‚“。Sharkeyã®ä½¿ã„方をより詳ã—ã知るã«ã¯ã€{link}ã‚’ã”覧ãã ã•ã„。" +_timelineDescription: + bubble: "ãƒãƒ–ルタイムラインã§ã¯ã€ç®¡ç†è€…ãŒé¸æŠžã—ãŸæŽ¥ç¶šã‚µãƒ¼ãƒãƒ¼ã‹ã‚‰ã®æŠ•ç¨¿ã‚’表示ã§ãã¾ã™ã€‚" +_serverSettings: + sidebarLogoUrl: "ãƒã‚´URL" + sidebarLogoDescription: "高精細ã€ãƒ€ã‚¤ãƒŠãƒŸãƒƒã‚¯å¹…ã®ã‚·ãƒŠãƒªã‚ªã§é€šå¸¸ã®ã‚¢ã‚¤ã‚³ãƒ³ã®ä»£ã‚ã‚Šã«ä½¿ç”¨ã™ã‚‹ãƒã‚´ã‚’指定ã—ã¾ã™ã€‚" + sidebarLogoUsageExample: "例:サイドãƒãƒ¼ã€è¨ªå•è€…用ã€ã€Œæƒ…å ±ã€ãƒšãƒ¼ã‚¸" + inquiryUrl: "å•ã„åˆã‚ã›å…ˆURL" + inquiryUrlDescription: "サーãƒãƒ¼é‹å–¶è€…ã¸ã®ãŠå•ã„åˆã‚ã›ãƒ•ã‚©ãƒ¼ãƒ ã®URLã‚„ã€é‹å–¶è€…ã®é€£çµ¡å…ˆç‰ãŒè¨˜è¼‰ã•ã‚ŒãŸWebページã®URLを指定ã—ã¾ã™ã€‚" +_accountMigration: + moveAccountDescription: "æ–°ã—ã„アカウントã¸ç§»è¡Œã—ã¾ã™ã€‚\n ・フォãƒãƒ¯ãƒ¼ãŒæ–°ã—ã„アカウントを自動ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¾ã™\n ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼ã¯å…¨ã¦è§£é™¤ã•ã‚Œã¾ã™\n ・ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã¯ãƒŽãƒ¼ãƒˆã®ä½œæˆãªã©ãŒã§ããªããªã‚Šã¾ã™\n\nフォãƒãƒ¯ãƒ¼ã®ç§»è¡Œã¯è‡ªå‹•ã§ã™ãŒã€ãƒ•ã‚©ãƒãƒ¼ã®ç§»è¡Œã¯æ‰‹å‹•ã§è¡Œã†å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚移行å‰ã«ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ãƒ•ã‚©ãƒãƒ¼ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã—ã€ç§»è¡Œå¾Œã™ãã«ç§»è¡Œå…ˆã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚’è¡Œãªã£ã¦ãã ã•ã„。\nリスト・ミュート・ブãƒãƒƒã‚¯ã«ã¤ã„ã¦ã‚‚åŒæ§˜ã§ã™ã®ã§ã€æ‰‹å‹•ã§ç§»è¡Œã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\n\n(ã“ã®èª¬æ˜Žã¯ã“ã®ã‚µãƒ¼ãƒãƒ¼ï¼ˆSharkey v13.12.0以é™ï¼‰ã®ä»•æ§˜ã§ã™ã€‚Mastodonãªã©ã®ä»–ã®ActivityPubソフトウェアã§ã¯æŒ™å‹•ãŒç•°ãªã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚)" +_achievements: + _types: + _notes1: + title: "just setting up my shonk" + flavor: "良ã„Sharkeyライフをï¼" + _login1000: + flavor: "Sharkeyを使ã£ã¦ãã‚Œã¦ã‚ã‚ŠãŒã¨ã†ï¼" + _iLoveMisskey: + title: "I Love Sharkey" + description: "\"I ⤠#Sharkey\"を投稿ã—ãŸ" + flavor: "Sharkeyを使ã£ã¦ãã ã•ã‚Šã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ï¼ by 開発ãƒãƒ¼ãƒ " + _client30min: + description: "クライアントを起動ã—ã¦ã‹ã‚‰30分以上経éŽã—ãŸ" + _client60min: + title: "Sharkeyã®è¦‹ã™ãŽ" + description: "クライアントを起動ã—ã¦ã‹ã‚‰60分以上経éŽã—ãŸ" + _brainDiver: + flavor: "Misskey-Misskey La-Tu-Ma" + _tutorialCompleted: + title: "Sharkeyåˆå¿ƒè€…講座 修了証" +_role: + _options: + btlAvailable: "ãƒãƒ–ルタイムラインã®é–²è¦§" + canImportNotes: "ノートã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆãŒå¯èƒ½" + canUpdateBioMedia: "アイコンã¨ãƒãƒŠãƒ¼ã®æ›´æ–°ã‚’許å¯" + _condition: + isLocked: "éµã‚¢ã‚«ã‚¦ãƒ³ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼" + isExplorable: "「アカウントを見ã¤ã‘ã‚„ã™ãã™ã‚‹ã€ãŒæœ‰åŠ¹ãªãƒ¦ãƒ¼ã‚¶ãƒ¼" +_emailUnavailable: + banned: "ã“ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã§ã¯ç™»éŒ²ã§ãã¾ã›ã‚“" +_signup: + approvalPending: "アカウントãŒä½œæˆã•ã‚Œã€æ‰¿èªå¾…ã¡ã®çŠ¶æ…‹ã§ã™ã€‚" + reasonInfo: "インスタンスã«å‚åŠ ã—ãŸã„ç†ç”±ã‚’入力ã—ã¦ãã ã•ã„。" +_aboutMisskey: + about: "Sharkeyã¯ã€Misskeyをベースã«ã—ãŸã‚ªãƒ¼ãƒ—ンソースã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã§ã™ã€‚" + original: "Misskey オリジナル" + original_sharkey: "Sharkey オリジナル" + thisIsModifiedVersion: "{name}ã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã®Sharkeyを改変ã—ãŸãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’使用ã—ã¦ã„ã¾ã™ã€‚" + translation: "Sharkeyを翻訳" + donate_sharkey: "Sharkeyã«å¯„付" + testers: "テスター" +_serverDisconnectedBehavior: + disabled: "è¦å‘Šã‚’無効ã«ã™ã‚‹" +_channel: + allowRenoteToExternal: "ãƒãƒ£ãƒ³ãƒãƒ«å¤–ã¸ã®ãƒ–ーストã¨å¼•ç”¨ãƒ–ーストを許å¯ã™ã‚‹" +_instanceMute: + instanceMuteDescription: "ミュートã—ãŸã‚µãƒ¼ãƒãƒ¼ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¸ã®è¿”ä¿¡ã‚’å«ã‚ã¦ã€è¨å®šã—ãŸã‚µãƒ¼ãƒãƒ¼ã®å…¨ã¦ã®ãƒŽãƒ¼ãƒˆã¨ãƒ–ーストをミュートã—ã¾ã™ã€‚" +_theme: + keys: + renote: "Boost" +_soundSettings: + driveFileDurationWarnDescription: "é•·ã„音声を使用ã™ã‚‹ã¨Sharkeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ" + driveFileError: "音声ãŒèªã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚è¨å®šã‚’変更ã—ã¦ãã ã•ã„" +_2fa: + moreDetailedGuideHere: "詳細ãªã‚¬ã‚¤ãƒ‰ã¯ã“ã¡ã‚‰" +_widgets: + search: "検索" +_poll: + multiple: "複数ã®é¸æŠžè‚¢" +_profile: + updateBanner: "æ›´æ–°ãƒãƒŠãƒ¼" + removeBanner: "ãƒãƒŠãƒ¼ã‚’削除" + changeBackground: "背景を変更ã™ã‚‹" + updateBackground: "背景を更新ã™ã‚‹" + removeBackground: "背景を削除ã™ã‚‹" +_timelines: + bubble: "ãƒãƒ–ル" +_pages: + blocks: + dynamicDescription: "ã“ã®ãƒ–ãƒãƒƒã‚¯ã¯å»ƒæ¢ã•ã‚Œã¦ã„ã¾ã™ã€‚今後ã¯{play}を利用ã—ã¦ãã ã•ã„。" +_notification: + youRenoted: "{name}ãŒBoostã—ã¾ã—ãŸ" + renotedBySomeUsers: "{n}人ãŒãƒªãƒŽãƒ¼ãƒˆã—ã¾ã—ãŸ" + _types: + renote: "Boost" + edited: "編集済ã¿" + _actions: + renote: "ブースト" +_webhookSettings: + _events: + renote: "Boostã•ã‚ŒãŸã¨ã" + _systemEvents: + abuseReportResolved: "ユーザーã‹ã‚‰ã®é€šå ±ã‚’処ç†ã—ãŸã¨ã" +_abuseReport: + _notificationRecipient: + _recipientType: + _captions: + mail: "モデレーター権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã®ã¿)" + webhook: "指定ã—ãŸSystemWebhookã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã¨é€šå ±ã‚’解決ã—ãŸæ™‚ã«ãã‚Œãžã‚Œç™ºä¿¡)" +_moderationLogTypes: + approve: "承èªæ¸ˆã¿" + setRemoteInstanceNSFW: "Set remote instance as NSFW" + unsetRemoteInstanceNSFW: "Set remote instance as NSFW" + rejectRemoteInstanceReports: "Rejected reports from remote instance" + acceptRemoteInstanceReports: "Accepted reports from remote instance" +_mfm: + uncommonFeature: "ã“ã®æ©Ÿèƒ½ã¯ä¸€èˆ¬çš„ã«æ™®åŠã—ã¦ã„ãªã„ãŸã‚ã€ä»–ã®Misskeyフォークをå«ã‚ãŸå¤šãã®Fediverseソフトウェアã§è¡¨ç¤ºã§ããªã„ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" + intro: "MFM ã¯Misskey, Sharkey, Firefish, Akkomaãªã©ã€å¤šãã®å ´æ‰€ã§ä½¿ç”¨ã§ãるマークアップ言語ã§ã™ã€‚ã“ã“ã§ã¯ã€åˆ©ç”¨ã§ãã‚‹MFM構文ã®ä¸€è¦§ã‚’ã”覧ã„ãŸã ã‘ã¾ã™ã€‚" + dummy: "Sharkeyã§Fediverseã®ä¸–ç•ŒãŒåºƒãŒã‚Šã¾ã™" + mention: "メンション" + mentionDescription: "アットマーク + ユーザーåã§ã€ç‰¹å®šã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" + hashtag: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°" + hashtagDescription: "ナンãƒãƒ¼ã‚µã‚¤ãƒ³ + ã‚¿ã‚°ã§ã€ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" + url: "URL" + urlDescription: "URLを示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" + link: "リンク" + linkDescription: "æ–‡ç« ã®ç‰¹å®šã®ç¯„囲をã€URLã«ç´ã¥ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" + bold: "太å—" + boldDescription: "æ–‡å—を太ã表示ã—ã¦å¼·èª¿ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" + small: "å°æ–‡å—" + smallDescription: "内容をå°ã•ã・薄ã表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" + center: "ä¸å¤®å¯„ã›" + centerDescription: "内容をä¸å¤®å¯„ã›ã§è¡¨ç¤ºã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" + inlineCode: "コード(インライン)" + inlineCodeDescription: "プãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’インラインã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚" + blockCode: "コード(ブãƒãƒƒã‚¯ï¼‰" + blockCodeDescription: "複数行ã®ãƒ—ãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’ブãƒãƒƒã‚¯ã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚" + inlineMath: "æ•°å¼ï¼ˆã‚¤ãƒ³ãƒ©ã‚¤ãƒ³ï¼‰" + inlineMathDescription: "æ•°å¼ ï¼ˆKaTeXå½¢å¼ï¼‰ã‚’インラインã§è¡¨ç¤ºã—ã¾ã™ã€‚" + blockMath: "æ•°å¼ï¼ˆãƒ–ãƒãƒƒã‚¯ï¼‰" + blockMathDescription: "æ•°å¼ ï¼ˆKaTeXå½¢å¼ï¼‰ã‚’ブãƒãƒƒã‚¯ã§è¡¨ç¤ºã—ã¾ã™ã€‚" + quote: "引用" + quoteDescription: "内容ãŒå¼•ç”¨ã§ã‚ã‚‹ã“ã¨ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" + emoji: "カスタム絵文å—" + emojiDescription: "コãƒãƒ³ã§ã‚«ã‚¹ã‚¿ãƒ 絵文å—åを囲むã¨ã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—を表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" + search: "検索" + searchDescription: "検索ボックスを表示ã§ãã¾ã™ã€‚" + flip: "å転" + flipDescription: "内容を上下ã¾ãŸã¯å·¦å³ã«å転ã•ã›ã¾ã™ã€‚" + jelly: "アニメーション(ã³ã‚ˆã‚“ã³ã‚ˆã‚“)" + jellyDescription: "ゼリーãŒæºã‚Œã‚‹ã‚ˆã†ãªæ„Ÿã˜ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" + tada: "アニメーション(ã˜ã‚ƒãƒ¼ã‚“)" + tadaDescription: "「ã˜ã‚ƒãƒ¼ã‚“ï¼ã€ã¨å¼·èª¿ã™ã‚‹ã‚ˆã†ãªæ„Ÿã˜ã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" + jump: "アニメーション(ジャンプ)" + jumpDescription: "è·³ãるアニメーションをã•ã›ã¾ã™ã€‚" + bounce: "アニメーション(ãƒã‚¦ãƒ³ãƒ‰ï¼‰" + bounceDescription: "è·³ãã¦ç€åœ°ã™ã‚‹ã‚ˆã†ãªã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" + shake: "アニメーション(ã¶ã‚‹ã¶ã‚‹ï¼‰" + shakeDescription: "震ãˆã‚‹ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" + twitch: "アニメーション(ガタガタ)" + twitchDescription: "より激ã—ã震ãˆã‚‹ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’ã•ã›ã¾ã™ã€‚" + spin: "アニメーション(回転)" + spinDescription: "内容を回転ã•ã›ã¾ã™ã€‚" + x2: "大" + x2Description: "内容を大ãã表示ã•ã›ã¾ã™ã€‚" + x3: "特大" + x3Description: "内容をより大ãã表示ã•ã›ã¾ã™ã€‚" + x4: "超特大" + x4Description: "内容をã•ã‚‰ã«å¤§ãã表示ã•ã›ã¾ã™ã€‚" + blur: "ã¼ã‹ã—" + blurDescription: "内容をã¼ã‹ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ãƒã‚¤ãƒ³ã‚¿ãƒ¼ã‚’上ã«ä¹—ã›ã‚‹ã¨ã¯ã£ãり見ãˆã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã™ã€‚" + font: "フォント" + fontDescription: "内容ã®ãƒ•ã‚©ãƒ³ãƒˆã‚’指定ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚" + rainbow: "レインボー" + rainbowDescription: "内容を虹色ã§è¡¨ç¤ºã•ã›ã¾ã™ã€‚" + sparkle: "ã‚ラã‚ラ" + sparkleDescription: "ã‚ラã‚ラã¨æ˜Ÿåž‹ã®ãƒ‘ーティクルを表示ã•ã›ã¾ã™ã€‚" + rotate: "角度変更" + rotateDescription: "指定ã—ãŸè§’度ã§å›žè»¢ã•ã›ã¾ã™ã€‚" + position: "ä½ç½®å¤‰æ›´" + positionDescription: "ä½ç½®ã‚’ãšã‚‰ã™ã“ã¨ãŒã§ãã¾ã™ã€‚" + crop: "切りå–ã‚Š" + cropDescription: "内容を切り抜ãã¾ã™ã€‚" + followMouse: "マウス追従" + followMouseDescription: "内容ãŒãƒžã‚¦ã‚¹ã«è¿½å¾“ã—ã¾ã™ã€‚スマホã®å ´åˆã¯ã‚¿ãƒƒãƒ—ã—ãŸå ´æ‰€ã«è¿½å¾“ã—ã¾ã™ã€‚" + scale: "拡大" + scaleDescription: "内容を引ã伸ã°ã—ã¦è¡¨ç¤ºã—ã¾ã™ã€‚" + foreground: "æ–‡å—色" + foregroundDescription: "æ–‡å—色を変更ã—ã¾ã™ã€‚" + fade: 'フェード' + fadeDescription: '内容をフェードイン・フェードアウトã•ã›ã¾ã™ã€‚' + background: "背景色" + backgroundDescription: "背景色を変更ã—ã¾ã™ã€‚" + plain: "Plain" + plainDescription: "内å´ã®æ§‹æ–‡ã‚’å…¨ã¦ç„¡åŠ¹ã«ã—ã¾ã™ã€‚" +_animatedMFM: + play: "MFMアニメーションをå†ç”Ÿ" + stop: "MFMアニメーションåœæ¢" + _alert: + text: "MFMアニメーションã«ã¯ã€é«˜é€Ÿã§ç‚¹æ»…ã—ãŸã‚Šå‹•ã„ãŸã‚Šã™ã‚‹ãƒ†ã‚スト・絵文å—ã‚’å«ã‚€å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" + confirm: "å†ç”Ÿã™ã‚‹" +_dataRequest: + title: "データリクエスト" + warn: "データリクエストã¯3æ—¥ã”ã¨ã«å¯èƒ½ã§ã™ã€‚" + text: "データã®ä¿å˜ãŒå®Œäº†ã™ã‚‹ã¨ã€ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç™»éŒ²ã•ã‚Œã¦ã„るメールアドレスã«ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚" + button: "データリクエスト実行" +_dataSaver: + _avatar: + description: "アイコン画åƒã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ãŒåœæ¢ã—ã¾ã™ã€‚アニメーション画åƒã¯é€šå¸¸ã®ç”»åƒã‚ˆã‚Šãƒ•ã‚¡ã‚¤ãƒ«ã‚µã‚¤ã‚ºãŒå¤§ãã„ã“ã¨ãŒã‚ã‚‹ã®ã§ã€ãƒ‡ãƒ¼ã‚¿é€šä¿¡é‡ã‚’ã•ã‚‰ã«å‰Šæ¸›ã§ãã¾ã™ã€‚" +_urlPreviewSetting: + timeoutDescription: "プレビューå–å¾—ã®æ‰€è¦æ™‚é–“ãŒã“ã®å€¤ã‚’超ãˆãŸå ´åˆã€ãƒ—レビューã¯ç”Ÿæˆã•ã‚Œã¾ã›ã‚“。" + requireContentLength: "Content-LengthãŒå–å¾—ã§ããŸå ´åˆã®ã¿ãƒ—レビューを生æˆ" + summaryProxy: "プレビューを生æˆã™ã‚‹ãƒ—ãƒã‚ã‚·ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆ" + summaryProxyDescription: "Misskey本体ã§ã¯ãªãã€ã‚µãƒžãƒªãƒ¼ãƒ—ãƒã‚シを使用ã—ã¦ãƒ—レビューを生æˆã—ã¾ã™ã€‚" + summaryProxyDescription2: "プãƒã‚ã‚·ã«ã¯ä¸‹è¨˜ãƒ‘ラメータãŒã‚¯ã‚¨ãƒªæ–‡å—列ã¨ã—ã¦é€£æºã•ã‚Œã¾ã™ã€‚プãƒã‚ã‚·å´ãŒã“れらをサãƒãƒ¼ãƒˆã—ãªã„å ´åˆã€è¨å®šå€¤ã¯ç„¡è¦–ã•ã‚Œã¾ã™ã€‚" +_externalNavigationWarning: + title: "外部サイトã«ç§»å‹•ã—ã¾ã™" + description: "{host}を離れã¦å¤–部サイトã«ç§»å‹•ã—ã¾ã™" + trustThisDomain: "ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã§ä»Šå¾Œã“ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‚’ä¿¡é ¼ã™ã‚‹" diff --git a/sharkey-locales/ja-KS.yml b/sharkey-locales/ja-KS.yml new file mode 100644 index 0000000000000000000000000000000000000000..c9f9e898983bd109292fdaec2a1df481220e53b2 --- /dev/null +++ b/sharkey-locales/ja-KS.yml @@ -0,0 +1,79 @@ +--- +introMisskey: "よã†ãŠè¶Šã—ï¼Sharkeyã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®åˆ†æ•£åž‹ãƒžã‚¤ã‚¯ãƒãƒ–ãƒã‚°ã‚µãƒ¼ãƒ“スやãん。\n「ノートã€ã‚’作ã£ã¦ã€ã„ã¾èµ·ã“ã£ã¨ã‚‹ã“ã¨ã‚’共有ã—ãŸã‚Šã€ã‚ã‚“ãŸã«ã¤ã„ã¦çš†ã«ç™ºä¿¡ã—よã†ðŸ“¡\n「ツッコミã€æ©Ÿèƒ½ã§ã€çš†ã®ãƒŽãƒ¼ãƒˆã«ç´ æ—©ãåå¿œã‚’è¿½åŠ ã—ãŸã‚Šã‚‚ã§ãã‚‹ã§âœŒ\nã»ãªã€æ–°ã—ã„世界を探検ã—よã‹ðŸš€" +poweredByMisskeyDescription: "{name}ã¯ã€ã‚ªãƒ¼ãƒ—ンソースã®ãƒ—ラットフォーム<b>Sharkey</b>ã®ã‚µãƒ¼ãƒãƒ¼ã®ã²ã¨ã¤ãªã‚“ã‚„ã§ã€‚" +renotedBy: "{user}ãŒãƒ–ーストã—ãŸã§" +copyLinkRenote: "ブーストã®ãƒªãƒ³ã‚¯ã‚’コピーã™ã‚‹ã§ï¼Ÿ" +deleteAndEditConfirm: "ã“ã®ãƒŽãƒ¼ãƒˆã‚’ã»ã‹ã—ã¦ã‚‚ã£ã‹ã„ç›´ã™ï¼Ÿã“ã®ãƒŽãƒ¼ãƒˆã¸ã®ãƒ„ッコミã€ãƒ–ーストã€è¿”信も全部消ãˆã‚‹ã‚“ã‚„ã‘ã©ãã‚Œã§ã‚‚ãˆãˆã‚“?" +inChannelRenote: "ãƒãƒ£ãƒ³ãƒãƒ«ã®ä¸ã§ãƒ–ースト" +renoteMute: "ブーストã¯è¦‹ã„ã²ã‚“" +renoteUnmute: "ブーストもやã£ã±è¦‹ã‚‹ã‚" +flagAsBotDescription: "ã‚‚ã—ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’プãƒã‚°ãƒ©ãƒ 使ã†ã¦é‹ç”¨ã™ã‚‹ã‚“ã‚„ã£ãŸã‚‰ã€ã“ã®ãƒ•ãƒ©ã‚°ã‚’オンã«ã—ã¦ã‚„。オンã«ã™ã‚Œã°ã€åå¿œãŒãƒãƒ¼ãƒƒã¦é€£éŽ–ã›ã‚“よã†ã«é–‹ç™ºè€…ãŒä½¿ã†ãŸã‚Šã€Sharkeyã®ã‚·ã‚¹ãƒ†ãƒ 上ã§ã®æ‰±ã„ãŒBotã«åˆã£ãŸã‚‚ã‚“ã«ãªã‚‹ã‹ã‚‰ãªã€‚" +flagSpeakAsCat: "猫語ã§è©±ã™ã§" +flagSpeakAsCatDescription: "有効ã«ã™ã‚‹ã¨ã€ã‚ãªãŸã®æŠ•ç¨¿ã® 「ãªã€ã‚’「ã«ã‚ƒã€ã«ã™ã‚‹ã§ãƒ¼ã€‚" +intro: "Sharkeyã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ãŒå®Œäº†ã—ãŸã§ï¼ç®¡ç†è€…アカウントを作ã£ã¦ã‚„。" +aboutMisskey: "Sharkeyã£ã¦ãªã‚“や?" +disableDrawer: "メニューをドãƒãƒ¯ãƒ¼ã§è¡¨ç¤ºã›ãˆã¸ã‚“" +useSoundOnlyWhenActive: "SharkeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã¨ãã ã‘音出ã™" +scratchpadDescription: "スクラッãƒãƒ‘ッドã§ã¯AiScriptを色々試ã™ã“ã¨ãŒã§ãるんや。Sharkeyã«å¯¾ã—ã¦è‰²ã€…ã§ãるコードを書ã„ã¦å‹•ã‹ã—ã¦ã¿ãŸã‚Šã€çµæžœã‚’見ãŸã‚Šã§ãã‚‹ã§ã€‚" +reportAbuseRenote: "ブースト苦情ã ã™ã§ï¼Ÿ" +forwardReport: "リモートサーãƒãƒ¼ã«é€šå ±ã‚’転é€ã™ã‚‹ã§" +forwardReportIsAnonymous: "リモートサーãƒãƒ¼ã‹ã‚‰ã¯ã‚ã‚“ãŸã®æƒ…å ±ã¯è¦‹ãˆã‚“ãªã£ã¦ã€åŒ¿åã®ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦è¡¨ç¤ºã•ã‚Œã‚‹ã§ã€‚" +abuseMarkAsResolved: "対応ã—ãŸã§" +renotesCount: "ブーストã—ãŸæ•°ã‚„ã§" +renotedCount: "ブーストã•ã‚ŒãŸæ•°ã‚„ã§" +sendErrorReportsDescription: "オンã«ã—ãŸã‚‰ã€ãªã‚“ã‹å¤‰ãªã“ã¨ãŒèµ·ããŸã¨ãã€è©³ã—ã„ã®ãŒå…¨éƒ¨Sharkeyã«é€ã‚‰ã‚Œã¦ã€ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚’ã‚‚ã£ã¨è‰¯ã†ã™ã‚‹ã§ã€‚ã‚¨ãƒ©ãƒ¼æƒ…å ±ã«ã¯ã€OSã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã€ãƒ–ラウザã®ç¨®é¡žã€è¡Œå‹•å±¥æ´ãªã‚“ã‹ãŒå«ã¾ã‚Œã‚‹ãªã€‚" +misskeyUpdated: "SharkeyãŒæ›´æ–°ã•ã‚ŒãŸã§ï¼\nモデレーターã®äººã‚‰ã«æ„Ÿè¬ã›ãªã‚ã‹ã‚“ã§" +didYouLikeMisskey: "Sharkeyæ°—ã«å…¥ã£ã¦ãã‚ŒãŸï¼Ÿ" +pleaseDonate: "Sharkeyã¯{host}ãŒä½¿ã†ã¨ã‚‹ç„¡æ–™ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚„ã§ã€‚ã“ã‚Œã‹ã‚‰ã‚‚開発を続ã‘れるよã†ã«ã€å¯„付ã—ãŸã£ã¦ãªï½žã€‚" +collapseRenotes: "見ãŸã“ã¨ã‚るブーストã¯é£›ã°ã—ã¦è¡¨ç¤ºã™ã‚‹ã§" +collapseRenotesDescription: "リアクションやブーストをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã™ã‚‹ã§ã€‚" +renotesList: "ブースト一覧" +goToMisskey: "Sharkeyã¸" +renotes: "ブースト" +showRenotes: "ブースト出ã™" +_initialAccountSetting: + youCanContinueTutorial: "ã“ã‚“ã¾ã¾{name}(Sharkey)ã®ä½¿ã„æ–¹ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã«ã‚‚è¡Œã‘ã‚‹ã‘ã©ã€ã“ã“ã§ã‚„ã‚ã¦ã™ãã«ä½¿ã„始ã‚ã¦ã‚‚ãˆãˆã§ã€‚" +_initialTutorial: + _landing: + description: "ã“ã“ã§ã¯ã€Sharkeyã®ã‚«ãƒ³ã‚¿ãƒ³ãªä½¿ã„æ–¹ã¨ã‹æ©Ÿèƒ½ã‚’確ã‹ã‚れんã§ã€‚" + _note: + description: "Sharkeyã§ã®æŠ•ç¨¿ã¯ã€ŒãƒŽãƒ¼ãƒˆã€ã£ã¦å‘¼ã°ã‚Œã¦ã‚“ã§ã€‚ノートã¯é †ã€…ã«ã‚¿ã‚¤ãƒ ラインã«è¼‰ã£ã¦ã¦ã€ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ ã§æ–°ã—ããªã£ã¦ã£ã¦ã‚“ã§ã€‚" + _reaction: + letsTryReacting: "ノートã®ã€Œ{reaction}ã€ãƒœã‚¿ãƒ³ã§ãƒ„ッコã‚ã‚‹ã‚。試ã—ã«ä¸‹ã®ãƒŽãƒ¼ãƒˆã«ãƒ„ッコんã§ã¿ã€‚" + reactDone: "「ーã€ãƒœã‚¿ãƒ³ã§ãƒ„ッコミやã‚れるã§ã€‚" + _timeline: + description1: "Sharkeyã«ã¯ã€ã„ã‚ã„ã‚タイムラインãŒã‚ã‚“ã§ï¼ˆãŸã ã€ã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã£ã¦ã¯ç„¡åŠ¹åŒ–ã•ã‚Œã¦ã‚‹ã¨ã“ã‚ã‚‚ã‚ã‚‹ãªï¼‰ã€‚" + _postNote: + description1: "Sharkeyã«ãƒŽãƒ¼ãƒˆã‚’投稿ã™ã‚‹ã¨ãã€ã„ã‚ã‚“ãªã‚ªãƒ—ションãŒä»˜ã‘れるã§ã€‚投稿画é¢ã¯ã“ã‚“ãªæ„Ÿã˜ã‚„。" + _visibility: + home: "ホームタイムラインã«ã ã‘見ã›ã‚‹ã§ã€‚フォãƒãƒ¯ãƒ¼ã¨ã‹ã€ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’見ã«æ¥ãŸäººã€ãƒ–ーストã‹ã‚‰ã‚‚見れるã‹ã‚‰ã€å®Ÿè³ªã¯å…¨å“¡è¦‹ã‚Œã‚‹ã‘ã©ãªã€‚ã‚ã‚“ã¾ã—広ãŒã‚Šã«ãã„ã£ã¦ã“ã¨ã‚„。" + followers: "フォãƒãƒ¯ãƒ¼ã«ã ã‘見ã›ã‚‹ã§ã€‚自分以外ã¯ãƒ–ーストã§ãã¸ã‚“ã—ã€ãƒ•ã‚©ãƒãƒ¯ãƒ¼ä»¥å¤–ã¯çµ¶å¯¾ã«è¦‹ã‚Œã¸ã‚“。" +_achievements: + _types: + _notes1: + flavor: "Sharkeyを楽ã—ã‚“ã§ãªï½ž" + _login1000: + flavor: "Sharkeyよã†ã•ã‚“使ã¦ã‚‚ã‚ã¦ãŠãŠãã«ãªï¼" + _iLoveMisskey: + title: "Sharkey好ãã‚„ãã‚“" + description: "\"I ⤠#Sharkey\"を投稿ã—ãŸ" + flavor: "Sharkeyを使ã£ã¦ãã‚Œã¦ãŠãŠãã«ãªï½ž by 開発ãƒãƒ¼ãƒ " + _client60min: + title: "Sharkeyã®è¦‹éŽãŽã‚„ï¼" + _tutorialCompleted: + title: "Sharkeyã²ã‚ˆã£ã“講座 修了証" +_aboutMisskey: + about: "Sharkeyã¯ã€Misskeyをベースã«ã—ãŸã‚ªãƒ¼ãƒ—ンソースãªã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚„。" + thisIsModifiedVersion: "{name}ã¯ã‚ªãƒªã‚¸ãƒŠãƒ«ã®Sharkeyã‚’ã„ã˜ã£ãŸãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’ã¤ã“ã†ã¦ã‚‹ã§ã€‚" + translation: "Sharkeyを翻訳" + donate: "Sharkeyã«å¯„付" +_channel: + allowRenoteToExternal: "ãƒãƒ£ãƒ³ãƒãƒ«ã®å¤–ã«ãƒ–ーストã§ãるよã†ã«ã™ã‚‹" +_soundSettings: + driveFileDurationWarnDescription: "é•·ã„音使ã†ãŸã‚‰Sharkey使ã†ã®ã«è‰¯ã†ãªã„ã‹ã‚‚ã—ã‚Œã¸ã‚“ã§ã€‚ãã‚Œã§ã‚‚ãˆãˆã‹ï¼Ÿ" +_notification: + youRenoted: "{name}ãŒãƒ–ーストã—ãŸã¿ãŸã„ã‚„ã§" + renotedBySomeUsers: "{n}人ãŒãƒ–ーストã—ãŸã§" +_webhookSettings: + _events: + renote: "ブーストã•ã‚Œã‚‹ã¨ã~ï¼" diff --git a/sharkey-locales/jbo-EN.yml b/sharkey-locales/jbo-EN.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/jbo-EN.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/kab-KAB.yml b/sharkey-locales/kab-KAB.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/kab-KAB.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/kn-IN.yml b/sharkey-locales/kn-IN.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/kn-IN.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/ko-GS.yml b/sharkey-locales/ko-GS.yml new file mode 100644 index 0000000000000000000000000000000000000000..65440e07cdc371131cfeafa00f2adfe3e661a7ce --- /dev/null +++ b/sharkey-locales/ko-GS.yml @@ -0,0 +1,4 @@ +--- +disableDrawer: "드로어 메뉴 쓰지 않기" +forwardReport: "ì›¬ê² ì„œë²„ì— ì‹ ê³ ë³´ë‚´ê¸°" +fromUrl: "Van URL" diff --git a/sharkey-locales/ko-KR.yml b/sharkey-locales/ko-KR.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc14b78c1f16745886a7809a391eb83ce041e867 --- /dev/null +++ b/sharkey-locales/ko-KR.yml @@ -0,0 +1,11 @@ +--- +disableDrawer: "드로어 메뉴를 사용하지 않기" +forwardReport: "리모트 서버ì—ë„ ì‹ ê³ ë‚´ìš© 보내기" +forwardReportIsAnonymous: "리모트 서버ì—서는 ë‚˜ì˜ ì •ë³´ë¥¼ ë³¼ 수 없으며, ìµëª…ì˜ ì‹œìŠ¤í…œ ê³„ì •ìœ¼ë¡œ 표시ë©ë‹ˆë‹¤." +abuseMarkAsResolved: "í•´ê²°ë¨ìœ¼ë¡œ 표시" +_initialTutorial: + _reaction: + letsTryReacting: "ë¦¬ì•¡ì…˜ì€ ë…¸íŠ¸ì˜ '{reaction}' ë²„íŠ¼ì„ í´ë¦í•˜ì—¬ ë¶™ì¼ ìˆ˜ 있습니다. 지금 표시ë˜ëŠ” 샘플 ë…¸íŠ¸ì— ë¦¬ì•¡ì…˜ì„ ë‹¬ì•„ 보세요!" + reactDone: "'{undo}' ë²„íŠ¼ì„ ëˆŒëŸ¬ì„œ ë¦¬ì•¡ì…˜ì„ ì·¨ì†Œí• ìˆ˜ 있습니다." +_urlPreviewSetting: + requireContentLength: "Content-Length를 ì–»ì—ˆì„ ë•Œë§Œ 미리보기 만들기" diff --git a/sharkey-locales/lo-LA.yml b/sharkey-locales/lo-LA.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/lo-LA.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/nl-NL.yml b/sharkey-locales/nl-NL.yml new file mode 100644 index 0000000000000000000000000000000000000000..514def8d7b9f2ef0503f026442a24f9c55978d63 --- /dev/null +++ b/sharkey-locales/nl-NL.yml @@ -0,0 +1,2 @@ +--- +replies: "Antwoorden" diff --git a/sharkey-locales/no-NO.yml b/sharkey-locales/no-NO.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/no-NO.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/pl-PL.yml b/sharkey-locales/pl-PL.yml new file mode 100644 index 0000000000000000000000000000000000000000..05071db880bbf424746c34412bce7e1c7d99d0e6 --- /dev/null +++ b/sharkey-locales/pl-PL.yml @@ -0,0 +1,8 @@ +--- +disableDrawer: "Nie używaj menu w stylu szuflady" +forwardReport: "Przekaż zgÅ‚oszenie do innej instancji" +forwardReportIsAnonymous: "Zamiast twojego konta, anonimowe konto systemowe bÄ™dzie wyÅ›wietlone jako zgÅ‚aszajÄ…cy na instancji zdalnej." +abuseMarkAsResolved: "Oznacz zgÅ‚oszenie jako rozwiÄ…zane" +usernameInfo: "Nazwa, która identyfikuje Twoje konto spoÅ›ród innych na tym serwerze. Możesz użyć alfabetu (a~z, A~Z), cyfr (0~9) lub podkreÅ›lników (_). Nazwy użytkownika nie mogÄ… być później zmieniane." +replies: "Odpowiedzi" +renotes: "UdostÄ™pnieÅ„" diff --git a/sharkey-locales/pt-PT.yml b/sharkey-locales/pt-PT.yml new file mode 100644 index 0000000000000000000000000000000000000000..b3f7611ee32acb16dd9806fcb669126788bf95f3 --- /dev/null +++ b/sharkey-locales/pt-PT.yml @@ -0,0 +1,9 @@ +--- +disableDrawer: "Não mostrar o menu em formato de gaveta" +forwardReport: "Encaminhar a denúncia para o servidor remoto" +forwardReportIsAnonymous: "No servidor remoto, suas informações não serão visÃveis, e você será apresentado como uma conta do sistema anônima." +abuseMarkAsResolved: "Marcar denúncia como resolvida" +openRemoteProfile: "Abrir perfil remoto" +allowClickingNotifications: "Permitir clicar em notificações" +pinnedOnly: "Fixado" +blockingYou: "Bloqueando você" diff --git a/sharkey-locales/ro-RO.yml b/sharkey-locales/ro-RO.yml new file mode 100644 index 0000000000000000000000000000000000000000..d1d04b081ecd100f75b020015ff5f6e026a64574 --- /dev/null +++ b/sharkey-locales/ro-RO.yml @@ -0,0 +1,6 @@ +--- +disableDrawer: "Nu folosi meniuri în stil sertar" +forwardReport: "RedirecÈ›ionează raportul către instanÈ›a externă" +forwardReportIsAnonymous: "ÃŽn locul contului tău, va fi afiÈ™at un cont anonim, de sistem, ca raportor către instanÈ›a externă." +abuseMarkAsResolved: "Marchează raportul ca rezolvat" +replies: "Răspunsuri" diff --git a/sharkey-locales/ru-RU.yml b/sharkey-locales/ru-RU.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/ru-RU.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/si-LK.yml b/sharkey-locales/si-LK.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/si-LK.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/sk-SK.yml b/sharkey-locales/sk-SK.yml new file mode 100644 index 0000000000000000000000000000000000000000..3d2739a3333874aa5e33efc4e6c908deaa3ca037 --- /dev/null +++ b/sharkey-locales/sk-SK.yml @@ -0,0 +1,6 @@ +--- +disableDrawer: "NepoužÃvaÅ¥ Å¡uflÃkové menu" +forwardReport: "PreposlaÅ¥ nahlásenie na server" +forwardReportIsAnonymous: "Namiesto vášho úÄtu bude zobrazený anonymný systémový úÄet na vzdialenom serveri ako autor nahlásenia." +abuseMarkAsResolved: "OznaÄiÅ¥ nahlásenia ako vyrieÅ¡ené" +replies: "Odpovede" diff --git a/sharkey-locales/sv-SE.yml b/sharkey-locales/sv-SE.yml new file mode 100644 index 0000000000000000000000000000000000000000..a2bb34bb2cd39fc4892bd0cf7f3788e8eafb19ee --- /dev/null +++ b/sharkey-locales/sv-SE.yml @@ -0,0 +1,2 @@ +--- +replies: "Svar" diff --git a/sharkey-locales/th-TH.yml b/sharkey-locales/th-TH.yml new file mode 100644 index 0000000000000000000000000000000000000000..17a9a633e7e03dcee78d7d8dc44eaa4cffc7ec63 --- /dev/null +++ b/sharkey-locales/th-TH.yml @@ -0,0 +1,13 @@ +--- +disableDrawer: "ไม่à¹à¸ªà¸”งเมนูในรูปà¹à¸šà¸šà¸¥à¸´à¹‰à¸™à¸Šà¸±à¸" +forwardReport: "ส่งต่à¸à¸£à¸²à¸¢à¸‡à¸²à¸™à¹„ปยังเซิร์ฟเวà¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" +forwardReportIsAnonymous: "ข้à¸à¸¡à¸¹à¸¥à¸‚à¸à¸‡à¸„ุณจะไม่ปราà¸à¸à¸šà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹à¸¥à¸°à¸›à¸£à¸²à¸à¸à¹€à¸›à¹‡à¸™à¸šà¸±à¸à¸Šà¸µà¸£à¸°à¸šà¸šà¸—ี่ไม่ระบุชื่à¸" +abuseMarkAsResolved: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸£à¸²à¸¢à¸‡à¸²à¸™à¸§à¹ˆà¸²à¹à¸à¹‰à¹„ขà¹à¸¥à¹‰à¸§" +_initialTutorial: + _reaction: + letsTryReacting: "คุณสามารถเพิ่มรีà¹à¸à¸„ชั่นได้ด้วยà¸à¸²à¸£à¸„ลิà¸à¸›à¸¸à¹ˆà¸¡ “{reaction}†บนโน้ต ลà¸à¸‡à¸£à¸µà¹à¸à¸„ชั่นโน้ตตัวà¸à¸¢à¹ˆà¸²à¸‡à¸™à¸µà¹‰à¸”ูสิ!" + reactDone: "คุณสามารถยà¸à¹€à¸¥à¸´à¸à¸£à¸µà¹à¸à¸„ชั่นได้โดยà¸à¸²à¸£à¸à¸”ปุ่ม “{undo}â€" +_achievements: + _types: + _notes1: + title: "just setting up my shonk" diff --git a/sharkey-locales/tr-TR.yml b/sharkey-locales/tr-TR.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/tr-TR.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/ug-CN.yml b/sharkey-locales/ug-CN.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed97d539c095cf1413af30cc23dea272095b97dd --- /dev/null +++ b/sharkey-locales/ug-CN.yml @@ -0,0 +1 @@ +--- diff --git a/sharkey-locales/uk-UA.yml b/sharkey-locales/uk-UA.yml new file mode 100644 index 0000000000000000000000000000000000000000..f730399e1f4b7673243364966e8fe45a021c168f --- /dev/null +++ b/sharkey-locales/uk-UA.yml @@ -0,0 +1,5 @@ +--- +disableDrawer: "Ðе викориÑтовувати виÑувні меню" +forwardReport: "ПереÑлати звіт на віддалений інÑтанÑ" +forwardReportIsAnonymous: "ЗаміÑÑ‚ÑŒ вашого облікового запиÑу анонімний ÑиÑтемний обліковий Ð·Ð°Ð¿Ð¸Ñ Ð±ÑƒÐ´Ðµ відображатиÑÑ Ñк доповідач на віддаленому інÑтанÑÑ–" +abuseMarkAsResolved: "Позначити Ñкаргу Ñк вирішену" diff --git a/sharkey-locales/uz-UZ.yml b/sharkey-locales/uz-UZ.yml new file mode 100644 index 0000000000000000000000000000000000000000..4e38de281322f2a13e9632a43c5a5ceeb9ad32cc --- /dev/null +++ b/sharkey-locales/uz-UZ.yml @@ -0,0 +1,6 @@ +--- +disableDrawer: "Slayd menyusidan foydalanmang" +forwardReport: "Xabarni masofadagi serverga yuborish" +forwardReportIsAnonymous: "Sizning yuborayotgan xabaringiz o'z akkountingiz emas balki anonim tarzda qoladi" +abuseMarkAsResolved: "Yuborilgan xabarni hal qilingan deb belgilash" +replies: "Javoblar" diff --git a/sharkey-locales/vi-VN.yml b/sharkey-locales/vi-VN.yml new file mode 100644 index 0000000000000000000000000000000000000000..ec56a58c7183f54a81151233b3e8473b6feae32b --- /dev/null +++ b/sharkey-locales/vi-VN.yml @@ -0,0 +1,9 @@ +--- +disableDrawer: "Không dùng menu thanh bên" +forwardReport: "Chuyển tiếp báo cáo cho máy chủ từ xa" +forwardReportIsAnonymous: "Thay vì tà i khoản của bạn, má»™t tà i khoản hệ thống ẩn danh sẽ được hiển thị dÆ°á»›i dạng ngÆ°á»i báo cáo ở máy chủ từ xa." +abuseMarkAsResolved: "Äánh dấu đã xá» lý" +_achievements: + _types: + _notes1: + title: "just setting up my shonk" diff --git a/sharkey-locales/zh-CN.yml b/sharkey-locales/zh-CN.yml new file mode 100644 index 0000000000000000000000000000000000000000..aca700337d806950aca9382ad1c1e0b06e506d5a --- /dev/null +++ b/sharkey-locales/zh-CN.yml @@ -0,0 +1,9 @@ +--- +disableDrawer: "ä¸æ˜¾ç¤ºæŠ½å±‰èœå•" +forwardReport: "将该举报信æ¯è½¬å‘给远程æœåŠ¡å™¨" +forwardReportIsAnonymous: "在远程实例上显示的报告者是匿å的系统账å·ï¼Œè€Œä¸æ˜¯æ‚¨çš„è´¦å·ã€‚" +abuseMarkAsResolved: "处ç†å®Œæ¯•" +_initialTutorial: + _reaction: + letsTryReacting: "回应å¯ä»¥é€šè¿‡ç‚¹å‡»å¸–åä¸çš„「{reaction}ã€æŒ‰é’®æ¥æ·»åŠ 。试ç€ç»™è¿™ä¸ªç¤ºä¾‹å¸–åæ·»åŠ ä¸€ä¸ªå›žåº”ï¼" + reactDone: "通过按下「{undo}ã€æŒ‰é’®ï¼Œå¯ä»¥å–消已ç»æ·»åŠ 的回应" diff --git a/sharkey-locales/zh-TW.yml b/sharkey-locales/zh-TW.yml new file mode 100644 index 0000000000000000000000000000000000000000..c36f8dde2122d6c3760d1755197f1dbca80ea0db --- /dev/null +++ b/sharkey-locales/zh-TW.yml @@ -0,0 +1,9 @@ +--- +disableDrawer: "ä¸é¡¯ç¤ºä¸‹æ‹‰å¼é¸å–®" +forwardReport: "å°‡å ±å‘Šè½‰é€çµ¦é 端伺æœå™¨" +forwardReportIsAnonymous: "在é 端實例上看ä¸åˆ°æ‚¨çš„è³‡è¨Šï¼Œé¡¯ç¤ºçš„å ±å‘Šè€…æ˜¯åŒ¿å的系统帳戶。" +abuseMarkAsResolved: "處ç†å®Œç•¢" +_initialTutorial: + _reaction: + letsTryReacting: "å¯ä»¥é€éŽé»žæ“Šè²¼æ–‡ä¸Šçš„「{reaction}ã€æŒ‰éˆ•ä¾†æ·»åŠ å應。請嘗試在æ¤ç¯„ä¾‹è²¼æ–‡æ·»åŠ å應ï¼" + reactDone: "按下「{undo}ã€æŒ‰éˆ•å¯ä»¥å–消å應。"