diff --git a/.config/ci.yml b/.config/ci.yml index c381d21d921d9d345fe74dfee7ae8a2390fa102f..44092d36623b4ff32fec95a83473623eccac2e2f 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -106,7 +106,7 @@ redis: # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global +# You can set scope to local (default value) or global # (include notes from remote). #meilisearch: @@ -198,13 +198,18 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false # For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". +# but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' diff --git a/.config/docker_example.env b/.config/docker_example.env index 4fe8e76b784b05bfc1868cc8dced53f25bf51516..c61248da2e46503006ab679f263c40d353be65ba 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -1,5 +1,11 @@ +# misskey settings +# MISSKEY_URL=https://example.tld/ + # db settings POSTGRES_PASSWORD=example-misskey-pass +# DATABASE_PASSWORD=${POSTGRES_PASSWORD} POSTGRES_USER=example-misskey-user +# DATABASE_USER=${POSTGRES_USER} POSTGRES_DB=misskey +# DATABASE_DB=${POSTGRES_DB} DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}" diff --git a/.config/docker_example.yml b/.config/docker_example.yml index c22bd83c2e0bdd3a51678365ff5377aaf9868244..de95f1b21a2d736aa675dbec4ff3e7449e01dd7d 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -63,6 +63,7 @@ #───┘ URL └───────────────────────────────────────────────────── # Final accessible URL seen by a user. +# You can set url from an environment variable instead. url: https://example.tld/ # ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE @@ -95,9 +96,11 @@ db: port: 5432 # Database name + # You can set db from an environment variable instead. db: misskey # Auth + # You can set user and pass from environment variables instead. user: example-misskey-user pass: example-misskey-pass @@ -270,8 +273,13 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/.config/example.yml b/.config/example.yml index ae55b983bb548ed297c6ea495bca83bd2afe3063..21e85b7b8904566136155c4fbed59f6d83d6076a 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -285,8 +285,13 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/compose.yml similarity index 93% rename from .devcontainer/docker-compose.yml rename to .devcontainer/compose.yml index 2809cd2ca4694e61bd889ecc664f9bde470d0296..d02d2a8f4a89b034ec6703adf075b97bf4aba282 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: app: build: @@ -8,6 +6,7 @@ services: volumes: - ../:/workspace:cached + - node_modules:/workspace/node_modules command: sleep infinity @@ -46,6 +45,7 @@ services: volumes: postgres-data: redis-data: + node_modules: networks: internal_network: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 31b6212cb5c484d5d3e14137c46e8b3ebd493a44..fbf959d449e55a850e10396e48404067b324e19e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,16 +1,16 @@ { "name": "Misskey", - "dockerComposeFile": "docker-compose.yml", + "dockerComposeFile": "compose.yml", "service": "app", "workspaceFolder": "/workspace", "features": { "ghcr.io/devcontainers/features/node:1": { - "version": "20.12.2" + "version": "20.16.0" }, "ghcr.io/devcontainers-contrib/features/corepack:1": {} }, "forwardPorts": [3000], - "postCreateCommand": "sudo chmod 755 .devcontainer/init.sh && .devcontainer/init.sh", + "postCreateCommand": "/bin/bash .devcontainer/init.sh", "customizations": { "vscode": { "extensions": [ diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index 729e1a9d2d9dad9e7357c50e2dadda742305b995..55fb1e6fa6873c51aea3b54ca25c233b6eeae520 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -2,7 +2,8 @@ set -xe -sudo chown -R node /workspace +sudo chown node node_modules +git config --global --add safe.directory /workspace git submodule update --init corepack install corepack enable diff --git a/.dockerignore b/.dockerignore index 1de0c7982bcdacbdd8b03b756a31a5b6a1824fd8..f204349160a0b96c1019ef1c9069c83c464808c5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,12 +7,11 @@ Dockerfile build/ built/ db/ -docker-compose.yml +.devcontainer/compose.yml node_modules/ packages/*/node_modules redis/ files/ -misskey-assets/ fluent-emojis/ .pnp.* @@ -28,4 +27,4 @@ fluent-emojis/ .idea/ packages/*/.vscode/ -packages/backend/test/docker-compose.yml +packages/backend/test/compose.yml diff --git a/.gitignore b/.gitignore index 2b6a5c1ebfb6b010b34e76ffb65834f86d1dae42..a8887eab92e4b1a652e2c5c7b753247d3a8032e2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,9 @@ coverage !/.config/docker_example.yml !/.config/docker_example.env docker-compose.yml -!/.devcontainer/docker-compose.yml +compose.yml +.devcontainer/compose.yml +!/.devcontainer/compose.yml # misskey /build @@ -59,6 +61,7 @@ ormconfig.json temp /packages/frontend/src/**/*.stories.ts tsdoc-metadata.json +misskey-assets # Sharkey /packages/megalodon/lib diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84d29851b2c135e62e454fc2aa78600180dfb853..2e773eddf9aecc1e3f9efa3322a75f27b33a6550 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ testCommit: - pnpm install --frozen-lockfile - pnpm run build - pnpm run migrate - - pnpm run --filter='!megalodon' test + - pnpm run --filter='!megalodon' --workspace-concurrency=1 test - pnpm run --filter=backend lint - pnpm run --filter=frontend eslint cache: diff --git a/.gitmodules b/.gitmodules index a3ca76cc960696348889c5c6cb7c1d651eeb133b..1a68b48180d6c09ffdfcb2ed6b8e860872645319 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "misskey-assets"] - path = misskey-assets - url = https://github.com/misskey-dev/assets.git [submodule "fluent-emojis"] path = fluent-emojis url = https://github.com/misskey-dev/emojis.git diff --git a/.node-version b/.node-version index 87834047a6fa6597c8fbf1f42e10ce3f9ef41d5b..8ce7030825b5ee92e1b87aab772de43fff35b7a5 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.12.2 +20.16.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index e2528594c3bc482a02b0c925ea954f74b94f04eb..0986b6eae32c1206d2771e5e266f8ace94cb468d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,167 @@ -## Unreleased +## 2024.8.0 ### General +- Enhance: モデレーターã¯ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ•ã‚©ãƒãƒ¼ãƒ»ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã®ä¸€è¦§ã‚’見られるよã†ã« +- Enhance: アカウントã®å‰Šé™¤ã®ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãƒã‚°ã‚’残ã™ã‚ˆã†ã« +- Enhance: ä¸é©åˆ‡ãªãƒšãƒ¼ã‚¸ã€ã‚®ãƒ£ãƒ©ãƒªãƒ¼ã€Playを管ç†è€…権é™ã§å‰Šé™¤ã§ãるよã†ã« +- Fix: リモートユーザã®ãƒ•ã‚©ãƒãƒ¼ãƒ»ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã®ä¸€è¦§ãŒéžå…¬é–‹è¨å®šã®å ´åˆã‚‚表示ã§ãã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ + +### Client +- Enhance: 「自分ã®Playã€ãƒšãƒ¼ã‚¸ã«ãŠã„ã¦PlayãŒéžå…¬é–‹ã‹ã©ã†ã‹ãŒä¸€ç›®ã§ã‚ã‹ã‚‹ã‚ˆã†ã« +- Enhance: ä¸é©åˆ‡ãªãƒšãƒ¼ã‚¸ã€ã‚®ãƒ£ãƒ©ãƒªãƒ¼ã€Playã‚’é€šå ±ã§ãるよã†ã« +- Fix: Play編集時ã«å…¬é–‹ç¯„囲ãŒã€Œãƒ‘ブリックã€ã«ãƒªã‚»ãƒƒãƒˆã•ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: ページé·ç§»ã«å¤±æ•—ã™ã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: iOSã§ãƒ¦ãƒ¼ã‚¶ãƒ¼åãªã©ãŒãƒªãƒ³ã‚¯ã¨ã—ã¦èª¤æ¤œçŸ¥ã•ã‚Œã‚‹ç¾è±¡ã‚’抑制 +- Fix: mCaptchaを使用ã—ã¦ã„ã¦ã‚‚botプãƒãƒ†ã‚¯ã‚·ãƒ§ãƒ³ã«é–¢ã™ã‚‹è¦å‘ŠãŒæ¶ˆãˆãªã„ã®ã‚’ä¿®æ£ +- Fix: ユーザーã®ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãƒšãƒ¼ã‚¸ã«ãŠã„ã¦ãƒ¦ãƒ¼ã‚¶ãƒ¼åã«ãƒ‰ãƒƒãƒˆãŒå…¥ã£ã¦ã„ã‚‹ã¨ã‚·ã‚¹ãƒ†ãƒ アカウントã¨ã—ã¦è¡¨ç¤ºã•ã‚Œã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ +- Fix: 特定ã®æ¡ä»¶ä¸‹ã§ãƒŽãƒ¼ãƒˆã®å‰Šé™¤ãƒœã‚¿ãƒ³ãŒå‡ºãªã„ã®ã‚’ä¿®æ£ + +### Server +- Enhance: 照会時ã«URLãŒhtmlã‹ã¤headタグ内ã«`rel="alternate"`, `type="application/activity+json"`ã®`link`ã‚¿ã‚°ãŒã‚ã‚‹å ´åˆã«è¿½ã£ã¦ãƒªãƒ³ã‚¯å…ˆã‚’照会ã§ãるよã†ã« +- Enhance: å‡çµã•ã‚ŒãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ•ã‚©ãƒãƒ¼ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’表示ã—ãªã„よã†ã« +- Fix: WSã®`readAllNotifications` メッセージ㌠`body` ã‚’æŒãŸãªã„å ´åˆã«å‹•ä½œã—ãªã„å•é¡Œ #14374 + - 通知ページや通知カラム(デッã‚)ã‚’é–‹ã„ã¦ã„る状態ã«ãŠã„ã¦ã€æ–°ãŸã«ç™ºç”Ÿã—ãŸé€šçŸ¥ãŒæ—¢èªã•ã‚Œãªã„å•é¡ŒãŒä¿®æ£ã•ã‚Œã¾ã™ã€‚ + - ã“ã‚Œã«ã‚ˆã‚Šã€ãƒ—ッシュ通知ãŒæœ‰åŠ¹ãªåŒæ¡ä»¶ä¸‹ã®ç’°å¢ƒã«ãŠã„ã¦ã€ãƒ—ッシュ通知ãŒå¸¸ã«ç™ºç”Ÿã—ã¦ã—ã¾ã†å•é¡Œã‚‚ä¿®æ£ã•ã‚Œã¾ã™ã€‚ +- Fix: Playå„種エンドãƒã‚¤ãƒ³ãƒˆã®è¿”り値ã«`visibility`ãŒå«ã¾ã‚Œã¦ã„ãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: サーãƒãƒ¼æƒ…å ±å–å¾—ã®éš›ã«ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼é™å®šã®æƒ…å ±ãŒå–å¾—ã§ããªã„ã“ã¨ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582) +- Fix: 公開範囲ãŒãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã®ãƒŽãƒ¼ãƒˆã‚’ユーザーアクティビティã®ãƒãƒ£ãƒ¼ãƒˆç”Ÿæˆã«ä½¿ç”¨ã—ãªã„よã†ã« + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679) +- Fix: ActivityPubã®ã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã‚¿ã‚¤ãƒ—判定ã§ä¸æ˜Žãªã‚¿ã‚¤ãƒ—ã‚’å—ã‘å–ã£ãŸå ´åˆã§ã‚‚処ç†ã‚’継続ã™ã‚‹ã‚ˆã†ã« + - ã‚ュー処ç†ã®ã¤ã¾ã‚ŠãŒæ”¹å–„ã•ã‚Œã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ +- Fix: リãƒãƒ¼ã‚·ã®å¯¾å±€è¨å®šã®å¤‰æ›´ãŒåæ˜ ã•ã‚Œãªã„ã®ã‚’ä¿®æ£ +- Fix: 無制é™ã«ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã®ãƒãƒ£ãƒ³ãƒãƒ«ã«æŽ¥ç¶šã§ãã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: ベースãƒãƒ¼ãƒ«ã®ãƒãƒªã‚·ãƒ¼ã‚’変更ã—ãŸéš›ã«ãƒ¢ãƒ‡ãƒã‚°ã«è¨˜éŒ²ã•ã‚Œãªã„ã®ã‚’ä¿®æ£ + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700) +- Fix: Prevent memory leak from memory caches (#14310) +- Fix: More reliable memory cache eviction (#14311) + +## 2024.7.0 + +### Note +- デッã‚UIã®æ–°ç€ãƒŽãƒ¼ãƒˆã‚’サウンドã§é€šçŸ¥ã™ã‚‹æ©Ÿèƒ½ã®è¿½åŠ (v2024.5.0)ã«ä¼´ã„ã€ä»¥å‰ã‹ã‚‰å‹•ä½œã—ãªããªã£ã¦ã„ãŸã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆè¨å®šå†…ã®ã€Œã‚¢ãƒ³ãƒ†ãƒŠå—ä¿¡ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«é€šçŸ¥ã€ã‚µã‚¦ãƒ³ãƒ‰ã‚’削除ã—ã¾ã—ãŸã€‚ +- Streaming APIã«ã¦å…¥åŠ›ãŒä¸æ£ãªå ´åˆã«ã¯ãã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’無視ã™ã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã—ãŸã€‚ #14251 + +### General +- Feat: é€šå ±ã‚’å—ã‘ãŸéš›ã€ã¾ãŸã¯è§£æ±ºã—ãŸéš›ã«ã€äºˆã‚登録ã—ãŸå®›å…ˆã«é€šçŸ¥ã‚’飛ã°ã›ã‚‹ã‚ˆã†ã«(mail or webhook) #13705 +- Feat: ユーザーã®ã‚¢ã‚¤ã‚³ãƒ³/ãƒãƒŠãƒ¼ã®å¤‰æ›´å¯å¦ã‚’ãƒãƒ¼ãƒ«ã§è¨å®šå¯èƒ½ã« + - 変更ä¸å¯ã¨ãªã£ã¦ã„ã¦ã‚‚ã€è¨å®šæ¸ˆã¿ã®ã‚‚ã®ã‚’解除ã—ã¦ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆç”»åƒã«æˆ»ã™ã“ã¨ã¯å‡ºæ¥ã¾ã™ +- Feat: ユーザ作æˆæ™‚ã«SystemWebhookã‚’é€ä¿¡å¯èƒ½ã« #14281 +- Feat: メディアサイレンスを実装 #13842 + - メディアサイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã™ã¹ã¦ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã¨ã—ã¦æ‰±ã‚ã‚Œã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—ãŒä½¿ç”¨ã§ããªã„よã†ã«ãªã‚Šã¾ã™ã€‚ +- Enhance: 管ç†ç”»é¢ã§ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã«ã—ãŸãŠçŸ¥ã‚‰ã›ã‚’表示・編集ã§ãるよã†ã« - Fix: é…ä¿¡åœæ¢ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ä¸€è¦§ãŒè¦‹ã‚Œãªããªã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: Dockerコンテナã®ç«‹ã¡ä¸Šã’時ã«`pnpm`ã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã§å›ºã¾ã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œ +- Fix: デフォルトテーマã«ç„¡åŠ¹ãªãƒ†ãƒ¼ãƒžã‚³ãƒ¼ãƒ‰ã‚’入力ã™ã‚‹ã¨UIãŒä½¿ç”¨ã§ããªããªã‚‹å•é¡Œã‚’ä¿®æ£ +- 翻訳ã®æ›´æ–° +- ä¾å˜é–¢ä¿‚ã®æ›´æ–° ### Client -- +- Feat: ユーザーページã‹ã‚‰ã€Œã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆã‚’検索ã€ã§ãるよã†ã« (#14128) +- Feat: 検索ページã¯ã‚¯ã‚¨ãƒªã‚’å—ã‘付ã‘るよã†ã«ãªã‚Šã¾ã—㟠(#14128) +- Enhance: 検索ページã®UI改善 (#14128) +- Enhance: 内蔵APIドã‚ュメントã®ãƒ‡ã‚¶ã‚¤ãƒ³ãƒ»ãƒ‘フォーマンスを改善 +- Enhance: éžãƒã‚°ã‚¤ãƒ³æ™‚ã«ä»–サーãƒãƒ¼ã«é·ç§»ã™ã‚‹ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’è¿½åŠ +- Enhance: éžãƒã‚°ã‚¤ãƒ³æ™‚ã®ãƒã‚¤ãƒ©ã‚¤ãƒˆTLã®ãƒ‡ã‚¶ã‚¤ãƒ³ã‚’改善 +- Enhance: フãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰ã®ã‚¢ã‚¯ã‚»ã‚·ãƒ“リティ改善 + (Based on https://github.com/taiyme/misskey/pull/226) +- Enhance: サーãƒãƒ¼æƒ…å ±ãƒšãƒ¼ã‚¸ãƒ»ãŠå•ã„åˆã‚ã›ãƒšãƒ¼ã‚¸ã‚’改善 + (Cherry-picked from https://github.com/taiyme/misskey/pull/238) +- Enhance: AiScriptã‚’0.19.0ã«ã‚¢ãƒƒãƒ—デート +- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) +- Enhance: センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã‚’é–‹ãéš›ã«ç¢ºèªãƒ€ã‚¤ã‚¢ãƒã‚°ã‚’出ã›ã‚‹ã‚ˆã†ã« +- Enhance: 検索(ノート/ユーザー)㧠`#` ã‹ã‚‰å§‹ã¾ã‚‹æ–‡å—列を入力ã™ã‚‹ã¨ã€ãã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒŽãƒ¼ãƒˆ/ユーザー一覧ページãŒè¡¨ç¤ºã§ãるよã†ã« +- Enhance: 検索(ノート/ユーザー)ã«ãŠã„ã¦ã€å…¥åŠ›ã«ç©ºç™½ãŒå«ã¾ã‚Œã¦ã„ã‚‹å ´åˆã¯ç…§ä¼šã‚’è¡Œã‚ãªã„よã†ã« +- Enhance: 検索(ノート/ユーザー)ã«ãŠã„ã¦ã€ç…§ä¼šã‚’è¡Œã†ã‹ã©ã†ã‹ã€ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒŽãƒ¼ãƒˆ/ユーザー一覧ページを表示ã™ã‚‹ã‹ã©ã†ã‹ã®ç¢ºèªãƒ€ã‚¤ã‚¢ãƒã‚°ã‚’出ã™ã‚ˆã†ã« +- Enhance: 検索(ノート/ユーザー)㧠`@` ã‹ã‚‰å§‹ã¾ã‚‹æ–‡å—列(`@user@host`ãªã©)を入力ã™ã‚‹ã¨ã€ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’照会ã§ãるよã†ã« +- Enhance: ドライブã®ãƒ•ã‚¡ã‚¤ãƒ«ãƒ»ãƒ•ã‚©ãƒ«ãƒ€ã‚’ドラッグã—ãªãã¦ã‚‚移動ã§ãるよã†ã« + (Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99) +- Enhance: デッã‚ã®ã‚¢ãƒ³ãƒ†ãƒŠãƒ»ãƒªã‚¹ãƒˆé¸æŠžç”»é¢ã‹ã‚‰ãã‚Œãžã‚Œã‚’æ–°è¦ä½œæˆã§ãるよã†ã« +- Enhance: ブラウザã®ã‚³ãƒ³ãƒ†ã‚ストメニューを使用ã§ãるよã†ã« +- Enhance: 連åˆã®ã€Œé€£åˆä¸ã€,「購èªä¸ã€,「é…ä¿¡ä¸ã€ã«å¯¾ã—ã¦ãƒ–ãƒãƒƒã‚¯ã—ã¦ã„るサーãƒãƒ¼ã€é…ä¿¡åœæ¢ã—ã¦ã„るサーãƒãƒ¼ã‚’å«ã‚ãªã„よã†ã« +- Fix: `/about#federation` ページãªã©ã§å„インスタンスã®ãƒãƒ£ãƒ¼ãƒˆãŒè¡¨ç¤ºã•ã‚Œãªããªã£ã¦ã„ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: ユーザーページã®è¿½åŠ æƒ…å ±ã®ãƒ©ãƒ™ãƒ«ã‚’投稿者ã®ã‚µãƒ¼ãƒãƒ¼ã®çµµæ–‡å—ã§è¡¨ç¤ºã™ã‚‹ (#13968) +- Fix: リãƒãƒ¼ã‚·ã®å¯¾å±€ã‚’æ£ã—ã共有ã§ããªã„ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: コントãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«ã§ãƒ™ãƒ¼ã‚¹ãƒãƒ¼ãƒ«ã®ãƒãƒªã‚·ãƒ¼ã‚’編集ã—ã¦ã‚‚UI上ã§ã¯å¤‰æ›´ãŒåæ˜ ã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: アンテナã®ç·¨é›†ç”»é¢ã®ãƒœã‚¿ãƒ³ã«éš™é–“ã‚’è¿½åŠ +- Fix: テーマプレビューãŒè¦‹ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: ショートカットã‚ーãŒé€£æ‰“ã§ãã‚‹å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://github.com/taiyme/misskey/pull/234) +- Fix: MkSignin.vueã®credentialRequestã‹ã‚‰Reactivityを削除(ProxyãŒPasskeyèªè¨¼å‡¦ç†ã«æ¸¡ã‚‹ã“ã¨ã‚’é¿ã‘ã‚‹ãŸã‚) +- Fix: 「アニメーション画åƒã‚’å†ç”Ÿã—ãªã„ã€ãŒã‚ªãƒ³ã®ã¨ãã§ã‚‚サーãƒãƒ¼ã®ãƒãƒŠãƒ¼ç”»åƒãƒ»èƒŒæ™¯ç”»åƒãŒã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã—ã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574) +- Fix: Twitchã®åŸ‹ã‚è¾¼ã¿ãŒé–‹ã‘ãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: åメニューã®é«˜ã•ãŒã‚¦ã‚£ãƒ³ãƒ‰ã‚¦ã‹ã‚‰ã¯ã¿å‡ºã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: 個人宛ã¦ã®ãƒ€ã‚¤ã‚¢ãƒã‚°å½¢å¼ã®ãŠçŸ¥ã‚‰ã›ãŒå³æ™‚表示ã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: 一部ã®ç”»åƒãŒã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–指定ã•ã‚Œã¦ã„ã‚‹ã¨ãã«ç”»é¢ã«ä½•ã‚‚表示ã•ã‚Œãªã„ã“ã¨ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ +- Fix: リアクションã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ä¸€è¦§ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åãŒã¯ã¿å‡ºã‚‹å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672) +- Fix: `/share`ページã«ãŠã„ã¦çµµæ–‡å—ピッカーを開ãã“ã¨ãŒã§ããªã„å•é¡Œã‚’ä¿®æ£ +- Fix: deck uiã®é€šçŸ¥éŸ³ãŒé‡ãªã‚‹å•é¡Œ (#14029) +- Fix: ダイレクト投稿ã®"削除ã—ã¦ç·¨é›†"ã«ãŠã„ã¦ã€å®›å…ˆãŒä¿æŒã•ã‚Œã¦ã„ãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: 投稿フォームã¸ã®URL貼り付ã‘ã«ã‚ˆã‚‹å¼•ç”¨ãŒä¸‹æ›¸ãã«ä¿å˜ã•ã‚Œã¦ã„ãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: "削除ã—ã¦ç·¨é›†"や下書ãã«ãŠã„ã¦ã€ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã®å—ã‘入れè¨å®šãŒä¿æŒ/ä¿å˜ã•ã‚Œã¦ã„ãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: 照会㫠`#` ã‹ã‚‰å§‹ã¾ã‚‹æ–‡å—列を入力ã—ã¦ãã®ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒšãƒ¼ã‚¸ã‚’表示ã™ã‚‹éš›ã€å…¥åŠ›ãŒ `#` ã®ã¿ã®å ´åˆã«ã€ŒæŒ‡å®šã•ã‚ŒãŸURLã«è©²å½“ã™ã‚‹ãƒšãƒ¼ã‚¸ã¯ã‚ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ã€ãŒè¡¨ç¤ºã•ã‚Œã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ +- Fix: 照会㫠`@` ã‹ã‚‰å§‹ã¾ã‚‹æ–‡å—列を入力ã—ã¦ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’照会ã™ã‚‹éš›ã€å…¥åŠ›ãŒ `@` ã®ã¿ã®å ´åˆã«ã€Œå•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€ãŒè¡¨ç¤ºã•ã‚Œã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ +- Fix: 投稿フォームã«ãƒŽãƒ¼ãƒˆã®URLを貼り付ã‘ã¦"引用ã¨ã—ã¦æ·»ä»˜"ã—ãŸå ´åˆã€æŠ•ç¨¿æ–‡ã‚’空ã«ã™ã‚‹ã“ã¨ã«ã‚ˆã‚‹Renote化ãŒå‡ºæ¥ãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: フォãƒãƒ¼ä¸ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«é–¢ã™ã‚‹"TLã«ä»–ã®äººã¸ã®è¿”ä¿¡ã‚’å«ã‚ã‚‹"ã®è¨å®šãŒåˆ†ã‹ã‚Šã¥ã‚‰ã„å•é¡Œã‚’ä¿®æ£ +- Fix: タイムラインページを開ã„ãŸæ™‚ã€`TLã«ä»–ã®äººã¸ã®è¿”ä¿¡ã‚’å«ã‚ã‚‹`ãŒã‚ªãƒ•ã®ã¨ãã«`ファイル付ãã®ã¿`をオンã«ã§ããªã„å•é¡Œã‚’ä¿®æ£ +- Fix: deck uiã§ã‚¿ã‚¤ãƒ ラインを切り替ãˆãŸéš›ã«TLã®è¨å®šé …ç›®ãŒæ›´æ–°ã•ã‚Œãšã€`TLã«ä»–ã®äººã¸ã®è¿”ä¿¡ã‚’å«ã‚ã‚‹`ã®ãƒˆã‚°ãƒ«ãŒè¡¨ç¤ºã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: ウィジェットã®ã‚¿ã‚¤ãƒ ラインé¸æŠžæ¬„ã«ç„¡åŠ¹åŒ–ã•ã‚ŒãŸã‚¿ã‚¤ãƒ ラインãŒè¡¨ç¤ºã•ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: サウンドã«ãƒ‰ãƒ©ã‚¤ãƒ–ã®éŸ³å£°ã‚’使用ã—ã¦ã„ã‚‹éš›ã«ãƒ‰ãƒ©ã‚¤ãƒ–ã®éŸ³å£°ãŒå†ç”Ÿã§ããªããªã‚‹ã¨è¨å®šãŒå¤‰æ›´ã§ããªããªã‚‹å•é¡Œã‚’ä¿®æ£ ### Server -- ãƒãƒ£ãƒ¼ãƒˆç”Ÿæˆæ™‚ã«instance.suspentionStateã«ç½®ãæ›ãˆã‚‰ã‚ŒãŸinstance.isSuspendedãŒå‚ç…§ã•ã‚Œã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ +- Feat: レートリミット制é™ã«å¼•ã£ã‹ã‹ã£ãŸã¨ãã«`Retry-After`ヘッダーを返ã™ã‚ˆã†ã« (#13949) +- Enhance: エンドãƒã‚¤ãƒ³ãƒˆ`clips/update`ã®å¿…é ˆé …ç›®ã‚’`clipId`ã®ã¿ã« +- Enhance: エンドãƒã‚¤ãƒ³ãƒˆ`admin/roles/update`ã®å¿…é ˆé …ç›®ã‚’`roleId`ã®ã¿ã« +- Enhance: エンドãƒã‚¤ãƒ³ãƒˆ`pages/update`ã®å¿…é ˆé …ç›®ã‚’`pageId`ã®ã¿ã« +- Enhance: エンドãƒã‚¤ãƒ³ãƒˆ`gallery/posts/update`ã®å¿…é ˆé …ç›®ã‚’`postId`ã®ã¿ã« +- Enhance: エンドãƒã‚¤ãƒ³ãƒˆ`i/webhook/update`ã®å¿…é ˆé …ç›®ã‚’`webhookId`ã®ã¿ã« +- Enhance: エンドãƒã‚¤ãƒ³ãƒˆ`admin/ad/update`ã®å¿…é ˆé …ç›®ã‚’`id`ã®ã¿ã« +- Enhance: `default.yml`内ã®`url`, `db.db`, `db.user`, `db.pass`を環境変数ã‹ã‚‰èªã¿è¾¼ã‚るよã†ã« +- Enhance: エンドãƒã‚¤ãƒ³ãƒˆ`api/meta`ã«ãƒ—ãƒãƒ‘ティ`noteSearchableScope`ãŒå¢—ãˆã€`string`値`local`ã¾ãŸã¯`global`ã‚’è¿”å´ã—ã¾ã™ +- Fix: ãƒãƒ£ãƒ¼ãƒˆç”Ÿæˆæ™‚ã«instance.suspensionStateã«ç½®ãæ›ãˆã‚‰ã‚ŒãŸinstance.isSuspendedãŒå‚ç…§ã•ã‚Œã¦ã—ã¾ã†å•é¡Œã‚’ä¿®æ£ +- Fix: ユーザーã®ãƒ•ã‚£ãƒ¼ãƒ‰ãƒšãƒ¼ã‚¸ã®MFMã‚’HTMLã«å±•é–‹ã™ã‚‹ã‚ˆã†ã« (#14006) +- Fix: アンテナ・クリップ・リスト・ウェブフックãŒãƒãƒ¼ãƒ«ãƒãƒªã‚·ãƒ¼ã®ä¸Šé™ã‚ˆã‚Šä¸€ã¤å¤šã作れã¦ã—ã¾ã†ã®ã‚’ä¿®æ£ (#14036) - Fix: notRespondingSinceãŒå®Ÿè£…ã•ã‚Œã‚‹å‰ã«ä¸é€šã«ãªã£ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ãŒè‡ªå‹•çš„ã«é…ä¿¡åœæ¢ã«ãªã‚‰ãªã„ (#14059) +- Fix: FTT有効時ã€ã‚¿ã‚¤ãƒ ライン用エンドãƒã‚¤ãƒ³ãƒˆã§`sinceId`ã«ã‚ャッシュ内最å¤ã®ã‚‚ã®ã‚ˆã‚Šå¤ã„ã‚‚ã®ã‚’指定ã—ãŸå ´åˆã«æ£ã—ãçµæžœãŒè¿”ã£ã¦ã“ãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: 自分以外ã®ã‚¯ãƒªãƒƒãƒ—内ã®ãƒŽãƒ¼ãƒˆå€‹æ•°ãŒè¦‹ãˆã‚‹ã“ã¨ãŒã‚ã‚‹ã®ã‚’ä¿®æ£ +- Fix: 空文å—列ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã¯ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ã•ã‚Œã‚‹ã‚ˆã†ã« +- Fix: リノートã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ããªã„よã†ã« +- Fix: ユーザーåã®å‰å¾Œã«ç©ºç™½æ–‡å—列ãŒã‚ã‚‹å ´åˆã¯çœç•¥ã™ã‚‹ã‚ˆã†ã« +- Fix: プãƒãƒ•ã‚£ãƒ¼ãƒ«ç·¨é›†æ™‚ã«åå‰ã‚’空白文å—列ã®ã¿ã«ã§ãã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: ユーザåã®ã‚µã‚¸ã‚§ã‚¹ãƒˆæ™‚ã«è¡¨ç¤ºã•ã‚Œã‚‹å†…容ã¨é †ç•ªã‚’調整(以下ã®é †ç•ªã«ãªã‚Šã¾ã™) #14149 + 1. フォãƒãƒ¼ä¸ã‹ã¤ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªãƒ¦ãƒ¼ã‚¶ + 2. フォãƒãƒ¼ä¸ã‹ã¤éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªãƒ¦ãƒ¼ã‚¶ + 3. フォãƒãƒ¼ã—ã¦ã„ãªã„アクティブãªãƒ¦ãƒ¼ã‚¶ + 4. フォãƒãƒ¼ã—ã¦ã„ãªã„éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªãƒ¦ãƒ¼ã‚¶ + + ã¾ãŸã€è‡ªåˆ†è‡ªèº«ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚‚サジェストã•ã‚Œã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã—ãŸã€‚ +- Fix: 一般ユーザーã‹ã‚‰è¦‹ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒãƒƒã‚¸ã®ä¸€è¦§ã«å…¬é–‹ã•ã‚Œã¦ã„ãªã„ã‚‚ã®ãŒå«ã¾ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652) +- Fix: ユーザーã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ä¸€è¦§ã§ãƒŸãƒ¥ãƒ¼ãƒˆ/ブãƒãƒƒã‚¯ãŒæ©Ÿèƒ½ã—ã¦ã„ãªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: FTT有効時ã«ãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒŽãƒ¼ãƒˆãŒHTLã«ã‚ャッシュã•ã‚Œã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: 一部ã®é€šçŸ¥ãŒãƒãƒ¼ã‚«ãƒ«ä¸Šã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã«å¯¾ã—ã¦è¡Œã‚ã‚Œã¦ã„ãŸå•é¡Œã‚’ä¿®æ£ +- Fix: エラーメッセージã®èª¤å—ã‚’ä¿®æ£ (#14213) +- Fix: ソーシャルタイムラインã«ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインã«è¡¨ç¤ºã•ã‚Œã‚‹è‡ªåˆ†ã¸ã®ãƒªãƒ—ライãŒè¡¨ç¤ºã•ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ +- Fix: リノートã®ãƒŸãƒ¥ãƒ¼ãƒˆãŒé©ç”¨ã•ã‚Œã‚‹ã¾ã§ã«æ™‚é–“ãŒã‹ã‹ã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ + (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1) +- Fix: Steaming APIãŒä¸æ£ãªãƒ‡ãƒ¼ã‚¿ã‚’å—ã‘ãŸå ´åˆã®å‹•ä½œãŒä¸å®‰å®šã§ã‚ã‚‹å•é¡Œ #14251 +- Fix: `users/search`ã«ãŠã„㦠`@` ã‹ã‚‰å§‹ã¾ã‚‹æ–‡å—列ãŒä¸Žãˆã‚‰ã‚ŒãŸéš›ã®å‡¦ç†ãŒæ£ã—ããªã‹ã£ãŸå•é¡Œã‚’ä¿®æ£ + - åå‰ã‚„自己紹介㫠`@` ã‹ã‚‰å§‹ã¾ã‚‹æ–‡è¨€ãŒå«ã¾ã‚Œã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚‚検索ã§ãるよã†ã«ãªã‚Šã¾ã™ +- Fix: 一部ã®Misskey以外ã®ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‹ã‚‰ãƒ•ã‚¡ã‚¤ãƒ«ã‚’å—ã‘å–ã‚Œãªã„å•é¡Œ + (Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76) + - NOTE: `drive_file`ã®`url`, `uri`, `src`ã®ä¸Šé™ãŒ512ã‹ã‚‰1024ã«å¤‰æ›´ã•ã‚Œã¾ã™ + Migrationã§ã¯ã‚«ãƒ©ãƒ 定義ã®å¤‰æ›´ã®ã¿ãŒè¡Œã‚ã‚Œã¾ã™ã€‚ + サーãƒãƒ¼ç®¡ç†è€…ã¯å„サーãƒãƒ¼ã®å¿…è¦ã«å¿œã˜`drive_file` `("uri")`ã«å¯¾ã™ã‚‹ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’張りãªãŠã™ã“ã¨ã§ã‚ˆã‚Šå®‰å®šã—DBã®æŽ¢ç´¢ãŒè¡Œã‚れるå¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚詳細 㯠[GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)ã§ç¢ºèªå¯èƒ½ã§ã™ +- Fix: 自分ã®ãƒ•ã‚©ãƒãƒ¯ãƒ¼é™å®šæŠ•ç¨¿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライãŒãƒ›ãƒ¼ãƒ タイムラインã§è¦‹ãˆãªã„ã“ã¨ãŒæœ‰ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザã«ã‚ˆã‚‹ãƒ•ã‚©ãƒãƒ¯ãƒ¼é™å®šæŠ•ç¨¿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライãŒã‚½ãƒ¼ã‚·ãƒ£ãƒ«ã‚¿ã‚¤ãƒ ラインã§è¡¨ç¤ºã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹å•é¡Œã‚’ä¿®æ£ +- Fix: ActivityPubã®ã‚¨ãƒ³ãƒ†ã‚£ãƒ†ã‚£ã‚¿ã‚¤ãƒ—判定ã§ä¸æ˜Žãªã‚¿ã‚¤ãƒ—ã‚’å—ã‘å–ã£ãŸå ´åˆã§ã‚‚処ç†ã‚’継続ã™ã‚‹ã‚ˆã†ã« + - ã‚ュー処ç†ã®ã¤ã¾ã‚ŠãŒæ”¹å–„ã•ã‚Œã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ + +### Misskey.js +- Feat: `/drive/files/create` ã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾å¿œï¼ˆ`multipart/form-data`ã«å¯¾å¿œï¼‰ +- Feat: `/admin/role/create` ã®ãƒãƒ¼ãƒ«ãƒãƒªã‚·ãƒ¼ã®åž‹ã‚’ä¿®æ£ ## 2024.5.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2acacd6dfa6c1f3768e278936e9d42dace6a5c83..a50b550b3a482cc42b8ccf916bf6cb706707ef1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,10 +11,12 @@ Before creating an issue, please check the following: - Issues should only be used to feature requests, suggestions, and bug tracking. - Please ask questions or troubleshooting in [Discord](https://discord.gg/6VgKmEqHNk). -> **Warning** +> [!WARNING] > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. -## Before implementation +### Recommended discussing before implementation +We welcome your proposal. + When you want to add a feature or fix a bug, *please open an issue*, don't just start writing code. We may suggest different approaches, or show that the "bug" is actually intended behaviour (and offer @@ -25,7 +27,20 @@ Misskey. Each of these examples have actually happened! On the other hand, it's very likely that we'll tell you "go ahead!". We try our best to incorporate improvements from our users! -Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. +Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask Committer to assign you). +By expressing your intention to work on the Issue, you can prevent conflicts in the work. + +To the Committers: you should not assign someone on it before the Final Decision. + +### How issues are triaged + +The Committers may: +* close an issue that is not reproducible on latest stable release, +* merge an issue into another issue, +* split an issue into multiple issues, +* or re-open that has been closed for some reason which is not applicable anymore. + +@syuilo reserves the Final Decision rights including whether the project will implement feature and how to implement, these rights are not always exercised. ## Well-known branches - **`stable`** branch is tracking the latest release and used for production purposes. @@ -35,14 +50,14 @@ Also, when you start implementation, assign yourself to the Issue (if you cannot ## Creating a PR Thank you for your PR! Before creating a PR, please check the following: - If possible, prefix the title with a keyword that identifies the type of this PR, as shown below. - - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc - - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. + - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc + - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. - If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. - Please add the summary of the changes to [`CHANGELOG.md`](CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring. - Check if there are any documents that need to be created or updated due to this change. - If you have added a feature or fixed a bug, please add a test case if possible. - Please make sure that tests and Lint are passed in advance. - - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) + - You can run it with `pnpm test` and `pnpm lint`. [See more info](#testing) - If this PR includes UI changes, please attach a screenshot in the text. Thanks for your cooperation 🤗 @@ -52,8 +67,8 @@ Be willing to comment on the good points and not just the things you want fixed ### Review perspective - Scope - - Are the goals of the PR clear? - - Is the granularity of the PR appropriate? + - Are the goals of the PR clear? + - Is the granularity of the PR appropriate? - Security - Does merging this PR create a vulnerability? - Performance @@ -68,7 +83,7 @@ Be willing to comment on the good points and not just the things you want fixed ## Release ### Release Instructions -1. Commit version changes in the `develop` branch ([package.json](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/package.json)) +1. Commit version changes in the `develop` branch ([package.json](package.json)) 2. Create a release PR. - Into `stable` from `develop` branch. - The title must be in the format `Release: x.y.z`. @@ -79,7 +94,7 @@ Be willing to comment on the good points and not just the things you want fixed - The target branch must be `stable` - The tag name must be the version -> **Note** +> [!NOTE] > Why this instruction is necessary: > - To perform final QA checks > - To distribute responsibility @@ -96,13 +111,52 @@ If your language is not listed in Crowdin, please open an issue.  +## Icon Font (Shark Font) +Sharkey has its own Icon Font called Shark Font which can be found at https://activitypub.software/TransFem-org/shark-font +Build Instructions can all be found over there in the `README`. + +If you have an Icon Suggestion or want to add an Icon please open an issue/merge request over at that repo. + +When Updating the Font make sure to copy **all generated files** from the `dest` folder into `packages/backend/assets/fonts/sharkey-icons` +For the CSS simply copy the file content and replace the old content in `style.css` and for the WOFF, TTF and SVG simply replace them. + ## Development -During development, it is useful to use the +### Setup +Before developing, you have to set up environment. Misskey requires Redis, PostgreSQL, and FFmpeg. + +You would want to install Meilisearch to experiment related features. Technically, meilisearch is not strict requirement, but some features and tests require it. + +There are a few ways to proceed. + +#### Use system-wide software +You could install them in system-wide (such as from package manager). + +#### Use `docker compose` +You could obtain middleware container by typing `docker compose -f $PROJECT_ROOT/compose.local-db.yml up -d`. + +#### Use Devcontainer +Devcontainer also has necessary setting. This method can be done by connecting from VSCode. +Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. +To use Dev Container, open the project directory on VSCode with Dev Containers installed. +**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled. + +It will run the following command automatically inside the container. +``` bash +git submodule update --init +pnpm install --frozen-lockfile +cp .devcontainer/devcontainer.yml .config/default.yml +pnpm build +pnpm migrate +``` + +After finishing the migration, you can proceed. + +### Start developing +During development, it is useful to use the ``` pnpm dev ``` - command. - Server-side source files and automatically builds them if they are modified. Automatically start the server process(es). @@ -126,26 +180,6 @@ MK_DEV_PREFER=backend pnpm dev - To change the port of Vite, specify with `VITE_PORT` environment variable. - HMR may not work in some environments such as Windows. -### Dev Container -Instead of running `pnpm` locally, you can use Dev Container to set up your development environment. -To use Dev Container, open the project directory on VSCode with Dev Containers installed. -**Note:** If you are using Windows, please clone the repository with WSL. Using Git for Windows will result in broken files due to the difference in how newlines are handled. - -It will run the following command automatically inside the container. -``` bash -git submodule update --init -pnpm install --frozen-lockfile -cp .devcontainer/devcontainer.yml .config/default.yml -pnpm build -pnpm migrate -``` - -After finishing the migration, run the `pnpm dev` command to start the development server. - -``` bash -pnpm dev -``` - ## Testing - Test codes are located in [`/packages/backend/test`](packages/backend/test). @@ -156,7 +190,7 @@ cp .github/misskey/test.yml .config/ ``` Prepare DB/Redis for testing. ``` -docker compose -f packages/backend/test/docker-compose.yml up +docker compose -f packages/backend/test/compose.yml up ``` Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. @@ -195,7 +229,7 @@ niraxã¯ã€Misskeyã§ä½¿ç”¨ã—ã¦ã„るオリジナルã®ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰ ### ルート定義 ルート定義ã¯ã€ä»¥ä¸‹ã®å½¢å¼ã®ã‚ªãƒ–ジェクトã®é…列ã§ã™ã€‚ -``` ts +```ts { name?: string; path: string; @@ -208,7 +242,7 @@ niraxã¯ã€Misskeyã§ä½¿ç”¨ã—ã¦ã„るオリジナルã®ãƒ•ãƒãƒ³ãƒˆã‚¨ãƒ³ãƒ‰ } ``` -> **Warning** +> [!WARNING] > ç¾çŠ¶ã€ãƒ«ãƒ¼ãƒˆã¯å®šç¾©ã•ã‚ŒãŸé †ã«è©•ä¾¡ã•ã‚Œã¾ã™ã€‚ > ãŸã¨ãˆã°ã€`/foo/:id`ルート定義ã®æ¬¡ã«`/foo/bar`ルート定義ãŒã•ã‚Œã¦ã„ãŸå ´åˆã€å¾Œè€…ãŒãƒžãƒƒãƒã™ã‚‹ã“ã¨ã¯ã‚ã‚Šã¾ã›ã‚“。 @@ -270,7 +304,7 @@ export const Default = { parameters: { layout: 'centered', }, -} satisfies StoryObj<typeof MkAvatar>; +} satisfies StoryObj<typeof MyComponent>; ``` If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file. @@ -381,7 +415,7 @@ describe('test', () => { }) .useMocker(... .compile(); - + fooService = app.get<FooService>(FooService); barService = app.get<BarService>(BarService) as jest.Mocked<BarService>; @@ -502,13 +536,13 @@ pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name> - 作æˆã•ã‚ŒãŸã‚¹ã‚¯ãƒªãƒ—トã¯ä¸å¿…è¦ãªå¤‰æ›´ã‚’å«ã‚€ãŸã‚除去ã—ã¦ãã ã•ã„ ### JSON Schemaã®objectã§anyOfを使ã†ã¨ã -JSON Schemaã§ã€objectã«å¯¾ã—ã¦anyOfを使ã†å ´åˆã€anyOfã®ä¸ã§propertiesを定義ã—ãªã„ã“ã¨ã€‚ -ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³ãŒåŠ¹ã‹ãªã„ãŸã‚。(SchemaTypeã‚‚ãã®ã‚ˆã†ã«ä½œã‚‰ã‚Œã¦ãŠã‚Šã€objectã®anyOf内ã®propertiesã¯æ¨ã¦ã‚‰ã‚Œã¾ã™ï¼‰ +JSON Schemaã§ã€objectã«å¯¾ã—ã¦anyOfを使ã†å ´åˆã€anyOfã®ä¸ã§propertiesを定義ã—ãªã„ã“ã¨ã€‚ +ãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³ãŒåŠ¹ã‹ãªã„ãŸã‚。(SchemaTypeã‚‚ãã®ã‚ˆã†ã«ä½œã‚‰ã‚Œã¦ãŠã‚Šã€objectã®anyOf内ã®propertiesã¯æ¨ã¦ã‚‰ã‚Œã¾ã™ï¼‰ https://github.com/misskey-dev/misskey/pull/10082 テã‚ストhogeãŠã‚ˆã³fugaã«ã¤ã„ã¦ã€ç‰‡æ–¹ã‚’å¿…é ˆã¨ã—ã¤ã¤ä¸¡æ–¹ã®æŒ‡å®šã‚‚ã‚ã‚Šã†ã‚‹å ´åˆ: -``` +```ts export const paramDef = { type: 'object', properties: { @@ -555,30 +589,50 @@ seems to do a decent job) *after that commit*, do all the extra work, on the same branch: -* copy all changes: - * from `NoteCreateService.create` to `NoteCreateService.import` (and - vice versa if `git` got confused!) - * from `NoteCreateService` to `NoteEditService` - * from `ApNoteService.createNote` to `ApNoteService.updateNote` - * from `endoints/notes/create.ts` to `endoints/notes/edit.ts` - * from `MkNote*` to `SkNote*` (if sensible) +* 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 + `packages/frontend/src/components/SkNote*.vue` (if sensible) * from the global timeline to the bubble timeline (`packages/backend/src/server/api/stream/channels/global-timeline.ts`, `packages/backend/src/server/api/stream/channels/bubble-timeline.ts`, + `packages/frontend/src/timelines.ts`, `packages/frontend/src/components/MkTimeline.vue`, `packages/frontend/src/pages/timeline.vue`, `packages/frontend/src/ui/deck/tl-column.vue`, `packages/frontend/src/widgets/WidgetTimeline.vue`) +* 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) * make sure there aren't any new `ti-*` classes (Tabler Icons), and - replace them with appropriate `ph-*` ones (Phosphor Icons). - `git grep '["'\'']ti[ -](?!fw)'` should show you what to change. + replace them with appropriate `ph-*` ones (Phosphor Icons): + `grep -rP '["'\'']ti[ -](?!fw)' -- built/` should show you what to change. NOTE: `ti-fw` is a special class that's defined by Misskey, leave it alone -* re-generate `misskey-js`: `pnpm build-misskey-js-with-types` -* run tests `pnpm test` and fix as much as you can - * right now `megalodon` doesn't pass its tests, you probably need to - run `pnpm --filter=backend test` (requires a test database, [see - above](#testing)) and `pnpm --filter=frontend test` + + after every change, re-build the frontend and check again, until + there are no more `ti-*` classes in the built files + + commit! +* double-check the new migration, that they won't conflict with our db + changes: `git diff develop -- packages/backend/migration/` +* `pnpm clean; pnpm build` +* run tests `pnpm --filter='!megalodon' test` (requires a test + database, [see above](#testing)) and fix as much as you can + * right now `megalodon` doesn't pass its tests, so we skip them * run lint `pnpm --filter=backend lint` + `pnpm --filter=frontend eslint` and fix as much as you can diff --git a/Dockerfile b/Dockerfile index b937c69cdbb0d94a23268a7d85c81e2a614b3ff5..288e97481c298e53a41cb32c1ff8227aa6d6bd5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=20.12.2-alpine3.19 +ARG NODE_VERSION=20.16.0-alpine3.20 FROM node:${NODE_VERSION} as build @@ -45,6 +45,10 @@ RUN apk add ffmpeg tini jemalloc \ USER sharkey WORKDIR /sharkey +# add package.json to add pnpm +COPY --chown=sharkey:sharkey ./package.json ./package.json +RUN corepack install + COPY --chown=sharkey:sharkey --from=build /sharkey/node_modules ./node_modules COPY --chown=sharkey:sharkey --from=build /sharkey/packages/backend/node_modules ./packages/backend/node_modules COPY --chown=sharkey:sharkey --from=build /sharkey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules @@ -61,7 +65,6 @@ 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 package.json ./package.json COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json COPY --chown=sharkey:sharkey packages/backend/scripts/check_connect.js ./packages/backend/scripts/check_connect.js diff --git a/README.md b/README.md index 68405032432a234e80ba68fb71f304e6aaf772b3..f9198c06c0d9cb950ba8c21ec222105f36abe519 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,6 @@ --- -<a href="https://fedidb.org/software/sharkey"> - <img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=sharkey&labelColor=363B40" alt="find an instance"/></a> - <a href="https://docs.joinsharkey.org/docs/install/fresh/"> <img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a> @@ -19,8 +16,8 @@ <a href="https://discord.gg/6VgKmEqHNk"> <img src="https://custom-icon-badges.herokuapp.com/badge/join_the-community-5865F2?logoColor=5865F2&style=for-the-badge&logo=discord&labelColor=363B40" alt="join the community"/></a> -<a href="https://ko-fi.com/transfem"> - <img src="https://custom-icon-badges.herokuapp.com/badge/donate-F96854?logoColor=F96854&style=for-the-badge&logo=kofi&labelColor=363B40" alt="donate"/></a> +<a href="https://opencollective.com/sharkey"> + <img src="https://custom-icon-badges.herokuapp.com/badge/donate-81ACF4?logoColor=81ACF4&style=for-the-badge&logo=opencollective&labelColor=363B40" alt="donate"/></a> --- diff --git a/chart/files/default.yml b/chart/files/default.yml index 2e1381ec574ac1c684823a6fb4d91608d3b3f79f..aab7ed6ce12edd997908bdaafb14d28b8fa30d08 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -208,8 +208,13 @@ id: "aidx" # Media Proxy #mediaProxy: https://example.com/proxy -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/docker-compose.local-db.yml b/compose.local-db.yml similarity index 98% rename from docker-compose.local-db.yml rename to compose.local-db.yml index 16ba4b49e1792a1b6b675bbd827222fd3d7aab4e..3835cb23dbada0e7d8722a56ef24437beb8a7a73 100644 --- a/docker-compose.local-db.yml +++ b/compose.local-db.yml @@ -1,5 +1,3 @@ -version: "3" - # ã“ã®configã¯ã€ dockerã§Misskey本体を起動ã›ãšã€ redisã¨postgresql ãªã©ã ã‘ã‚’èµ·å‹•ã—ã¾ã™ services: diff --git a/docker-compose_example.yml b/compose_example.yml similarity index 95% rename from docker-compose_example.yml rename to compose_example.yml index 647f6f0c77afff19722e3eea8089b33c2c20ce57..15df128eff4dee0aaac3a7e9dd272ce2dc1d2947 100644 --- a/docker-compose_example.yml +++ b/compose_example.yml @@ -1,5 +1,3 @@ -version: "3" - services: web: # image: registry.activitypub.software/transfem-org/sharkey:latest @@ -19,6 +17,8 @@ services: - "3000:3000" networks: - shonk + # env_file: + # - .config/docker.env volumes: - ./files:/sharkey/files - ./.config:/sharkey/.config:ro @@ -53,8 +53,7 @@ services: # restart: always # image: mcaptcha/mcaptcha:latest # networks: -# internal_network: -# external_network: +# shonks: # aliases: # - localhost # ports: @@ -73,7 +72,7 @@ services: # mcaptcha_redis: # image: mcaptcha/cache:latest # networks: -# - internal_network +# - shonks # healthcheck: # test: "redis-cli ping" # interval: 5s diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 955d672c1d05c6c6d9d6228ec190271ece98b323..b6bfbfa68281d05b84fc19ae1a3371a722b37eb8 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1262,8 +1262,6 @@ _sfx: note: "الملاØظات" noteMy: "ملاØظتي" notification: "الإشعارات" - antenna: "الهوائيات" - channel: "إشعارات القنات" _ago: future: "المستقبَل" justNow: "اللØظة" @@ -1566,6 +1564,10 @@ _webhookSettings: active: "Ù…ÙÙعّل" _events: reaction: "عند التÙاعل" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "البريد الإلكتروني " _moderationLogTypes: suspend: "علÙÙ‚" deleteDriveFile: "ØÙذ٠الملÙ" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index abcf07da831eefc86951134477116830a28574d9..6fb51ea5d8606930dbb07c4bce80c4b1e83b86df 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1033,8 +1033,6 @@ _sfx: note: "নোটগà§à¦²à¦¿" noteMy: "নোট (আপনার)" notification: "বিজà§à¦žà¦ªà§à¦¤à¦¿" - antenna: "অà§à¦¯à¦¾à¦¨à§à¦Ÿà§‡à¦¨à¦¾à¦—à§à¦²à¦¿" - channel: "চà§à¦¯à¦¾à¦¨à§‡à¦²à§‡à¦° বিজà§à¦žà¦ªà§à¦¤à¦¿" _ago: future: "à¦à¦¬à¦¿à¦·à§à¦¯à§Ž" justNow: "à¦à¦‡à¦®à¦¾à¦¤à§à¦°" @@ -1346,6 +1344,10 @@ _deck: _webhookSettings: name: "নাম" active: "চালà§" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "ইমেইল" _moderationLogTypes: suspend: "সà§à¦¥à¦—িত করা" resetPassword: "পাসওয়ারà§à¦¡ রিসেট করà§à¦¨" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index bda8579e27afcf234628631b72c1399fa8b4adfc..0b2acf8f05e75926e6cc9a7be93a8d9901856809 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -540,7 +540,7 @@ 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." @@ -1739,7 +1739,7 @@ _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." @@ -1893,8 +1893,6 @@ _sfx: note: "Notes" noteMy: "Nota (per mi)" notification: "Notificacions" - antenna: "Antenes" - channel: "Notificacions dels canals" reaction: "Quan se selecciona una reacció " _soundSettings: driveFile: "Fer servir un fitxer d'à udio del disc" @@ -2225,6 +2223,10 @@ _deck: _webhookSettings: name: "Nom" active: "Activat" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Correu electrònic" _moderationLogTypes: suspend: "Suspèn" resetPassword: "Restableix la contrasenya" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index bcab28db2c91bc40ca0c24b80d10deb35bbdef6e..0983e05ad9fd56c92bae9d771ecc2f56ab72477b 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1645,8 +1645,6 @@ _sfx: note: "Poznámky" noteMy: "Moje poznámka" notification: "OznámenÃ" - antenna: "Antény" - channel: "Oznámenà kanálu" _ago: future: "BudoucÃ" justNow: "TeÄ" @@ -2012,7 +2010,6 @@ _webhookSettings: createWebhook: "VytvoÅ™it Webhook" name: "Jméno" secret: "Tajné" - events: "Události Webhook" active: "Zapnuto" _events: follow: "PÅ™i sledovánà uživatele" @@ -2022,6 +2019,10 @@ _webhookSettings: renote: "PÅ™i renotaci poznámky" reaction: "PÅ™i obdrženà reakce" mention: "PÅ™i zmÃnce" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Email" _moderationLogTypes: suspend: "Zmrazit" resetPassword: "Resetovat heslo" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 47b97a8e82ea447110053e70ce0d091993e2fdbb..8c7d372c3f420672ff33e39c01ed7cb4e9bab14d 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1803,8 +1803,6 @@ _sfx: note: "Notizen" noteMy: "Meine Notizen" notification: "Benachrichtigungen" - antenna: "Antennen" - channel: "Kanalbenachrichtigung" _ago: future: "Zukunft" justNow: "Gerade eben" @@ -2196,7 +2194,6 @@ _webhookSettings: createWebhook: "Webhook erstellen" name: "Name" secret: "Secret" - events: "Webhook-Ereignisse" active: "Aktiviert" _events: follow: "Wenn du jemandem folgst" @@ -2206,6 +2203,10 @@ _webhookSettings: renote: "Wenn du ein Renote erhältst" reaction: "Wenn du eine Reaktion erhältst" mention: "Wenn du erwähnt wirst" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Email" _moderationLogTypes: createRole: "Rolle erstellt" deleteRole: "Rolle gelöscht" diff --git a/locales/el-GR.yml b/locales/el-GR.yml index 2098c7ef50eca6a82d0d5f176fc8bc02ae621fde..5eca348e186f883ff54dcaf9e69f8a5465696aa5 100644 --- a/locales/el-GR.yml +++ b/locales/el-GR.yml @@ -301,8 +301,6 @@ _theme: _sfx: note: "Σημειώματα" notification: "Ειδοποιήσεις" - antenna: "ΑντÎνες" - channel: "Ειδοποιήσεις καναλιών" _ago: future: "Μελλοντικό" justNow: "Μόλις Ï„ÏŽÏα" diff --git a/locales/en-US.yml b/locales/en-US.yml index fe7bc3c4514bfc88da2e6514d169fd5fff3e60d2..58712657e81e3f3ee36c5506bc9a6591cba98fdc 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -62,6 +62,7 @@ copyFileId: "Copy file ID" copyFolderId: "Copy folder ID" copyProfileUrl: "Copy profile URL" searchUser: "Search for a user" +searchThisUsersNotes: "Search this user’s notes" reply: "Reply" loadMore: "Load more" showMore: "Show more" @@ -129,8 +130,8 @@ add: "Add" reaction: "Reactions" reactions: "Reactions" emojiPicker: "Emoji picker" -pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting." -pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker" +pinnedEmojisForReactionSettingDescription: "Set the emojis to be pinned and displayed when reacting." +pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker." emojiPickerDisplay: "Emoji picker display" overwriteFromPinnedEmojisForReaction: "Override from reaction settings" overwriteFromPinnedEmojis: "Override from general settings" @@ -162,6 +163,7 @@ editList: "Edit list" selectChannel: "Select a channel" selectAntenna: "Select an antenna" editAntenna: "Edit antenna" +createAntenna: "Create an antenna" selectWidget: "Select a widget" editWidgets: "Edit widgets" editWidgetsExit: "Done" @@ -173,7 +175,7 @@ emojiUrl: "Emoji URL" addEmoji: "Add an emoji" settingGuide: "Recommended settings" cacheRemoteFiles: "Cache remote files" -cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." +cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote servers. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." youCanCleanRemoteFilesCache: "You can clear the cache by clicking the ðŸ—‘ï¸ button in the file management view." cacheRemoteSensitiveFiles: "Cache sensitive remote files" cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching." @@ -190,6 +192,10 @@ 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" +specifyServerHost: "Specify a server host directly" +inputHostName: "Enter the domain" general: "General" wallpaper: "Wallpaper" setWallpaper: "Set wallpaper" @@ -200,6 +206,7 @@ followConfirm: "Are you sure that you want to follow {name}?" proxyAccount: "Proxy account" proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead." host: "Host" +selectSelf: "Select myself" selectUser: "Select a user" recipient: "Recipient" annotation: "Comments" @@ -215,6 +222,7 @@ perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" silenceThisInstance: "Silence this instance" +mediaSilenceThisInstance: "Silence media from this instance" operations: "Operations" software: "Software" version: "Version" @@ -235,7 +243,9 @@ 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 hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked 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." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" @@ -321,6 +331,7 @@ 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" @@ -399,7 +410,7 @@ mcaptcha: "mCaptcha" enableMcaptcha: "Enable mCaptcha" mcaptchaSiteKey: "Site key" mcaptchaSecretKey: "Secret key" -mcaptchaInstanceUrl: "mCaptcha instance URL" +mcaptchaInstanceUrl: "mCaptcha server URL" recaptcha: "reCAPTCHA" enableRecaptcha: "Enable reCAPTCHA" recaptchaSiteKey: "Site key" @@ -490,6 +501,7 @@ 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." invitations: "Invites" invitationCode: "Invitation code" checking: "Checking..." @@ -901,7 +913,7 @@ 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" @@ -1153,6 +1165,8 @@ preservedUsernames: "Reserved usernames" preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected." createNoteFromTheFile: "Compose note from this file" archive: "Archive" +archived: "Archived" +unarchive: "Unarchive" channelArchiveConfirmTitle: "Really archive {name}?" channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore." thisChannelArchived: "This channel has been archived." @@ -1163,6 +1177,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" failedToPreviewUrl: "Could not preview" update: "Update" rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction" @@ -1302,10 +1319,15 @@ keepOriginalFilenameDescription: "If you turn off this setting, files names will noDescription: "No description" 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?" +createdLists: "Created lists" +createdAntennas: "Created antennas" _delivery: status: "Delivery status" - stop: "Suspended" - resume: "Delivery resume" + stop: "Suspend delivery" + resume: "Resume delivery" _type: none: "Publishing" manuallySuspended: "Manually suspended" @@ -1404,7 +1426,7 @@ _initialTutorial: _exampleNote: cw: "This will surely make you hungry!" note: "Just had a chocolate-glazed donut ðŸ©ðŸ˜‹" - useCases: "This is used when following the server guidelines for necessary notes or for self-restriction of spoiler or sensitive text." + useCases: "This is used when following the server guidelines, for necessary notes, or for self-restriction of spoiler or sensitive text." _howToMakeAttachmentsSensitive: title: "How to Mark Attachments as Sensitive?" description: "For attachments that are required by server guidelines or that should not be left intact, add a \"sensitive\" flag." @@ -1756,6 +1778,7 @@ _role: canManageAvatarDecorations: "Manage avatar decorations" driveCapacity: "Drive capacity" alwaysMarkNsfw: "Always mark files as NSFW" + canUpdateBioMedia: "Allow users to edit their avatar or banner" pinMax: "Maximum number of pinned notes" antennaMax: "Maximum number of antennas" wordMuteMax: "Maximum number of characters allowed in word mutes" @@ -2005,8 +2028,6 @@ _sfx: note: "New note" noteMy: "Own note" notification: "Notifications" - antenna: "Antennas" - channel: "Channel notifications" reaction: "On choosing a reaction" _soundSettings: driveFile: "Use an audio file in Drive." @@ -2015,6 +2036,7 @@ _soundSettings: 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." _ago: future: "Future" justNow: "Just now" @@ -2132,7 +2154,7 @@ _permissions: "read:admin:invite-codes": "View invite codes" "write:admin:announcements": "Manage announcements" "read:admin:announcements": "View announcements" - "write:admin:avatar-decorations": "Manage avatar decorations" + "write:admin:avatar-decorations": "Can manage avatar decorations" "read:admin:avatar-decorations": "View avatar decorations" "write:admin:federation": "Manage federation data" "write:admin:account": "Manage user account" @@ -2320,6 +2342,7 @@ _timelines: local: "Local" social: "Social" global: "Global" + bubble: "Bubble" _play: new: "Create Play" edit: "Edit Play" @@ -2370,6 +2393,7 @@ _pages: eyeCatchingImageSet: "Set thumbnail" eyeCatchingImageRemove: "Delete thumbnail" chooseBlock: "Add a block" + enterSectionTitle: "Enter a section title" selectType: "Select a type" contentBlocks: "Content" inputBlocks: "Input" @@ -2431,6 +2455,7 @@ _notification: roleAssigned: "Role given" achievementEarned: "Achievement unlocked" app: "Notifications from linked apps" + edited: "Edits" _actions: followBack: "followed you back" reply: "Reply" @@ -2439,7 +2464,7 @@ _deck: alwaysShowMainColumn: "Always show main column" columnAlign: "Align columns" addColumn: "Add column" - newNoteNotificationSettings: "New note notification" + newNoteNotificationSettings: "Notification setting for new notes" configureColumn: "Column settings" swapLeft: "Swap with the left column" swapRight: "Swap with the right column" @@ -2478,9 +2503,10 @@ _drivecleaner: orderByCreatedAtAsc: "Ascending Dates" _webhookSettings: createWebhook: "Create Webhook" + modifyWebhook: "Modify Webhook" name: "Name" secret: "Secret" - events: "Webhook Events" + trigger: "Trigger" active: "Enabled" _events: follow: "When following a user" @@ -2490,6 +2516,26 @@ _webhookSettings: renote: "When boosted" reaction: "When receiving a reaction" mention: "When being mentioned" + _systemEvents: + abuseReport: "When received a new abuse report" + abuseReportResolved: "When resolved abuse reports" + userCreated: "When user is created" + deleteConfirm: "Are you sure you want to delete the Webhook?" +_abuseReport: + _notificationRecipient: + createRecipient: "Add a recipient for abuse reports" + modifyRecipient: "Edit a recipient for abuse 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." + keywords: "Keywords" + notifiedUser: "Users to notify" + notifiedWebhook: "Webhook to use" + deleteConfirm: "Are you sure that you want to delete the notification recipient?" _moderationLogTypes: createRole: "Role created" deleteRole: "Role deleted" @@ -2528,6 +2574,17 @@ _moderationLogTypes: 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." @@ -2608,6 +2665,7 @@ _mfm: backgroundDescription: "Change the background color of text." plain: "Plain" plainDescription: "Deactivates the effects of all MFM contained within this MFM effect." + _fileViewer: title: "File details" type: "File type" @@ -2677,7 +2735,7 @@ _dataSaver: 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." @@ -2753,3 +2811,8 @@ _mediaControls: pip: "Picture in Picture" playbackRate: "Playback Speed" loop: "Loop playback" +_contextMenu: + title: "Context menu" + app: "Application" + appWithShift: "Application with shift key" + native: "Native" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 209c2dec2dff3b4e3cb3c1ef949a45da3c6919d5..f5d1d11036916e75bd0f1b946755e74d3925034c 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -60,6 +60,7 @@ copyFileId: "Copiar ID del archivo" copyFolderId: "Copiar ID de carpeta" copyProfileUrl: "Copiar la URL del perfil" searchUser: "Buscar un usuario" +searchThisUsersNotes: "" reply: "Responder" loadMore: "Ver más" showMore: "Ver más" @@ -302,7 +303,7 @@ location: "Lugar" theme: "Tema" themeForLightMode: "Tema para usar en Modo Linterna" themeForDarkMode: "Tema para usar en Modo Oscuro" -light: "Linterna" +light: "Claro" dark: "Oscuro" lightThemes: "Tema claro" darkThemes: "Tema oscuro" @@ -867,7 +868,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" @@ -1920,8 +1921,6 @@ _sfx: note: "Notas" noteMy: "Nota (a mà mismo)" notification: "Notificaciones" - antenna: "Antena receptora" - channel: "Notificaciones del canal" reaction: "Al seleccionar una reacción" _soundSettings: driveFile: "Usar un archivo de audio en Drive" @@ -2356,7 +2355,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" @@ -2385,7 +2384,6 @@ _webhookSettings: createWebhook: "Crear Webhook" name: "Nombre" secret: "Secreto" - events: "Eventos de webhook" active: "Activado" _events: follow: "Cuando se sigue a alguien" @@ -2395,6 +2393,10 @@ _webhookSettings: renote: "Cuando reciba un \"re-note\"" reaction: "Cuando se recibe una reacción" mention: "Cuando hay una mención" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Correo" _moderationLogTypes: createRole: "Rol creado" deleteRole: "Rol eliminado" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 22a9e2449f7b038929c443a35c93b902e604455a..f615349b89b259f7a6ed43bb05d069b134a452a3 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1094,6 +1094,8 @@ preservedUsernames: "Noms d'utilisateur·rice réservés" preservedUsernamesDescription: "Énumérez les noms d'utilisateur à réserver, séparés par des nouvelles lignes. Les noms d'utilisateur spécifiés ici ne seront plus utilisables lors de la création d'un compte, sauf la création manuelle par un administrateur. De plus, les comptes existants ne seront pas affectés." createNoteFromTheFile: "Rédiger une note de ce fichier" archive: "Archive" +archived: "Archivé" +unarchive: "Annuler l'archivage" channelArchiveConfirmTitle: "Voulez-vous vraiment archiver {name} ?" channelArchiveConfirmDescription: "Une fois archivé, le canal n'apparaîtra plus dans la liste des canaux ni dans les résultats de recherche, et la publication des nouvelles notes sera impossible." thisChannelArchived: "Ce canal a été archivé." @@ -1224,7 +1226,10 @@ enableHorizontalSwipe: "Glisser pour changer d'onglet" loading: "Chargement en cours" surrender: "Annuler" gameRetry: "Réessayer" +launchApp: "Lancer l'app" +inquiry: "Contact" _delivery: + status: "Statut de la diffusion" stop: "Suspendu·e" _type: none: "Publié" @@ -1245,7 +1250,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" @@ -1712,8 +1717,6 @@ _sfx: note: "Nouvelle note" noteMy: "Ma note" notification: "Notifications" - antenna: "Réception de l’antenne" - channel: "Notifications de canal" reaction: "Lors de la sélection de la réaction" _soundSettings: driveFile: "Utiliser un effet sonore sur le Disque" @@ -2073,6 +2076,10 @@ _drivecleaner: _webhookSettings: name: "Nom" active: "Activé" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "E-mail " _moderationLogTypes: createRole: "Rôle créé" deleteRole: "Rôle supprimé" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 01fa02e678c0999a04042d3bbe8131e52a2cde1d..2f225c1199583e556f4931cbca1cc35b337ebaae 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -180,6 +180,10 @@ addAccount: "Tambahkan akun" reloadAccountsList: "Muat ulang daftar akun" loginFailed: "Gagal untuk masuk" showOnRemote: "Lihat profil asli" +continueOnRemote: "Lihat di peladen asal" +chooseServerOnMisskeyHub: "Pilih peladen dari Misskey Hub" +specifyServerHost: "Tentukan domain peladen" +inputHostName: "Masukkan nama domain" general: "Umum" wallpaper: "Wallpaper" setWallpaper: "Atur wallpaper" @@ -316,6 +320,7 @@ selectFile: "Pilih berkas" selectFiles: "Pilih berkas" selectFolder: "Pilih folder" selectFolders: "Pilih folder" +fileNotSelected: "Tidak ada file yang dipilih" renameFile: "Ubah nama berkas" folderName: "Nama folder" createFolder: "Buat folder" @@ -579,7 +584,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" @@ -1239,6 +1244,7 @@ keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas noDescription: "Tidak ada deskripsi" alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti" inquiry: "Hubungi kami" +tryAgain: "Silahkan coba lagi." _delivery: status: "Status pengiriman" stop: "Ditangguhkan" @@ -1739,7 +1745,7 @@ _emailUnavailable: smtp: "Peladen alamat surel ini tidak merespon" banned: "Kamu tidak dapat mendaftar dengan alamat surel ini" _ffVisibility: - public: "Terbitkan" + public: "Publik" followers: "Tampil untuk pengikut saja" private: "Tersembunyi" _signup: @@ -1932,8 +1938,6 @@ _sfx: note: "Catatan" noteMy: "Catatan (Saya)" notification: "Notifikasi" - antenna: "Penerimaan Antenna" - channel: "Notifikasi Kanal" reaction: "Ketika memilih reaksi" _soundSettings: driveFile: "Menggunakan berkas audio dalam Drive" @@ -2396,9 +2400,9 @@ _drivecleaner: orderByCreatedAtAsc: "Tanggal (Naik)" _webhookSettings: createWebhook: "Buat Webhook" + modifyWebhook: "Sunting Webhook" name: "Nama" secret: "Secret" - events: "Webhook Events" active: "Aktif" _events: follow: "Ketika mengikuti pengguna" @@ -2408,6 +2412,11 @@ _webhookSettings: renote: "Ketika direnote" reaction: "Ketika menerima reaksi" mention: "Ketika sedang disebut" + deleteConfirm: "Apakah kamu yakin ingin menghapus Webhook?" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Surel" _moderationLogTypes: createRole: "Peran telah dibuat" deleteRole: "Peran telah dihapus" diff --git a/locales/index.d.ts b/locales/index.d.ts index 73141517086559d3253b5ba3c1ae745a68d4255e..55a95f8fd991ae0cea5afdfac9707ae49645b707 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -264,6 +264,10 @@ export interface Locale extends ILocale { * ユーザーを検索 */ "searchUser": string; + /** + * ユーザーã®ãƒŽãƒ¼ãƒˆã‚’検索 + */ + "searchThisUsersNotes": string; /** * 返信 */ @@ -664,6 +668,10 @@ export interface Locale extends ILocale { * アンテナを編集 */ "editAntenna": string; + /** + * ã‚¢ãƒ³ãƒ†ãƒŠã‚’ä½œæˆ + */ + "createAntenna": string; /** * ウィジェットをé¸æŠž */ @@ -776,6 +784,22 @@ export interface Locale extends ILocale { * リモートã§è¡¨ç¤º */ "showOnRemote": string; + /** + * リモートã§ç¶šè¡Œ + */ + "continueOnRemote": string; + /** + * Misskey Hubã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã‚’é¸æŠž + */ + "chooseServerOnMisskeyHub": string; + /** + * サーãƒãƒ¼ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‚’直接指定 + */ + "specifyServerHost": string; + /** + * ドメインを入力ã—ã¦ãã ã•ã„ + */ + "inputHostName": string; /** * 全般 */ @@ -816,6 +840,10 @@ export interface Locale extends ILocale { * ホスト */ "host": string; + /** + * 自分をé¸æŠž + */ + "selectSelf": string; /** * ユーザーをé¸æŠž */ @@ -869,13 +897,17 @@ export interface Locale extends ILocale { */ "stopActivityDelivery": string; /** - * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’ブãƒãƒƒã‚¯ + * ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’ブãƒãƒƒã‚¯ */ "blockThisInstance": string; /** - * インスタンスをサイレンス + * サーãƒãƒ¼ã‚’サイレンス */ "silenceThisInstance": string; + /** + * サーãƒãƒ¼ã‚’メディアサイレンス + */ + "mediaSilenceThisInstance": string; /** * æ“作 */ @@ -960,6 +992,14 @@ export interface Locale extends ILocale { * サイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚サイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã™ã¹ã¦ã€Œã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã€ã¨ã—ã¦æ‰±ã‚ã‚Œã€ãƒ•ã‚©ãƒãƒ¼ãŒã™ã¹ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。 */ "silencedInstancesDescription": string; + /** + * メディアサイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼ + */ + "mediaSilencedInstances": string; + /** + * メディアサイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚メディアサイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã™ã¹ã¦ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã¨ã—ã¦æ‰±ã‚ã‚Œã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—ãŒä½¿ç”¨ã§ããªã„よã†ã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。 + */ + "mediaSilencedInstancesDescription": string; /** * ミュートã¨ãƒ–ãƒãƒƒã‚¯ */ @@ -1300,6 +1340,10 @@ export interface Locale extends ILocale { * ドライブ */ "drive": string; + /** + * 検索ドライブ + */ + "driveSearchbarPlaceholder": string; /** * ファイルå */ @@ -1973,9 +2017,13 @@ export interface Locale extends ILocale { */ "onlyOneFileCanBeAttached": string; /** - * 続行ã™ã‚‹å‰ã«ã€ã‚µã‚¤ãƒ³ã‚¢ãƒƒãƒ—ã¾ãŸã¯ã‚µã‚¤ãƒ³ã‚¤ãƒ³ãŒå¿…è¦ã§ã™ + * 続行ã™ã‚‹å‰ã«ã€ç™»éŒ²ã¾ãŸã¯ãƒã‚°ã‚¤ãƒ³ãŒå¿…è¦ã§ã™ */ "signinRequired": string; + /** + * 続行ã™ã‚‹ã«ã¯ã€ãŠä½¿ã„ã®ã‚µãƒ¼ãƒãƒ¼ã«ç§»å‹•ã™ã‚‹ã‹ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«ç™»éŒ²ãƒ»ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ + */ + "signinOrContinueOnRemote": string; /** * 招待 */ @@ -2140,6 +2188,10 @@ export interface Locale extends ILocale { * ç”»åƒã‚’æ–°ã—ã„タブã§é–‹ã */ "openImageInNewTab": string; + /** + * 代替テã‚ストを入れ忘れãŸã¨ãã«è¦å‘Šã™ã‚‹ + */ + "warnForMissingAltText": string; /** * ダッシュボード */ @@ -2269,11 +2321,11 @@ export interface Locale extends ILocale { */ "s3ForcePathStyleDesc": string; /** - * DeepLX-JS を使用ã™ã‚‹ (èªè¨¼ã‚ーãªã—) + * DeepLX-JS を使用ã™ã‚‹ (èªè¨¼ã‚ーä¸è¦) */ "deeplFreeMode": string; /** - * ヘルプãŒå¿…è¦ã§ã™ã‹? DeepLX-JSã®ã‚»ãƒƒãƒˆã‚¢ãƒƒãƒ—方法ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ãã ã•ã„。 + * DeepLX-JSã®è¨å®šæ–¹æ³•ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ãã ã•ã„。 */ "deeplFreeModeDescription": string; /** @@ -2337,7 +2389,7 @@ export interface Locale extends ILocale { */ "notUseSound": string; /** - * MisskeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹ + * SharkeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹ */ "useSoundOnlyWhenActive": string; /** @@ -2797,19 +2849,19 @@ export interface Locale extends ILocale { */ "enableFaviconNotificationDot": string; /** - * 通知ドットãŒã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§æ©Ÿèƒ½ã™ã‚‹ã‹ã©ã†ã‹ã‚’確èªã—ã¾ã™ã€‚ + * タブアイコン強調機能ã®å‹•ä½œç¢ºèª */ "verifyNotificationDotWorkingButton": string; /** - * 残念ãªãŒã‚‰ã€ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯ç¾æ™‚点ã§ã¯é€šçŸ¥ãƒ‰ãƒƒãƒˆæ©Ÿèƒ½ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 + * ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ç¾æ™‚点ã§ã¯ã‚¿ãƒ–アイコン強調機能をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。 */ "notificationDotNotWorking": string; /** - * 通知ドットã¯ã€ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§æ£ã—ã機能ã—ã¦ã„ã¾ã™ã€‚ + * タブアイコン強調機能ã¯ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§æ£ã—ã機能ã—ã¦ã„ã¾ã™ã€‚ */ "notificationDotWorking": string; /** - * 通知ドットãŒæ©Ÿèƒ½ã—ãªã„å ´åˆã¯ã€ç®¡ç†è€…ã«ãƒ‰ã‚ュメントを確èªã™ã‚‹ã‚ˆã†ã«ä¾é ¼ã—ã¦ãã ã•ã„ {link} + * タブアイコン強調機能ãŒæ©Ÿèƒ½ã—ãªã„å ´åˆã¯ã€ç®¡ç†è€…ã«ãƒ‰ã‚ュメントを確èªã™ã‚‹ã‚ˆã†ã«ä¾é ¼ã—ã¦ãã ã•ã„ {link} */ "notificationDotNotWorkingAdvice": ParameterizedString<"link">; /** @@ -2869,7 +2921,7 @@ export interface Locale extends ILocale { */ "reportAbuseOf": ParameterizedString<"name">; /** - * é€šå ±ç†ç”±ã®è©³ç´°ã‚’記入ã—ã¦ãã ã•ã„。対象ã®ãƒŽãƒ¼ãƒˆãŒã‚ã‚‹å ´åˆã¯ãã®URLも記入ã—ã¦ãã ã•ã„。 + * é€šå ±ç†ç”±ã®è©³ç´°ã‚’記入ã—ã¦ãã ã•ã„。対象ã®ãƒŽãƒ¼ãƒˆã‚„ページãªã©ãŒã‚ã‚‹å ´åˆã¯ãã®URLも記入ã—ã¦ãã ã•ã„。 */ "fillAbuseReportDescription": string; /** @@ -3073,15 +3125,15 @@ export interface Locale extends ILocale { */ "searchEngine": string; /** - * ä»– + * カスタム*/ "searchEngineOther": string; /** - * カスタムURI ã¯ã€"https://www.google.com/search?q=\{query}" ã‚„ "https://www.google.com/search?q=%s" ã®ã‚ˆã†ãªå½¢å¼ã§å…¥åŠ›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ + * カスタム検索エンジンã®URIã¯ã€"https://www.google.com/search?q=\{query}" ã‚„ "https://www.google.com/search?q=%s" ã®ã‚ˆã†ãªå½¢å¼ã§å…¥åŠ›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ */ "searchEngineCustomURIDescription": string; /** - * カスタムURI + * カスタム検索エンジン URI */ "searchEngineCusomURI": string; /** @@ -3677,7 +3729,7 @@ export interface Locale extends ILocale { */ "emailRequiredForSignup": string; /** - * æ–°è¦ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æ‰¿èªãŒå¿…è¦ + * アカウント登録を承èªåˆ¶ã«ã™ã‚‹ */ "approvalRequiredForSignup": string; /** @@ -3886,7 +3938,7 @@ export interface Locale extends ILocale { */ "thereIsUnresolvedAbuseReportWarning": string; /** - * 承èªå¾…ã¡ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„る。 + * 承èªå¾…ã¡ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„ã¾ã™ã€‚ */ "pendingUserApprovals": string; /** @@ -3958,7 +4010,7 @@ export interface Locale extends ILocale { */ "numberOfReplies": string; /** - * ã“ã®æ•°å€¤ã‚’大ããã™ã‚‹ã¨ã€ã‚ˆã‚Šå¤šãã®è¿”ä¿¡ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®å€¤ã‚’大ããã—ã™ãŽã‚‹ã¨ã€è¿”ä¿¡ãŒçª®å±ˆã«ãªã‚Šã€èªã‚ãªããªã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚ + * ã“ã®æ•°å€¤ã‚’大ããã™ã‚‹ã¨ã€ã‚ˆã‚Šå¤šãã®è¿”ä¿¡ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®å€¤ã‚’大ããã—ã™ãŽã‚‹ã¨ã€UIãŒçª®å±ˆã«ãªã£ã¦èªã¿ã«ãããªã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚ */ "numberOfRepliesDescription": string; /** @@ -3966,15 +4018,15 @@ export interface Locale extends ILocale { */ "boostSettings": string; /** - * å¯è¦–性セレクタを表示 + * 公開範囲セレクターを表示 */ "showVisibilitySelectorOnBoost": string; /** - * 無効ã®å ´åˆã€ä»¥ä¸‹ã§å®šç¾©ã•ã‚Œã‚‹ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å¯è¦–性ãŒä½¿ç”¨ã•ã‚Œã€ã‚»ãƒ¬ã‚¯ã‚¿ã¯è¡¨ç¤ºã•ã‚Œã¾ã›ã‚“。 + * 無効ã®å ´åˆã€ä»¥ä¸‹ã§è¨å®šã—ãŸãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å…¬é–‹ç¯„囲ãŒä½¿ç”¨ã•ã‚Œã€ã‚»ãƒ¬ã‚¯ã‚¿ãƒ¼ã¯è¡¨ç¤ºã•ã‚Œã¾ã›ã‚“。 */ "showVisibilitySelectorOnBoostDescription": string; /** - * デフォルトã®ãƒ–ーストå¯è¦–性ã®è¨å®š + * デフォルトã®ãƒ–ースト公開範囲 */ "visibilityOnBoost": string; /** @@ -4302,7 +4354,7 @@ export interface Locale extends ILocale { */ "thisPostIsMissingAltTextIgnore": string; /** - * ã“ã®æŠ•ç¨¿ã«æ·»ä»˜ã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«ã® 1 ã¤ã«ä»£æ›¿ãƒ†ã‚ストãŒã‚ã‚Šã¾ã›ã‚“。ã™ã¹ã¦ã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã«ä»£æ›¿ãƒ†ã‚ストãŒå«ã¾ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。 + * 代替テã‚ストãŒãªã„ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ã¾ã™ã€‚ã™ã¹ã¦ã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã«ä»£æ›¿ãƒ†ã‚ストをå«ã‚€ã‚ˆã†ã«ã—ã¦ãã ã•ã„。 */ "thisPostIsMissingAltText": string; /** @@ -4314,7 +4366,7 @@ export interface Locale extends ILocale { */ "collapseRenotesDescription": string; /** - * 返信ã•ã‚ŒãŸãƒŽãƒ¼ãƒˆçœç•¥ + * 返信元ã®ãƒŽãƒ¼ãƒˆã‚’折りãŸãŸã‚€ */ "collapseNotesRepliedTo": string; /** @@ -4322,7 +4374,7 @@ export interface Locale extends ILocale { */ "collapseFiles": string; /** - * 返信ã«ä¼šè©±ã‚’èªã¿è¾¼ã‚€ + * 会話スレッドを自動ã§èªã¿è¾¼ã‚€ */ "autoloadConversation": string; /** @@ -4366,7 +4418,7 @@ export interface Locale extends ILocale { */ "invitationRequiredToRegister": string; /** - * ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯ã€ç™»éŒ²ç†ç”±ã‚’指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ã‚’å—ã‘入れã¦ã„ã¾ã™ã€‚ + * ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‰¿èªåˆ¶ã§ã™ã€‚å‚åŠ ã—ãŸã„ç†ç”±ã‚’記入ã—ã€æ‰¿èªã•ã‚ŒãŸæ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚ */ "approvalRequiredToRegister": string; /** @@ -4538,7 +4590,7 @@ export interface Locale extends ILocale { */ "forceShowAds": string; /** - * 猫å‹é” :3 + * ã«ã‚ƒã‚“ã“フレンド :3 */ "oneko": string; /** @@ -4625,6 +4677,14 @@ export interface Locale extends ILocale { * アーカイブ */ "archive": string; + /** + * アーカイブ済㿠+ */ + "archived": string; + /** + * アーカイブ解除 + */ + "unarchive": string; /** * {name}をアーカイブã—ã¾ã™ã‹ï¼Ÿ */ @@ -4665,6 +4725,18 @@ export interface Locale extends ILocale { * ユーザー指定 */ "specifyUser": string; + /** + * 照会ã—ã¾ã™ã‹ï¼Ÿ + */ + "lookupConfirm": string; + /** + * ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒšãƒ¼ã‚¸ã‚’é–‹ãã¾ã™ã‹ï¼Ÿ + */ + "openTagPageConfirm": string; + /** + * ホスト指定 + */ + "specifyHost": string; /** * プレビューã§ãã¾ã›ã‚“ */ @@ -4970,7 +5042,7 @@ export interface Locale extends ILocale { */ "repositoryUrl": string; /** - * ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Misskeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://github.com/misskey-dev/misskey ã¨è¨˜å…¥ã—ã¾ã™ã€‚ + * ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Sharkeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://activitypub.software/TransFem-org/Sharkey/ ã¨è¨˜å…¥ã—ã¾ã™ã€‚ */ "repositoryUrlDescription": string; /** @@ -5221,6 +5293,26 @@ export interface Locale extends ILocale { * ãŠå•ã„åˆã‚ã› */ "inquiry": string; + /** + * ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。 + */ + "tryAgain": string; + /** + * センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã‚’表示ã™ã‚‹ã¨ã確èªã™ã‚‹ + */ + "confirmWhenRevealingSensitiveMedia": string; + /** + * センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã§ã™ã€‚表示ã—ã¾ã™ã‹ï¼Ÿ + */ + "sensitiveMediaRevealConfirm": string; + /** + * 作æˆã—ãŸãƒªã‚¹ãƒˆ + */ + "createdLists": string; + /** + * 作æˆã—ãŸã‚¢ãƒ³ãƒ†ãƒŠ + */ + "createdAntennas": string; "_delivery": { /** * é…信状態 @@ -6839,6 +6931,10 @@ export interface Locale extends ILocale { * ファイルã«NSFWを常ã«ä»˜ä¸Ž */ "alwaysMarkNsfw": string; + /** + * アイコンã¨ãƒãƒŠãƒ¼ã®æ›´æ–°ã‚’è¨±å¯ + */ + "canUpdateBioMedia": string; /** * ノートã®ãƒ”ン留ã‚ã®æœ€å¤§æ•° */ @@ -7784,14 +7880,6 @@ export interface Locale extends ILocale { * 通知 */ "notification": string; - /** - * アンテナå—ä¿¡ - */ - "antenna": string; - /** - * ãƒãƒ£ãƒ³ãƒãƒ«é€šçŸ¥ - */ - "channel": string; /** * リアクションé¸æŠžæ™‚ */ @@ -7819,9 +7907,13 @@ export interface Locale extends ILocale { */ "driveFileDurationWarn": string; /** - * é•·ã„音声を使用ã™ã‚‹ã¨Misskeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ + * é•·ã„音声を使用ã™ã‚‹ã¨Sharkeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ */ "driveFileDurationWarnDescription": string; + /** + * 音声ãŒèªã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚è¨å®šã‚’変更ã—ã¦ãã ã•ã„ + */ + "driveFileError": string; }; "_ago": { /** @@ -9002,6 +9094,10 @@ export interface Locale extends ILocale { * ã‚°ãƒãƒ¼ãƒãƒ« */ "global": string; + /** + * ãƒãƒƒãƒƒãƒ–ル + */ + "bubble": string; }; "_play": { /** @@ -9198,6 +9294,10 @@ export interface Locale extends ILocale { * ブãƒãƒƒã‚¯ã‚’è¿½åŠ */ "chooseBlock": string; + /** + * セクションタイトルを入力 + */ + "enterSectionTitle": string; /** * 種類をé¸æŠž */ @@ -9431,6 +9531,10 @@ export interface Locale extends ILocale { * 連æºã‚¢ãƒ—リã‹ã‚‰ã®é€šçŸ¥ */ "app": string; + /** + * 編集済㿠+ */ + "edited": string; }; "_actions": { /** @@ -9442,7 +9546,7 @@ export interface Locale extends ILocale { */ "reply": string; /** - * Boost + * ブースト */ "renote": string; }; @@ -9606,6 +9710,10 @@ export interface Locale extends ILocale { * Webhookã‚’ä½œæˆ */ "createWebhook": string; + /** + * Webhookを編集 + */ + "modifyWebhook": string; /** * åå‰ */ @@ -9615,9 +9723,9 @@ export interface Locale extends ILocale { */ "secret": string; /** - * Webhookを実行ã™ã‚‹ã‚¿ã‚¤ãƒŸãƒ³ã‚° + * トリガー */ - "events": string; + "trigger": string; /** * 有効 */ @@ -9652,6 +9760,76 @@ export interface Locale extends ILocale { */ "mention": string; }; + "_systemEvents": { + /** + * ユーザーã‹ã‚‰é€šå ±ãŒã‚ã£ãŸã¨ã + */ + "abuseReport": string; + /** + * ユーザーã‹ã‚‰ã®é€šå ±ã‚’処ç†ã—ãŸã¨ã + */ + "abuseReportResolved": string; + /** + * ユーザーãŒä½œæˆã•ã‚ŒãŸã¨ã + */ + "userCreated": string; + }; + /** + * Webhookを削除ã—ã¾ã™ã‹ï¼Ÿ + */ + "deleteConfirm": string; + }; + "_abuseReport": { + "_notificationRecipient": { + /** + * é€šå ±ã®é€šçŸ¥å…ˆã‚’è¿½åŠ + */ + "createRecipient": string; + /** + * é€šå ±ã®é€šçŸ¥å…ˆã‚’編集 + */ + "modifyRecipient": string; + /** + * 通知先ã®ç¨®é¡ž + */ + "recipientType": string; + "_recipientType": { + /** + * メール + */ + "mail": string; + /** + * Webhook + */ + "webhook": string; + "_captions": { + /** + * モデレーター権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã®ã¿) + */ + "mail": string; + /** + * 指定ã—ãŸSystemWebhookã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã¨é€šå ±ã‚’解決ã—ãŸæ™‚ã«ãã‚Œãžã‚Œç™ºä¿¡) + */ + "webhook": string; + }; + }; + /** + * ã‚ーワード + */ + "keywords": string; + /** + * 通知先ユーザー + */ + "notifiedUser": string; + /** + * 使用ã™ã‚‹Webhook + */ + "notifiedWebhook": string; + /** + * 通知先を削除ã—ã¾ã™ã‹ï¼Ÿ + */ + "deleteConfirm": string; + }; }; "_moderationLogTypes": { /** @@ -9802,6 +9980,364 @@ export interface Locale extends ILocale { * ユーザーã®ãƒãƒŠãƒ¼ã‚’解除 */ "unsetUserBanner": string; + /** + * SystemWebhookã‚’ä½œæˆ + */ + "createSystemWebhook": string; + /** + * SystemWebhookã‚’æ›´æ–° + */ + "updateSystemWebhook": string; + /** + * SystemWebhookを削除 + */ + "deleteSystemWebhook": string; + /** + * é€šå ±ã®é€šçŸ¥å…ˆã‚’ä½œæˆ + */ + "createAbuseReportNotificationRecipient": string; + /** + * é€šå ±ã®é€šçŸ¥å…ˆã‚’æ›´æ–° + */ + "updateAbuseReportNotificationRecipient": string; + /** + * é€šå ±ã®é€šçŸ¥å…ˆã‚’削除 + */ + "deleteAbuseReportNotificationRecipient": string; + /** + * アカウントを削除 + */ + "deleteAccount": string; + /** + * ページを削除 + */ + "deletePage": string; + /** + * Playを削除 + */ + "deleteFlash": string; + /** + * ギャラリーã®æŠ•ç¨¿ã‚’削除 + */ + "deleteGalleryPost": string; + }; + "_mfm": { + /** + * ã“ã®æ©Ÿèƒ½ã¯ä¸€èˆ¬çš„ã«æ™®åŠã—ã¦ã„ãªã„ãŸã‚ã€ä»–ã®Misskeyフォークをå«ã‚ãŸå¤šãã®Fediverseソフトウェアã§è¡¨ç¤ºã§ããªã„ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚ + */ + "uncommonFeature": string; + /** + * MFM ã¯Misskey, Sharkey, Firefish, Akkomaãªã©ã€å¤šãã®å ´æ‰€ã§ä½¿ç”¨ã§ãるマークアップ言語ã§ã™ã€‚ã“ã“ã§ã¯ã€åˆ©ç”¨ã§ãã‚‹MFM構文ã®ä¸€è¦§ã‚’ã”覧ã„ãŸã ã‘ã¾ã™ã€‚ + */ + "intro": string; + /** + * Sharkeyã§Fediverseã®ä¸–ç•ŒãŒåºƒãŒã‚Šã¾ã™ + */ + "dummy": string; + /** + * メンション + */ + "mention": string; + /** + * アットマーク + ユーザーåã§ã€ç‰¹å®šã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "mentionDescription": string; + /** + * ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚° + */ + "hashtag": string; + /** + * ナンãƒãƒ¼ã‚µã‚¤ãƒ³ + ã‚¿ã‚°ã§ã€ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã‚’示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "hashtagDescription": string; + /** + * URL + */ + "url": string; + /** + * URLを示ã™ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "urlDescription": string; + /** + * リンク + */ + "link": string; + /** + * æ–‡ç« ã®ç‰¹å®šã®ç¯„囲をã€URLã«ç´ã¥ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "linkDescription": string; + /** + * å¤ªå— + */ + "bold": string; + /** + * æ–‡å—を太ã表示ã—ã¦å¼·èª¿ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "boldDescription": string; + /** + * å°æ–‡å— + */ + "small": string; + /** + * 内容をå°ã•ã・薄ã表示ã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "smallDescription": string; + /** + * ä¸å¤®å¯„ã› + */ + "center": string; + /** + * 内容をä¸å¤®å¯„ã›ã§è¡¨ç¤ºã•ã›ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + */ + "centerDescription": string; + /** + * コード(インライン) + */ + "inlineCode": string; + /** + * プãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’インラインã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚ + */ + "inlineCodeDescription": string; + /** + * コード(ブãƒãƒƒã‚¯ï¼‰ + */ + "blockCode": string; + /** + * 複数行ã®ãƒ—ãƒã‚°ãƒ©ãƒ ãªã©ã®ã‚³ãƒ¼ãƒ‰ã‚’ブãƒãƒƒã‚¯ã§ã‚·ãƒ³ã‚¿ãƒƒã‚¯ã‚¹ãƒã‚¤ãƒ©ã‚¤ãƒˆã—ã¾ã™ã€‚ + */ + "blockCodeDescription": 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": { /** @@ -9980,7 +10516,7 @@ export interface Locale extends ILocale { "stop": string; "_alert": { /** - * MFMアニメーションã«ã¯ã€ç‚¹æ»…ã™ã‚‹ãƒ©ã‚¤ãƒˆã‚„高速ã§å‹•ãテã‚スト/絵文å—ã‚’å«ã¾ã‚Œã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚ + * MFMアニメーションã«ã¯ã€é«˜é€Ÿã§ç‚¹æ»…ã—ãŸã‚Šå‹•ã„ãŸã‚Šã™ã‚‹ãƒ†ã‚スト・絵文å—ã‚’å«ã‚€å ´åˆãŒã‚ã‚Šã¾ã™ã€‚ */ "text": string; /** @@ -9999,18 +10535,18 @@ export interface Locale extends ILocale { */ "warn": string; /** - * データã®ä¿å˜ãŒå®Œäº†ã™ã‚‹ã¨ã€ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç™»éŒ²ã•ã‚Œã¦ã„ã‚‹Eメールアドレスã«ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚ + * データã®ä¿å˜ãŒå®Œäº†ã™ã‚‹ã¨ã€ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç™»éŒ²ã•ã‚Œã¦ã„るメールアドレスã«ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚ */ "text": string; /** - * リクエスト + * データリクエスト実行 */ "button": string; }; "_dataSaver": { "_media": { /** - * メディアã®èªã¿è¾¼ã¿ + * メディアã®èªã¿è¾¼ã¿ã‚’無効化 */ "title": string; /** @@ -10020,7 +10556,7 @@ export interface Locale extends ILocale { }; "_avatar": { /** - * ã‚¢ã‚¤ã‚³ãƒ³ç”»åƒ + * アイコン画åƒã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’無効化 */ "title": string; /** @@ -10030,7 +10566,7 @@ export interface Locale extends ILocale { }; "_urlPreview": { /** - * URLプレビューã®ã‚µãƒ ãƒã‚¤ãƒ« + * URLプレビューã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’éžè¡¨ç¤º */ "title": string; /** @@ -10040,7 +10576,7 @@ export interface Locale extends ILocale { }; "_code": { /** - * コードãƒã‚¤ãƒ©ã‚¤ãƒˆ + * コードãƒã‚¤ãƒ©ã‚¤ãƒˆã‚’éžè¡¨ç¤º */ "title": string; /** @@ -10315,6 +10851,24 @@ export interface Locale extends ILocale { */ "loop": string; }; + "_contextMenu": { + /** + * コンテã‚ストメニュー + */ + "title": string; + /** + * アプリケーション + */ + "app": string; + /** + * Shiftã‚ーã§ã‚¢ãƒ—リケーション + */ + "appWithShift": string; + /** + * ブラウザã®UI + */ + "native": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/index.js b/locales/index.js index c7a693fb779f24b7890d0024a06483131a8076f2..48ff85f1a5358e99261f8773e57f3e0bb8815976 100644 --- a/locales/index.js +++ b/locales/index.js @@ -56,7 +56,11 @@ const primaries = { const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '').replaceAll(new RegExp(/\\+\{/,'g'), '{'); export function build() { - const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); + // vitestã®æŒ™å‹•ã‚’調整ã™ã‚‹ãŸã‚ã€ä¸€åº¦ãƒãƒ¼ã‚«ãƒ«å¤‰æ•°åŒ–ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ + // 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 removeEmpty = (obj) => { diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 2b04c5abfb0c13459d01f93eda570fd94514658d..734b4bd0b19903422bdb004e609573c240f3051f 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -109,11 +109,14 @@ enterEmoji: "Inserisci emoji" renote: "Rinota" unrenote: "Elimina la Rinota" renoted: "Rinotata!" +renotedToX: "Rinota da {name}." cantRenote: "È impossibile rinotare questa nota." cantReRenote: "È impossibile rinotare una Rinota." quote: "Citazione" inChannelRenote: "Rinota nel canale" inChannelQuote: "Cita nel canale" +renoteToChannel: "Rinota al canale" +renoteToOtherChannel: "Rinota a un altro canale" pinnedNote: "Nota in primo piano" pinned: "Fissa sul profilo" you: "Tu" @@ -178,6 +181,10 @@ addAccount: "Aggiungi profilo" reloadAccountsList: "Ricarica l'elenco dei profili" loginFailed: "Accesso non riuscito" showOnRemote: "Leggi sull'istanza remota" +continueOnRemote: "Continua da remoto" +chooseServerOnMisskeyHub: "Scegli l'istanza sul sito Misskey Hub" +specifyServerHost: "Indica l'indirizzo dell'istanza" +inputHostName: "Digita il nome del dominio " general: "Generali" wallpaper: "Sfondo" setWallpaper: "Imposta sfondo" @@ -314,6 +321,7 @@ selectFile: "Scelta allegato" selectFiles: "Scelta allegato" selectFolder: "Seleziona cartella" selectFolders: "Seleziona cartella" +fileNotSelected: "Nessun file selezionato" renameFile: "Rinomina file" folderName: "Nome della cartella" createFolder: "Nuova cartella" @@ -469,10 +477,12 @@ retype: "Conferma" noteOf: "Note di {user}" quoteAttached: "Citazione allegata" quoteQuestion: "Vuoi aggiungere una citazione?" +attachAsFileQuestion: "Il testo copiato eccede le dimensioni, vuoi allegarlo?" noMessagesYet: "Ancora nessuna chat" newMessageExists: "Hai ricevuto un nuovo messaggio" onlyOneFileCanBeAttached: "È possibile allegare al messaggio soltanto uno file" signinRequired: "Occorre avere un profilo registrato su questa istanza" +signinOrContinueOnRemote: "Per continuare, devi accedere alla tua istanza o registrarti su questa e poi accedere" invitations: "Invita" invitationCode: "Codice di invito" checking: "Confermando" @@ -696,7 +706,7 @@ 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: "Contrassegna la segnalazione come risolta" +abuseMarkAsResolved: "Risolvi segnalazione" openInNewTab: "Apri in una nuova scheda" openInSideView: "Apri in vista laterale" defaultNavigationBehaviour: "Navigazione preimpostata" @@ -835,6 +845,7 @@ administration: "Gestione" accounts: "Profilo" switch: "Cambia" noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore." +noInquiryUrlWarning: "Non è stata impostata la URL di contatto" noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot" configure: "Imposta" postToGallery: "Pubblicare nella galleria" @@ -1025,6 +1036,7 @@ thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale" thisPostMayBeAnnoyingCancel: "Annulla" thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso" collapseRenotes: "Comprimi le Rinota già viste" +collapseRenotesDescription: "Comprimi le Note con cui hai già interagito." internalServerError: "Errore interno del server" internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server" copyErrorInfo: "Copia le informazioni sull'errore" @@ -1237,10 +1249,20 @@ useNativeUIForVideoAudioPlayer: "Riprodurre audio/video usando le funzionalità keepOriginalFilename: "Mantieni il nome file originale" keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali." noDescription: "Manca la descrizione" +alwaysConfirmFollow: "Richiedi conferma per i Follow" +inquiry: "Contattaci" +tryAgain: "Per favore riprova" +confirmWhenRevealingSensitiveMedia: "Richiedi conferma prima di mostrare gli allegati espliciti" +sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?" _delivery: - stop: "Sospensione" + status: "Stato della distribuzione di attività " + stop: "Sospendi la distribuzione di attività " + resume: "Riprendi la distribuzione di attività " _type: none: "Pubblicazione" + manuallySuspended: "Sospesa manualmente" + goneSuspended: "Sospensione server a causa dell'eliminazione" + autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta" _bubbleGame: howToPlay: "Come giocare" hold: "Tieni" @@ -1366,6 +1388,8 @@ _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." + inquiryUrl: "URL di contatto" + inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" @@ -1682,6 +1706,7 @@ _role: canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo" driveCapacity: "Capienza del Drive" alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)" + canUpdateBioMedia: "Può aggiornare foto profilo e di testata" pinMax: "Quantità massima di Note in primo piano" antennaMax: "Quantità massima di Antenne" wordMuteMax: "Lunghezza massima del filtro parole" @@ -1700,6 +1725,11 @@ _role: roleAssignedTo: "Assegnato a ruoli manualmente" isLocal: "Profilo locale" isRemote: "Profilo remoto" + isCat: "È un gattino" + isBot: "È un bot" + isSuspended: "È sospeso" + isLocked: "È in stato privato" + isExplorable: "Autorizza la pubblicazione nei cataloghi" createdLessThan: "Profilo creato da meno di N" createdMoreThan: "Profilo creato da più di N" followersLessThanOrEq: "Profilo con N follower o meno" @@ -1753,7 +1783,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" @@ -1921,8 +1951,6 @@ _sfx: note: "Nota" noteMy: "Mia nota" notification: "Notifiche" - antenna: "Ricezione dell'antenna" - channel: "Notifiche di canale" reaction: "Quando seleziono una reazione" _soundSettings: driveFile: "Suoni del Drive" @@ -2347,6 +2375,7 @@ _deck: alwaysShowMainColumn: "Mostra sempre la colonna principale" columnAlign: "Allineare colonne" addColumn: "Aggiungi colonna" + newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note" configureColumn: "Impostazioni colonna" swapLeft: "Sposta a sinistra" swapRight: "Sposta a destra" @@ -2376,7 +2405,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" @@ -2385,9 +2414,9 @@ _drivecleaner: orderByCreatedAtAsc: "Dal più vecchio al più recente" _webhookSettings: createWebhook: "Creazione Webhook" + modifyWebhook: "Modifica Webhook" name: "Nome" secret: "Segreto" - events: "Quando eseguire il Webhook" active: "Attivo" _events: follow: "Quando segui un profilo" @@ -2397,6 +2426,25 @@ _webhookSettings: renote: "Quando la Nota è Rinotata" reaction: "Quando ricevo una reazione" mention: "Quando mi menzionano" + _systemEvents: + abuseReport: "Quando arriva una segnalazione" + abuseReportResolved: "Quando una segnalazione è risolta" + deleteConfirm: "Vuoi davvero eliminare il Webhook?" +_abuseReport: + _notificationRecipient: + createRecipient: "Aggiungi destinatario della segnalazione" + modifyRecipient: "Modifica destinatario della segnalazione" + recipientType: "Tipo di notifica" + _recipientType: + mail: "Email" + webhook: "Webhook" + _captions: + mail: "Quando ricevi un abuso, notifica l'amministrazione via email" + webhook: "Spedire una notifica al SystemWebhook specificato (sia quando si riceve una segnalazione, che quando viene risolta)" + keywords: "Parole chiave" + notifiedUser: "Profili da notificare" + notifiedWebhook: "Webhook da usare" + deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?" _moderationLogTypes: createRole: "Ruolo creato" deleteRole: "Ruolo eliminato" @@ -2434,6 +2482,12 @@ _moderationLogTypes: deleteAvatarDecoration: "Eliminazione decorazione della foto profilo" unsetUserAvatar: "Rimossa foto profilo" unsetUserBanner: "Rimossa intestazione profilo" + createSystemWebhook: "Crea un SystemWebhook" + updateSystemWebhook: "Modifica SystemWebhook" + deleteSystemWebhook: "Elimina SystemWebhook" + createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni" + updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni" + deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni" _fileViewer: title: "Dettagli del file" type: "Tipo di file" @@ -2559,6 +2613,8 @@ _urlPreviewSetting: userAgent: "User-Agent" userAgentDescription: "Definire con quale User-Agent si intende identificarsi durante l'acquisizione di un'anteprima. Se è vuoto, useremo il valore predefinito." summaryProxy: "Endpoint proxy che genera l'anteprima" + summaryProxyDescription: "Genera anteprime utilizzando un proxy Summaly anziché Misskey." + summaryProxyDescription2: "I parametri sono collegano al proxy come stringa query. Se il proxy non li supporta, verranno ignorati." _mediaControls: pip: "Sovraimpressione" playbackRate: "Velocità di riproduzione" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8485037111ff01789475d02726bde91f05c132e6..da208147a16c15440c899f7f0984014dfc115b02 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -62,6 +62,7 @@ copyFileId: "ファイルIDをコピー" copyFolderId: "フォルダーIDをコピー" copyProfileUrl: "プãƒãƒ•ã‚£ãƒ¼ãƒ«URLをコピー" searchUser: "ユーザーを検索" +searchThisUsersNotes: "ユーザーã®ãƒŽãƒ¼ãƒˆã‚’検索" reply: "返信" loadMore: "ã‚‚ã£ã¨è¦‹ã‚‹" showMore: "ã‚‚ã£ã¨è¦‹ã‚‹" @@ -162,6 +163,7 @@ editList: "リストを編集" selectChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã‚’é¸æŠž" selectAntenna: "アンテナをé¸æŠž" editAntenna: "アンテナを編集" +createAntenna: "アンテナを作æˆ" selectWidget: "ウィジェットをé¸æŠž" editWidgets: "ウィジェットを編集" editWidgetsExit: "編集を終了" @@ -190,6 +192,10 @@ addAccount: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’è¿½åŠ " reloadAccountsList: "アカウントリストã®æƒ…å ±ã‚’æ›´æ–°" loginFailed: "ãƒã‚°ã‚¤ãƒ³ã«å¤±æ•—ã—ã¾ã—ãŸ" showOnRemote: "リモートã§è¡¨ç¤º" +continueOnRemote: "リモートã§ç¶šè¡Œ" +chooseServerOnMisskeyHub: "Misskey Hubã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã‚’é¸æŠž" +specifyServerHost: "サーãƒãƒ¼ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‚’直接指定" +inputHostName: "ドメインを入力ã—ã¦ãã ã•ã„" general: "全般" wallpaper: "å£ç´™" setWallpaper: "å£ç´™ã‚’è¨å®š" @@ -200,6 +206,7 @@ followConfirm: "{name}をフォãƒãƒ¼ã—ã¾ã™ã‹ï¼Ÿ" proxyAccount: "プãƒã‚シアカウント" proxyAccountDescription: "プãƒã‚シアカウントã¯ã€ç‰¹å®šã®æ¡ä»¶ä¸‹ã§ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚©ãƒãƒ¼ã‚’代行ã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã™ã€‚例ãˆã°ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’リストã«å…¥ã‚ŒãŸã¨ãã€ãƒªã‚¹ãƒˆã«å…¥ã‚Œã‚‰ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’誰もフォãƒãƒ¼ã—ã¦ã„ãªã„ã¨ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティãŒã‚µãƒ¼ãƒãƒ¼ã«é…é”ã•ã‚Œãªã„ãŸã‚ã€ä»£ã‚ã‚Šã«ãƒ—ãƒã‚シアカウントãŒãƒ•ã‚©ãƒãƒ¼ã™ã‚‹ã‚ˆã†ã«ã—ã¾ã™ã€‚" host: "ホスト" +selectSelf: "自分をé¸æŠž" selectUser: "ユーザーをé¸æŠž" recipient: "宛先" annotation: "注釈" @@ -213,8 +220,9 @@ charts: "ãƒãƒ£ãƒ¼ãƒˆ" perHour: "1時間ã”ã¨" perDay: "1æ—¥ã”ã¨" stopActivityDelivery: "アクティビティã®é…é€ã‚’åœæ¢" -blockThisInstance: "ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’ブãƒãƒƒã‚¯" -silenceThisInstance: "インスタンスをサイレンス" +blockThisInstance: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’ブãƒãƒƒã‚¯" +silenceThisInstance: "サーãƒãƒ¼ã‚’サイレンス" +mediaSilenceThisInstance: "サーãƒãƒ¼ã‚’メディアサイレンス" operations: "æ“作" software: "ソフトウェア" version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³" @@ -236,6 +244,8 @@ blockedInstances: "ブãƒãƒƒã‚¯ã—ãŸã‚µãƒ¼ãƒãƒ¼" blockedInstancesDescription: "ブãƒãƒƒã‚¯ã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚ブãƒãƒƒã‚¯ã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã¯ã€ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¨ã‚„ã‚Šå–ã‚Šã§ããªããªã‚Šã¾ã™ã€‚" silencedInstances: "サイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼" silencedInstancesDescription: "サイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚サイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã™ã¹ã¦ã€Œã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã€ã¨ã—ã¦æ‰±ã‚ã‚Œã€ãƒ•ã‚©ãƒãƒ¼ãŒã™ã¹ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。" +mediaSilencedInstances: "メディアサイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼" +mediaSilencedInstancesDescription: "メディアサイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¾ã™ã€‚メディアサイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã™ã¹ã¦ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã¨ã—ã¦æ‰±ã‚ã‚Œã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—ãŒä½¿ç”¨ã§ããªã„よã†ã«ãªã‚Šã¾ã™ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã—ã¾ã›ã‚“。" muteAndBlock: "ミュートã¨ãƒ–ãƒãƒƒã‚¯" mutedUsers: "ミュートã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" blockedUsers: "ブãƒãƒƒã‚¯ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼" @@ -321,6 +331,7 @@ lightThemes: "明るã„テーマ" darkThemes: "æš—ã„テーマ" syncDeviceDarkMode: "デãƒã‚¤ã‚¹ã®ãƒ€ãƒ¼ã‚¯ãƒ¢ãƒ¼ãƒ‰ã¨åŒæœŸã™ã‚‹" drive: "ドライブ" +driveSearchbarPlaceholder: "検索ドライブ" fileName: "ファイルå" selectFile: "ファイルをé¸æŠž" selectFiles: "ファイルをé¸æŠž" @@ -489,7 +500,8 @@ attachAsFileQuestion: "クリップボードã®ãƒ†ã‚ストãŒé•·ã„ã§ã™ã€‚テ noMessagesYet: "ã¾ã ãƒãƒ£ãƒƒãƒˆã¯ã‚ã‚Šã¾ã›ã‚“" newMessageExists: "æ–°ã—ã„メッセージãŒã‚ã‚Šã¾ã™" onlyOneFileCanBeAttached: "メッセージã«æ·»ä»˜ã§ãるファイルã¯ã²ã¨ã¤ã§ã™" -signinRequired: "続行ã™ã‚‹å‰ã«ã€ã‚µã‚¤ãƒ³ã‚¢ãƒƒãƒ—ã¾ãŸã¯ã‚µã‚¤ãƒ³ã‚¤ãƒ³ãŒå¿…è¦ã§ã™" +signinRequired: "続行ã™ã‚‹å‰ã«ã€ç™»éŒ²ã¾ãŸã¯ãƒã‚°ã‚¤ãƒ³ãŒå¿…è¦ã§ã™" +signinOrContinueOnRemote: "続行ã™ã‚‹ã«ã¯ã€ãŠä½¿ã„ã®ã‚µãƒ¼ãƒãƒ¼ã«ç§»å‹•ã™ã‚‹ã‹ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«ç™»éŒ²ãƒ»ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™" invitations: "招待" invitationCode: "招待コード" checking: "確èªã—ã¦ã„ã¾ã™" @@ -531,6 +543,7 @@ mediaListWithOneImageAppearance: "ç”»åƒãŒ1æžšã®ã¿ã®ãƒ¡ãƒ‡ã‚£ã‚¢ãƒªã‚¹ãƒˆã® limitTo: "{x}を上é™ã«" noFollowRequests: "フォãƒãƒ¼ç”³è«‹ã¯ã‚ã‚Šã¾ã›ã‚“" openImageInNewTab: "ç”»åƒã‚’æ–°ã—ã„タブã§é–‹ã" +warnForMissingAltText: "代替テã‚ストを入れ忘れãŸã¨ãã«è¦å‘Šã™ã‚‹" dashboard: "ダッシュボード" local: "ãƒãƒ¼ã‚«ãƒ«" remote: "リモート" @@ -563,8 +576,8 @@ objectStorageUseProxy: "Proxyを利用ã™ã‚‹" objectStorageUseProxyDesc: "API接続ã«proxyを利用ã—ãªã„å ´åˆã¯ã‚ªãƒ•ã«ã—ã¦ãã ã•ã„" objectStorageSetPublicRead: "アップãƒãƒ¼ãƒ‰æ™‚ã«'public-read'ã‚’è¨å®šã™ã‚‹" s3ForcePathStyleDesc: "s3ForcePathStyleを有効ã«ã™ã‚‹ã¨ã€ãƒã‚±ãƒƒãƒˆåã‚’URLã®ãƒ›ã‚¹ãƒˆåã§ã¯ãªãパスã®ä¸€éƒ¨ã¨ã—ã¦æŒ‡å®šã™ã‚‹ã“ã¨ã‚’強制ã—ã¾ã™ã€‚セルフホストã•ã‚ŒãŸMinioãªã©ã®ä½¿ç”¨æ™‚ã«æœ‰åŠ¹ã«ã™ã‚‹å¿…è¦ãŒã‚ã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" -deeplFreeMode: "DeepLX-JS を使用ã™ã‚‹ (èªè¨¼ã‚ーãªã—)" -deeplFreeModeDescription: "ヘルプãŒå¿…è¦ã§ã™ã‹? DeepLX-JSã®ã‚»ãƒƒãƒˆã‚¢ãƒƒãƒ—方法ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ãã ã•ã„。" +deeplFreeMode: "DeepLX-JS を使用ã™ã‚‹ (èªè¨¼ã‚ーä¸è¦)" +deeplFreeModeDescription: "DeepLX-JSã®è¨å®šæ–¹æ³•ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ãã ã•ã„。" serverLogs: "サーãƒãƒ¼ãƒã‚°" deleteAll: "å…¨ã¦å‰Šé™¤" showFixedPostForm: "タイムライン上部ã«æŠ•ç¨¿ãƒ•ã‚©ãƒ¼ãƒ を表示ã™ã‚‹" @@ -580,7 +593,7 @@ popout: "ãƒãƒƒãƒ—アウト" volume: "音é‡" masterVolume: "マスター音é‡" notUseSound: "サウンドを出力ã—ãªã„" -useSoundOnlyWhenActive: "MisskeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹" +useSoundOnlyWhenActive: "SharkeyãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªæ™‚ã®ã¿ã‚µã‚¦ãƒ³ãƒ‰ã‚’出力ã™ã‚‹" details: "詳細" chooseEmoji: "絵文å—ã‚’é¸æŠž" unableToProcess: "æ“作を完了ã§ãã¾ã›ã‚“" @@ -695,10 +708,10 @@ create: "作æˆ" notificationSetting: "通知è¨å®š" notificationSettingDesc: "表示ã™ã‚‹é€šçŸ¥ã®ç¨®åˆ¥ã‚’é¸æŠžã—ã¦ãã ã•ã„。" enableFaviconNotificationDot: "未èªã®é€šçŸ¥ãŒã‚ã‚‹ã¨ãã«ã‚¿ãƒ–ã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’目立ãŸã›ã‚‹" -verifyNotificationDotWorkingButton: "通知ドットãŒã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§æ©Ÿèƒ½ã™ã‚‹ã‹ã©ã†ã‹ã‚’確èªã—ã¾ã™ã€‚" -notificationDotNotWorking: "残念ãªãŒã‚‰ã€ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯ç¾æ™‚点ã§ã¯é€šçŸ¥ãƒ‰ãƒƒãƒˆæ©Ÿèƒ½ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。" -notificationDotWorking: "通知ドットã¯ã€ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã§æ£ã—ã機能ã—ã¦ã„ã¾ã™ã€‚" -notificationDotNotWorkingAdvice: "通知ドットãŒæ©Ÿèƒ½ã—ãªã„å ´åˆã¯ã€ç®¡ç†è€…ã«ãƒ‰ã‚ュメントを確èªã™ã‚‹ã‚ˆã†ã«ä¾é ¼ã—ã¦ãã ã•ã„ {link}" +verifyNotificationDotWorkingButton: "タブアイコン強調機能ã®å‹•ä½œç¢ºèª" +notificationDotNotWorking: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯ç¾æ™‚点ã§ã¯ã‚¿ãƒ–アイコン強調機能をサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。" +notificationDotWorking: "タブアイコン強調機能ã¯ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§æ£ã—ã機能ã—ã¦ã„ã¾ã™ã€‚" +notificationDotNotWorkingAdvice: "タブアイコン強調機能ãŒæ©Ÿèƒ½ã—ãªã„å ´åˆã¯ã€ç®¡ç†è€…ã«ãƒ‰ã‚ュメントを確èªã™ã‚‹ã‚ˆã†ã«ä¾é ¼ã—ã¦ãã ã•ã„ {link}" useGlobalSetting: "ã‚°ãƒãƒ¼ãƒãƒ«è¨å®šã‚’使ã†" useGlobalSettingDesc: "オンã«ã™ã‚‹ã¨ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®é€šçŸ¥è¨å®šãŒä½¿ç”¨ã•ã‚Œã¾ã™ã€‚オフã«ã™ã‚‹ã¨ã€å€‹åˆ¥ã«è¨å®šã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚" other: "ãã®ä»–" @@ -713,7 +726,7 @@ abuseReports: "é€šå ±" reportAbuse: "é€šå ±" reportAbuseRenote: "ãƒ–ãƒ¼ã‚¹ãƒˆã‚’é€šå ±" reportAbuseOf: "{name}ã‚’é€šå ±ã™ã‚‹" -fillAbuseReportDescription: "é€šå ±ç†ç”±ã®è©³ç´°ã‚’記入ã—ã¦ãã ã•ã„。対象ã®ãƒŽãƒ¼ãƒˆãŒã‚ã‚‹å ´åˆã¯ãã®URLも記入ã—ã¦ãã ã•ã„。" +fillAbuseReportDescription: "é€šå ±ç†ç”±ã®è©³ç´°ã‚’記入ã—ã¦ãã ã•ã„。対象ã®ãƒŽãƒ¼ãƒˆã‚„ページãªã©ãŒã‚ã‚‹å ´åˆã¯ãã®URLも記入ã—ã¦ãã ã•ã„。" abuseReported: "内容ãŒé€ä¿¡ã•ã‚Œã¾ã—ãŸã€‚ã”å ±å‘Šã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã—ãŸã€‚" reporter: "é€šå ±è€…" reporteeOrigin: "é€šå ±å…ˆ" @@ -764,9 +777,9 @@ lockedAccountInfo: "フォãƒãƒ¼ã‚’承èªåˆ¶ã«ã—ã¦ã‚‚ã€ãƒŽãƒ¼ãƒˆã®å…¬é–‹ç¯„ alwaysMarkSensitive: "デフォルトã§ãƒ¡ãƒ‡ã‚£ã‚¢ã‚’センシティブè¨å®šã«ã™ã‚‹" loadRawImages: "添付画åƒã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’オリジナル画質ã«ã™ã‚‹" searchEngine: "検索MFMã®æ¤œç´¢ã‚¨ãƒ³ã‚¸ãƒ³" -searchEngineOther: "ä»–" -searchEngineCustomURIDescription: "カスタムURI ã¯ã€\"https://www.google.com/search?q=\\{query}\" ã‚„ \"https://www.google.com/search?q=%s\" ã®ã‚ˆã†ãªå½¢å¼ã§å…¥åŠ›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" -searchEngineCusomURI: "カスタムURI" +searchEngineOther: "カスタム" +searchEngineCustomURIDescription: "カスタム検索エンジンã®URIã¯ã€\"https://www.google.com/search?q=\\{query}\" ã‚„ \"https://www.google.com/search?q=%s\" ã®ã‚ˆã†ãªå½¢å¼ã§å…¥åŠ›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚" +searchEngineCusomURI: "カスタム検索エンジン URI" disableShowingAnimatedImages: "アニメーション画åƒã‚’å†ç”Ÿã—ãªã„" highlightSensitiveMedia: "メディアãŒã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã§ã‚ã‚‹ã“ã¨ã‚’分ã‹ã‚Šã‚„ã™ã表示" verificationEmailSent: "確èªã®ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡ã—ã¾ã—ãŸã€‚メールã«è¨˜è¼‰ã•ã‚ŒãŸãƒªãƒ³ã‚¯ã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ã€è¨å®šã‚’完了ã—ã¦ãã ã•ã„。" @@ -915,7 +928,7 @@ itsOff: "オフã«ãªã£ã¦ã„ã¾ã™" on: "オン" off: "オフ" emailRequiredForSignup: "アカウント登録ã«ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’å¿…é ˆã«ã™ã‚‹" -approvalRequiredForSignup: "æ–°è¦ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æ‰¿èªãŒå¿…è¦" +approvalRequiredForSignup: "アカウント登録を承èªåˆ¶ã«ã™ã‚‹" unread: "未èª" filter: "フィルタ" controlPanel: "コントãƒãƒ¼ãƒ«ãƒ‘ãƒãƒ«" @@ -967,7 +980,7 @@ recentNHours: "ç›´è¿‘{n}時間" recentNDays: "ç›´è¿‘{n}æ—¥" noEmailServerWarning: "メールサーãƒãƒ¼ã®è¨å®šãŒã•ã‚Œã¦ã„ã¾ã›ã‚“。" thereIsUnresolvedAbuseReportWarning: "未対応ã®é€šå ±ãŒã‚ã‚Šã¾ã™ã€‚" -pendingUserApprovals: "承èªå¾…ã¡ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„る。" +pendingUserApprovals: "承èªå¾…ã¡ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„ã¾ã™ã€‚" recommended: "推奨" check: "ãƒã‚§ãƒƒã‚¯" driveCapOverrideLabel: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ‰ãƒ©ã‚¤ãƒ–容é‡ä¸Šé™ã‚’変更" @@ -985,11 +998,11 @@ document: "ドã‚ュメント" numberOfPageCache: "ページã‚ャッシュ数" numberOfPageCacheDescription: "多ãã™ã‚‹ã¨åˆ©ä¾¿æ€§ãŒå‘上ã—ã¾ã™ãŒã€è² è·ã¨ãƒ¡ãƒ¢ãƒªä½¿ç”¨é‡ãŒå¢—ãˆã¾ã™ã€‚" numberOfReplies: "スレッド内ã®è¿”ä¿¡æ•°" -numberOfRepliesDescription: "ã“ã®æ•°å€¤ã‚’大ããã™ã‚‹ã¨ã€ã‚ˆã‚Šå¤šãã®è¿”ä¿¡ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®å€¤ã‚’大ããã—ã™ãŽã‚‹ã¨ã€è¿”ä¿¡ãŒçª®å±ˆã«ãªã‚Šã€èªã‚ãªããªã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" +numberOfRepliesDescription: "ã“ã®æ•°å€¤ã‚’大ããã™ã‚‹ã¨ã€ã‚ˆã‚Šå¤šãã®è¿”ä¿¡ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®å€¤ã‚’大ããã—ã™ãŽã‚‹ã¨ã€UIãŒçª®å±ˆã«ãªã£ã¦èªã¿ã«ãããªã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚" boostSettings: "ブーストè¨å®š" -showVisibilitySelectorOnBoost: "å¯è¦–性セレクタを表示" -showVisibilitySelectorOnBoostDescription: "無効ã®å ´åˆã€ä»¥ä¸‹ã§å®šç¾©ã•ã‚Œã‚‹ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å¯è¦–性ãŒä½¿ç”¨ã•ã‚Œã€ã‚»ãƒ¬ã‚¯ã‚¿ã¯è¡¨ç¤ºã•ã‚Œã¾ã›ã‚“。" -visibilityOnBoost: "デフォルトã®ãƒ–ーストå¯è¦–性ã®è¨å®š" +showVisibilitySelectorOnBoost: "公開範囲セレクターを表示" +showVisibilitySelectorOnBoostDescription: "無効ã®å ´åˆã€ä»¥ä¸‹ã§è¨å®šã—ãŸãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®å…¬é–‹ç¯„囲ãŒä½¿ç”¨ã•ã‚Œã€ã‚»ãƒ¬ã‚¯ã‚¿ãƒ¼ã¯è¡¨ç¤ºã•ã‚Œã¾ã›ã‚“。" +visibilityOnBoost: "デフォルトã®ãƒ–ースト公開範囲" logoutConfirm: "ãƒã‚°ã‚¢ã‚¦ãƒˆã—ã¾ã™ã‹ï¼Ÿ" lastActiveDate: "最終利用日時" statusbar: "ステータスãƒãƒ¼" @@ -1071,12 +1084,12 @@ thisPostMayBeAnnoyingCancel: "ã‚„ã‚ã‚‹" thisPostMayBeAnnoyingIgnore: "ã“ã®ã¾ã¾æŠ•ç¨¿" thisPostIsMissingAltTextCancel: "ã‚„ã‚ã‚‹" thisPostIsMissingAltTextIgnore: "ã“ã®ã¾ã¾æŠ•ç¨¿" -thisPostIsMissingAltText: "ã“ã®æŠ•ç¨¿ã«æ·»ä»˜ã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«ã® 1 ã¤ã«ä»£æ›¿ãƒ†ã‚ストãŒã‚ã‚Šã¾ã›ã‚“。ã™ã¹ã¦ã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã«ä»£æ›¿ãƒ†ã‚ストãŒå«ã¾ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。" +thisPostIsMissingAltText: "代替テã‚ストãŒãªã„ファイルãŒæ·»ä»˜ã•ã‚Œã¦ã„ã¾ã™ã€‚ã™ã¹ã¦ã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã«ä»£æ›¿ãƒ†ã‚ストをå«ã‚€ã‚ˆã†ã«ã—ã¦ãã ã•ã„。" collapseRenotes: "ブーストã®ã‚¹ãƒžãƒ¼ãƒˆçœç•¥" collapseRenotesDescription: "リアクションやブーストをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã—ã¾ã™ã€‚" -collapseNotesRepliedTo: "返信ã•ã‚ŒãŸãƒŽãƒ¼ãƒˆçœç•¥" +collapseNotesRepliedTo: "返信元ã®ãƒŽãƒ¼ãƒˆã‚’折りãŸãŸã‚€" collapseFiles: "ファイルを折りãŸãŸã‚€" -autoloadConversation: "返信ã«ä¼šè©±ã‚’èªã¿è¾¼ã‚€" +autoloadConversation: "会話スレッドを自動ã§èªã¿è¾¼ã‚€" internalServerError: "サーãƒãƒ¼å†…部エラー" internalServerErrorDescription: "サーãƒãƒ¼å†…部ã§äºˆæœŸã—ãªã„エラーãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" copyErrorInfo: "ã‚¨ãƒ©ãƒ¼æƒ…å ±ã‚’ã‚³ãƒ”ãƒ¼" @@ -1087,7 +1100,7 @@ disableFederationConfirm: "連åˆãªã—ã«ã—ã¾ã™ã‹ï¼Ÿ" disableFederationConfirmWarn: "連åˆãªã—ã«ã—ã¦ã‚‚投稿ã¯éžå…¬é–‹ã«ãªã‚Šã¾ã›ã‚“。ã»ã¨ã‚“ã©ã®å ´åˆã€é€£åˆãªã—ã«ã™ã‚‹å¿…è¦ã¯ã‚ã‚Šã¾ã›ã‚“。" disableFederationOk: "連åˆãªã—ã«ã™ã‚‹" invitationRequiredToRegister: "ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‹›å¾…制ã§ã™ã€‚招待コードをãŠæŒã¡ã®æ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚" -approvalRequiredToRegister: "ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯ã€ç™»éŒ²ç†ç”±ã‚’指定ã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ã‚’å—ã‘入れã¦ã„ã¾ã™ã€‚" +approvalRequiredToRegister: "ç¾åœ¨ã“ã®ã‚µãƒ¼ãƒãƒ¼ã¯æ‰¿èªåˆ¶ã§ã™ã€‚å‚åŠ ã—ãŸã„ç†ç”±ã‚’記入ã—ã€æ‰¿èªã•ã‚ŒãŸæ–¹ã®ã¿ç™»éŒ²ã§ãã¾ã™ã€‚" emailNotSupported: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã§ã¯ãƒ¡ãƒ¼ãƒ«é…ä¿¡ã¯ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“" postToTheChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã«æŠ•ç¨¿" cannotBeChangedLater: "後ã‹ã‚‰å¤‰æ›´ã§ãã¾ã›ã‚“。" @@ -1130,7 +1143,7 @@ accountMoved: "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯æ–°ã—ã„アカウントã«ç§»è¡Œã—ã¾ã— accountMovedShort: "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ç§»è¡Œã•ã‚Œã¦ã„ã¾ã™" operationForbidden: "ã“ã®æ“作ã¯ã§ãã¾ã›ã‚“" forceShowAds: "常ã«åºƒå‘Šã‚’表示ã™ã‚‹" -oneko: "猫å‹é” :3" +oneko: "ã«ã‚ƒã‚“ã“フレンド :3" addMemo: "ãƒ¡ãƒ¢ã‚’è¿½åŠ " editMemo: "メモを編集" reactionsList: "リアクション一覧" @@ -1152,6 +1165,8 @@ preservedUsernames: "予約ユーザーå" preservedUsernamesDescription: "予約ã™ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼åを改行ã§åˆ—挙ã—ã¾ã™ã€‚ã“ã“ã§æŒ‡å®šã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼åã¯ã‚¢ã‚«ã‚¦ãƒ³ãƒˆä½œæˆæ™‚ã«ä½¿ãˆãªããªã‚Šã¾ã™ãŒã€ç®¡ç†è€…ã«ã‚ˆã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆä½œæˆæ™‚ã¯ã“ã®åˆ¶é™ã‚’å—ã‘ã¾ã›ã‚“。ã¾ãŸã€æ—¢ã«å˜åœ¨ã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚‚影響をå—ã‘ã¾ã›ã‚“。" createNoteFromTheFile: "ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‹ã‚‰ãƒŽãƒ¼ãƒˆã‚’作æˆ" archive: "アーカイブ" +archived: "アーカイブ済ã¿" +unarchive: "アーカイブ解除" channelArchiveConfirmTitle: "{name}をアーカイブã—ã¾ã™ã‹ï¼Ÿ" channelArchiveConfirmDescription: "アーカイブã™ã‚‹ã¨ã€ãƒãƒ£ãƒ³ãƒãƒ«ä¸€è¦§ã‚„検索çµæžœã«è¡¨ç¤ºã•ã‚Œãªããªã‚Šã€æ–°ãŸãªæ›¸ãè¾¼ã¿ã‚‚ã§ããªããªã‚Šã¾ã™ã€‚" thisChannelArchived: "ã“ã®ãƒãƒ£ãƒ³ãƒãƒ«ã¯ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã•ã‚Œã¦ã„ã¾ã™ã€‚" @@ -1162,6 +1177,9 @@ preventAiLearning: "生æˆAIã«ã‚ˆã‚‹å¦ç¿’ã‚’æ‹’å¦" preventAiLearningDescription: "外部ã®æ–‡ç« 生æˆAIã‚„ç”»åƒç”ŸæˆAIã«å¯¾ã—ã¦ã€æŠ•ç¨¿ã—ãŸãƒŽãƒ¼ãƒˆã‚„ç”»åƒãªã©ã®ã‚³ãƒ³ãƒ†ãƒ³ãƒ„ã‚’å¦ç¿’ã®å¯¾è±¡ã«ã—ãªã„よã†ã«è¦æ±‚ã—ã¾ã™ã€‚ã“ã‚Œã¯noaiフラグをHTMLレスãƒãƒ³ã‚¹ã«å«ã‚ã‚‹ã“ã¨ã«ã‚ˆã£ã¦å®Ÿç¾ã•ã‚Œã¾ã™ãŒã€ã“ã®è¦æ±‚ã«å¾“ã†ã‹ã¯ãã®AI次第ã§ã‚ã‚‹ãŸã‚ã€å¦ç¿’を完全ã«é˜²æ¢ã™ã‚‹ã‚‚ã®ã§ã¯ã‚ã‚Šã¾ã›ã‚“。" options: "オプション" specifyUser: "ユーザー指定" +lookupConfirm: "照会ã—ã¾ã™ã‹ï¼Ÿ" +openTagPageConfirm: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒšãƒ¼ã‚¸ã‚’é–‹ãã¾ã™ã‹ï¼Ÿ" +specifyHost: "ホスト指定" failedToPreviewUrl: "プレビューã§ãã¾ã›ã‚“" update: "æ›´æ–°" rolesThatCanBeUsedThisEmojiAsReaction: "リアクションã¨ã—ã¦ä½¿ãˆã‚‹ãƒãƒ¼ãƒ«" @@ -1238,7 +1256,7 @@ externalServices: "外部サービス" sourceCode: "ソースコード" sourceCodeIsNotYetProvided: "ソースコードã¯ã¾ã æä¾›ã•ã‚Œã¦ã„ã¾ã›ã‚“。ã“ã®å•é¡Œã®ä¿®æ£ã«ã¤ã„ã¦ç®¡ç†è€…ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。" repositoryUrl: "リãƒã‚¸ãƒˆãƒªURL" -repositoryUrlDescription: "ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Misskeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://github.com/misskey-dev/misskey ã¨è¨˜å…¥ã—ã¾ã™ã€‚" +repositoryUrlDescription: "ソースコードãŒå…¬é–‹ã•ã‚Œã¦ã„るリãƒã‚¸ãƒˆãƒªãŒã‚ã‚‹å ´åˆã€ãã®URLを記入ã—ã¾ã™ã€‚Sharkeyã‚’ç¾çŠ¶ã®ã¾ã¾ï¼ˆã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã«ã„ã‹ãªã‚‹å¤‰æ›´ã‚‚åŠ ãˆãšã«ï¼‰ä½¿ç”¨ã—ã¦ã„ã‚‹å ´åˆã¯ https://activitypub.software/TransFem-org/Sharkey/ ã¨è¨˜å…¥ã—ã¾ã™ã€‚" repositoryUrlOrTarballRequired: "リãƒã‚¸ãƒˆãƒªã‚’公開ã—ã¦ã„ãªã„å ´åˆã€ä»£ã‚ã‚Šã«tarballã‚’æä¾›ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚詳細ã¯.config/example.ymlã‚’å‚ç…§ã—ã¦ãã ã•ã„。" feedback: "フィードãƒãƒƒã‚¯" feedbackUrl: "フィードãƒãƒƒã‚¯URL" @@ -1301,6 +1319,11 @@ keepOriginalFilenameDescription: "ã“ã®è¨å®šã‚’オフã«ã™ã‚‹ã¨ã€ã‚¢ãƒƒãƒ— noDescription: "説明文ã¯ã‚ã‚Šã¾ã›ã‚“" alwaysConfirmFollow: "フォãƒãƒ¼ã®éš›å¸¸ã«ç¢ºèªã™ã‚‹" inquiry: "ãŠå•ã„åˆã‚ã›" +tryAgain: "ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。" +confirmWhenRevealingSensitiveMedia: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã‚’表示ã™ã‚‹ã¨ã確èªã™ã‚‹" +sensitiveMediaRevealConfirm: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã§ã™ã€‚表示ã—ã¾ã™ã‹ï¼Ÿ" +createdLists: "作æˆã—ãŸãƒªã‚¹ãƒˆ" +createdAntennas: "作æˆã—ãŸã‚¢ãƒ³ãƒ†ãƒŠ" _delivery: status: "é…信状態" @@ -1766,6 +1789,7 @@ _role: canManageAvatarDecorations: "ã‚¢ãƒã‚¿ãƒ¼ãƒ‡ã‚³ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã®ç®¡ç†" driveCapacity: "ドライブ容é‡" alwaysMarkNsfw: "ファイルã«NSFWを常ã«ä»˜ä¸Ž" + canUpdateBioMedia: "アイコンã¨ãƒãƒŠãƒ¼ã®æ›´æ–°ã‚’許å¯" pinMax: "ノートã®ãƒ”ン留ã‚ã®æœ€å¤§æ•°" antennaMax: "アンテナã®ä½œæˆå¯èƒ½æ•°" wordMuteMax: "ワードミュートã®æœ€å¤§æ–‡å—æ•°" @@ -2038,8 +2062,6 @@ _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - antenna: "アンテナå—ä¿¡" - channel: "ãƒãƒ£ãƒ³ãƒãƒ«é€šçŸ¥" reaction: "リアクションé¸æŠžæ™‚" _soundSettings: @@ -2048,7 +2070,8 @@ _soundSettings: driveFileTypeWarn: "ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã¯å¯¾å¿œã—ã¦ã„ã¾ã›ã‚“" driveFileTypeWarnDescription: "音声ファイルをé¸æŠžã—ã¦ãã ã•ã„" driveFileDurationWarn: "音声ãŒé•·ã™ãŽã¾ã™" - driveFileDurationWarnDescription: "é•·ã„音声を使用ã™ã‚‹ã¨Misskeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ" + driveFileDurationWarnDescription: "é•·ã„音声を使用ã™ã‚‹ã¨Sharkeyã®ä½¿ç”¨ã«æ”¯éšœã‚’ããŸã™å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚ãã‚Œã§ã‚‚続行ã—ã¾ã™ã‹ï¼Ÿ" + driveFileError: "音声ãŒèªã¿è¾¼ã‚ã¾ã›ã‚“ã§ã—ãŸã€‚è¨å®šã‚’変更ã—ã¦ãã ã•ã„" _ago: future: "未æ¥" @@ -2372,6 +2395,7 @@ _timelines: local: "ãƒãƒ¼ã‚«ãƒ«" social: "ソーシャル" global: "ã‚°ãƒãƒ¼ãƒãƒ«" + bubble: "ãƒãƒƒãƒƒãƒ–ル" _play: new: "Playã®ä½œæˆ" @@ -2424,6 +2448,7 @@ _pages: eyeCatchingImageSet: "アイã‚ャッãƒç”»åƒã‚’è¨å®š" eyeCatchingImageRemove: "アイã‚ャッãƒç”»åƒã‚’削除" chooseBlock: "ブãƒãƒƒã‚¯ã‚’è¿½åŠ " + enterSectionTitle: "セクションタイトルを入力" selectType: "種類をé¸æŠž" contentBlocks: "コンテンツ" inputBlocks: "入力" @@ -2489,11 +2514,12 @@ _notification: roleAssigned: "ãƒãƒ¼ãƒ«ãŒä»˜ä¸Žã•ã‚ŒãŸ" achievementEarned: "実績ã®ç²å¾—" app: "連æºã‚¢ãƒ—リã‹ã‚‰ã®é€šçŸ¥" + edited: "編集済ã¿" _actions: followBack: "フォãƒãƒ¼ãƒãƒƒã‚¯" reply: "返信" - renote: "Boost" + renote: "ブースト" _deck: alwaysShowMainColumn: "常ã«ãƒ¡ã‚¤ãƒ³ã‚«ãƒ©ãƒ を表示" @@ -2543,9 +2569,10 @@ _drivecleaner: _webhookSettings: createWebhook: "Webhookを作æˆ" + modifyWebhook: "Webhookを編集" name: "åå‰" secret: "シークレット" - events: "Webhookを実行ã™ã‚‹ã‚¿ã‚¤ãƒŸãƒ³ã‚°" + trigger: "トリガー" active: "有効" _events: follow: "フォãƒãƒ¼ã—ãŸã¨ã" @@ -2555,6 +2582,27 @@ _webhookSettings: renote: "Boostã•ã‚ŒãŸã¨ã" reaction: "リアクションãŒã‚ã£ãŸã¨ã" mention: "メンションã•ã‚ŒãŸã¨ã" + _systemEvents: + abuseReport: "ユーザーã‹ã‚‰é€šå ±ãŒã‚ã£ãŸã¨ã" + abuseReportResolved: "ユーザーã‹ã‚‰ã®é€šå ±ã‚’処ç†ã—ãŸã¨ã" + userCreated: "ユーザーãŒä½œæˆã•ã‚ŒãŸã¨ã" + deleteConfirm: "Webhookを削除ã—ã¾ã™ã‹ï¼Ÿ" + +_abuseReport: + _notificationRecipient: + createRecipient: "é€šå ±ã®é€šçŸ¥å…ˆã‚’è¿½åŠ " + modifyRecipient: "é€šå ±ã®é€šçŸ¥å…ˆã‚’編集" + recipientType: "通知先ã®ç¨®é¡ž" + _recipientType: + mail: "メール" + webhook: "Webhook" + _captions: + mail: "モデレーター権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã®ã¿)" + webhook: "指定ã—ãŸSystemWebhookã«é€šçŸ¥ã‚’é€ã‚Šã¾ã™(é€šå ±ã‚’å—ã‘ãŸæ™‚ã¨é€šå ±ã‚’解決ã—ãŸæ™‚ã«ãã‚Œãžã‚Œç™ºä¿¡)" + keywords: "ã‚ーワード" + notifiedUser: "通知先ユーザー" + notifiedWebhook: "使用ã™ã‚‹Webhook" + deleteConfirm: "通知先を削除ã—ã¾ã™ã‹ï¼Ÿ" _moderationLogTypes: createRole: "ãƒãƒ¼ãƒ«ã‚’作æˆ" @@ -2594,6 +2642,97 @@ _moderationLogTypes: deleteAvatarDecoration: "アイコンデコレーションを削除" unsetUserAvatar: "ユーザーã®ã‚¢ã‚¤ã‚³ãƒ³ã‚’解除" unsetUserBanner: "ユーザーã®ãƒãƒŠãƒ¼ã‚’解除" + createSystemWebhook: "SystemWebhookを作æˆ" + updateSystemWebhook: "SystemWebhookã‚’æ›´æ–°" + deleteSystemWebhook: "SystemWebhookを削除" + createAbuseReportNotificationRecipient: "é€šå ±ã®é€šçŸ¥å…ˆã‚’作æˆ" + updateAbuseReportNotificationRecipient: "é€šå ±ã®é€šçŸ¥å…ˆã‚’æ›´æ–°" + deleteAbuseReportNotificationRecipient: "é€šå ±ã®é€šçŸ¥å…ˆã‚’削除" + deleteAccount: "アカウントを削除" + deletePage: "ページを削除" + 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: "ファイルã®è©³ç´°" @@ -2650,27 +2789,27 @@ _animatedMFM: play: "MFMアニメーションをå†ç”Ÿ" stop: "MFMアニメーションåœæ¢" _alert: - text: "MFMアニメーションã«ã¯ã€ç‚¹æ»…ã™ã‚‹ãƒ©ã‚¤ãƒˆã‚„高速ã§å‹•ãテã‚スト/絵文å—ã‚’å«ã¾ã‚Œã‚‹å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" + text: "MFMアニメーションã«ã¯ã€é«˜é€Ÿã§ç‚¹æ»…ã—ãŸã‚Šå‹•ã„ãŸã‚Šã™ã‚‹ãƒ†ã‚スト・絵文å—ã‚’å«ã‚€å ´åˆãŒã‚ã‚Šã¾ã™ã€‚" confirm: "å†ç”Ÿã™ã‚‹" _dataRequest: title: "データリクエスト" warn: "データリクエストã¯3æ—¥ã”ã¨ã«å¯èƒ½ã§ã™ã€‚" - text: "データã®ä¿å˜ãŒå®Œäº†ã™ã‚‹ã¨ã€ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç™»éŒ²ã•ã‚Œã¦ã„ã‚‹Eメールアドレスã«ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚" - button: "リクエスト" + text: "データã®ä¿å˜ãŒå®Œäº†ã™ã‚‹ã¨ã€ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç™»éŒ²ã•ã‚Œã¦ã„るメールアドレスã«ãƒ¡ãƒ¼ãƒ«ãŒé€ä¿¡ã•ã‚Œã¾ã™ã€‚" + button: "データリクエスト実行" _dataSaver: _media: - title: "メディアã®èªã¿è¾¼ã¿" + title: "メディアã®èªã¿è¾¼ã¿ã‚’無効化" description: "ç”»åƒãƒ»å‹•ç”»ãŒè‡ªå‹•ã§èªã¿è¾¼ã¾ã‚Œã‚‹ã®ã‚’防æ¢ã—ã¾ã™ã€‚éš ã‚Œã¦ã„ã‚‹ç”»åƒãƒ»å‹•ç”»ã¯ã‚¿ãƒƒãƒ—ã™ã‚‹ã¨èªã¿è¾¼ã¾ã‚Œã¾ã™ã€‚" _avatar: - title: "アイコン画åƒ" + title: "アイコン画åƒã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ã‚’無効化" description: "アイコン画åƒã®ã‚¢ãƒ‹ãƒ¡ãƒ¼ã‚·ãƒ§ãƒ³ãŒåœæ¢ã—ã¾ã™ã€‚アニメーション画åƒã¯é€šå¸¸ã®ç”»åƒã‚ˆã‚Šãƒ•ã‚¡ã‚¤ãƒ«ã‚µã‚¤ã‚ºãŒå¤§ãã„ã“ã¨ãŒã‚ã‚‹ã®ã§ã€ãƒ‡ãƒ¼ã‚¿é€šä¿¡é‡ã‚’ã•ã‚‰ã«å‰Šæ¸›ã§ãã¾ã™ã€‚" _urlPreview: - title: "URLプレビューã®ã‚µãƒ ãƒã‚¤ãƒ«" + title: "URLプレビューã®ã‚µãƒ ãƒã‚¤ãƒ«ã‚’éžè¡¨ç¤º" description: "URLプレビューã®ã‚µãƒ ãƒã‚¤ãƒ«ç”»åƒãŒèªã¿è¾¼ã¾ã‚Œãªããªã‚Šã¾ã™ã€‚" _code: - title: "コードãƒã‚¤ãƒ©ã‚¤ãƒˆ" + title: "コードãƒã‚¤ãƒ©ã‚¤ãƒˆã‚’éžè¡¨ç¤º" description: "MFMãªã©ã§ã‚³ãƒ¼ãƒ‰ãƒã‚¤ãƒ©ã‚¤ãƒˆè¨˜æ³•ãŒä½¿ã‚ã‚Œã¦ã„ã‚‹å ´åˆã€ã‚¿ãƒƒãƒ—ã™ã‚‹ã¾ã§èªã¿è¾¼ã¾ã‚Œãªããªã‚Šã¾ã™ã€‚コードãƒã‚¤ãƒ©ã‚¤ãƒˆã§ã¯ãƒã‚¤ãƒ©ã‚¤ãƒˆã™ã‚‹è¨€èªžã”ã¨ã«ãã®å®šç¾©ãƒ•ã‚¡ã‚¤ãƒ«ã‚’èªã¿è¾¼ã‚€å¿…è¦ãŒã‚ã‚Šã¾ã™ãŒã€ãれらãŒè‡ªå‹•ã§èªã¿è¾¼ã¾ã‚Œãªããªã‚‹ãŸã‚ã€é€šä¿¡é‡ã®å‰Šæ¸›ãŒè¦‹è¾¼ã‚ã¾ã™ã€‚" _hemisphere: @@ -2746,3 +2885,9 @@ _mediaControls: pip: "ピクãƒãƒ£ã‚¤ãƒ³ãƒ”クãƒãƒ£" playbackRate: "å†ç”Ÿé€Ÿåº¦" loop: "ループå†ç”Ÿ" + +_contextMenu: + title: "コンテã‚ストメニュー" + app: "アプリケーション" + appWithShift: "Shiftã‚ーã§ã‚¢ãƒ—リケーション" + native: "ブラウザã®UI" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 641425aa17e92612b5688cf7acaed8b45e1a2b57..448355eb4e4668529ba8742fc7f70020cac74cad 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -60,6 +60,7 @@ copyFileId: "ファイルIDをコピー" copyFolderId: "フォルダーIDをコピー" copyProfileUrl: "プãƒãƒ•ã‚£ãƒ¼ãƒ«URLをコピー" searchUser: "ユーザーを探ã™" +searchThisUsersNotes: "ユーザーã®ãƒŽãƒ¼ãƒˆã‚’検索" reply: "返事" loadMore: "ã¾ã ã¾ã ã‚ã‚‹ã§ï¼" showMore: "ã¾ã ã¾ã ã‚ã‚‹ã§ï¼" @@ -105,14 +106,17 @@ followRequests: "フォãƒãƒ¼ç”³è«‹" unfollow: "フォãƒãƒ¼ã‚„ã‚ã‚‹" followRequestPending: "フォãƒãƒ¼è¨±ã—ã¦ãれるん待ã£ã¨ã‚‹" enterEmoji: "絵文å—を入れã¦ã‚„" -renote: "ブースト" -unrenote: "ブーストやã‚ã‚‹" -renoted: "ブーストã—ãŸã§ã€‚" -cantRenote: "ã“ã®æŠ•ç¨¿ã¯ãƒ–ーストã§ãã¸ã‚“ã£ã½ã„。" -cantReRenote: "ブースト自体ã¯ãƒ–ーストã§ãã¸ã‚“ã§ã€‚" +renote: "リノート" +unrenote: "リノートやã‚ã‚‹" +renoted: "リノートã—ãŸã§ã€‚" +renotedToX: "{name}ã«ãƒªãƒŽãƒ¼ãƒˆã—ãŸã§" +cantRenote: "ã“ã®æŠ•ç¨¿ã¯ãƒªãƒŽãƒ¼ãƒˆã§ãã¸ã‚“ã£ã½ã„。" +cantReRenote: "リノート自体ã¯ãƒªãƒŽãƒ¼ãƒˆã§ãã¸ã‚“ã§ã€‚" quote: "引用" inChannelRenote: "ãƒãƒ£ãƒ³ãƒãƒ«ã®ä¸ã§ãƒ–ースト" inChannelQuote: "ãƒãƒ£ãƒ³ãƒãƒ«å†…引用" +renoteToChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒªãƒŽãƒ¼ãƒˆ" +renoteToOtherChannel: "ä»–ã®ãƒãƒ£ãƒ³ãƒãƒ«ã«ãƒªãƒŽãƒ¼ãƒˆ" pinnedNote: "ピン留ã‚ã•ã‚Œã¨ã‚‹ãƒŽãƒ¼ãƒˆ" pinned: "ピン留ã‚ã—ã¨ã" you: "ã‚ã‚“ãŸ" @@ -151,6 +155,7 @@ editList: "リストã„ã˜ã‚‹" selectChannel: "ãƒãƒ£ãƒ³ãƒãƒ«ã‚’é¸ã¶" selectAntenna: "アンテナをé¸ã¶" editAntenna: "アンテナã„ã˜ã‚‹" +createAntenna: "アンテナを作æˆ" selectWidget: "ウィジェットをé¸ã¶" editWidgets: "ウィジェットをã„ã˜ã‚‹" editWidgetsExit: "ã„ã˜ã‚‹ã®ã‚’ã‚„ã‚ã‚‹" @@ -179,6 +184,10 @@ addAccount: "ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’è¿½åŠ " reloadAccountsList: "アカウントリストã®æƒ…å ±ã‚’æ›´æ–°" loginFailed: "ãƒã‚°ã‚¤ãƒ³ã«å¤±æ•—ã—ã¦ã‚‚ã†ãŸâ€¦" showOnRemote: "リモートã§è¦‹ã‚‹" +continueOnRemote: "リモートã§ç¶šè¡Œ" +chooseServerOnMisskeyHub: "Misskey Hubã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã‚’é¸æŠž" +specifyServerHost: "サーãƒãƒ¼ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‚’直接指定" +inputHostName: "ドメインを入力ã›ãˆã‚„" general: "全般" wallpaper: "å£ç´™" setWallpaper: "å£ç´™ã‚’è¨å®š" @@ -189,6 +198,7 @@ followConfirm: "{name}をフォãƒãƒ¼ã—ã¦ãˆãˆã‹ï¼Ÿ" proxyAccount: "プãƒã‚シアカウント" proxyAccountDescription: "プãƒã‚シアカウントã¯ã€ä»£ã‚ã‚Šã«ãƒ•ã‚©ãƒãƒ¼ã—ã¦ãれるアカウントや。例ãˆã°ã€551ã«è±šã¾ã‚“ãŒç„¡ã„ã¨ãã‚„ã£ãŸã‚Šã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’アカウントã«å…¥ã‚ŒãŸã¨ãã€ãƒªã‚¹ãƒˆã«å…¥ã‚Œã‚‰ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒèª°ã‹ã‚‰ã‚‚フォãƒãƒ¼ã•ã‚Œã¦ãªã„ã¨å¯‚ã—ã„やん。寂ã—ã„ã—ã€ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティもé…é”ã•ã‚Œã¸ã‚“ã‹ã‚‰ã€ãƒ—ãƒã‚シアカウントãŒãƒ•ã‚©ãƒãƒ¼ã—ã¦ãれるã§ã€‚ãˆãˆã‚„ã¤ã‚„ん…" host: "ホスト" +selectSelf: "自分をé¸æŠž" selectUser: "ユーザーをé¸ã¶" recipient: "宛先" annotation: "注釈" @@ -204,6 +214,7 @@ perDay: "1æ—¥ã”ã¨" stopActivityDelivery: "アクティビティã®é…é€ã‚’ã‚„ã‚ã‚‹" blockThisInstance: "ã“ã®ã‚µãƒ¼ãƒãƒ¼ã‚’ブãƒãƒƒã‚¯ã™ã‚“ã§" silenceThisInstance: "サーãƒãƒ¼ã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã™ã‚“ã§ï¼Ÿ" +mediaSilenceThisInstance: "サーãƒãƒ¼ã‚’メディアサイレンス" operations: "æ“作" software: "ソフトウェア" version: "ãƒãƒ¼ã‚¸ãƒ§ãƒ³" @@ -225,6 +236,8 @@ blockedInstances: "ブãƒãƒƒã‚¯ã—ãŸã‚µãƒ¼ãƒãƒ¼" blockedInstancesDescription: "ブãƒãƒƒã‚¯ã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã—ã¦ãªã€‚ブãƒãƒƒã‚¯ã•ã‚Œã¦ã‚‚ã†ãŸã‚µãƒ¼ãƒãƒ¼ã¨ã¯ã‚‚ã†é‡‘輪際やりå–ã‚Šã§ãã²ã‚“ããªã‚‹ã§ã€‚" silencedInstances: "サーãƒãƒ¼ã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã•ã‚Œã¦ã‚“ãã‚“" silencedInstancesDescription: "サイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã™ã‚“ã§ã€‚サイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ã™ã¹ã¦ã€Œã‚µã‚¤ãƒ¬ãƒ³ã‚¹ã€ã¨ã—ã¦æ‰±ã‚ã‚Œã€ãƒ•ã‚©ãƒãƒ¼ãŒã™ã¹ã¦ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«ãªã‚Šã€ãƒ•ã‚©ãƒãƒ¯ãƒ¼ã§ãªã„ãƒãƒ¼ã‚«ãƒ«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã¯ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã§ããªããªã‚“ãん。ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã›ãƒ¼ã¸ã‚“ã§ã€‚" +mediaSilencedInstances: "メディアサイレンスã—ãŸã‚µãƒ¼ãƒãƒ¼" +mediaSilencedInstancesDescription: "メディアサイレンスã—ãŸã„サーãƒãƒ¼ã®ãƒ›ã‚¹ãƒˆã‚’改行ã§åŒºåˆ‡ã£ã¦è¨å®šã™ã‚‹ã§ã€‚メディアサイレンスã•ã‚ŒãŸã‚µãƒ¼ãƒãƒ¼ã«æ‰€å±žã™ã‚‹ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚ˆã‚‹ãƒ•ã‚¡ã‚¤ãƒ«ã¯ã™ã¹ã¦ã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ã¨ã—ã¦æ‰±ã‚ã‚Œã¦ãªã€ã‚«ã‚¹ã‚¿ãƒ 絵文å—ãŒä½¿ãˆã¸ã‚“よã†ã«ãªã‚‹ã§ã€‚ブãƒãƒƒã‚¯ã—ãŸã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã«ã¯å½±éŸ¿ã›ãˆã¸ã‚“ã§ã€‚" muteAndBlock: "ミュートã¨ãƒ–ãƒãƒƒã‚¯" mutedUsers: "ミュートã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼" blockedUsers: "ブãƒãƒƒã‚¯ã—ã¨ã‚‹ãƒ¦ãƒ¼ã‚¶ãƒ¼" @@ -315,6 +328,7 @@ selectFile: "ファイルé¸ã‚“ã§ã‚„" selectFiles: "ファイルé¸ã‚“ã§ã‚„" selectFolder: "フォルダé¸ã‚“ã§ã‚„" selectFolders: "フォルダé¸ã‚“ã§ã‚„" +fileNotSelected: "ファイルãŒé¸æŠžã•ã‚Œã¦ã¸ã‚“ã§" renameFile: "ファイルåã‚’ã„らã†" folderName: "フォルダーå" createFolder: "フォルダー作る" @@ -470,10 +484,12 @@ retype: "ã‚‚ã£ã‹ã„入力" noteOf: "{user}ã¯ã‚“ã®ãƒŽãƒ¼ãƒˆ" quoteAttached: "引用付ã„ã¨ã‚‹ã§" quoteQuestion: "引用ã¨ã—ã¦æ·»ä»˜ã—ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" +attachAsFileQuestion: "クリップボードã®ãƒ†ã‚ストãŒé•·ã™ãŽã‚‹ã‹ã‚‰ãƒ†ã‚ストファイルã¨ã—ã¦æ·»ä»˜ã—ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" noMessagesYet: "ã¾ã ãƒãƒ£ãƒƒãƒˆã¯ã‚らã¸ã‚“ã§" newMessageExists: "æ–°ã—ã„メッセージãŒããŸã§" onlyOneFileCanBeAttached: "ã”ã‚ã‚“ãªã€ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã«æ·»ä»˜ã§ãるファイルã¯ã²ã¨ã¤ã ã‘ãªã‚“よ。" signinRequired: "ãƒã‚°ã‚¤ãƒ³ã—ã¦ãã‚Œã¸ã‚“?" +signinOrContinueOnRemote: "続行ã™ã‚‹ã«ã¯ã€ãŠä½¿ã„ã®ã‚µãƒ¼ãƒãƒ¼ã«ç§»å‹•ã™ã‚‹ã‹ã€ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«ç™»éŒ²ãƒ»ãƒã‚°ã‚¤ãƒ³ã™ã‚‹å¿…è¦ãŒã‚ã‚‹ã§" invitations: "æ¥ã¦ã‚„" invitationCode: "招待コード" checking: "確èªã—ã¨ã‚‹ã§" @@ -834,6 +850,7 @@ administration: "管ç†" accounts: "アカウント" switch: "切り替ãˆ" noMaintainerInformationWarning: "管ç†è€…æƒ…å ±ãŒè¨å®šã•ã‚Œã¦ã¸ã‚“ã§" +noInquiryUrlWarning: "å•ã„åˆã‚ã›å…ˆURLãŒè¨å®šã•ã‚Œã¦ã¸ã‚“ã§ã€‚" noBotProtectionWarning: "Botプãƒãƒ†ã‚¯ã‚·ãƒ§ãƒ³ãŒè¨å®šã•ã‚Œã¦ã¸ã‚“ã§ã€‚" configure: "è¨å®šã™ã‚‹" postToGallery: "ギャラリーã¸æŠ•ç¨¿" @@ -1023,6 +1040,7 @@ thisPostMayBeAnnoyingHome: "ホームã«æŠ•ç¨¿" thisPostMayBeAnnoyingCancel: "ã‚„ã‚ã¨ã" thisPostMayBeAnnoyingIgnore: "ã“ã®ã¾ã¾æŠ•ç¨¿" collapseRenotes: "見ãŸã“ã¨ã‚るブーストã¯é£›ã°ã—ã¦è¡¨ç¤ºã™ã‚‹ã§" +collapseRenotesDescription: "リアクションやブーストをã—ãŸã“ã¨ãŒã‚るノートをãŸãŸã‚“ã§è¡¨ç¤ºã™ã‚‹ã§ã€‚" internalServerError: "サーãƒãƒ¼å†…部エラー" internalServerErrorDescription: "サーãƒãƒ¼ã§ãªã‚“ã‹å¤‰ãªã“ã¨èµ·ã“ã£ã¨ã‚‹ã‚。" copyErrorInfo: "ã‚¨ãƒ©ãƒ¼æƒ…å ±ã‚’ã‚³ãƒ”ã‚‹ã§" @@ -1096,6 +1114,8 @@ preservedUsernames: "予約ユーザーå" preservedUsernamesDescription: "予約ã—ã¨ãユーザーåã‚’è¡Œã”ã¨ã«æŒ™ã’ã‚‹ã§ã€‚ã“ã“ã§æŒ‡å®šã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼åã¯ã‚¢ã‚«ã‚¦ãƒ³ãƒˆä½œã‚‹ã¨ãã«ä½¿ãˆã¸ã‚“ããªã‚‹ã‘ã©ã€ç®¡ç†è€…ã¯ä¾‹å¤–や。ã‚ã¨ã€ã‚‚ã†ã‚るアカウントも例外やãªã€‚" createNoteFromTheFile: "ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ä½¿ã†ã¦ãƒŽãƒ¼ãƒˆä½œã‚‹ã§" archive: "アーカイブ" +archived: "アーカイブ済ã¿" +unarchive: "アーカイブ解除" channelArchiveConfirmTitle: "{name}をアーカイブã—ã¦ãˆãˆã‹ï¼Ÿ" channelArchiveConfirmDescription: "アーカイブã—ãŸã‚‰ã€ãƒãƒ£ãƒ³ãƒãƒ«ä¸€è¦§ã¨ã‹æ¤œç´¢çµæžœã‹ã‚‰ãªããªã‚‹ã—ã€æ–°ã—ã書ãè¾¼ã¿ã‚‚ã§ãã¸ã‚“ãªã‚‹ã§ã€‚" thisChannelArchived: "ã“ã®ãƒãƒ£ãƒ³ãƒãƒ«ã€ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã•ã‚Œã¨ã‚‹ã§ã€‚" @@ -1106,6 +1126,9 @@ preventAiLearning: "生æˆAIã®å¦ç¿’ã«ä½¿ã‚ã‚“ã¨ã„ã¦" preventAiLearningDescription: "ä»–ã®æ–‡ç« 生æˆAIã¨ã‹ç”»åƒç”ŸæˆAIã«ã€æŠ•ç¨¿ã—ãŸãƒŽãƒ¼ãƒˆã¨ã‹ç”»åƒãªã‚“ã‹ã‚’å‹æ‰‹ã«ä½¿ã‚んよã†ã«é ¼ã‚€ã§ã€‚具体的ã«ã¯noaiフラグをHTMLレスãƒãƒ³ã‚¹ã«å«ã‚ã‚‹ã‚“ã‚„ã‘ã©ã€ã“ã‚Œèžã„ã¦ãれるんã¯AIã®æ°—分次第やã‹ã‚‰ã€ä½¿ã‚れるå¯èƒ½æ€§ã‚‚ã¡ã‚‡ã£ã¨ã¯ã‚ã‚‹ãªã€‚" options: "オプション" specifyUser: "ユーザー指定" +lookupConfirm: "照会ã™ã‚‹ã‘ã©ãˆãˆã‹ï¼Ÿ" +openTagPageConfirm: "ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°ã®ãƒšãƒ¼ã‚¸ã‚’é–‹ãã‚“ã‹ï¼Ÿ" +specifyHost: "ホスト指定" failedToPreviewUrl: "プレビューã§ãã¸ã‚“" update: "æ›´æ–°" rolesThatCanBeUsedThisEmojiAsReaction: "ツッコミã¨ã—ã¦ä½¿ãˆã‚‹ãƒãƒ¼ãƒ«" @@ -1237,10 +1260,20 @@ keepOriginalFilenameDescription: "ã“ã®è¨å®šã‚’オフã«ã™ã‚‹ã¨ã€ã‚¢ãƒƒãƒ— noDescription: "説明文ã¯ã‚らã¸ã‚“ã§" alwaysConfirmFollow: "フォãƒãƒ¼ã®éš›å¸¸ã«ç¢ºèªã™ã‚‹" inquiry: "å•ã„åˆã‚ã›" +tryAgain: "ã‚‚ã†ä¸€åº¦è©¦ã—ã„や。" +confirmWhenRevealingSensitiveMedia: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã‚’表示ã™ã‚‹ã¨ã確èªã™ã‚‹" +sensitiveMediaRevealConfirm: "センシティブãªãƒ¡ãƒ‡ã‚£ã‚¢ã‚„ã§ã€‚表示ã™ã‚‹ã‚“ã‹ï¼Ÿ" +createdLists: "作æˆã—ãŸãƒªã‚¹ãƒˆ" +createdAntennas: "作æˆã—ãŸã‚¢ãƒ³ãƒ†ãƒŠ" _delivery: + status: "é…信状態" stop: "é…ä¿¡ã›ã‡ã¸ã‚“" + resume: "é…ä¿¡å†é–‹" _type: none: "é…ä¿¡ã—ã¨ã‚‹" + manuallySuspended: "手動åœæ¢ä¸" + goneSuspended: "サーãƒãƒ¼å‰Šé™¤ã®ãŸã‚åœæ¢ä¸" + autoSuspendedForNotResponding: "サーãƒãƒ¼å¿œç”ã›ãˆã¸ã‚“ã‹ã‚‰åœæ¢ä¸" _bubbleGame: howToPlay: "éŠã³æ–¹" hold: "ホールド" @@ -1366,6 +1399,8 @@ _serverSettings: fanoutTimelineDescription: "入れるã¨ã€ãŠã®ãŠã®ã‚¿ã‚¤ãƒ ラインをå–å¾—ã™ã‚‹ã¨ãã«ã‚ã¡ã‚ƒã‚ã¡ã‚ƒå‹•ããŒè‰¯ã†ãªã£ã¦ã€ãƒ‡ãƒ¼ã‚¿ãƒ™ãƒ¼ã‚¹ãŒè»½ããªã‚‹ã‚。ã§ã‚‚ã€Redisã®ãƒ¡ãƒ¢ãƒªä½¿ã†é‡ãŒå¢—ãˆã‚‹ã‹ã‚‰æ³¨æ„ãªã€‚サーãƒãƒ¼ã®ãƒ¡ãƒ¢ãƒªãŒè¶³ã‚Šã‚“ã¨ãã¨ã‹ã€å‹•ããŒå¤‰ãªã¨ãã¯åˆ‡ã‚Œã‚‹ã§ã€‚" fanoutTimelineDbFallback: "データベースã«ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ã™ã‚‹" fanoutTimelineDbFallbackDescription: "有効ã«ã—ãŸã‚‰ã€ã‚¿ã‚¤ãƒ ラインãŒã‚ャッシュんä¸ã«å…¥ã£ã¦ãªã„ã¨ãã«DBã«ã‚‚ã£ã‹ã„å•ã„åˆã‚ã›ã‚‹ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã£ã¦ã®ã‚’ã‚„ã£ã¨ãã§ã€‚切ã£ãŸã‚‰ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯å‡¦ç†ã‚’やらんã‹ã‚‰ã‚µãƒ¼ãƒãƒ¼ã¯ã‚‚ã£ã¨è»½ããªã‚“ãã‚“ã‘ã©ã€ã‚¿ã‚¤ãƒ ラインã®å–得範囲ãŒã¡ã‚‡ã£ã¨æ¸›ã‚‹ã§ã€‚" + inquiryUrl: "å•ã„åˆã‚ã›å…ˆURL" + inquiryUrlDescription: "サーãƒãƒ¼é‹å–¶è€…ã¸ã®ãŠå•ã„åˆã‚ã›ãƒ•ã‚©ãƒ¼ãƒ ã®URLã‚„ã€é‹å–¶è€…ã®é€£çµ¡å…ˆç‰ãŒè¨˜è¼‰ã•ã‚ŒãŸWebページã®URLを指定ã™ã‚‹ã§ã€‚" _accountMigration: moveFrom: "別ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‹ã‚‰ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«å¼•ã£è¶Šã™" moveFromSub: "別ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¸ã‚¨ã‚¤ãƒªã‚¢ã‚¹ã‚’作る" @@ -1682,6 +1717,7 @@ _role: canManageAvatarDecorations: "ã‚¢ãƒã‚¿ãƒ¼ã‚’飾るモンã®ç®¡ç†" driveCapacity: "ドライブ容é‡" alwaysMarkNsfw: "å‹æ‰‹ã«ãƒ•ã‚¡ã‚¤ãƒ«ã«NSFWã‚’ãã£ã¤ã‘ã‚‹" + canUpdateBioMedia: "アイコンã¨ãƒãƒŠãƒ¼ã®æ›´æ–°ã‚’許å¯" pinMax: "ノートピン留ã‚ã§ãã‚‹æ•°" antennaMax: "アンテナ作れる数" wordMuteMax: "ワードミュートã®æœ€å¤§æ–‡å—æ•°" @@ -1925,8 +1961,6 @@ _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - antenna: "アンテナå—ä¿¡" - channel: "ãƒãƒ£ãƒ³ãƒãƒ«é€šçŸ¥" reaction: "ツッコミé¸ã‚“ã©ã‚‹ã¨ã" _soundSettings: driveFile: "ドライブんä¸ã®éŸ³ä½¿ã†" @@ -1935,6 +1969,7 @@ _soundSettings: driveFileTypeWarnDescription: "音声ファイルをé¸ã³ã‚„" driveFileDurationWarn: "音ãŒé•·ã™ãŽã‚‹ã‚" driveFileDurationWarnDescription: "é•·ã„音使ã†ãŸã‚‰Sharkey使ã†ã®ã«è‰¯ã†ãªã„ã‹ã‚‚ã—ã‚Œã¸ã‚“ã§ã€‚ãã‚Œã§ã‚‚ãˆãˆã‹ï¼Ÿ" + driveFileError: "音声ãŒèªã¿è¾¼ã‚ã¸ã‚“ã‹ã£ãŸã§ã€‚è¨å®šã‚’変更ã›ãˆã‚„" _ago: future: "未æ¥" justNow: "ã¤ã„ã•ã£ã" @@ -2351,6 +2386,7 @@ _deck: alwaysShowMainColumn: "ã„ã¤ã‚‚メインカラムを表示" columnAlign: "カラムã®å¯„ã›" addColumn: "ã‚«ãƒ©ãƒ ã‚’è¿½åŠ " + newNoteNotificationSettings: "æ–°ç€ãƒŽãƒ¼ãƒˆé€šçŸ¥ã®è¨å®š" configureColumn: "カラムã®è¨å®š" swapLeft: "å·¦ã«ç§»å‹•" swapRight: "å³ã«ç§»å‹•" @@ -2389,9 +2425,10 @@ _drivecleaner: orderByCreatedAtAsc: "è¿½åŠ æ—¥ã®å¤ã„é †" _webhookSettings: createWebhook: "Webhookã‚’ã¤ãã‚‹" + modifyWebhook: "Webhookを編集" name: "åå‰" secret: "シークレット" - events: "Webhookを投ã’るタイミング" + trigger: "トリガー" active: "有効" _events: follow: "フォãƒãƒ¼ã—ãŸã¨ã~ï¼" @@ -2401,6 +2438,26 @@ _webhookSettings: renote: "ブーストã•ã‚Œã‚‹ã¨ã~ï¼" reaction: "ツッコã¾ã‚ŒãŸã¨ã~ï¼" mention: "メンションãŒã‚ã‚‹ã¨ã~ï¼" + _systemEvents: + abuseReport: "ユーザーã‹ã‚‰é€šå ±ãŒã‚ã£ãŸã¨ã" + abuseReportResolved: "ユーザーã‹ã‚‰ã®é€šå ±ã‚’処ç†ã—ãŸã¨ã" + userCreated: "ユーザーãŒä½œæˆã•ã‚ŒãŸã¨ã" + deleteConfirm: "ã»ã‚“ã¾ã«Webhookã‚’ã»ã‹ã—ã¦ã‚‚ãˆãˆã‚“ã‹ï¼Ÿ" +_abuseReport: + _notificationRecipient: + createRecipient: "é€šå ±ã®é€šçŸ¥å…ˆã‚’è¿½åŠ " + modifyRecipient: "é€šå ±ã®é€šçŸ¥å…ˆã‚’編集" + recipientType: "通知先ã®ç¨®é¡ž" + _recipientType: + mail: "メール" + webhook: "Webhook" + _captions: + mail: "モデレーター権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ¡ã‚¢ãƒ‰ã«é€šçŸ¥ã‚’é€ã‚‹ã§(é€šå ±ã‚’å—ã‘ãŸæ™‚ã®ã¿)" + webhook: "指定ã—ãŸSystemWebhookã«é€šçŸ¥ã‚’é€ã‚‹ã§(é€šå ±ã‚’å—ã‘ãŸæ™‚ã¨é€šå ±ã‚’解決ã—ãŸæ™‚ã«ãã‚Œãžã‚Œç™ºä¿¡)" + keywords: "ã‚ーワード" + notifiedUser: "通知先ユーザー" + notifiedWebhook: "使用ã™ã‚‹Webhook" + deleteConfirm: "通知先を削除ã—ã¦ã‚‚ãˆãˆã‹ï¼Ÿ" _moderationLogTypes: createRole: "ãƒãƒ¼ãƒ«ã‚’è¿½åŠ ã™ã‚“ã§" deleteRole: "ãƒãƒ¼ãƒ«ã»ã‹ã™" @@ -2438,6 +2495,8 @@ _moderationLogTypes: deleteAvatarDecoration: "アイコンデコレーションを削除" unsetUserAvatar: "ã“ã®åã®ã‚¢ã‚¤ã‚³ãƒ³å…ƒã«æˆ»ã™" unsetUserBanner: "ã“ã®åã®ãƒãƒŠãƒ¼å…ƒã«æˆ»ã™" + createSystemWebhook: "SystemWebhookを作æˆ" + updateSystemWebhook: "SystemWebhookã‚’æ›´æ–°" _fileViewer: title: "ファイルã®è©³ã—ã„æƒ…å ±" type: "ファイルã®ç¨®é¡ž" diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml index 22e24d3baac0f6f51bd684a234699a31b12b90be..d4aa36fa7093a77ebd83b271fdc059add4b5175d 100644 --- a/locales/kab-KAB.yml +++ b/locales/kab-KAB.yml @@ -104,3 +104,7 @@ _deck: _columns: notifications: "IlÉ£uyen" list: "Tibdarin" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Imayl" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 9466aff01f419c2f172fdcb8010c61eb40377c4c..9323ed2a26ae54927368c6e10d3477006f35e4b4 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -805,6 +805,10 @@ _deck: mentions: "받언 멘션" _webhookSettings: name: "ì´ëŸ¼" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "ì „ìžìš°íŽœ" _moderationLogTypes: suspend: "얼우기" deleteNote: "노트 ë‰ìºê¸°" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 69e1216ce48a4a95b4f8f3a88ed114284ea48da7..64bae5a9d7b27db0bbc6bb579b575dfb03960fbb 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -52,14 +52,14 @@ deleteAndEditConfirm: "ì´ ë…¸íŠ¸ë¥¼ ì‚ì œí•œ ë’¤ 다시 íŽ¸ì§‘í•˜ì‹œê² ìŠµë‹ˆ addToList: "ë¦¬ìŠ¤íŠ¸ì— ì¶”ê°€" addToAntenna: "ì•ˆí…Œë‚˜ì— ì¶”ê°€" sendMessage: "메시지 보내기" -copyRSS: "RSS 복사" -copyUsername: "ì‚¬ìš©ìž ì´ë¦„ 복사" -copyUserId: "ì‚¬ìš©ìž ID 복사" +copyRSS: "RSS 주소 복사" +copyUsername: "ìœ ì €ëª… 복사" +copyUserId: "ìœ ì € ID 복사" copyNoteId: "노트 ID 복사" copyFileId: "íŒŒì¼ ID 복사" copyFolderId: "í´ë” ID 복사" copyProfileUrl: "프로필 URL 복사" -searchUser: "ì‚¬ìš©ìž ê²€ìƒ‰" +searchUser: "ìœ ì € 검색" reply: "답글" loadMore: "ë” ë³´ê¸°" showMore: "ë” ë³´ê¸°" @@ -108,22 +108,25 @@ enterEmoji: "ì´ëª¨ì§€ ìž…ë ¥" renote: "리노트" unrenote: "리노트 취소" renoted: "리노트했습니다" +renotedToX: "{name}ëª…ì´ ë¦¬ë…¸íŠ¸í–ˆìŠµë‹ˆë‹¤." cantRenote: "ì´ ê²Œì‹œë¬¼ì€ ë¦¬ë…¸íŠ¸ í• ìˆ˜ 없습니다." -cantReRenote: "리노트를 ë¦¬ë…¸íŠ¸í• ìˆ˜ 없습니다." +cantReRenote: "리노트를 리노트 í• ìˆ˜ 없습니다." quote: "ì¸ìš©" inChannelRenote: "ì±„ë„ ë‚´ 리노트" inChannelQuote: "ì±„ë„ ë‚´ ì¸ìš©" +renoteToChannel: "채ë„ì— ë¦¬ë…¸íŠ¸" +renoteToOtherChannel: "다른 채ë„ì— ë¦¬ë…¸íŠ¸" pinnedNote: "ê³ ì •ëœ ë…¸íŠ¸" pinned: "ê³ ì •í•˜ê¸°" you: "나" clickToShow: "í´ë¦í•˜ì—¬ 보기" sensitive: "열람 주ì˜" add: "추가" -reaction: "ë°˜ì‘" -reactions: "ë°˜ì‘" +reaction: "리액션" +reactions: "리액션" emojiPicker: "ì´ëª¨ì§€ ì„ íƒê¸°" -pinnedEmojisForReactionSettingDescription: "ë¦¬ì•¡ì…˜ì„ í• ë•Œ í”„ë¡œí•„ì— ê³ ì •í•˜ì—¬ í‘œì‹œí• ì´ëª¨ì§€ë¥¼ ì„¤ì •í• ìˆ˜ 있습니다" -pinnedEmojisSettingDescription: "ì´ëª¨ì§€ë¥¼ ìž…ë ¥í• ë•Œ í”„ë¡œí•„ì— ê³ ì •í•˜ì—¬ í‘œì‹œí• ì´ëª¨ì§€ë¥¼ ì„¤ì •í• ìˆ˜ 있습니다" +pinnedEmojisForReactionSettingDescription: "ë¦¬ì•¡ì…˜ì„ í• ë•Œ ì´ëª¨ì§€ ì„ íƒê¸° ìƒë‹¨ì— í‘œì‹œí• ì´ëª¨ì§€ë¥¼ ì„¤ì •í• ìˆ˜ 있습니다." +pinnedEmojisSettingDescription: "ì´ëª¨ì§€ë¥¼ ìž…ë ¥í• ë•Œ ì´ëª¨ì§€ ì„ íƒê¸° ìƒë‹¨ì— í‘œì‹œí• ì´ëª¨ì§€ë¥¼ ì„¤ì •í• ìˆ˜ 있습니다." emojiPickerDisplay: "ì„ íƒê¸° 표시" overwriteFromPinnedEmojisForReaction: "리액션 ì„¤ì •ì„ ë®ì–´ì“°ê¸°" overwriteFromPinnedEmojis: "ì¼ë°˜ ì„¤ì •ì„ ë®ì–´ì“°ê¸°" @@ -136,7 +139,7 @@ unmarkAsSensitive: "ì—´ëžŒì£¼ì˜ í•´ì œ" enterFileName: "파ì¼ëª…ì„ ìž…ë ¥" mute: "뮤트" unmute: "뮤트 í•´ì œ" -renoteMute: "리노트 뮤트하기" +renoteMute: "리노트 뮤트" renoteUnmute: "리노트 뮤트 í•´ì œ" block: "차단" unblock: "차단 í•´ì œ" @@ -174,12 +177,16 @@ flagShowTimelineReplies: "타임ë¼ì¸ì— ë…¸íŠ¸ì˜ ë‹µê¸€ì„ í‘œì‹œí•˜ê¸°" flagShowTimelineRepliesDescription: "ì´ ì„¤ì •ì„ í™œì„±í™”í•˜ë©´ 타임ë¼ì¸ì— 다른 ìœ ì € ê°„ì˜ ë‹µê¸€ì„ í‘œì‹œí•©ë‹ˆë‹¤." autoAcceptFollowed: "팔로우 ì¤‘ì¸ ìœ ì €ë¡œë¶€í„°ì˜ íŒ”ë¡œìš° ìš”ì²ì„ ìžë™ 수ë½" addAccount: "ê³„ì • 추가" -reloadAccountsList: "ê³„ì • 리스트 ì •ë³´ ê°±ì‹ " +reloadAccountsList: "ê³„ì • ëª©ë¡ ìƒˆë¡œê³ ì¹¨" loginFailed: "로그ì¸ì— 실패했습니다" showOnRemote: "리모트ì—ì„œ 보기" +continueOnRemote: "리모트ì—ì„œ 계ì†" +chooseServerOnMisskeyHub: "Misskey Hubì—ì„œ 서버 찾아보기" +specifyServerHost: "서버 ë„ë©”ì¸ ì§ì ‘ ì§€ì •" +inputHostName: "ë„ë©”ì¸ì„ ìž…ë ¥í•˜ì„¸ìš”" general: "ì¼ë°˜" wallpaper: "ë°°ê²½" -setWallpaper: "배경화면 ì„¤ì •" +setWallpaper: "ë°°ê²½ ì„¤ì •" removeWallpaper: "ë°°ê²½ ì œê±°" searchWith: "검색: {q}" youHaveNoLists: "리스트가 없습니다" @@ -187,7 +194,7 @@ followConfirm: "{name}ë‹˜ì„ íŒ”ë¡œìš° í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" proxyAccount: "프ë¡ì‹œ ê³„ì •" proxyAccountDescription: "프ë¡ì‹œ ê³„ì •ì€ íŠ¹ì • ì¡°ê±´ 하ì—ì„œ ìœ ì €ì˜ ë¦¬ëª¨íŠ¸ 팔로우를 대행하는 ê³„ì •ìž…ë‹ˆë‹¤. 예를 들면, ìœ ì €ê°€ 리모트 ìœ ì €ë¥¼ ë¦¬ìŠ¤íŠ¸ì— ë„£ì—ˆì„ ë•Œ, ë¦¬ìŠ¤íŠ¸ì— ë“¤ì–´ê°„ ìœ ì €ë¥¼ ì•„ë¬´ë„ íŒ”ë¡œìš°í•œ ì ì´ ì—†ë‹¤ë©´ 액티비티가 서버로 배달ë˜ì§€ 않기 때문ì—, ëŒ€ì‹ í”„ë¡ì‹œ ê³„ì •ì´ í•´ë‹¹ ìœ ì €ë¥¼ 팔로우하ë„ë¡ í•©ë‹ˆë‹¤." host: "호스트" -selectUser: "ì‚¬ìš©ìž ì„ íƒ" +selectUser: "ìœ ì € ì„ íƒ" recipient: "ìˆ˜ì‹ ì¸" annotation: "ë‚´ìš©ì— ëŒ€í•œ 주ì„" federation: "ì—°í•©" @@ -230,7 +237,7 @@ noUsers: "ì•„ë¬´ë„ ì—†ìŠµë‹ˆë‹¤" editProfile: "프로필 ìˆ˜ì •" noteDeleteConfirm: "ì´ ë…¸íŠ¸ë¥¼ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" pinLimitExceeded: "ë” ì´ìƒ ê³ ì •í• ìˆ˜ 없습니다." -intro: "Misskeyì˜ ì„¤ì¹˜ë¥¼ 완료했습니다! ê´€ë¦¬ìž ê³„ì •ì„ ë§Œë“¤ì–´ 주세요." +intro: "Misskeyì˜ ì„¤ì¹˜ê°€ 완료ë˜ì—ˆìŠµë‹ˆë‹¤! ê´€ë¦¬ìž ê³„ì •ì„ ìƒì„±í•´ì£¼ì„¸ìš”." done: "완료" processing: "처리중" preview: "미리보기" @@ -247,7 +254,7 @@ publishing: "ë°°í¬ ì¤‘" notResponding: "ì‘답 ì—†ìŒ" instanceFollowing: "ì„œë²„ì˜ íŒ”ë¡œìž‰" instanceFollowers: "ì„œë²„ì˜ íŒ”ë¡œì›Œ" -instanceUsers: "ì„œë²„ì˜ ìœ ì €" +instanceUsers: "ì„œë²„ì˜ ì‚¬ìš©ìž" changePassword: "비밀번호 변경" security: "보안" retypedNotMatch: "ìž…ë ¥ì´ ì¼ì¹˜í•˜ì§€ 않습니다." @@ -263,12 +270,12 @@ lookup: "찾아보기" announcements: "공지사í•" imageUrl: "ì´ë¯¸ì§€ URL" remove: "ì‚ì œ" -removed: "ì‚ì œí•˜ì˜€ìŠµë‹ˆë‹¤" +removed: "ì‚ì œí–ˆìŠµë‹ˆë‹¤" removeAreYouSure: "\"{x}\" ì„(를) ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" deleteAreYouSure: "\"{x}\" ì„(를) ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" resetAreYouSure: "초기화 í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" areYouSure: "ê³„ì† ì§„í–‰í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" -saved: "ì €ìž¥í•˜ì˜€ìŠµë‹ˆë‹¤" +saved: "ì €ìž¥í–ˆìŠµë‹ˆë‹¤" messaging: "대화" upload: "업로드" keepOriginalUploading: "ì›ë³¸ ì´ë¯¸ì§€ë¥¼ ìœ ì§€" @@ -296,7 +303,7 @@ activity: "활ë™" images: "ì´ë¯¸ì§€" image: "ì´ë¯¸ì§€" birthday: "ìƒì¼" -yearsOld: "만 {age} 세" +yearsOld: "{age}세" registeredDate: "등ë¡ì¼" location: "장소" theme: "테마" @@ -313,6 +320,7 @@ selectFile: "íŒŒì¼ ì„ íƒ" selectFiles: "íŒŒì¼ ì„ íƒ" selectFolder: "í´ë” ì„ íƒ" selectFolders: "í´ë” ì„ íƒ" +fileNotSelected: "파ì¼ì„ ì„ íƒí•˜ì§€ 않았습니다" renameFile: "íŒŒì¼ ì´ë¦„ 변경" folderName: "í´ë” ì´ë¦„" createFolder: "í´ë” 만들기" @@ -370,7 +378,7 @@ inMb: "메가바ì´íŠ¸ 단위" bannerUrl: "배너 ì´ë¯¸ì§€ URL" backgroundImageUrl: "ë°°ê²½ ì´ë¯¸ì§€ URL" basicInfo: "기본 ì •ë³´" -pinnedUsers: "ê³ ì •ëœ ìœ ì €" +pinnedUsers: "ê³ ì •í•œ 사용ìž" pinnedUsersDescription: "\"발견하기\" 페ì´ì§€ ë“±ì— ê³ ì •í•˜ê³ ì‹¶ì€ ìœ ì €ë¥¼ í•œ ì¤„ì— í•œ 명씩 ì 습니다." pinnedPages: "ê³ ì •í•œ 페ì´ì§€" pinnedPagesDescription: "ì„œë²„ì˜ ëŒ€ë¬¸ì— ê³ ì •í•˜ê³ ì‹¶ì€ íŽ˜ì´ì§€ì˜ 경로를 í•œ ì¤„ì— í•˜ë‚˜ì”© ì 습니다." @@ -437,13 +445,13 @@ moderationNote: "ì¡°ì • 기ë¡" addModerationNote: "ì¡°ì • ê¸°ë¡ ì¶”ê°€í•˜ê¸°" moderationLogs: "모ë”ë ˆì´ì…˜ 로그" nUsersMentioned: "{n}ëª…ì´ ì–¸ê¸‰í•¨" -securityKeyAndPasskey: "보안 키 ë˜ëŠ” 패스 키" +securityKeyAndPasskey: "보안 키 ë˜ëŠ” 패스키" securityKey: "보안 키" lastUsed: "마지막 사용" lastUsedAt: "마지막 사용: {t}" unregister: "ë“±ë¡ í•´ì œ" passwordLessLogin: "비밀번호 ì—†ì´ ë¡œê·¸ì¸" -passwordLessLoginDescription: "비밀번호를 사용하지 ì•Šê³ ë³´ì•ˆ 키 ë˜ëŠ” 패스 키 등으로만 로그ì¸í•©ë‹ˆë‹¤." +passwordLessLoginDescription: "비밀번호 ì—†ì´ ë³´ì•ˆ 키 ë˜ëŠ” 패스키만 사용해서 로그ì¸í•©ë‹ˆë‹¤." resetPassword: "비밀번호 ìž¬ì„¤ì •" newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다" reduceUiAnimation: "UIì˜ ì• ë‹ˆë©”ì´ì…˜ì„ 줄ì´ê¸°" @@ -468,10 +476,12 @@ retype: "다시 ìž…ë ¥" noteOf: "{user}ì˜ ë…¸íŠ¸" quoteAttached: "ì¸ìš©í•¨" quoteQuestion: "ì¸ìš©í•´ì„œ ìž‘ì„±í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" +attachAsFileQuestion: "ë¶™ì—¬ë„£ìœ¼ë ¤ëŠ” ê¸€ì´ ë„ˆë¬´ ê¹ë‹ˆë‹¤. í…스트 파ì¼ë¡œ ì²¨ë¶€í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" noMessagesYet: "ì•„ì§ ëŒ€í™”ê°€ 없습니다" newMessageExists: "새 메시지가 있습니다" onlyOneFileCanBeAttached: "ë©”ì‹œì§€ì— ì²¨ë¶€í• ìˆ˜ 있는 파ì¼ì€ 하나까지입니다" signinRequired: "진행하기 ì „ì— ë¡œê·¸ì¸ì„ í•´ 주세요" +signinOrContinueOnRemote: "계ì†í•˜ë ¤ë©´ 사용하는 서버로 ì´ë™í•˜ê±°ë‚˜ ì´ ì„œë²„ì— ë¡œê·¸ì¸í•´ì•¼ 합니다." invitations: "초대" invitationCode: "초대 코드" checking: "확ì¸í•˜ëŠ” 중입니다" @@ -486,7 +496,7 @@ strongPassword: "ê°•í•œ 비밀번호" passwordMatched: "ì¼ì¹˜í•©ë‹ˆë‹¤" passwordNotMatched: "ì¼ì¹˜í•˜ì§€ 않습니다" signinWith: "{x}ë¡œ 로그ì¸" -signinFailed: "로그ì¸í• 수 없습니다. 사용ìžëª…ê³¼ 비밀번호를 확ì¸í•˜ì—¬ 주ì‹ì‹œì˜¤." +signinFailed: "로그ì¸í• 수 없습니다. ì‚¬ìš©ìž ì´ë¦„ê³¼ 비밀번호를 확ì¸í•´ 주ì‹ì‹œì˜¤." or: "혹ì€" language: "언어" uiLanguage: "UI 표시 언어" @@ -494,7 +504,7 @@ aboutX: "{x}ì— ëŒ€í•˜ì—¬" emojiStyle: "ì´ëª¨ì§€ 스타ì¼" native: "기본" disableDrawer: "드로어 메뉴를 사용하지 않기" -showNoteActionsOnlyHover: "노트 ì•¡ì…˜ ë²„íŠ¼ì„ ë§ˆìš°ìŠ¤ë¥¼ ì˜¬ë ¸ì„ ë•Œì—만 표시" +showNoteActionsOnlyHover: "마우스가 올ë¼ê°„ ë•Œì—만 노트 ë™ìž‘ ë²„íŠ¼ì„ í‘œì‹œí•˜ê¸°" showReactionsCount: "ë…¸íŠ¸ì˜ ë°˜ì‘ ìˆ˜ë¥¼ 표시하기" noHistory: "기ë¡ì´ 없습니다" signinHistory: "ë¡œê·¸ì¸ ê¸°ë¡" @@ -559,7 +569,7 @@ popout: "새 창으로 열기" volume: "ìŒëŸ‰" masterVolume: "마스터 볼륨" notUseSound: "ìŒì†Œê±° 하기" -useSoundOnlyWhenActive: "Misskeyê°€ 활성화 ë˜ì–´ì ¸ ìžˆì„ ë•Œë§Œ 소리 ì¶œë ¥í•˜ê¸°" +useSoundOnlyWhenActive: "Misskey를 활성화한 ë•Œì—만 소리를 ì¶œë ¥í•˜ê¸°" details: "ìžì„¸ížˆ" chooseEmoji: "ì´ëª¨ì§€ ì„ íƒ" unableToProcess: "ìž‘ì—…ì„ ì™„ë£Œí• ìˆ˜ 없습니다" @@ -588,7 +598,7 @@ deleteAllFiles: "ëª¨ë“ íŒŒì¼ ì‚ì œ" deleteAllFilesConfirm: "ëª¨ë“ íŒŒì¼ì„ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" removeAllFollowing: "ëª¨ë“ íŒ”ë¡œìž‰ í•´ì œ" removeAllFollowingDescription: "{host} ì„œë²„ì˜ ëª¨ë“ íŒ”ë¡œìž‰ì„ í•´ì œí•©ë‹ˆë‹¤. 해당 서버가 ë” ì´ìƒ 존재하지 않는 경우 ë“±ì— ì‹¤í–‰í•´ 주세요." -userSuspended: "ì´ ê³„ì •ì€ ì •ì§€ëœ ìƒíƒœìž…니다." +userSuspended: "ì´ ì‚¬ìš©ìžëŠ” ì •ì§€ë˜ì—ˆìŠµë‹ˆë‹¤." userSilenced: "ì´ ê³„ì •ì€ ì‚¬ì¼ëŸ°ìŠ¤ëœ ìƒíƒœìž…니다." yourAccountSuspendedTitle: "ê³„ì •ì´ ì •ì§€ë˜ì—ˆìŠµë‹ˆë‹¤" yourAccountSuspendedDescription: "ì´ ê³„ì •ì€ ì„œë²„ì˜ ì´ìš© ì•½ê´€ì„ ìœ„ë°˜í•˜ê±°ë‚˜, 기타 다른 ì´ìœ ë¡œ ì¸í•´ ì •ì§€ë˜ì—ˆìŠµë‹ˆë‹¤. ìžì„¸í•œ 사í•ì€ 관리ìžì—게 문ì˜í•´ 주ì‹ì‹œì˜¤. ê³„ì •ì„ ìƒˆë¡œ ìƒì„±í•˜ì§€ 마ì‹ì‹œì˜¤." @@ -752,7 +762,7 @@ experimentalFeatures: "실험실" experimental: "실험실" thisIsExperimentalFeature: "ì´ ê¸°ëŠ¥ì€ ì‹¤í—˜ì ì¸ ê¸°ëŠ¥ìž…ë‹ˆë‹¤. ì‚¬ì–‘ì´ ë³€ê²½ë˜ê±°ë‚˜ ì •ìƒì 으로 ë™ìž‘하지 ì•Šì„ ê°€ëŠ¥ì„±ì´ ìžˆìŠµë‹ˆë‹¤." developer: "개발ìž" -makeExplorable: "\"발견하기\"ì— ë‚´ ê³„ì • ë³´ì´ê¸°" +makeExplorable: "ê³„ì •ì„ ì‰½ê²Œ 발견하ë„ë¡ í•˜ê¸°" makeExplorableDescription: "비활성화하면 \"발견하기\"ì— ë‚˜ì˜ ê³„ì •ì„ í‘œì‹œí•˜ì§€ 않습니다." showGapBetweenNotesInTimeline: "타임ë¼ì¸ì˜ 노트 사ì´ë¥¼ ë„워서 표시" duplicate: "ë³µì œ" @@ -798,7 +808,7 @@ emailNotification: "ë©”ì¼ ì•Œë¦¼" publish: "게시" inChannelSearch: "채ë„ì—ì„œ 검색" useReactionPickerForContextMenu: "ìš°í´ë¦í•˜ì—¬ 리액션 ì„ íƒê¸° 열기" -typingUsers: "{users} ë‹˜ì´ ìž…ë ¥í•˜ê³ ìžˆì–´ìš”.." +typingUsers: "{users}ë‹˜ì´ ìž…ë ¥ 중" jumpToSpecifiedDate: "íŠ¹ì • ë‚ ì§œë¡œ ì´ë™" showingPastTimeline: "ê³¼ê±°ì˜ íƒ€ìž„ë¼ì¸ì„ í‘œì‹œí•˜ê³ ìžˆì–´ìš”" clear: "지우기" @@ -832,6 +842,7 @@ administration: "관리" accounts: "ê³„ì •" switch: "ì „í™˜" noMaintainerInformationWarning: "ê´€ë¦¬ìž ì •ë³´ê°€ ì„¤ì •ë˜ì–´ 있지 않습니다." +noInquiryUrlWarning: "문ì˜ì²˜ 주소를 ì„¤ì •í•˜ì§€ 않았습니다." noBotProtectionWarning: "Bot ë°©ì–´ê°€ ì„¤ì •ë˜ì–´ 있지 않습니다." configure: "ì„¤ì •í•˜ê¸°" postToGallery: "ê°¤ëŸ¬ë¦¬ì— ì—…ë¡œë“œ" @@ -1021,6 +1032,7 @@ thisPostMayBeAnnoyingHome: "í™ˆì— ê²Œì‹œ" thisPostMayBeAnnoyingCancel: "그만ë‘기" thisPostMayBeAnnoyingIgnore: "ì´ëŒ€ë¡œ 게시" collapseRenotes: "ì´ë¯¸ 본 리노트를 간략화하기" +collapseRenotesDescription: "ë°˜ì‘ì´ë‚˜ 리노트를 í•œ 노트를 ì ‘ì–´ì„œ 표시합니다." internalServerError: "내부 서버 오류" internalServerErrorDescription: "내부 서버ì—ì„œ 예기치 ì•Šì€ ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤." copyErrorInfo: "오류 ì •ë³´ 복사" @@ -1090,7 +1102,7 @@ serverRules: "서버 규칙" pleaseConfirmBelowBeforeSignup: "ì´ ì„œë²„ì— ê°€ìž…í•˜ê¸° ì „ì— ì•„ëž˜ 사í•ì„ 확ì¸í•˜ì—¬ 주ì‹ì‹œì˜¤." pleaseAgreeAllToContinue: "계ì†í•˜ì‹œë ¤ë©´ ëª¨ë“ í•ëª©ì— ë™ì˜í•˜ì‹ì‹œì˜¤." continue: "계ì†" -preservedUsernames: "ì˜ˆì•½ëœ ì‚¬ìš©ìžëª…" +preservedUsernames: "예약한 ì‚¬ìš©ìž ì´ë¦„" preservedUsernamesDescription: "ì˜ˆì•½í• ì‚¬ìš©ìžëª…ì„ í•œ ì¤„ì— í•˜ë‚˜ì”© ìž…ë ¥í•©ë‹ˆë‹¤. 여기ì—ì„œ ì§€ì •í•œ 사용ìžëª…으로는 ê³„ì •ì„ ìƒì„±í• 수 없게 ë©ë‹ˆë‹¤. 단, ê´€ë¦¬ìž ê¶Œí•œìœ¼ë¡œ ê³„ì •ì„ ìƒì„±í• ë•Œì—는 해당ë˜ì§€ 않으며, ì´ë¯¸ 존재하는 ê³„ì •ë„ ì˜í–¥ì„ 받지 않습니다." createNoteFromTheFile: "ì´ íŒŒì¼ë¡œ 노트를 작성" archive: "ì•„ì¹´ì´ë¸Œ" @@ -1230,10 +1242,22 @@ useTotp: "ì¼íšŒìš© 비밀번호 사용" useBackupCode: "백업 코드 사용" launchApp: "앱 실행" useNativeUIForVideoAudioPlayer: "브ë¼ìš°ì € UIì—ì„œ 미디어 재ìƒ" +keepOriginalFilename: "ì›ë³¸ íŒŒì¼ ì´ë¦„ì„ ìœ ì§€" +keepOriginalFilenameDescription: "ì´ ì„¤ì •ì„ ë„ë©´ 업로드를 í• ë•Œ íŒŒì¼ ì´ë¦„ì´ ìžë™ìœ¼ë¡œ 무작위 문ìžì—´ë¡œ ë°”ë€ë‹ˆë‹¤." +noDescription: "ì„¤ëª…ë¬¸ì´ ì—†ìŠµë‹ˆë‹¤" +alwaysConfirmFollow: "íŒ”ë¡œìš°ì¼ ë•Œ í•ìƒ 확ì¸í•˜ê¸°" +inquiry: "문ì˜í•˜ê¸°" +tryAgain: "다시 ì‹œë„í•´ 주세요." +confirmWhenRevealingSensitiveMedia: "민ê°í•œ 미디어를 ì—´ ë•Œ ë‘ ë²ˆ 확ì¸" _delivery: + status: "ì „ì†¡ ìƒíƒœ" stop: "ì •ì§€ë¨" + resume: "ì „ì†¡ 다시 시작" _type: none: "ë°°í¬ ì¤‘" + manuallySuspended: "ìˆ˜ë™ ì •ì§€ 중" + goneSuspended: "서버 ì‚ì œë¥¼ ì´ìœ ë¡œ ì •ì§€ 중" + autoSuspendedForNotResponding: "서버 ì‘답 ì—†ìŒì„ ì´ìœ ë¡œ ì •ì§€ 중" _bubbleGame: howToPlay: "설명" hold: "홀드" @@ -1359,6 +1383,8 @@ _serverSettings: fanoutTimelineDescription: "활성화하면 ê°ì¢… 타임ë¼ì¸ì„ ê°€ì ¸ì˜¬ ë•Œì˜ ì„±ëŠ¥ì„ ëŒ€í í–¥ìƒí•˜ë©°, ë°ì´í„°ë² ì´ìŠ¤ì˜ 부하를 ì¤„ì¼ ìˆ˜ 있습니다. 단, Redisì˜ ë©”ëª¨ë¦¬ ì‚¬ìš©ëŸ‰ì´ ì¦ê°€í•©ë‹ˆë‹¤. ì„œë²„ì˜ ë©”ëª¨ë¦¬ ìš©ëŸ‰ì´ ìž‘ê±°ë‚˜, 서비스가 ë¶ˆì•ˆì •í•´ì§€ëŠ” 경우 ë¹„í™œì„±í™”í• ìˆ˜ 있습니다." fanoutTimelineDbFallback: "ë°ì´í„°ë² ì´ìŠ¤ë¥¼ 예비로 사용하기" fanoutTimelineDbFallbackDescription: "활성화하면 타임ë¼ì¸ì˜ ìºì‹œë˜ì–´ 있지 ì•Šì€ ë¶€ë¶„ì— ëŒ€í•´ DBì— ì§ˆì˜í•˜ì—¬ ì •ë³´ë¥¼ ê°€ì ¸ì˜µë‹ˆë‹¤. 비활성화하면 ì´ë¥¼ 실행하지 ì•ŠìŒìœ¼ë¡œì¨ ì„œë²„ì˜ ë¶€í•˜ë¥¼ ì¤„ì¼ ìˆ˜ 있지만, 타임ë¼ì¸ì—ì„œ ê°€ì ¸ì˜¬ 수 있는 게시물 범위가 í•œì •ë©ë‹ˆë‹¤." + inquiryUrl: "문ì˜ì²˜ URL" + inquiryUrlDescription: "서버 ìš´ì˜ìžì—게 보내는 ë¬¸ì˜ ì–‘ì‹ì˜ URLì´ë‚˜ ìš´ì˜ìžì˜ ì—°ë½ì²˜ ë“±ì´ ì 힌 웹 페ì´ì§€ì˜ URLì„ ì„¤ì •í•©ë‹ˆë‹¤." _accountMigration: moveFrom: "다른 ê³„ì •ì—ì„œ ì´ ê³„ì •ìœ¼ë¡œ ì´ì‚¬" moveFromSub: "다른 ê³„ì •ì— ëŒ€í•œ 별ì¹ì„ ìƒì„±" @@ -1675,10 +1701,11 @@ _role: canManageAvatarDecorations: "아바타 꾸미기 관리" driveCapacity: "ë“œë¼ì´ë¸Œ 용량" alwaysMarkNsfw: "파ì¼ì„ í•ìƒ NSFWë¡œ ì§€ì •" + canUpdateBioMedia: "아바타 ë° ë°°ë„ˆ ì´ë¯¸ì§€ 변경 허용" pinMax: "ê³ ì •í• ìˆ˜ 있는 노트 수" antennaMax: "만들 수 있는 안테나 수" wordMuteMax: "단어 ë®¤íŠ¸í• ìˆ˜ 있는 ë¬¸ìž ìˆ˜" - webhookMax: "만들 수 있는 ì›¹í›„í¬ ìˆ˜" + webhookMax: "만들 수 있는 Webhook 수" clipMax: "만들 수 있는 í´ë¦½ 수" noteEachClipsMax: "í´ë¦½ì— ë„£ì„ ìˆ˜ 있는 노트 수" userListMax: "만들 수 있는 ì‚¬ìš©ìž ë¦¬ìŠ¤íŠ¸ 수" @@ -1693,6 +1720,11 @@ _role: roleAssignedTo: "ìˆ˜ë™ ì—í• ì— ì´ë¯¸ í• ë‹¹ë¨" isLocal: "로컬 사용ìž" isRemote: "리모트 사용ìž" + isCat: "ê³ ì–‘ì´ ì‚¬ìš©ìž" + isBot: "ë´‡ 사용ìž" + isSuspended: "ì •ì§€ëœ ì‚¬ìš©ìž" + isLocked: "ìž ê¸ˆ ê³„ì • 사용ìž" + isExplorable: "â€˜ê³„ì •ì„ ì‰½ê²Œ 발견하ë„ë¡ í•˜ê¸°â€™ë¥¼ 활성화한 사용ìž" createdLessThan: "가입한 지 ë‹¤ìŒ ì¼ìˆ˜ ì´ë‚´ì¸ ìœ ì €" createdMoreThan: "가입한 지 ë‹¤ìŒ ì¼ìˆ˜ ì´ìƒì¸ ìœ ì €" followersLessThanOrEq: "팔로워 수가 ë‹¤ìŒ ì´í•˜ì¸ ìœ ì €" @@ -1913,8 +1945,6 @@ _sfx: note: "새 노트" noteMy: "ë‚´ 노트" notification: "알림" - antenna: "안테나 ìˆ˜ì‹ " - channel: "ì±„ë„ ì•Œë¦¼" reaction: "리액션 ì„ íƒ" _soundSettings: driveFile: "ë“œë¼ì´ë¸Œì— 있는 오디오를 사용" @@ -1975,6 +2005,7 @@ _2fa: backupCodesDescription: "ì¸ì¦ ì•±ì„ ì‚¬ìš©í• ìˆ˜ 없게 ëœ ê²½ìš° 아래 백업 코드를 사용하여 ê³„ì •ì— ì•¡ì„¸ìŠ¤ í• ìˆ˜ 있습니다.ì´ ì½”ë“œë“¤ì€ ë°˜ë“œì‹œ ì•ˆì „í•œ ìž¥ì†Œì— ë³´ê´€í•˜ì‹ì‹œì˜¤.ê° ì½”ë“œëŠ” í•œ 번만 ì‚¬ìš©í• ìˆ˜ 있습니다." backupCodeUsedWarning: "백업 코드가 사용ë˜ì—ˆìŠµë‹ˆë‹¤.ì¸ì¦ ì•±ì„ ì‚¬ìš©í• ìˆ˜ 없게 ëœ ê²½ìš°, ì¡°ì†ížˆ ì¸ì¦ ì•±ì„ ë‹¤ì‹œ ì„¤ì •í•´ 주ì‹ì‹œì˜¤." backupCodesExhaustedWarning: "백업 코드가 ëª¨ë‘ ì‚¬ìš©ë˜ì—ˆìŠµë‹ˆë‹¤.ì¸ì¦ ì•±ì„ ì‚¬ìš©í• ìˆ˜ 없는 경우 ë” ì´ìƒ ê³„ì •ì— ì•¡ì„¸ìŠ¤í•˜ëŠ” ê²ƒì´ ë¶ˆê°€ëŠ¥í•©ë‹ˆë‹¤.ì¸ì¦ ì•±ì„ ë‹¤ì‹œ 등ë¡í•´ 주세요." + moreDetailedGuideHere: "ì—¬ê¸°ì— ìžì„¸í•œ ì„¤ëª…ì´ ìžˆìŠµë‹ˆë‹¤" _permissions: "read:account": "ê³„ì •ì˜ ì •ë³´ë¥¼ 봅니다" "write:account": "ê³„ì •ì˜ ì •ë³´ë¥¼ 변경합니다" @@ -2163,7 +2194,7 @@ _postForm: c: "ë¬´ì—‡ì„ ìƒê°í•˜ê³ 있나요?" d: "ë§í•˜ê³ ì‹¶ì€ ê²Œ 있나요?" e: "ì—¬ê¸°ì— ì ì–´ 주세요" - f: "글 쓰기를 ê¸°ë‹¤ë ¤ìš”â€¦" + f: "작성해주시길 ê¸°ë‹¤ë¦¬ê³ ìžˆì–´ìš”..." _profile: name: "ì´ë¦„" username: "ì‚¬ìš©ìž ì´ë¦„" @@ -2338,6 +2369,7 @@ _deck: alwaysShowMainColumn: "ë©”ì¸ ì¹¼ëŸ¼ í•ìƒ 표시" columnAlign: "칼럼 ì •ë ¬" addColumn: "칼럼 추가" + newNoteNotificationSettings: "새 노트 알림 ì„¤ì •" configureColumn: "칼럼 ì„¤ì •" swapLeft: "왼쪽으로 ì´ë™" swapRight: "오른쪽으로 ì´ë™" @@ -2376,9 +2408,9 @@ _drivecleaner: orderByCreatedAtAsc: "등ë¡ì¼ì´ ì˜¤ëž˜ëœ ìˆœ" _webhookSettings: createWebhook: "Webhook ìƒì„±" + modifyWebhook: "Webhook ìˆ˜ì •" name: "ì´ë¦„" secret: "ì‹œí¬ë¦¿" - events: "Webhookì„ ì‹¤í–‰í• íƒ€ì´ë°" active: "활성화" _events: follow: "누군가를 íŒ”ë¡œìš°í–ˆì„ ë•Œ" @@ -2388,6 +2420,26 @@ _webhookSettings: renote: "누군가 ë‚´ ê¸€ì„ ë¦¬ë…¸íŠ¸í–ˆì„ ë•Œ" reaction: "누군가 ë‚´ ë…¸íŠ¸ì— ë¦¬ì•¡ì…˜í–ˆì„ ë•Œ" mention: "누군가 나를 ë©˜ì…˜í–ˆì„ ë•Œ" + _systemEvents: + abuseReport: "ìœ ì €ë¡œë¶€í„° ì‹ ê³ ë¥¼ ë°›ì•˜ì„ ë•Œ" + abuseReportResolved: "ë°›ì€ ì‹ ê³ ë¥¼ ì²˜ë¦¬í–ˆì„ ë•Œ" + userCreated: "ìœ ì €ê°€ ìƒì„±ë˜ì—ˆì„ ë•Œ" + deleteConfirm: "Webhookì„ ì‚ì œí• ê¹Œìš”?" +_abuseReport: + _notificationRecipient: + createRecipient: "ì‹ ê³ ìˆ˜ì‹ ìž ì¶”ê°€" + modifyRecipient: "ì‹ ê³ ìˆ˜ì‹ ìž íŽ¸ì§‘" + recipientType: "알림 ìˆ˜ì‹ ìœ í˜•" + _recipientType: + mail: "ì´ë©”ì¼" + webhook: "Webhook" + _captions: + mail: "모ë”ë ˆì´í„° ê¶Œí•œì„ ê°€ì§„ 사용ìžì˜ ì´ë©”ì¼ ì£¼ì†Œì— ì•Œë¦¼ì„ ë³´ëƒ…ë‹ˆë‹¤ (ì‹ ê³ ë¥¼ ë°›ì€ ë•Œì—만)" + webhook: "ì§€ì •í•œ SystemWebhookì— ì•Œë¦¼ì„ ë³´ëƒ…ë‹ˆë‹¤ (ì‹ ê³ ë¥¼ ë°›ì€ ë•Œì™€ í•´ê²°í–ˆì„ ë•Œì— ì†¡ì‹ )" + keywords: "키워드" + notifiedUser: "ì‹ ê³ ì•Œë¦¼ì„ ë³´ë‚¼ ìœ ì €" + notifiedWebhook: "ì‚¬ìš©í• Webhook" + deleteConfirm: "ìˆ˜ì‹ ìžë¥¼ ì‚ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?" _moderationLogTypes: createRole: "ì—í• ìƒì„±" deleteRole: "ì—í• ì‚ì œ" @@ -2403,7 +2455,7 @@ _moderationLogTypes: updateUserNote: "ì¡°ì • ê¸°ë¡ ê°±ì‹ " deleteDriveFile: "íŒŒì¼ ì‚ì œ" deleteNote: "노트 ì‚ì œ" - createGlobalAnnouncement: "ëª¨ë“ ê³µì§€ì‚¬í• ë§Œë“¤ê¸°" + createGlobalAnnouncement: "ì „ì— ê³µì§€ì‚¬í• ìƒì„±" createUserAnnouncement: "ì‚¬ìš©ìž ê³µì§€ì‚¬í• ë§Œë“¤ê¸°" updateGlobalAnnouncement: "ëª¨ë“ ê³µì§€ì‚¬í• ìˆ˜ì •" updateUserAnnouncement: "ì‚¬ìš©ìž ê³µì§€ì‚¬í• ìˆ˜ì •" @@ -2425,6 +2477,12 @@ _moderationLogTypes: deleteAvatarDecoration: "아바타 ìž¥ì‹ ì‚ì œ" unsetUserAvatar: "ìœ ì € 아바타 ì œê±°" unsetUserBanner: "ìœ ì € 배너 ì œê±°" + createSystemWebhook: "SystemWebhookì„ ìƒì„±" + updateSystemWebhook: "SystemWebhookì„ ìˆ˜ì •" + deleteSystemWebhook: "SystemWebhookì„ ì‚ì œ" + createAbuseReportNotificationRecipient: "ì‹ ê³ ì•Œë¦¼ ìˆ˜ì‹ ìž ìƒì„±" + updateAbuseReportNotificationRecipient: "ì‹ ê³ ì•Œë¦¼ ìˆ˜ì‹ ìž íŽ¸ì§‘" + deleteAbuseReportNotificationRecipient: "ì‹ ê³ ì•Œë¦¼ ìˆ˜ì‹ ìž ì‚ì œ" _fileViewer: title: "íŒŒì¼ ìƒì„¸" type: "íŒŒì¼ ìœ í˜•" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 087bac374524c904051c9ca0e494478c031c3c74..1bead5635dfa1500ac0dadc1063a6d25fc59c5a2 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -18,15 +18,15 @@ enterUsername: "ປ້àºàº™àºŠàº·à»ˆàºœàº¹à»‰à»ƒàºŠà»‰" renotedBy: "Renoted ໂດຠ{user}" noNotes: "ບà»à»ˆàº¡àºµ note" noNotifications: "ບà»à»ˆàº¡àºµàºàº²àº™à»àºˆà»‰àº‡à»€àº•àº·àºàº™" -instance: "àºàºµàº™àºªàº°à»àº•àº™" -settings: "àºàº³àº™àº»àº”ຄ່າ" +instance: "ເຊີຟເວີຣ໌" +settings: "ຕັ້ງຄ່າ" notificationSettings: "ຕັ້ງຄ່າàºàº²àº™à»àºˆà»‰àº‡à»€àº•àº·àºàº™" basicSettings: "àºàº²àº™àº•àº±à»‰àº‡àº„່າພື້ນຖານ" otherSettings: "àºàº²àº™àº•àº±à»‰àº‡àº„່າàºàº·à»ˆàº™à»†" -openInWindow: "ເປີດໃນປ່àºàº‡àº¢à»‰àº½àº¡" -profile: "ໂພຼຟາàº" +openInWindow: "ເປີດໃນ window" +profile: "ໂປຣໄຟລ໌" timeline: "ໄທມ໌ໄລນ໌" -noAccountDescription: "ຜູ້ໃຊ້ນີ້àºàº±àº‡àºšà»à»ˆà»„ດ້ຂຽນໃນຊີວະປະຫວັດຂàºàº‡à»€àº‚ົາເຈົ້າເທື່àº" +noAccountDescription: "ຜູ້ໃຊ້ຄົນນີ້àºàº±àº‡àºšà»à»ˆà»„ດ້ຂຽນຄຳà»àº™àº°àº™àº³à»‚ຕ" login: "ເຂົ້າ​ສູ່​ລະ​ບົບ" loggingIn: "àºàº³àº¥àº±àº‡à»€àº‚ົ້າສູ່ລະບົບ..." logout: "àºàºàºâ€‹àºˆàº²àºâ€‹àº¥àº°â€‹àºšàº»àºš" @@ -37,7 +37,7 @@ users: "ຜູ້ໃຊ້" addUser: "ເພີ່ມຜູ້ໃຊ້" favorite: "ເພີ່ມໃສ່ລາàºàºàº²àº™àº—ີ່ມັàº" favorites: "ລາàºàºàº²àº™àº—ີ່ມັàº" -unfavorite: "ລຶບàºàºàºàºˆàº²àºàº¥àº²àºàºàº²àº™àº—ີ່ມັàº" +unfavorite: "ເàºàº»àº²àºàºàºàºˆàº²àºàº¥àº²àºàºàº²àº™àº—ີ່ມັàº" favorited: "ເພີ່ມໃສ່ລາàºàºàº²àº™àº—ີ່ມັàºà»àº¥à»‰àº§" alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາàºàºàº²àº™àº—ີ່ມັàºà»àº¥à»‰àº§." cantFavorite: "ບà»à»ˆàºªàº²àº¡àº²àº”ເພີ່ມໃສ່ລາàºàºàº²àº™àº—ີ່ມັàºà»„ດ້." @@ -48,41 +48,41 @@ copyLink: "ຄັດລàºàºàº¥àº´à»‰àº‡" copyLinkRenote: "ຄັດລàºàºàº¥àº´à»‰àº‡àº‚àºàº‡ renote" delete: "ລຶບ" deleteAndEdit: "ລຶບ​à»àº¥àº°â€‹à»àºà»‰â€‹à»„ຂ​" -deleteAndEditConfirm: "ເຈົ້າ​à»àº™à»ˆâ€‹à»ƒàºˆâ€‹àºšà»à»ˆ? ທີ່ທ່ານຕ້àºàº‡àºàº²àº™àº—ີ່ຈະລຶບ note ນີ້ à»àº¥àº°à»àºà»‰à»„ຂມັນ ທ່ານàºàº²àº”ຈະສູນເສຠreaction, renote, à»àº¥àº°àºàº²àº™àº•àºàºšàºàº±àºšàº—ັງà»àº»àº”" +deleteAndEditConfirm: "ຕ້àºàº‡àºàº²àº™àº¥àº¶àºš note ນີ້à»àº¥àº°à»àºà»‰à»„ຂໃà»à»ˆà»àº¡à»ˆàº™àºšà»à»ˆ? reaction, renote à»àº¥àº°àºàº²àº™àº•àºàºšàºàº±àºšàº•à»à»ˆ note ນີ້ ທັງເບິດຈະຖືàºàº¥àº¶àºšàºàºàº" addToList: "ເພີ່ມໃສ່ລາàºàºŠàº·à»ˆ" addToAntenna: "ເພີ່ມໃສ່ເສົາàºàº²àºàº²àº”" sendMessage: "ສົ່ງຂà»à»‰àº„ວາມ" -copyRSS: "ສຳເນົາ RSS" -copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້" -copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້" -copyNoteId: "ສຳເນົາ ID ບັນທຶàº" -copyFileId: "ສຳເນົາ ID ໄຟລ໌" -copyFolderId: "ສຳເນົາ ID ໂຟນເດີ" -copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌" +copyRSS: "ຄັດລàºàº RSS" +copyUsername: "ຄັດລàºàºàºŠàº·à»ˆàºœàº¹à»‰à»ƒàºŠà»‰" +copyUserId: "ຄັດລàºàº ID ຜູ້ໃຊ້" +copyNoteId: "ຄັດລàºàº ID ຂàºàº‡ note" +copyFileId: "ຄັດລàºàº ID ໄຟລ໌" +copyFolderId: "ຄັດລàºàº ID ໂຟລ໌ເດີຣ໌" +copyProfileUrl: "ຄັດລàºàº URL ໂປຣໄຟລ໌" searchUser: "ຄົ້ນຫາຜູ້ໃຊ້" -reply: "ຕàºàºšâ€‹à»„ປ​ທີ" +reply: "ຕàºàºšâ€‹àºàº±àºš" loadMore: "ໂຫຼດເພີ່ມເຕີມ" showMore: "ໂຫຼດເພີ່ມເຕີມ" showLess: "ປິດ" -youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ" -receiveFollowRequest: "ປະຕິບັດຕາມຄà»àº²àº®à»‰àºàº‡àº‚à»àº—ີ່ໄດ້ຮັບ" -followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້àºàºàº¡àº®àº±àºšàº„à»àº²àº®à»‰àºàº‡àº‚à»àº‚àºàº‡àº—່ານ" -mention: "àºà»ˆàº²àº§àº–ືງ" -mentions: "àºà»ˆàº²àº§à»€àº–ິງ" +youGotNewFollower: "ໄດ້ຕິດຕາມເຈົ້າ" +receiveFollowRequest: "ມີຄຳຂà»àº•àº´àº”ຕາມສົ່ງມາ" +followRequestAccepted: "àºàº²àº™àº•àº´àº”ຕາມໄດ້ຮັບàºàº™àº¸àºàº²àº”à»àº¥à»‰àº§" +mention: "ເວົ້າເຖີງ" +mentions: "ເວົ້າເຖີງເຈົ້າ" directNotes: "ໂພສ Direct note" importAndExport: "ນà»àº²à»€àº‚ົ້າ / ສົ່ງàºàºàº" import: "ນຳເຂົ້າ" export: "ສົ່ງàºàºàº" files: "ໄຟລ໌" download: "ດາວໂຫລດ" -driveFileDeleteConfirm: "ທ່ານà»àº™à»ˆà»ƒàºˆàºšà»à»ˆàº§à»ˆàº²àº•à»‰àºàº‡àºàº²àº™àº¥àº¶àºšà»„ຟລ໌ \"{name}\"? note ທີ່ມີໄຟລ໌à»àº™àºšàº™àºµà»‰àºˆàº°àº–ືàºàº¥àº¶àºšàº–ິ້ມ" -unfollowConfirm: "ທ່ານà»àº™à»ˆà»ƒàºˆàºšà»à»ˆàº§à»ˆàº²àº•à»‰àºàº‡àºàº²àº™à»€àºŠàº»àº²àº•àº´àº”ຕາມ {name}?" -exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້àºàº‡àº‚à»àºàº²àº™àºªàº»à»ˆàº‡àºàºàº ມັນàºàº²àº”ຈະໃຊ້ເວລາບາງເວລາ à»àº¥àº°àº¡àº±àº™àºˆàº°àº–ືàºà»€àºžàºµà»ˆàº¡à»ƒàºªà»ˆ drive ຂàºàº‡àº—່ານເມື່àºàº¡àº±àº™àºªàº³à»€àº¥àº±àº”à»àº¥à»‰àº§" -importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້àºàº‡àº‚à»àºàº²àº™àº™à»àº²à»€àº‚ົ້າ ມັນàºàº²àº”ຈະໃຊ້ເວລາບາງເວລາ" +driveFileDeleteConfirm: "ຕ້àºàº‡àºàº²àº™àº¥àº¶àºšà»„ຟລ໌ “{name}†à»àº¡à»ˆàº™àºšà»à»ˆ? Note ທີ່à»àº™àºšàº¡àº²àºàº±àºšà»„ຟລ໌ນີ້ຈະຖືàºàº¥àº¶àºšàºàºàº" +unfollowConfirm: "ຕ້àºàº‡àºàº²àº™à»€àº¥àºµàºàº•àº´àº”ຕາມ {name} à»àº¡à»ˆàº™àºšà»à»ˆ?" +exportRequested: "ເຈົ້າໄດ້ຮ້àºàº‡àº‚à»àºàº²àº™àºªàº»à»ˆàº‡àºàºàº àºàº²àº”ໃຊ້ເວລາຈັàºà»œà»ˆàºàº ເມື່àºà»àº¥à»‰àº§àºˆàº°àº–ືàºà»€àºžàºµà»ˆàº¡à»ƒàºªà»ˆ drive" +importRequested: "ເຈົ້າໄດ້ຮ້àºàº‡àº‚à»àºàº²àº™àº™àº³à»€àº‚ົ້າ àºàº²àº™àº”ຳເນິນàºàº²àº™àº™àºµà»‰àºàº²àº”ໃຊ້ເວລາຈັàºà»œà»ˆàºàº" lists: "ລາàºàºàº²àº™" -noLists: "ທ່ານ​ບà»à»ˆâ€‹àº¡àºµâ€‹àº¥àº²àºâ€‹àºàº²àº™â€‹à»ƒàº”ໆ​" -note: "ບັນທຶàº" -notes: "ບັນທຶàº" +noLists: "ບà»à»ˆâ€‹àº¡àºµâ€‹àº¥àº²àºâ€‹àºàº²àº™â€‹à»ƒàº”ໆ​" +note: "Note" +notes: "Note" following: "àºàº³àº¥àº±àº‡àº•àº´àº”ຕາມ" followers: "ຜູ້ຕິດຕາມ" followsYou: "ຕິດ​ຕາມ​ເຈົ້າ" @@ -124,11 +124,11 @@ reactions: "reaction" attachCancel: "ເàºàº»àº²à»„ຟລ໌à»àº™àºš" mute: "ປີດສຽງ" unmute: "ເປີດສຽງ" -block: "ບ໋àºàº" -unblock: "àºàº»àºà»€àº¥àºµàºàºàº²àº®àº»àºšàº¥àº±àºàº" +block: "ບລັàºàº" +unblock: "ເລີàºàºšàº¥àº±àºàº" suspend: "ລະງັບ" unsuspend: "ເຊົາ​ລະ​ງັບ" -selectList: "ເລືàºàºàºšàº±àº™àºŠàºµàº¥àº²àºàºàº²àº™" +selectList: "ເລືàºàºàº¥àº²àºàºŠàº·à»ˆ" editList: "à»àºà»‰à»„ຂລາàºàºŠàº·à»ˆ" selectChannel: "ເລືàºàºàºŠà»ˆàºàº‡" selectAntenna: "ເລືàºàºà»€àºªàº»àº²àºàº²àºàº²àº”" @@ -151,30 +151,30 @@ flagShowTimelineRepliesDescription: "ສະà»àº”ງàºàº²àº™àº•àºàºšàºàº±àºš autoAcceptFollowed: "àºàº°àº™àº¸àº¡àº±àº”àºàº±àº”ຕະໂນມັດຕາມຄຳຮ້àºàº‡àº‚à»àºˆàº²àºàºœàº¹à»‰à»ƒàºŠà»‰àº—ີ່ທ່ານàºàº³àº¥àº±àº‡àº•àº´àº”ຕາມຢູ່" addAccount: "ເພີ່ມບັນຊີ" loginFailed: "àºàº²àº™à»€àº‚ົ້າສູ່ລະບົບບà»à»ˆàºªàº³à»€àº¥àº±àº”" -showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລàºàº°à»„àº" +showOnRemote: "ເບິ່ງໃນເຊີຟເວີຣ໌ໄລàºàº°à»„àº" general: "ທົ່ວໄປ" wallpaper: "ພາບພື້ນຫລັງ" setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ" removeWallpaper: "ລຶບຮູບວà»à»€àº›à»€àº›àºµàºàºàº" searchWith: "ຊàºàºàº«àº²: {q}" -youHaveNoLists: "ທ່ານ​ບà»à»ˆâ€‹àº¡àºµâ€‹àº¥àº²àºâ€‹àºàº²àº™â€‹à»ƒàº”ໆ​" +youHaveNoLists: "ເຈົ້າບà»à»ˆàº¡àºµàº¥àº²àºàºŠàº·à»ˆà»ƒàº”ໆ" proxyAccount: "ບັນຊີພຣັàºàºàºŠàºµ" -host: "ໂຮດສ" +host: "ໂຮສຕ໌" selectUser: "ເລືàºàºàºœàº¹à»‰à»ƒàºŠà»‰" recipient: "ເຖິງ" annotation: "ຄຳເຫັນ" federation: "ສະຫະພັນ" -instances: "àºàºµàº™àºªàº°à»àº•àº™" +instances: "ເຊີຟເວີຣ໌" registeredAt: "ລົງທະບຽນຢູ່" storageUsage: "ບ່àºàº™â€‹àºˆàº±àº”​ເàºàº±àºšâ€‹àº‚à»à»‰â€‹àº¡àº¹àº™àº—ີ່ໃຊ້" -charts: "àºàº±àº™àº”ັບເພງ" +charts: "à»àºœàº™àºžàº¹àº¡" perHour: "ຕà»à»ˆàºŠàº»à»ˆàº§à»‚ມງ" perDay: "ຕà»à»ˆâ€‹àº¡àº·à»‰" stopActivityDelivery: "ຢຸດເຊົາàºàº²àº™àºªàº»à»ˆàº‡àºàº´àº”ຈະàºà»àº²" blockThisInstance: "ຂັດຂວາງຕົວຢ່າງນີ້" operations: "àºàº²àº™àº”ຳເນີນງານ" software: "ຊàºàºšà»àº§" -version: "ສະບັບ" +version: "ເວີຣ໌ຊັນ" metadata: "Metadata" withNFiles: "{n} ໄຟລ໌(s)" monitor: "ຈà»àºžàº²àºš" @@ -199,15 +199,15 @@ federating: "ສະຫະພັນ" blocked: "ບລັàºàºà»àº¥à»‰àº§ " suspended: "ໂຈະ" all: "ທັງà»àº»àº”" -subscribing: "ສະà»àº±àºàºªàº°àº¡àº²àºŠàº´àºà»àº¥àº±àº§" -publishing: "àºàº²àº™â€‹àºžàº´àº¡â€‹à»€àºœàºµàºâ€‹à»àºœà»ˆ" +subscribing: "àºàº³àº¥àº±àº‡àºªàº°àº¡àº±àºàºªàº°àº¡àº²àºŠàº´àº" +publishing: "àºàº³àº¥àº±àº‡â€‹à»€àºœàºµàºâ€‹à»àºžà»ˆ" notResponding: "ບà»à»ˆàº•àºàºšàºªàº°à»œàºàº‡" -instanceFollowing: "àºàº³àº¥àº±àº‡àº•àº´àº”ຕາມສຸດຕົວຢ່າງ" -instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ" -instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂàºàº‡â€‹àº•àº»àº§â€‹àº¢à»ˆàº²àº‡â€‹àº™àºµà»‰â€‹" +instanceFollowing: "àºàº³àº¥àº±àº‡àº•àº´àº”ຕາມບົນເຊີຟເວີຣ໌" +instanceFollowers: "ຜູ້ຕິດຕາມຂàºàº‡à»€àºŠàºµàºŸà»€àº§àºµàº£à»Œ" +instanceUsers: "ຜູ້​ໃຊ້​ຂàºàº‡â€‹à»€àºŠàºµàºŸà»€àº§àºµàº£à»Œàº™àºµà»‰" changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ" security: "ຄວາມປàºàº”ໄພ" -retypedNotMatch: "ວັດສະດຸປ້àºàº™àºšà»à»ˆàºàº»àº‡àºàº±àº™" +retypedNotMatch: "ປ້àºàº™àº‚à»à»‰àº¡àº¹àº™àºšà»à»ˆàºàº»àº‡àºàº±àº™" currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ" newPassword: "ລະຫັດຜ່ານໃà»à»ˆ" newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃà»à»ˆàºàºµàºà»€àº—ື່àºà»œàº¶à»ˆàº‡" @@ -223,14 +223,14 @@ remove: "ລຶບ" removed: "ລຶບà»àº¥à»‰àº§" resetAreYouSure: "ຣີ​ເຊັດບà»?" saved: "ບັນທຶàºà»àº¥à»‰àº§" -messaging: "à»àºŠà»‹àº”" +messaging: "à»àºŠàº±àº•" upload: "àºàº±àºšà»‚ຫຼດ" keepOriginalUploading: "ຮັàºàºªàº²àº®àº¹àºšàºžàº²àºšàº•àº»à»‰àº™àºªàº°àºšàº±àºš" fromDrive: "ຈາຠDrive" fromUrl: "ຈາຠURL" uploadFromUrl: "àºàº±àºšà»‚ຫຼດຈາຠURL" uploadFromUrlDescription: "URL ຂàºàº‡à»„ຟລ໌ທີ່ທ່ານຕ້àºàº‡àºàº²àº™àºàº±àºšà»‚ຫລດ" -uploadFromUrlRequested: "ຮ້àºàº‡àº‚à»àºàº²àº™àºàº±àºšà»‚ຫລດ" +uploadFromUrlRequested: "ຮ້àºàº‡àº‚à»àºàº²àº™àºàº±àºšà»‚ຫລດà»àº¥à»‰àº§" explore: "ສຳຫຼວດ" messageRead: "àºà»ˆàº²àº™à»àº¥à»‰àº§" startMessaging: "ເລີ່ມàºàº²àº™àºªàº»àº™àº—ະນາໃà»à»ˆ" @@ -244,47 +244,47 @@ images: "ຮູບພາບ" image: "ຮູບພາບ" birthday: "ວັນເàºàºµàº”" yearsOld: "{age} ປີ" -registeredDate: "ວັນທີ່ເປັນສະມາຊິàº" +registeredDate: "ວັນທີ່ລົງທະບຽນ" location: "ທີ່ຕັ້ງ" -theme: "à»àº—໋ມ" -themeForLightMode: "ຮູບà»àºšàºšàºªàºµàºªàº±àº™à»€àºžàº·à»ˆàºà»ƒàºŠà»‰à»ƒàº™à»‚à»àº”à»àºªàº‡" -themeForDarkMode: "ຮູບà»àºšàºšàºªàºµàºªàº±àº™àº—ີ່ຈະໃຊ້ຢູ່ໃນໂà»àº”ມືດ" +theme: "Theme" +themeForLightMode: "Theme ໃຊ້ໃນໂà»àº”ສະຫວ່າງ" +themeForDarkMode: "Theme ໃຊ້ໃນໂà»àº”ມືດ" light: "ສະຫວ່າງ" dark: "ມືດ" lightThemes: "ຊຸດຮູບà»àºšàºšàºªàº°àº«àº§à»ˆàº²àº‡" darkThemes: "ຮູບà»àºšàºšàºªàºµàºªàº±àº™àº¡àº·àº”" syncDeviceDarkMode: "ຊິງຄ໌ໂà»àº”ມືດàºàº±àºšàºàº²àº™àº•àº±à»‰àº‡àº„່າທົ່ວàºàº¸àº›àº°àºàºàº™" -drive: "ຂັບ" +drive: "Drive" fileName: "ຊື່ໄຟລ໌" selectFile: "ເລືàºàºà»„ຟລ໌" selectFiles: "ເລືàºàºà»„ຟລ໌" selectFolder: "ເລືàºàºà»‚ຟລເດີ" selectFolders: "ເລືàºàºà»‚ຟລເດີ" renameFile: "ປ່ຽນຊື່ໄຟລ໌" -folderName: "ຊື່ໂຟນເດີ" +folderName: "ຊື່ໂຟລເດີຣ໌" createFolder: "​ສ້າງ​ໂຟ​ລ​ເດີ" renameFolder: "ປ່ຽນຊື່ໂຟນເດີນີ້" deleteFolder: "ລົບໂຟ​ລ​ເດີ​" addFile: "ເພີ່ມໄຟລ໌" emptyDrive: "Drive ຂàºàº‡àº—່ານຫວ່າງເປົ່າ" -emptyFolder: "ໂຟນເດີນີ້ເປົ່າຫວ່າງ" +emptyFolder: "ໂຟລເດີຣ໌ນີ້ວ່າງເປົ່າ" unableToDelete: "ບà»à»ˆâ€‹àºªàº²â€‹àº¡àº²àº”ລົບໄດ້" inputNewFileName: "ໃສ່ຊື່ໄຟລ໌ໃà»à»ˆ" inputNewDescription: "ໃສ່ຄຳບັນàºàº²àºà»ƒà»à»ˆ" inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃà»à»ˆ" circularReferenceFolder: "ໂຟນເດີປາàºàº—າງà»àº¡à»ˆàº™à»‚ຟນເດີàºà»ˆàºàºàº‚àºàº‡à»‚ຟນເດີທີ່ທ່ານຕ້àºàº‡àºàº²àº™àºà»‰àº²àº" rename: "ປ່ຽນຊື່" -doNothing: "ບà»à»ˆàºªàº»àº™à»ƒàºˆ" -watch: "ເບິ່ງ" -unwatch: "ຢຸດເບິ່ງ" +doNothing: "ຢ່າມັນ" +watch: "ເພັ່ງເລັງ" +unwatch: "ຢຸດເພັ່ງເລັງ" accept: "àºàº°àº™àº¸àºàº²àº”" reject: "ປະຕິເສດ" normal: "ປົàºàºàº°àº•àº´" instanceName: "ຊື່ເຊີເວີ້" -instanceDescription: "ຄà»àº²àºàº°àº—ິບາàºàº•àº»àº§àº¢à»ˆàº²àº‡" +instanceDescription: "ຄຳàºàº°àº—ິບາàºà»àº™àº°àº™àº³à»€àºŠàºµàºŸà»€àº§àºµàº£à»Œ" maintainerName: "ຜູ້ດູà»àº¥" -maintainerEmail: "àºàºµà»€àº¡àº§ admin" -tosUrl: "ເງື່àºàº™à»„ຂàºàº²àº™à»ƒàº«à»‰àºšà»àº¥àº´àºàº²àº™ URL" +maintainerEmail: "àºàºµà»€àº¡àº¥àºœàº¹à»‰àº”ູà»àº¥" +tosUrl: " URL ເງື່àºàº™à»„ຂàºàº²àº™à»ƒàº«à»‰àºšà»àº¥àº´àºàº²àº™" thisYear: "ປີນີ້" thisMonth: "ເດືàºàº™àº™àºµà»‰" today: "ມື້ນີ້" @@ -292,34 +292,34 @@ dayX: "ວັນ {day}" monthX: "ເດືàºàº™ {month}" yearX: "ປີ {year}" pages: "ໜ້າ" -integration: "ຄວາມສຳພັນຂàºàº‡" +integration: "ເຊື່àºàº¡à»‚àºàº‡" connectService: "ເຊື່àºàº¡àº•à»à»ˆ" disconnectService: "ຕັດàºàº²àº™à»€àºŠàº·à»ˆàºàº¡àº•à»à»ˆ" enableLocalTimeline: "ເປີດໃຊ້ທາມລາàºàº—້àºàº‡àº–ິ່ນ" enableGlobalTimeline: "ເປີດໃຊ້ທາມລາàºàº—ົ່ວໂລàº" -disablingTimelinesInfo: "ຜູ້ເບິ່ງà»àºàº‡àº¥àº°àºšàº»àºš à»àº¥àº°àºœàº¹à»‰àº„ວບຄຸມຈະມີàºàº²àº™à»€àº‚ົ້າເຖິງທຸàºàºàº³àº™àº»àº”ເວລາ, ເຖິງà»àº¡à»ˆàº™àº§à»ˆàº²àºˆàº°àºšà»à»ˆà»„ດ້ເປີດໃຊ້ງານàºà»àº•àº²àº¡" +disablingTimelinesInfo: "ຜູ້ດູà»àº¥àº¥àº°àºšàºšà»àº¥àº°àºœàº¹à»‰àº„ວບຄຸມຈະສາມາດເຂົ້າເຖີງໄທມ໌ໄລນ໌ທັ້ງເບີດ ເຖີງວ່າຈະບà»à»ˆà»„ດ້ເປີດໃຊ້ງານàºà»à»ˆàº•àº²àº¡" registration: "ລົງທະບຽນ" enableRegistration: "ເປີດໃຊ້àºàº²àº™àº¥àº»àº‡àº—ະບຽນຜູ້ໃຊ້ໃà»à»ˆ" invite: "ເຊີນ" -driveCapacityPerLocalAccount: "ຄວາມàºàº²àº”ສາມາດຂັບຕà»à»ˆàºœàº¹à»‰à»ƒàºŠà»‰àº—້àºàº‡àº–ິ່ນ" -driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມàºàº²àº”ສາມາດຕà»à»ˆàºœàº¹à»‰à»ƒàºŠà»‰àº—າງໄàº" +driveCapacityPerLocalAccount: "ຄວາມຈຸຂàºàº‡ drive ຕà»à»ˆàºœàº¹à»‰à»ƒàºŠà»‰àº—້àºàº‡àº–ິ່ນ" +driveCapacityPerRemoteAccount: "ຄວາມຈຸຂàºàº‡ drive ຕà»à»ˆàºœàº¹à»‰à»ƒàºŠà»‰à»„ລàºàº°à»„àº" basicInfo: "ຂà»à»‰àº¡àº¸àº™à»€àºšàº·à»‰àºàº‡àº•àº»à»‰àº™" -pinnedNotes: "ບັນທຶàºàº—ີ່ປັàºà»àº¸àº”ໄວ້" -hcaptchaSiteKey: "àºàº°à»àºˆà»„ຊທ໌" -hcaptchaSecretKey: "àºàº°à»àºˆàº¥àº±àºš" -mcaptchaSiteKey: "àºàº°à»àºˆà»„ຊທ໌" -mcaptchaSecretKey: "àºàº°à»àºˆàº¥àº±àºš" +pinnedNotes: "Note ທີ່ປັàºà»àº¸àº”ໄວ້" +hcaptchaSiteKey: "Site key" +hcaptchaSecretKey: "Secret key" +mcaptchaSiteKey: "Site key" +mcaptchaSecretKey: "Secret Key" recaptcha: "reCAPTCHA" -enableRecaptcha: "ເປີດໃຊ້ງານລີà»àº„໋ບຈາ" -recaptchaSiteKey: "àºàº°à»àºˆà»„ຊທ໌" -recaptchaSecretKey: "àºàº°à»àºˆàº¥àº±àºš" -turnstileSiteKey: "àºàº°à»àºˆà»„ຊທ໌" -turnstileSecretKey: "àºàº°à»àºˆàº¥àº±àºš" +enableRecaptcha: "ເປີດໃຊ້ງານ reCAPTCHA" +recaptchaSiteKey: "Site key" +recaptchaSecretKey: "Secret key" +turnstileSiteKey: "Site key" +turnstileSecretKey: "Secret key" name: "ຊື່" userList: "ລາàºàºàº²àº™" about: "àºà»ˆàº½àº§àºàº±àºš" aboutMisskey: "àºà»ˆàº½àº§àºàº±àºš Misskey" -administrator: "ຜູ້ບà»àº¥àº´àº«àº²àº™" +administrator: "ຜູ້ດູà»àº¥" token: "ໂທເຄັນ" share: "à»àºšà»ˆàº‡àº›àº±àº™" notFound: "ບà»à»ˆàºžàº»àºš" @@ -332,27 +332,27 @@ title: "ຫົວຂà»à»‰" text: "ຂà»à»‰àº„ວາມ" enable: "ເປີດໃຊ້" next: "ຕà»à»ˆà»„ປ" -retype: "ເຂົ້າໄປàºàºµàºàº„ັ້ງ" -quoteAttached: "ວົງຢືມ" +retype: "ລàºàº‡àºžàº´àº¡àº¥àº°àº«àº±àº”àºàºµàºà»€àº—ື່àºà»œàº¶à»ˆàº‡" +quoteAttached: "àºà»‰àº²àº‡àºàº´àº‡" invitations: "ເຊີນ" unavailable: "ບà»à»ˆâ€‹àºªàº²â€‹àº¡àº²àº”​ໃຊ້​ໄດ້" language: "ພາສາ" aboutX: "àºà»ˆàº½àº§àºàº±àºš {x}" emojiStyle: "ຮູບà»àºšàºšàºàºµà»‚ມຈິ" native: "ພາ​ສາ​à»àº¡à»ˆ" -noHistory: "​ບà»à»ˆâ€‹àº¡àºµâ€‹àº¥àº²àºâ€‹àºàº²àº™â€‹àº¢àº¹à»ˆâ€‹àºšà»ˆàºàº™â€‹àº™àºµà»‰" +noHistory: "​ບà»à»ˆâ€‹àº¡àºµàº›àº°àº«àº§àº±àº”" doing: "àºàº³àº¥àº±àº‡àº›àº°àº¡àº§àº™àºœàº»àº™..." category: "ຫມວດຫມູ່" -tags: "à»àº—໋àº" +tags: "Aliases" createAccount: "ສ້າງບັນຊີ" -existingAccount: "ທີ່ມີຢູ່" -dashboard: "ໜ້າປັດ" +existingAccount: "ບັນຊີທີ່ມີຢູ່à»àº¥à»‰àº§" +dashboard: "Dashboard" local: "ທ້àºàº‡àº–ິ່ນ" numberOfDays: "ຈຳນວນມື້" objectStorageBucket: "Bucket" objectStoragePrefix: "Prefix" objectStorageEndpoint: "Endpoint" -objectStorageRegion: "ພາàºâ€‹àºžàº·à»‰àº™" +objectStorageRegion: "ພູມິພາàº" deleteAll: "ລຶບທັງà»àº»àº”" sounds: "ສຽງ" sound: "ສຽງ" @@ -365,11 +365,11 @@ state: "ສະຖານະ" sort: "ຈັດຮຽງໂດàº" ascendingOrder: "ນ້àºàºà»„ປຫາໃຫàºà»ˆ" descendingOrder: "ໃຫàºà»ˆàº«àº²àº™à»‰àºàº" -output: "ຜົນຜະລິດ" -script: "ບົດ​ຄວາມ" +output: "Output" +script: "Script" menu: "ເມນູ" -rearrange: "ຈັດລຽງຄືນ" -poll: "àºàº²àº™àºžàº¹àº™" +rearrange: "ຈັດລຽງໃà»à»ˆ" +poll: "Poll" description: "ລາàºàº¥àº°àºàº½àº”" author: "ຜູ້ຂຽນ" manage: "àºàº²àº™àºˆàº±àº”àºàº²àº™" @@ -383,7 +383,7 @@ permission: "àºàº²àº™àºàº°àº™àº¸àºàº²àº”" notificationType: "​ປະເພດàºàº²àº™â€‹à»àºˆà»‰àº‡â€‹à»€àº•àº·àºàº™" edit: "à»àºà»‰à»„ຂ" email: "àºàºµà»€àº¡àº§" -smtpHost: "ໂຮດສ" +smtpHost: "ໂຮສຕ໌" smtpUser: "ຊື່ຜູ້ໃຊ້" smtpPass: "ລະຫັດຜ່ານ" clearCache: "ລຶບລ້າງà»àº„ສ" @@ -393,12 +393,12 @@ administration: "àºàº²àº™àºˆàº±àº”àºàº²àº™" middle: "ປານàºàº²àº‡" searchByGoogle: "ຄົ້ນຫາ" file: "ໄຟລ໌" -replies: "ຕàºàºšâ€‹à»„ປ​ທີ" +replies: "ຕàºàºšâ€‹àºàº±àºš" renotes: "Renote" _delivery: stop: "ໂຈະ" _type: - none: "àºàº²àº™â€‹àºžàº´àº¡â€‹à»€àºœàºµàºâ€‹à»àºœà»ˆ" + none: "àºàº³àº¥àº±àº‡â€‹à»€àºœàºµàºâ€‹à»àºžà»ˆ" _role: _priority: middle: "ປານàºàº²àº‡" @@ -416,8 +416,8 @@ _sfx: _2fa: renewTOTPCancel: "ບà»à»ˆâ€‹à»àº¡à»ˆàº™â€‹àº•àºàº™â€‹àº™àºµà»‰" _widgets: - profile: "ໂພຼຟາàº" - instanceInfo: "àºàºµàº™àºªàº°à»àº•àº™" + profile: "ໂປຣໄຟລ໌" + instanceInfo: "ຂà»à»‰àº¡àº¹àº¥à»€àºŠàºµàºŸà»€àº§àºµàº£à»Œ" notifications: "àºàº²àº™à»àºˆà»‰àº‡à»€àº•àº·àºàº™" timeline: "​ເສັ້ນàºàº³â€‹àº™àº»àº”​ເວ​ລາ​" activity: "àºàº´àº”ຈະàºàº³" @@ -436,28 +436,28 @@ _profile: _exportOrImport: followingList: "àºàº³àº¥àº±àº‡àº•àº´àº”ຕາມ" muteList: "ປີດສຽງ" - blockingList: "ບ໋àºàº" + blockingList: "ບລັàºàº" userLists: "ລາàºàºàº²àº™" _charts: federation: "ສະຫະພັນ" _timelines: home: "ໜ້າຫຼັàº" _play: - script: "ບົດ​ຄວາມ" + script: "Script" summary: "ລາàºàº¥àº°àºàº½àº”" _pages: blocks: image: "ຮູບພາບ" _notification: - youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ" + youWereFollowed: "ໄດ້ຕິດຕາມເຈົ້າ" _types: follow: "àºàº³àº¥àº±àº‡àº•àº´àº”ຕາມ" - mention: "ໄດ້àºà»ˆàº²àº§àº¡àº²" + mention: "ໄດ້àºà»ˆàº²àº§à»€àº–ິງ" renote: "Renote" - quote: "ລວມຂà»à»‰àº„ວາມàºà»‰àº²àº‡àºàºµàº‡" - reaction: "ປະຕິàºàº´àº¥àº´àºàº²" + quote: "àºà»‰àº²àº‡àºàºµàº‡" + reaction: "Reaction" _actions: - reply: "ຕàºàºšâ€‹à»„ປ​ທີ" + reply: "ຕàºàºšâ€‹àºàº±àºš" renote: "Renote" _deck: _columns: @@ -465,8 +465,12 @@ _deck: tl: "​ເສັ້ນàºàº³â€‹àº™àº»àº”​ເວ​ລາ​" list: "ລາàºàºàº²àº™" channel: "ຊ່àºàº‡" - mentions: "àºà»ˆàº²àº§à»€àº–ິງ" + mentions: "àºà»ˆàº²àº§à»€àº–ິງເຈົ້າ" _webhookSettings: name: "ຊື່" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "àºàºµà»€àº¡àº§" _moderationLogTypes: suspend: "ລະງັບ" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index 2154e248af3f2db643be365f725f886b9a117ac6..686d532c4c30ee24ae129dced23ce126fb1e23f9 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" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 2b4c9b77761f1b5221f3db92b1c9ca2c9bc33fc2..cd00ecf9abe6eaa494d8fd7bb4d188acf6321fda 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -721,5 +721,9 @@ _deck: direct: "Direkte" _webhookSettings: name: "Navn" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "E-post" _moderationLogTypes: suspend: "Suspender" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 4acd6af991b0705459ee7021f983fbd0b8cc72c5..b20eabf7f22e7d346ce159405cebff6d32d81320 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -865,7 +865,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" @@ -1221,8 +1221,6 @@ _sfx: note: "Wpisy" noteMy: "Mój wpis" notification: "Powiadomienia" - antenna: "Anteny" - channel: "Powiadomienia kanaÅ‚u" _ago: future: "W przyszÅ‚oÅ›ci" justNow: "Przed chwilÄ…" @@ -1546,7 +1544,6 @@ _webhookSettings: createWebhook: "Stwórz Webhook" name: "Nazwa" secret: "Sekret" - events: "Uruchomienie Webhooka" active: "WÅ‚aczono" _events: follow: "Po zaobserwowaniu użytkownika" @@ -1556,6 +1553,10 @@ _webhookSettings: renote: "Po udostÄ™pnieniu wpisu" reaction: "Po otrzymaniu reakcji" mention: "Po zostaniu wspomnianym" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Adres e-mail" _moderationLogTypes: suspend: "ZawieÅ›" resetPassword: "Zresetuj hasÅ‚o" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 0bfd1f778bcedbdcb0c1ba26d7760dc6d025eae6..71ba7c4371a9bbb9c724188a6f29ab6168b63809 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -2,14 +2,14 @@ _lang_: "Português" headlineMisskey: "Uma rede ligada por notas" introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações à s notas de outras pessoas usando a função \"Reações\" ðŸ‘\nVamos explorar um novo mundo 🚀" -poweredByMisskeyDescription: "{name} é um dos servidores da plataforma de código aberto <b>Misskey</b>." +poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto <b>Misskey</b>." monthAndDay: "{day}/{month}" search: "Pesquisar" notifications: "Notificações" username: "Nome de usuário" password: "Senha" forgotPassword: "Esqueci-me da senha" -fetchingAsApObject: "Buscando no Fediverso" +fetchingAsApObject: "Buscando no Fediverso..." ok: "OK" gotIt: "Entendi" cancel: "Cancelar" @@ -60,6 +60,7 @@ copyFileId: "Copiar o ID do arquivo" copyFolderId: "Copiar o ID da pasta" copyProfileUrl: "Copiar a URL do perfil" searchUser: "Pesquisar usuário" +searchThisUsersNotes: "Pesquisar as notas desse usuário" reply: "Responder" loadMore: "Carregar mais" showMore: "Ver mais" @@ -99,7 +100,7 @@ enterListName: "Insira um nome para a lista" privacy: "Privacidade" makeFollowManuallyApprove: "Pedidos de seguidores precisam ser aprovados" defaultNoteVisibility: "Visibilidade padrão" -follow: "Seguindo" +follow: "Seguir" followRequest: "Enviar pedido de seguidor" followRequests: "Pedidos de seguidor" unfollow: "Deixar de seguir" @@ -108,11 +109,14 @@ enterEmoji: "Inserir emoji" renote: "Repostar" unrenote: "Remover repostagem" renoted: "Repostado" +renotedToX: "Repostar em {name}." cantRenote: "Não é possÃvel repostar esta postagem" cantReRenote: "Não pode repostar este repost" quote: "Citar" inChannelRenote: "Repostar no canal" inChannelQuote: "Citar no canal" +renoteToChannel: "Repostar em canal" +renoteToOtherChannel: "Repostar em outro canal" pinnedNote: "Nota fixada" pinned: "Fixar no perfil" you: "Você" @@ -121,9 +125,16 @@ sensitive: "Conteúdo sensÃvel" add: "Adicionar" reaction: "Reações" reactions: "Reações" +emojiPicker: "Seleção de emoji" +pinnedEmojisForReactionSettingDescription: "Selecionar os emojis que serão fixados e exibidos ao reagir." +pinnedEmojisSettingDescription: "Selecionar os emojis que serão fixos e exibidos na seleção de emoji." +emojiPickerDisplay: "Janela de seleção de emoji" +overwriteFromPinnedEmojisForReaction: "Sobrescrever as opções de reação" +overwriteFromPinnedEmojis: "Sobrescrever as opções gerais" reactionSettingDescription2: "Arraste para reordenar, clique para excluir, pressione + para adicionar." rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" attachCancel: "Remover anexo" +deleteFile: "Excluir arquivo" markAsSensitive: "Marcar como sensÃvel" unmarkAsSensitive: "Desmarcar como sensÃvel" enterFileName: "Digite o nome do arquivo" @@ -144,6 +155,7 @@ editList: "Editar lista" selectChannel: "Selecionar canal" selectAntenna: "Selecione uma antena" editAntenna: "Editar antena" +createAntenna: "Criar uma antena" selectWidget: "Selecione um widget" editWidgets: "Editar widgets" editWidgetsExit: "Pronto" @@ -170,16 +182,21 @@ addAccount: "Adicionar Conta" reloadAccountsList: "Recarregar lista de contas" loginFailed: "Falha ao logar" showOnRemote: "Exibir remotamente" +continueOnRemote: "" +chooseServerOnMisskeyHub: "Escolher um servidor da Misskey Hub" +specifyServerHost: "Especificar uma instância diretamente" +inputHostName: "Insira o domÃnio" general: "Geral" wallpaper: "Papel de parede" setWallpaper: "Definir papel de parede" removeWallpaper: "Remover papel de parede" searchWith: "Buscar: {q}" youHaveNoLists: "Não tem nenhuma lista" -followConfirm: "Tem certeza que quer deixar de seguir {name}?" +followConfirm: "Tem certeza que quer seguir {name}?" proxyAccount: "Conta proxy" proxyAccountDescription: "Uma conta de proxy é uma conta que assume o acompanhamento remoto de um usuário sob certas condições especÃficas. Por exemplo, quando um usuário inclui um usuário remoto em uma lista, mas ninguém na lista está seguindo o usuário remoto, a atividade não é entregue ao servidor. Nesse caso, a conta de proxy entra em ação para seguir o usuário remoto em vez disso." host: "Host" +selectSelf: "Escolher manualmente" selectUser: "Selecionar usuário" recipient: "Destinatário" annotation: "Anotação" @@ -194,6 +211,8 @@ perHour: "Por Hora" perDay: "Por dia" stopActivityDelivery: "Parar a entrega de atividades" blockThisInstance: "Bloquear esta instância" +silenceThisInstance: "Silenciar essa instância" +mediaSilenceThisInstance: "Silenciar a mÃdia dessa instância" operations: "Operações" software: "Software" version: "Versão" @@ -213,6 +232,10 @@ clearCachedFiles: "Limpar o cache" clearCachedFilesConfirm: "Deseja excluir todos os arquivos remotos em cache?" blockedInstances: "Instância bloqueada" blockedInstancesDescription: "Configure os hosts dos servidores que deseja bloquear, separando-os por quebras de linha. Os servidores bloqueados não poderão interagir com este servidor, incluindo os subdomÃnios." +silencedInstances: "Instâncias silenciadas" +silencedInstancesDescription: "Liste o nome de hospedagem dos servidores que você deseja silenciar, separados por linha. Todas as contas desses servidores serão silenciada e poderão enviar solicitações para seguir, mas não poderão mencionar usuários locais sem segui-los. Isso não afetará servidores bloqueados." +mediaSilencedInstances: "Instâncias com mÃdia silenciadas" +mediaSilencedInstancesDescription: "Liste o nome de hospedagem dos servidores cuja mÃdia você deseja silenciar, separados por linha. Todas as contas desses servidores serão consideradas sensÃveis e não poderão utilizar emojis personalizados. Isso não afetará servidores bloqueados." muteAndBlock: "Silenciar e bloquear" mutedUsers: "Usuários silenciados" blockedUsers: "Usuários bloqueados" @@ -249,7 +272,7 @@ more: "Mais!" featured: "Destaques" usernameOrUserId: "Nome de usuário ou ID do usuário" noSuchUser: "Usuário não encontrado" -lookup: "Buscando" +lookup: "Consultar" announcements: "Avisos" imageUrl: "URL da imagem" remove: "Remover" @@ -257,6 +280,7 @@ removed: "Removido" removeAreYouSure: "Deseja excluir \"{x}\"?" deleteAreYouSure: "Deseja excluir \"{x}\"?" resetAreYouSure: "Deseja reiniciar?" +areYouSure: "Tem certeza?" saved: "Salvo" messaging: "Chat" upload: "Fazer upload" @@ -302,11 +326,13 @@ selectFile: "Selecione os arquivos" selectFiles: "Selecione os arquivos" selectFolder: "Selecionar uma pasta" selectFolders: "Selecionar uma pasta" +fileNotSelected: "Nenhuma pasta selecionada" renameFile: "Renomear ficheiro" folderName: "Nome da pasta" createFolder: "Criar pasta" renameFolder: "Renomear Pasta" deleteFolder: "Excluir pasta" +folder: "Pasta" addFile: "Adicionar arquivo" emptyDrive: "O drive está vazio" emptyFolder: "A pasta está vazia" @@ -368,8 +394,11 @@ hcaptcha: "hCaptcha" enableHcaptcha: "Ativar hCaptcha" hcaptchaSiteKey: "Chave do sÃtio ‘web’" hcaptchaSecretKey: "Chave secreta" +mcaptcha: "mCaptcha" +enableMcaptcha: "Habilitar mCaptcha" mcaptchaSiteKey: "Chave do sÃtio ‘web’" mcaptchaSecretKey: "Chave secreta" +mcaptchaInstanceUrl: "URL do servidor mCaptcha" recaptcha: "reCAPTCHA" enableRecaptcha: "Habilitar reCAPTCHA" recaptchaSiteKey: "Chave do sÃtio ‘web’" @@ -385,6 +414,7 @@ name: "Nome" antennaSource: "Origem de entrada" antennaKeywords: "Palavras-chave recebidas" antennaExcludeKeywords: "Palavras-chave negativas" +antennaExcludeBots: "Ignorar contas de bot" antennaKeywordsDescription: "Se você separá-lo com um espaço, será uma especificação AND, e se você separá-lo com uma quebra de linha, será uma especificação OR." notifyAntenna: "Notificar novas notas" withFileAntenna: "Apenas notas com arquivos anexados" @@ -417,6 +447,9 @@ totp: "Aplicativo Autenticador" totpDescription: "Digite a senha de uso único informado pelo aplicativo autenticador" moderator: "Moderador" moderation: "Moderação" +moderationNote: "Nota de moderação" +addModerationNote: "Adicionar nota de moderação" +moderationLogs: "Logs de moderação" nUsersMentioned: "Postado por {n} pessoas" securityKeyAndPasskey: "Chave de segurança / Chave de acesso" securityKey: "Chave de segurança" @@ -449,10 +482,12 @@ retype: "Digite novamente" noteOf: "Publicação de {user}" quoteAttached: "Com citação" quoteQuestion: "Anexar como citação?" +attachAsFileQuestion: "O texto na área de transferência é muito longo. Você gostaria de anexá-lo como um arquivo de texto?" noMessagesYet: "Sem conversas até o momento" newMessageExists: "Há uma nova mensagem" onlyOneFileCanBeAttached: "Apenas um arquivo pode ser anexado a uma mensagem" signinRequired: "É necessário se inscrever ou fazer login antes de continuar" +signinOrContinueOnRemote: "Para continuar, você precisa mover o seu servidor ou entrar/cadastrar-se nesse servidor." invitations: "Convidar" invitationCode: "Código de convite" checking: "Verificando..." @@ -476,6 +511,7 @@ 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" signinHistory: "Histórico de acesso" enableAdvancedMfm: "Habilitar MFM avançado" @@ -524,10 +560,11 @@ objectStorageUseProxy: "Usar proxy" objectStorageUseProxyDesc: "Se você não usa proxy para conexão de API, desative-o." objectStorageSetPublicRead: "Definir 'public-read' ao fazer o upload" s3ForcePathStyleDesc: "Ao habilitar s3ForcePathStyle, o nome do bucket é especificado como parte do caminho em vez de ser o nome do host na URL. Isso pode ser necessário ao usar serviços auto-hospedados como o Minio." -serverLogs: "Registro do servidor" +serverLogs: "Logs do servidor" deleteAll: "Excluir tudo" showFixedPostForm: "Exibir o formulário de postagem na parte superior da linha do tempo" showFixedPostFormInChannel: "Exibir o campo de postagem na parte superior da linha do tempo (canais)" +withRepliesByDefaultForNewlyFollowed: "Incluir respostas por usuários recém-seguidos na linha do tempo por padrão" newNoteRecived: "Nova nota recebida" sounds: "Sons" sound: "Sons" @@ -537,6 +574,8 @@ showInPage: "Ver na página" popout: "Sair" volume: "Volume" masterVolume: "volume principal" +notUseSound: "Desabilitar som" +useSoundOnlyWhenActive: "Apenas reproduzir sons quando Misskey estiver aberto." details: "Detalhes" chooseEmoji: "Selecione um emoji" unableToProcess: "Não é possÃvel concluir a operação" @@ -557,6 +596,10 @@ output: "Resultado" script: "Script" disablePagesScript: "Desabilitar scripts nas páginas" updateRemoteUser: "Atualizar informações do usuário remoto" +unsetUserAvatar: "Remover avatar" +unsetUserAvatarConfirm: "Você tem certeza de que deseja remover o avatar?" +unsetUserBanner: "Remover banner" +unsetUserBannerConfirm: "Você tem certeza de que deseja remover o banner?" deleteAllFiles: "Excluir todos os arquivos" deleteAllFilesConfirm: "Deseja excluir todos os arquivos?" removeAllFollowing: "Deseja remover todos os seguidores?" @@ -607,6 +650,7 @@ medium: "Médio" small: "Pequeno" generateAccessToken: "Gerar token de acesso" permission: "Permissões" +adminPermission: "Permissões de administrador" enableAll: "Habilitar tudo" disableAll: "Desabilitar tudo" tokenRequested: "Autorização de acesso à conta" @@ -628,6 +672,7 @@ smtpSecure: "Use SSL/TLS implÃcito para conexões SMTP" smtpSecureInfo: "Desative esta opção ao utilizar STARTTLS." testEmail: "Testar envio de e-mail" wordMute: "Silenciar palavras" +hardWordMute: "SIlenciamento pesado de palavra" regexpError: "Erro na expressão regular" regexpErrorDescription: "Ocorreu um erro na expressão regular na linha {line} da palavra mutada {tab}:" instanceMute: "Instâncias silenciadas" @@ -649,12 +694,13 @@ useGlobalSettingDesc: "Ao ativar, serão utilizadas as configurações de notifi other: "Outros" regenerateLoginToken: "Gerar novo token de login" regenerateLoginTokenDescription: "Gera novamente o token interno usado para o login. Normalmente, isso não é necessário. Ao regenerar, você será desconectado de todos os dispositivos." +theKeywordWhenSearchingForCustomEmoji: "Essa é a palavra-chave ao pesquisar por emojis personalizados" setMultipleBySeparatingWithSpace: "Você pode configurar vários itens separando-os por espaço." fileIdOrUrl: "ID do arquivo ou URL" behavior: "Comportamento" sample: "Exemplo" abuseReports: "Denúncias" -reportAbuse: "Denúncias" +reportAbuse: "Denunciar" reportAbuseRenote: "Reportar repostagem" reportAbuseOf: "Denunciar {name}" fillAbuseReportDescription: "Por favor, forneça detalhes sobre o motivo da denúncia. Se houver uma nota especÃfica envolvida, inclua também a URL dela." @@ -708,6 +754,7 @@ lockedAccountInfo: "Mesmo que você defina a aprovação para seguir, a menos qu alwaysMarkSensitive: "Marcar como sensÃvel por padrão" loadRawImages: "Exibir as imagens originais ao invés de miniaturas" disableShowingAnimatedImages: "Não reproduzir imagens animadas" +highlightSensitiveMedia: "Destacar mÃdia sensÃvel" verificationEmailSent: "Um e-mail de confirmação foi enviado. Siga o link no e-mail para concluir a verificação." notSet: "Não definido" emailVerified: "O endereço de e-mail foi confirmado" @@ -721,7 +768,7 @@ experimentalFeatures: "Funcionalidades Experimentais" experimental: "Experimental" thisIsExperimentalFeature: "Este é um recurso experimental. As funções podem mudar ou pode não funcionar corretamente." developer: "Programador" -makeExplorable: "Deixe a sua conta mais fácil de encontrar." +makeExplorable: "Deixe a sua conta encontrável em \"Explorar\"." makeExplorableDescription: "Se você desativá-lo, outros usuários não poderão encontrar a sua conta na aba Descoberta." showGapBetweenNotesInTimeline: "Mostrar um espaço entre as notas na linha de tempo" duplicate: "Duplicar" @@ -796,11 +843,12 @@ switchAccount: "Trocar conta" enabled: "Ativado" disabled: "Desativado" quickAction: "Ações rápidas" -user: "Usuários" +user: "Usuário" administration: "Administrar" accounts: "Contas" switch: "Trocar" noMaintainerInformationWarning: "A informação de administrador não foi configurada." +noInquiryUrlWarning: "URL de consulta não está definida" noBotProtectionWarning: "A proteção contra bots não foi configurada." configure: "Configurar" postToGallery: "Criar publicação em galeria" @@ -860,6 +908,8 @@ makeReactionsPublicDescription: "Isto vai deixar o histórico de todas as suas r classic: "Clássico" muteThread: "Silenciar esta conversa" unmuteThread: "Desativar silêncio desta conversa" +followingVisibility: "Visibilidade dos usuários seguidos" +followersVisibility: "Visibilidade dos seguidores" continueThread: "Ver mais desta conversa" deleteAccountConfirm: "Deseja realmente excluir a conta?" incorrectPassword: "Senha inválida." @@ -925,21 +975,33 @@ fast: "Rápido" sensitiveMediaDetection: "Detecção de conteúdo sensÃvel" localOnly: "Apenas local" remoteOnly: "Apenas remoto" +failedToUpload: "Falha ao enviar" +cannotUploadBecauseInappropriate: "Esse arquivo não pôde ser enviado porque partes dele foram detectadas como potencialmente inapropriadas." +cannotUploadBecauseNoFreeSpace: "Envio falhou devido à falta de capacidade no Drive." cannotUploadBecauseExceedsFileSizeLimit: "Não é possÃvel realizar o upload deste arquivo porque ele excede o tamanho máximo permitido." beta: "Beta" enableAutoSensitive: "Marcar automaticamente como conteúdo sensÃvel" enableAutoSensitiveDescription: "Quando disponÃvel, a marcação de mÃdia sensÃvel será automaticamente atribuÃdo ao conteúdo de mÃdia usando aprendizado de máquina. Mesmo que você desative essa função, em alguns servidores, isso pode ser configurado automaticamente." activeEmailValidationDescription: "A validação do endereço de e-mail do usuário será realizada de forma mais rigorosa, considerando se é um endereço descartável ou se é possÃvel realizar comunicação efetiva. Se desativado, apenas a validade do formato do endereço será verificada como uma sequência de caracteres." +navbar: "Barra de navegação" shuffle: "Aleatório" account: "Contas" move: "Mover" pushNotification: "Notificações Push" subscribePushNotification: "Ativar notificações push" unsubscribePushNotification: "Desativar notificações push" +pushNotificationAlreadySubscribed: "Notificações push já estão habilitadas" +pushNotificationNotSupported: "Seu navegador ou instância não tem suporte à s notificações push" +sendPushNotificationReadMessage: "Apagar notificações push quando elas foram lidas" +sendPushNotificationReadMessageCaption: "Pode aumentar o consumo de energia do dispositivo." +windowMaximize: "Maximizar" windowMinimize: "Minimizar" windowRestore: "Restaurar" caption: "legenda" +loggedInAsBot: "Atualmente conectado como bot" tools: "Ferramentas" +cannotLoad: "Não foi possÃvel carregar" +numberOfProfileView: "Visualizações do perfil" like: "Curtir" unlike: "Remover curtida" numberOfLikes: "Número de curtidas" @@ -948,6 +1010,7 @@ neverShow: "Não exibir novamente" remindMeLater: "Lembrar mais tarde" didYouLikeMisskey: "Você gostou do Misskey?" pleaseDonate: "O Misskey é um software gratuito utilizado por {host}. Para que possamos continuar o desenvolvimento, pedimos que considerem fazer doações. A sua contribuição é muito importante!" +correspondingSourceIsAvailable: "O código-fonte correspondente está disponÃvel em {anchor}" roles: "Cargos" role: "Cargo" noRole: "Nenhum cargo" @@ -957,6 +1020,7 @@ assign: "Atribuir" unassign: "Remover" color: "Cor" manageCustomEmojis: "Gerenciar Emojis customizados" +manageAvatarDecorations: "Gerenciar decorações de avatar" youCannotCreateAnymore: "Você atingiu o limite de criação." cannotPerformTemporary: "Ação temporariamente indisponÃvel" cannotPerformTemporaryDescription: "Esta ação não pôde ser concluÃda devido ao excesso de pedidos em sucessão. Tente novamente em alguns momentos." @@ -974,218 +1038,635 @@ thisPostMayBeAnnoyingHome: "Postar na linha do tempo inicial" thisPostMayBeAnnoyingCancel: "Cancelar" thisPostMayBeAnnoyingIgnore: "Postar mesmo assim" collapseRenotes: "Ocultar repostagens já visualizadas" +collapseRenotesDescription: "Colapsar notas em que você reagiu ou repostou." internalServerError: "Erro interno de servidor" +internalServerErrorDescription: "Houve um erro inesperado no servidor." +copyErrorInfo: "Copiar detalhes de erro" +joinThisServer: "Cadastrar-se na instância" +exploreOtherServers: "Buscar outra instância" +letsLookAtTimeline: "Dar uma olhada na linha do tempo" +disableFederationConfirm: "Realmente desabilitar a federação?" +disableFederationConfirmWarn: "Mesmo se defederado, publicações continuarão sendo públicas, a menos que seja definido o contrário. Você geralmente não precisa disso." +disableFederationOk: "Desabilitar" +invitationRequiredToRegister: "Essa instância é apenas para convidados. Você precisa inserir um código válido para se cadastrar." emailNotSupported: "O envio de e-mails não é suportado nesta instância" +postToTheChannel: "Publicar ao canal" +cannotBeChangedLater: "Isso não pode ser alterado." +reactionAcceptance: "Aceitação de Reações" likeOnly: "Apenas curtidas" likeOnlyForRemote: "Tudo (somente curtidas remotas)" +nonSensitiveOnly: "Apenas não-sensÃvel" nonSensitiveOnlyForLocalLikeOnlyForRemote: "Apenas não sensÃveis (somente curtidas remotas)" rolesAssignedToMe: "Cargos atribuÃdos a mim" +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" +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" +hiddenTagsDescription: "Selecione tags que não serão exibidas na lista de destaques. Várias tags podem ser escolhidas, separadas por linha." +notesSearchNotAvailable: "A pesquisa de notas está indisponÃvel." +license: "Licença" unfavoriteConfirm: "Deseja realmente remover dos favoritos?" +myClips: "Meus clipes" drivecleaner: "Limpeza do drive" +retryAllQueuesNow: "Tentar novamente todas as pendências" retryAllQueuesConfirmTitle: "Gostaria de tentar novamente agora?" +retryAllQueuesConfirmText: "Isso irá temporariamente aumentar a carga do servidor." +enableChartsForRemoteUser: "Gerar gráficos estatÃsticos de usuários remotos" +enableChartsForFederatedInstances: "Gerar gráficos estatÃsticos de instâncias remotas" +showClipButtonInNoteFooter: "Adicionar \"Clip\" ao menu de ação de notas" reactionsDisplaySize: "Tamanho de exibição das reações" +limitWidthOfReaction: "Limita o comprimento máximo de reações e as exibe em tamanho reduzido" +noteIdOrUrl: "ID ou URL de nota" +video: "VÃdeo" +videos: "VÃdeos" +audio: "Ãudio" +audioFiles: "Ãudio" +dataSaver: "Economia de Dados" +accountMigration: "Migração da Conta" +accountMoved: "Esse usuário moveu-se para uma nova conta:" +accountMovedShort: "Essa conta foi migrada." +operationForbidden: "Operação proibÃda" +forceShowAds: "Sempre mostrar propagandas" +addMemo: "Adicionar memorando" +editMemo: "Editar memorando" reactionsList: "Reações" renotesList: "Repostagens" +notificationDisplay: "Notificações" leftTop: "Superior esquerdo" rightTop: "Superior direito" leftBottom: "Inferior esquerdo" rightBottom: "Inferior direito" +stackAxis: "Eixo de empilhamento" vertical: "Vertical" horizontal: "Exibir painel lateral inteiro" position: "Posição" serverRules: "Regras do servidor" +pleaseConfirmBelowBeforeSignup: "Para cadastrar-se no servidor, você precisa ler e concordar como seguinte:" +pleaseAgreeAllToContinue: "Você precisa concordar com todos os campos acima para continuar." continue: "Continuar" +preservedUsernames: "Nomes de usuário reservados" preservedUsernamesDescription: "Liste os nomes de usuário que deseja reservar, separando-os por quebras de linha. Os nomes de usuário especificados aqui não poderão ser utilizados durante a criação de contas. No entanto, esta restrição não se aplica quando a conta é criada por um administrador. Além disso, as contas que já existem não serão afetadas." +createNoteFromTheFile: "Compor nota a partir desse arquivo" archive: "Arquivo" +archived: "Arquivado" +unarchive: "Desarquivar" channelArchiveConfirmTitle: "Deseja realmente arquivar {name}?" +channelArchiveConfirmDescription: "Um canal arquivado não irá aparecer na lista de canais e nem resultados de pesquisa. Novas publicações não poderão mais ser adicionadas." +thisChannelArchived: "Esse canal foi arquivado." +displayOfNote: "Exibição de nota" +initialAccountSetting: "Configuração inicial do perfil" youFollowing: "Seguindo" +preventAiLearning: "Rejeitar uso de Aprendizado de Máquina (IA Generativa)" preventAiLearningDescription: "Solicita-se que o conteúdo de notas e imagens enviadas não seja usado como objeto de aprendizado por sistemas externos de geração de texto ou imagens. Isso é alcançado incluindo a flag 'noai' na resposta HTML. No entanto, o cumprimento dessa solicitação depende do próprio sistema de IA, portanto, não é garantia total de prevenção de aprendizado." options: "Opções" +specifyUser: "Usuário especÃfico" +lookupConfirm: "Deseja buscar?" +openTagPageConfirm: "Deseja abrir a uma página de hashtag?" +specifyHost: "Especificar um hospedeiro" +failedToPreviewUrl: "Não foi possÃvel carregar prévia" +update: "Atualizar" rolesThatCanBeUsedThisEmojiAsReaction: "Cargos que podem utilizar este emoji como reação" rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription: "Se nenhum cargo for especificado, qualquer pessoa pode usar este emoji como reação." rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn: "Estes cargos devem ser públicos." +cancelReactionConfirm: "Realmente excluir a sua reação?" +changeReactionConfirm: "Realmente mudar a sua reação?" +later: "Talvez mais tarde" +goToMisskey: "Ao Misskey" +additionalEmojiDictionary: "Dicionários adicionais de emoji" +installed: "Instalado" +branding: "Marca" +enableServerMachineStats: "Publicar estatÃsticas do hardware do servidor" +enableIdenticonGeneration: "Habilitar geração de identicon de usuário" +turnOffToImprovePerformance: "Desligar isso pode melhorar o desempenho." +createInviteCode: "Gerar convite" +createWithOptions: "Criar com opções" +createCount: "Número de convites" +inviteCodeCreated: "Convite gerado" +inviteLimitExceeded: "Você excedeu o limite de convites que podem ser gerados." +createLimitRemaining: "Limite de convites: {limit}" +inviteLimitResetCycle: "Esse limite irá tornar-se {limit} em {time}." +expirationDate: "Data de expiração" +noExpirationDate: "Sem expiração" +inviteCodeUsedAt: "Código de convite usado em" +registeredUserUsingInviteCode: "Convite usado por" waitingForMailAuth: "Verificação de e-mail pendente " +inviteCodeCreator: "Convite criado por" +usedAt: "Usado em" +unused: "Não foi usado" +used: "Usado" +expired: "Expirado" +doYouAgree: "Concorda?" +beSureToReadThisAsItIsImportant: "Por favor, leia essa informação importante." +iHaveReadXCarefullyAndAgree: "Eu li o texto \"{x}\" e concordo." +dialog: "Diálogo" icon: "Avatar" -replies: "Respostas" -renotes: "Repostagens" +forYou: "Para você" +currentAnnouncements: "Anúncios atuais" +pastAnnouncements: "Anúncios passados" +youHaveUnreadAnnouncements: "Há anúncios não lidos." +useSecurityKey: "Por favor, siga as instruções do seu navegador ou dispositivo para utilizar uma chave de acesso." +replies: "Responder" +renotes: "Repostar" +loadReplies: "Mostrar respostas" +loadConversation: "Mostrar conversa" +pinnedList: "Lista fixada" keepScreenOn: "Manter a tela do dispositivo sempre ligada" +verifiedLink: "A autoria do link foi verificada" +notifyNotes: "Notificar sobre novas notas" +unnotifyNotes: "Deixar de notificar sobre novas notas" +authentication: "Autenticação" +authenticationRequiredToContinue: "Por favor, autentique-se para continuar" +dateAndTime: "Data e Hora" +showRenotes: "Exibir reposts" +edited: "Editado" +notificationRecieveConfig: "Configurações de Notificação" +mutualFollow: "Seguidor mútuo" +followingOrFollower: "Seguidor ou usuário seguido" +fileAttachedOnly: "Apenas notas com arquivos" +showRepliesToOthersInTimeline: "Mostrar respostas aos outros na linha do tempo" +hideRepliesToOthersInTimeline: "Esconder respostas dos outros na linha do tempo" +showRepliesToOthersInTimelineAll: "Mostrar respostas aos outros, mas apenas de quem você segue, na linha do tempo" +hideRepliesToOthersInTimelineAll: "Esconder respostas de todos que você segue na linha do tempo" +confirmShowRepliesAll: "Essa operação é irreversÃvel. Você gostaria de mostrar respostas a todos que você segue na sua linha do tempo?" +confirmHideRepliesAll: "Essa operação é irreversÃvel. Você gostaria de esconder respostas a todos que você segue na sua linha do tempo?" +externalServices: "Serviços Externos" +sourceCode: "Código-fonte" +sourceCodeIsNotYetProvided: "Código-fonte está indisponÃvel. Contate o administrador para resolver esse problema." +repositoryUrl: "URL do repositório" +repositoryUrlDescription: "Se você estiver utilizando Misskey como está (sem mudanças no código-fonte), insira https://github.com/misskey-dev/misskey" +repositoryUrlOrTarballRequired: "Se você não publicou um repositório, você precisa providenciar uma tarball em seu lugar. Veja .config/example.yml para mais informações." +feedback: "Feedback" +feedbackUrl: "Link para Feedback" +impressum: "Impressum" +impressumUrl: "URL de 'Impressum'" +impressumDescription: "Em alguns paÃses, como a Alemanha, a inclusão de informação de contato do operador de um serviço é legalmente exigida para websites comerciais." +privacyPolicy: "PolÃtica de Privacidade" +privacyPolicyUrl: "URL da PolÃtica de Privacidade" +tosAndPrivacyPolicy: "Termos de Serviço e PolÃtica de Privacidade" +avatarDecorations: "Decorações de avatar" +attach: "Anexar" +detach: "Remover" +detachAll: "Remover Tudo" +angle: "Ângulo" flip: "Inversão" +showAvatarDecorations: "Exibir decorações de avatar" +releaseToRefresh: "Solte para atualizar" +refreshing: "Atualizando..." +pullDownToRefresh: "Puxe para baixo para atualizar" +disableStreamingTimeline: "Desabilitar atualizações em tempo real da linha do tempo" +useGroupedNotifications: "Agrupar notificações" +signupPendingError: "Houve um problema ao verificar o endereço de email. O link pode ter expirado." +cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada." +doReaction: "Adicionar reação" +code: "Código" +reloadRequiredToApplySettings: "É necessário reiniciar para aplicar as configurações." +remainingN: "Restante: {n}" +overwriteContentConfirm: "Você tem certeza de que deseja sobrescrever o conteúdo atual?" +seasonalScreenEffect: "Efeito de Tela Sazonal" +decorate: "Decorar" +addMfmFunction: "Adicionar MFM" +enableQuickAddMfmFunction: "Exibir seleção avançada de MFM" +bubbleGame: "Bubble Game" +sfx: "Efeitos Sonoros" +soundWillBePlayed: "Sons serão reproduzidos" +showReplay: "Ver Replay" +replay: "Replay" +replaying: "Mostrando Replay" +endReplay: "Sair do Replay" +copyReplayData: "Copiar dados de Replay" +ranking: "Ranking" lastNDays: "Últimos {n} dias" +backToTitle: "Voltar à página inicial" +hemisphere: "Onde você se localiza" +withSensitive: "Incluir notas com arquivos sensÃveis" +userSaysSomethingSensitive: "Publicação de {name} contém conteúdo sensÃvel" +enableHorizontalSwipe: "Arraste para mudar de aba" +loading: "Carregando" surrender: "Cancelar" +gameRetry: "Tentar Novamente" +notUsePleaseLeaveBlank: "Deixe em branco caso inutilizado" +useTotp: "Digite a senha de uso único" +useBackupCode: "Usar códigos de “backupâ€" +launchApp: "Iniciar aplicação" +useNativeUIForVideoAudioPlayer: "Utilizar UI do navegador ao reproduzir vÃdeo e áudio" +keepOriginalFilename: "Manter nome original do arquivo" +keepOriginalFilenameDescription: "Se você desabilitar essa opção, os nomes de arquivos serão substituÃdos por uma sequência aleatória ao enviar arquivos." +noDescription: "Não há descrição" +alwaysConfirmFollow: "Sempre confirmar ao seguir" +inquiry: "Contato" +tryAgain: "Por favor, tente novamente mais tarde" +confirmWhenRevealingSensitiveMedia: "Confirmar ao revelar mÃdia sensÃvel" +sensitiveMediaRevealConfirm: "Essa mÃdia pode ser sensÃvel. Deseja revelá-la?" +createdLists: "Listas criadas" +createdAntennas: "Antenas criadas" +clipNoteLimitExceeded: "Não é possÃvel adicionar mais notas ao clipe." _delivery: + status: "Estado de entrega" stop: "Suspenso" + resume: "Continuar entrega" _type: none: "Publicando" + manuallySuspended: "Suspenso manualmente" + goneSuspended: "Servidor foi suspenso devido ao seu apagamento" + autoSuspendedForNotResponding: "Servidor foi suspenso por não responder" +_bubbleGame: + howToPlay: "Como jogar" + hold: "Próximos" + _score: + score: "Pontuação" + scoreYen: "Dinheiro recebido" + highScore: "Melhor pontuação" + maxChain: "Número máximo de encadeamentos" + yen: "{yen} Yen" + estimatedQty: "{qty} Peças" + scoreSweets: "{onigiriQtyWithUnit} Onigiri" + _howToPlay: + section1: "Ajuste a posição e solte o objeto na caixa." + section2: "Quando dois objetos do mesmo tipo tocam-se, eles tornam-se outro objeto e você ganha pontos." + section3: "O jogo acaba quando objetos transbordam da caixa. Busque uma pontuação alta ao fundir objetos enquanto evita transbordar a caixa." +_announcement: + forExistingUsers: "Apenas aos usuários existente" + forExistingUsersDescription: "Se habilitado, esse anúncio será exibido apenas para usuários existentes no tempo de publicação. Se desabilitado, novos usuários também o receberão. " + needConfirmationToRead: "Exigir confirmação de leitura" + needConfirmationToReadDescription: "Um lembrete adicional será exibido para confirmar a leitura do anúncio. Esse anúncio também será excluÃdo de qualquer forma de \"Marcar tudo como lido\"." + end: "Arquivar anúncio" + tooManyActiveAnnouncementDescription: "O excesso de anúncios pode atrapalhar a experiência do usuário. Considere arquivar anúncios obsoletos." + readConfirmTitle: "Marcar como lido?" + readConfirmText: "Isso marcará o conteúdo de \"{title}\" como lido." + shouldNotBeUsedToPresentPermanentInfo: "É preferÃvel utilizar anúncios para publicar informações atuais e de curto prazo, e não informações que serão relevantes por muito tempo." + dialogAnnouncementUxWarn: "O uso de duas ou mais notificações de diálogo simultaneamente pode impactar significativamente a experiência de usuário. Portanto, utilize-as cuidadosamente." + silence: "Sem notificação" + silenceDescription: "Habilitar isso irá pular a notificação desse anúncio e o usuário não precisará lê-lo." _initialAccountSetting: + accountCreated: "A sua conta foi criada com sucesso!" + letsStartAccountSetup: "Em primeiro lugar, vamos configurar o seu perfil." + letsFillYourProfile: "Primeiramente, vamos configurar o seu perfil." + profileSetting: "Configurações do perfil" + privacySetting: "Configurações de privacidade" + theseSettingsCanEditLater: "Você pode alterar estas configurações mais tarde." + youCanEditMoreSettingsInSettingsPageLater: "Há mais configurações na página \"Configurações\". Não se esqueça de visitá-la mais tarde." followUsers: "Siga usuários que lhe interessam para criar a sua linha do tempo." + pushNotificationDescription: "Habilitar notificações push o possibilitará receber notificações de {name} diretamente no seu dispositivo." + initialAccountSettingCompleted: "Configuração de perfil completa!" + haveFun: "Aproveite {name}!" + youCanContinueTutorial: "Você pode iniciar um tutorial de como utilizar {name} (Misskey) ou pode sair da configuração e começar o uso imediatamente." + startTutorial: "Iniciar Tutorial" + skipAreYouSure: "Deseja pular a configuração de perfil?" + laterAreYouSure: "Deseja adiar a configuração de perfil?" +_initialTutorial: + launchTutorial: "Iniciar Tutorial" + title: "Tutorial" + wellDone: "Ótimo!" + skipAreYouSure: "Sair do Tutorial?" + _landing: + title: "Bem-vindo ao Tutorial!" + description: "Aqui, você pode aprender o básico de como usar o Misskey e as suas funções." + _note: + title: "O que é uma Nota?" + description: "Publicações no Misskey chamam-se 'Notas'. Notas são organizadas cronologicamente na linha do tempo e atualizam em tempo real." + reply: "Clique nesse botão para responder a uma mensagem. Também é possÃvel responder respostas, continuando a conversa como uma \"thread\"." + renote: "Você pode compartilhar essa nota na sua linha do tempo. Você também pode citá-la com os seus comentários." + reaction: "Você pode adicionar reações à nota. Mais detalhes serão explicados na próxima página." + menu: "Você pode ver detalhes da nota, copiar links e realizar outras ações." + _reaction: + title: "O que são Reações?" + description: "É possÃvel reagir à s notas com diversos emojis. Reações permitem que você expresse sutilezas que não são possÃveis apenas com uma curtida." + letsTryReacting: "Reações podem ser adicionadas clicando no botão \"+\". Tente reagir à nota de exemplo." + reactToContinue: "Adicione uma reação para continuar." + reactNotification: "Você receberá notificações em tempo real quando alguém reagir à sua nota." + reactDone: "Você pode desfazer uma reação ao selecionar o botão \"-\"." + _timeline: + title: "O Conceito das Linhas do Tempo" + description1: "Misskey providencia diversas linhas do tempo baseadas na sua utilidade (algumas podem não estar disponÃveis a partir das configurações da instância)." + home: "Você pode ver as notas das contas seguidas. " + local: "Você pode ver notas de todos os usuários dessa instância." + social: "Notas da linha do tempo InÃcio e Local serão exibidas." + global: "Você pode ver notas de todos os servidores conectados." + description2: "Você pode alterar dentre as linhas do tempo no todo da tela a qualquer momento." + description3: "Adicionalmente, há \"listas\" e \"canais\". Para mais informações, acesse {link}." + _postNote: + title: "Opções de Postagem de Nota" + description1: "Ao postar uma nota no Misskey, diversas opções estão disponÃveis. A ficha de publicação parece com isto: " + _visibility: + description: "Você pode limitar quem vê a sua nota." + public: "Sua nota será visÃvel a todos os usuários." + home: "Publicar apenas na linha do tempo InÃcio. Pessoas visitando seu perfil, seja seguindo ou por um repost poderão vê-los." + followers: "VisÃvel apenas para seguidores. Apenas seguidores podem vê-la e mais ninguém, e ela não pode ser repostada pelos demais." + direct: "VisÃvel apenas para usuários especÃficos, e o destinatário será notificado. Pode ser usado como uma alternativa à s mensagens diretas." + doNotSendConfidencialOnDirect1: "Tenha cuidado ao enviar informações sensÃveis!" + doNotSendConfidencialOnDirect2: "Administradores do servidor podem ver o que foi escrito. Cuidado, também, ao enviar notas diretas a usuários de servidores não confiáveis." + localOnly: "Publicar com essa opção não federará a nota com outros servidores. Usuários desses servidores não poderão ver essas notas diretamente, independente das opções de visibilidade acima. " + _cw: + title: "Aviso de Conteúdo" + description: "Ao invés do corpo do texto, o conteúdo escrito na caixa \"anotação\" será exibido. Apertar \"Carregar mais\" irá revelar o corpo." + _exampleNote: + cw: "Isso irá te esfomear!" + note: "Acabei de comer um donut coberto de chocolate! ðŸ©ðŸ˜‹" + useCases: "Isso pode ser usado caso seja exigido, pelas diretrizes do servidor, o cuidado com algum tópico ou ao publicar conteúdo sensÃvel ou spoilers." + _howToMakeAttachmentsSensitive: + title: "Como Marcar Anexos como SensÃveis?" + description: "Para anexos cujo conteúdo é considerado sensÃvel pelas diretrizes do servidor ou quando pretende-se esconder o seu conteúdo, adicione o sinal \"sensÃvel\"." + tryThisFile: "Tente marcar a imagem anexada como sensÃvel!" + _exampleNote: + note: "Opa, me atrapalhei abrindo a tampa do natô..." + method: "Para marcar um anexo como sensÃvel, clique na sua miniatura, abra o menu e clique \"Marcar como sensÃvel\"." + sensitiveSucceeded: "Ao anexar arquivos, por favor atribua uma sensibilidade coerente com as diretrizes da instância." + doItToContinue: "Marque o anexo como sensÃvel para prosseguir." + _done: + title: "Você completou o tutorial! 🎉" + description: "As funções apresentadas aqui são apenas uma pequena parte. Para um conhecimento mais detalhado do uso do Misskey, acesse {link}." +_timelineDescription: + home: "Na linha do tempo InÃcio, você verá notas dos usuários que você segue." + local: "Na linha do tempo Local, você verá notas de todos os usuários da instância." + social: "Na linha do tempo Social, você verá notas do InÃcio e Local." + global: "Na linha do tempo Global, você verá notas de todas as instâncias conectadas." +_serverRules: + description: "Um grupo de regras a ser exibido antes de um cadastro. É recomendado que se faça um resumo dos Termos de Serviço." _serverSettings: iconUrl: "URL do Ãcone" + appIconDescription: "Especifica o Ãcone utilizado quando {host} é exibido como um app." + appIconUsageExample: "Exemplo: Como PWA, ou quando exibido num marcador de páginas ou na tela inicial de um celular" + appIconStyleRecommendation: "Como o Ãcone pode ser cortado para um quadrado ou cÃrculo, é recomendado adicionar um fundo colorido na imagem." + appIconResolutionMustBe: "A resolução mÃnima é {resolution}." + manifestJsonOverride: "Sobrescrever manifest.json" + shortName: "Abreviação" + shortNameDescription: "Uma abreviação do nome da instância que pode ser exibido caso o nome oficial completo seja muito longo." + fanoutTimelineDescription: "Melhora significativamente a performance do retorno da linha do tempo e reduz o impacto no banco de dados quando habilitado. Em contrapartida, o uso de memória do Redis aumentará. Considere desabilitar em casos de baixa disponibilidade de memória ou instabilidade do servidor." + fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados" + fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas." + inquiryUrl: "URL de inquérito" + inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato." _accountMigration: + moveFrom: "Migrar outra conta para essa" + moveFromSub: "Criar um 'alias' a outra conta" + moveFromLabel: "Conta original #{n}" moveFromDescription: "Se você deseja migrar de outra conta para esta, é necessário criar um alias aqui. Por favor, insira a conta de origem da migração no seguinte formato: @username@server.example.com. Para excluir o alias, deixe o campo em branco e clique em salvar (não recomendado)." + moveTo: "Migrar dessa conta para outra" + moveToLabel: "Conta para a qual se mover:" + moveCannotBeUndone: "A migração de conta não pode ser desfeita." moveAccountDescription: "Você está migrando para uma nova conta.\n ・Seus seguidores irão automaticamente seguir a nova conta.\n ・Todas as suas conexões de seguidores nesta conta serão removidas.\n ・Você não poderá mais criar novas notas nesta conta.\n\nA migração dos seguidores é automática, mas a migração das pessoas que você segue deve ser feita manualmente. Antes de migrar, exporte quem você está seguindo nesta conta e, assim que migrar, importe essa lista na nova conta.\nO mesmo se aplica para listas, silenciamentos e bloqueios, que também devem ser migrados manualmente.\n\n(Esta descrição se refere ao comportamento do servidor Misskey v13.12.0 ou posterior. Outros softwares ActivityPub, como Mastodon, podem ter comportamentos diferentes.)" moveAccountHowTo: "Para realizar a migração da conta, primeiro crie um alias para esta conta no destino da migração. Após criar o alias, insira a conta de destino da migração no seguinte formato: @username@server.example.com." + startMigration: "Migrar" migrationConfirm: "Tem certeza de que deseja migrar esta conta para '{account}'? Uma vez migrada, não poderá ser desfeita e não será possÃvel usar esta conta novamente em seu estado original." + movedAndCannotBeUndone: "Essa conta foi migrada. A migração não pode ser desfeita." postMigrationNote: "A remoção dos seguidores desta conta será realizada 24 horas após a operação de migração. O número de seguidores e seguidos desta conta se tornará zero. Os seguidores não serão removidos, portanto, eles continuarão a ver as postagens destinadas aos seguidores desta conta." + movedTo: "Conta para a qual se mover:" _achievements: earnedAt: "Data de aquisição" _types: _notes1: title: "Configurando o meu misskey" - description: "Postou uma nota pela primeira vez" + description: "Post uma nota pela primeira vez" flavor: "Divirta-se com o Misskey!" _notes10: title: "Algumas notas" - description: "Postou 10 notas" + description: "Poste 10 notas" _notes100: title: "Um monte de notas" - description: "Postou 100 notas" + description: "Poste 100 notas" _notes500: title: "Coberto por notas" - description: "Postou 500 notas" + description: "Poste 500 notas" _notes1000: title: "Uma montanha de notas" - description: "Postou 1000 notas" + description: "Poste 1 000 notas" _notes5000: title: "Enxurrada de notas" - description: "Postou 5000 notas" + description: "Poste 5000 notas" _notes10000: - title: "Super nota" - description: "Postou 10000 notas" + title: "Supernota" + description: "Poste 10 000 notas" _notes20000: title: "Preciso... de mais... notas..." - description: "Postou 20000 notas" + description: "Poste 20 000 notas" _notes30000: title: "Notas, Notas, NOTAS!" - description: "Postou 30000 notas" + description: "Poste 30 000 notas" _notes40000: title: "Fábrica de notas" - description: "Postou 40000 notas" + description: "Poste 40 000 notas" _notes50000: title: "Planeta de notas" - description: "Postou 50000 notas" + description: "Poste 50 000 notas" _notes60000: title: "Quasar de notas" - description: "Postou 60000 notas" + description: "Poste 60 000 notas" _notes70000: title: "Buraco negro de notas" - description: "Postou 70000 notas" + description: "Poste 70 000 notas" _notes80000: title: "Galáxia de notas" - description: "Postou 80000 notas" + description: "Poste 80 000 notas" _notes90000: title: "Universo de notas" - description: "Postou 90000 notas" + description: "Poste 90 000 notas" _notes100000: title: "ALL YOUR NOTE ARE BELONG TO US" - description: "Postou 100000 notas" + description: "Poste 100 000 notas" flavor: "Você realmente tem muita coisa para escrever" _login3: title: "Iniciante I" - description: "Fez login por um total de 3 dias" + description: "Faça login por um total de 3 dias" flavor: "De hoje em diante, me chame apenas de Misskist" _login7: title: "Iniciante II" - description: "Fez login por um total de 7 dias" + description: "Faça login por um total de 7 dias" flavor: "Pegando o jeito da coisa?" _login15: title: "Iniciante III" - description: "Fez login por um total de 15 dias" + description: "Faça login por um total de 15 dias" _login30: title: "Misskist I" - description: "Fez login por um total de 30 dias" + description: "Faça login por um total de 30 dias" _login60: title: "Misskist II" - description: "Fez login por um total de 60 dias" + description: "Faça login por um total de 60 dias" _login100: title: "Misskist III" - description: "Fez login por um total de 100 dias" + description: "Faça login por um total de 100 dias" flavor: "Misskist violento" _login200: title: "Freguês I" - description: "Fez login por um total de 200 dias" + description: "Faça login por um total de 200 dias" _login300: title: "Freguês II" - description: "Fez login por um total de 300 dias" + description: "Faça login por um total de 300 dias" _login400: title: "Freguês III" - description: "Fez login por um total de 400 dias" + description: "Faça login por um total de 400 dias" _login500: title: "Veterano I" - description: "Fez login por um total de 500 dias" + description: "Faça login por um total de 500 dias" flavor: "Cavalheiros, tudo o que peço são notas" _login600: title: "Veterano II" - description: "Fez login por um total de 600 dias" + description: "Faça login por um total de 600 dias" _login700: title: "Veterano III" - description: "Fez login por um total de 700 dias" + description: "Faça login por um total de 700 dias" _login800: - title: "Mestre das notas I" - description: "Fez login por um total de 800 dias" + title: "Mestre das Notas I" + description: "Faça login por um total de 800 dias" _login900: - title: "Mestre das notas II" - description: "Fez login por um total de 900 dias" + title: "Mestre das Notas II" + description: "Faça login por um total de 900 dias" _login1000: - title: "Mestre das notas III" - description: "Fez login por um total de 1000 dias" + title: "Mestre das Notas III" + description: "Faça login por um total de 1 000 dias" flavor: "Obrigado por utilizar o Misskey!" _noteClipped1: - title: "Não posso deixar de adicionar ao clipe" - description: "Adicionou a um clipe a sua primeira nota" + title: "Preciso... clipar..." + description: "Adicione a um clipe a sua primeira nota" _noteFavorited1: - title: "Astrônomo amador" - description: "Adicionou uma nota aos favoritos pela primeira vez" + title: "Astrônomo Amador" + description: "Adicione uma nota aos favoritos pela primeira vez" _myNoteFavorited1: title: "Cabeça nas estrelas" - description: "Teve uma das suas notas adicionada aos favoritos de alguém" + description: "Tenha uma das suas notas adicionada aos favoritos de alguém" _profileFilled: - title: "Tudo pronto" - description: "Configurou o seu perfil" + title: "Tudo Pronto" + description: "Configure o seu perfil" _markedAsCat: title: "Eu Sou Um Gato" - description: "Marcou a sua conta como um gato" + description: "Marque a sua conta como um gato" flavor: "Ainda não tenho um nome." _following1: title: "Primeira vez seguindo alguém" - description: "Seguiu um usuário pela primeira vez" + description: "Siga um usuário pela primeira vez" _following10: title: "Circulando, circulando" - description: "Seguiu 10 usuários" + description: "Siga 10 usuários" _following50: title: "Muitos amigos" - description: "Seguiu 50 usuários" + description: "Siga 50 usuários" _following100: - title: "100 amigos" - description: "Seguiu 100 usuários" + title: "100 Amigos" + description: "Siga 100 usuários" _following300: title: "Sobrecarga de amigos" - description: "Seguiu 300 usuários" + description: "Siga 300 usuários" _followers1: title: "Primeiro seguidor" - description: "Ganhou o seu primeiro seguidor" + description: "Ganhe o seu primeiro seguidor" _followers10: title: "Sigam-me os bons!" - description: "Ganhou 10 seguidores" + description: "Ganhe 10 seguidores" _followers50: title: "Aos montes" - description: "Ganhou 50 seguidores" + description: "Ganhe 50 seguidores" _followers100: title: "Popular" - description: "Ganhou 100 seguidores" + description: "Ganhe 100 seguidores" _followers300: title: "Em fila única, por favor" - description: "Ganhou 300 seguidores" + description: "Ganhe 300 seguidores" _followers500: title: "Torre de celular" - description: "Ganhou 500 seguidores" + description: "Ganhe 500 seguidores" _followers1000: title: "Influencer" - description: "Ganhou 1000 seguidores" + description: "Ganhe 1 000 seguidores" + _collectAchievements30: + title: "Coletor de Conquistas" + description: "Ganhe 30 conquistas" + _viewAchievements3min: + title: "Curte Conquistas" + description: "Olhe para a sua lista de conquistas por pelo menos 3 minutos" + _iLoveMisskey: + title: "Eu Amo Misskey" + description: "Poste \"I ⤠#Misskey\"" + flavor: "A equipe de desenvolvimento do Misskey aprecia profundamente o seu apoio!" + _foundTreasure: + title: "Caça ao Tesouro" + description: "Você achou o tesouro escondido" + _client30min: + title: "Pausinha" + description: "Deixe o Misskey aberto por pelo menos 30 minutos" + _client60min: + title: "Sem falta" + description: "Deixe o Misskey aberto por pelo menos 60 minutos" _noteDeletedWithin1min: title: "Deixa pra lá" - description: "Excluà a postagem dentro de 1 minuto após ter publicado" + description: "Exclua a postagem dentro de 1 minuto após a ter publicado" + _postedAtLateNight: + title: "Noturno" + description: "Poste uma nota tarde da noite" + flavor: "Tá na hora de ir dormir." + _postedAt0min0sec: + title: "Relógio Falante" + description: "Poste uma nota à meia-noite em ponto" + flavor: "Tic-Tac-Tic-Tac" + _selfQuote: + title: "Autorreferência" + description: "Cite sua própria nota" + _htl20npm: + title: "Linha do Tempo Fluida" + description: "Faça a velocidade da linha do tempo exceder 20 npm (notas por minuto)" + _viewInstanceChart: + title: "Analista" + description: "Veja os infográficos da instância" + _outputHelloWorldOnScratchpad: + title: "Olá, Mundo!" + description: "Produza \"hello world\" no Scratchpad" + _open3windows: + title: "Múlti-Janelas" + description: "Tenha ao mÃnimo 3 janelas abertas simultaneamente." _driveFolderCircularReference: title: "Referência circular" + description: "Tente criar uma pasta recursiva no Drive." + _reactWithoutRead: + title: "Você leu tudo isso?" + description: "Reaja a uma nota com mais de 100 caracteres dentro de 3 segundos após a sua publicação." + _clickedClickHere: + title: "Clique aqui" + description: "Você clicou aqui" + _justPlainLucky: + title: "Pura Sorte" + description: "Tem uma chance de ser obtido com uma probabilidade de 0.005% a cada 10 segundos." + _setNameToSyuilo: + title: "Complexo de Deus" + description: "Colocar seu nome como \"syuilo\"" + _passedSinceAccountCreated1: + title: "Aniversário de Um Ano" + description: "Um ano passou-se desde a criação da conta" + _passedSinceAccountCreated2: + title: "Aniversário de Dois Anos" + description: "Dois anos passaram-se desde a criação da conta" + _passedSinceAccountCreated3: + title: "Aniversário de Três Anos" + description: "Três anos passaram-se desde a criação da conta" + _loggedInOnBirthday: + title: "Feliz Aniversário" + description: "Entre no dia do seu aniversário" + _loggedInOnNewYearsDay: + title: "Feliz Ano Novo!" + description: "Entre no primeiro dia do ano" + flavor: "Para outro ótimo ano nessa instância" + _cookieClicked: + title: "Um jogo onde você clica em cookies" + description: "Clicou o cookie" + flavor: "Pera, você tá no website correto?" + _brainDiver: + title: "Brain Diver" + description: "Poste o link do Brain Diver" + flavor: "Misskey-Misskey La-Tu-Ma" + _smashTestNotificationButton: + title: "Teste de Transbordamento" + description: "Ative o teste de notificações repetidamente dentro de um curto perÃodo de tempo" + _tutorialCompleted: + title: "Diploma de Ensino Fundamental Misskey" + description: "Complete o tutorial" + _bubbleGameExplodingHead: + title: "🤯" + description: "O maior objeto no Bubble Game" + _bubbleGameDoubleExplodingHead: + title: "🤯 Duplo" + description: "Dois dos maiores objetos do Bubble Game ao mesmo tempo." + flavor: "Dá para encher uma lancheira com esses 🤯🤯." _role: new: "Novo cargo" edit: "Editar cargo" @@ -1195,8 +1676,10 @@ _role: descriptionOfPermission: "<b>Moderador</b> permite que você execute operações básicas relacionadas à moderação.\n<b>Administradores</b> podem alterar todas as configurações do servidor." assignTarget: "Atribuir" descriptionOfAssignTarget: "<b>Manual</b> para gerenciar manualmente quem está incluÃdo neste cargo.\n<b>Condicional</b> define uma condição e os usuários que corresponderem a ela serão incluÃdos automaticamente." - manual: "Documentação" + manual: "Manual" + manualRoles: "Cargos manuais" conditional: "Condicional" + conditionalRoles: "Cargos condicionais" condition: "Condição" isConditionalRole: "Este é um cargo condicional." isPublic: "Cargo público" @@ -1224,13 +1707,16 @@ _role: gtlAvailable: "Visualizar Linha do Tempo Global" ltlAvailable: "Visualizar Linha do Tempo Local" canPublicNote: "Permitir postagem pública" + mentionMax: "Número máximo de menções em uma nota" canInvite: "Permitir a criação de códigos de convites para a instância" inviteLimit: "Limite de códigos de convite" inviteLimitCycle: "Intervalo de emissão do código de convite" inviteExpirationTime: "Prazo de validade do código de convite" canManageCustomEmojis: "Permitir gerenciar emojis personalizados" + canManageAvatarDecorations: "Gerenciar decorações de avatar" driveCapacity: "Capacidade do drive" alwaysMarkNsfw: "Sempre marcar arquivos como NSFW" + canUpdateBioMedia: "Permitir a edição de Ãcone ou imagem do banner." pinMax: "Número máximo de notas fixadas" antennaMax: "Número máximo de antenas" wordMuteMax: "Número máximo de caracteres nas palavras silenciadas" @@ -1243,9 +1729,17 @@ _role: descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos." canHideAds: "Permitir ocultar anúncios" canSearchNotes: "Permitir a busca de notas" + canUseTranslator: "Uso do tradutor" + avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas" _condition: + roleAssignedTo: "AtribuÃdo a cargos manuais" isLocal: "Usuário local" isRemote: "Usuário remoto" + isCat: "Usuários Gatinho" + isBot: "Usuários Bot" + isSuspended: "Usuário suspenso" + isLocked: "Contas privadas" + isExplorable: "Encontrável em \"Explorar\"" createdLessThan: "Menos de X passados desde a criação da conta" createdMoreThan: "Mais de X passados desde a criação da conta" followersLessThanOrEq: "Possui X ou menos seguidores" @@ -1259,13 +1753,19 @@ _role: not: "Não ~ (Condicional)" _sensitiveMediaDetection: description: "Use o aprendizado de máquina para detectar automaticamente mÃdias sensÃveis para moderação. Isso pode aumentar ligeiramente a carga no servidor." + sensitivity: "Detecção de sensibilidade" sensitivityDescription: "Ao reduzir a sensibilidade, as detecções incorretas (falsos positivos) diminuem. Ao aumentar a sensibilidade, as falhas de detecção (falsos negativos) diminuem." + setSensitiveFlagAutomatically: "Marcar como sensÃvel" + setSensitiveFlagAutomaticallyDescription: "Os resultados da detecção interna serão mantidos mesmo se essa opção estiver desligada." + analyzeVideos: "Habilitar análise de vÃdeos" + analyzeVideosDescription: "Analisa vÃdeos em adição a imagens. Isso irá aumentar levemente a carga do servidor." _emailUnavailable: used: "O endereço de e-mail informado já está sendo utilizado" format: "Formado de e-mail inválido" disposable: "Endereços de e-mail descartáveis não devem ser utilizados" mx: "O servidor de informado é inválido" smtp: "O servidor de e-mail não está respondendo" + banned: "Você não pode se cadastrar com esse endereço de email" _ffVisibility: public: "Público" followers: "VisÃvel apenas para seguidores" @@ -1285,10 +1785,17 @@ _ad: back: "Voltar" reduceFrequencyOfThisAd: "Diminuir frequência deste anúncio" hide: "Não exibir anúncios" + timezoneinfo: "O dia da semana é determinado pelo fuso horário do servidor." + adsSettings: "Configurações de propaganda" + notesPerOneAd: "Intervalo de notas entre o anúncio nas atualizações em tempo real." + setZeroToDisable: "Selecione o valor 0 para desabilitar anúncios nas atualizações em tempo real." + adsTooClose: "O intervalo atual de anúncio pode impactar negativamente a experiência de usuário por ser muito baixo." _forgotPassword: enterEmail: "Por favor, insira o endereço de e-mail usado no cadastro de sua conta. Um link para redefinição de senha será enviado para esse endereço." ifNoEmail: "Caso você não tenha registrado um endereço de e-mail, por favor, entre em contato com o administrador." + contactAdmin: "Essa instância não possui suporte ao uso de endereços de email, contate seu administrador para mudar a sua senha." _gallery: + my: "Minha Galeria" liked: "Postagens curtidas" like: "Curtir" unlike: "Remover curtida" @@ -1297,40 +1804,224 @@ _email: title: "Você tem um novo seguidor" _receiveFollowRequest: title: "Você recebeu um pedido de seguidor" +_plugin: + install: "Instalar plugins" + installWarn: "Por favor, não instale plugins duvidosos." + manage: "Gerenciar plugins" + viewSource: "Ver código-fonte" + viewLog: "Mostrar registo" _preferencesBackups: + list: "Backups criados" + saveNew: "Salvar novo backup" + loadFile: "Carregar de arquivo" + apply: "Aplicar a este dispositivo" + save: "Salvar mudanças" + inputName: "Insira um nome para esse backup" cannotSave: "Não foi possÃvel salvar" + nameAlreadyExists: "Um backup chamado \"{name}\" já existe. Por favor, insira outro nome." applyConfirm: "Deseja aplicar o backup '{name}' ao dispositivo atual? As configurações atuais do dispositivo serão perdidas." + saveConfirm: "Salvar backup como \"{name}\"?" deleteConfirm: "Deseja excluir {name}?" + renameConfirm: "Renomear esse backup de \"{old}\" para \"{new}\"?" + noBackups: "Não há backups. Você pode configurar suas configurações de cliente nesse servidor ao selecionar \"Criar novo backup\"." + createdAt: "Criado em: {date} {time}" + updatedAt: "Atualizado em: {date} {time}" cannotLoad: "Não foi possÃvel carregar" + invalidFile: "Formato de arquivo inválido" +_registry: + scope: "Escopo" + key: "Chave" + keys: "Chave" + domain: "DomÃnio" + createKey: "Criar chave" +_aboutMisskey: + about: "Misskey é um software de código aberto desenvolvido por syulio desde 2014." + contributors: "Contribuidores principais" + allContributors: "Todos os contribuidores" + source: "Código-fonte" + original: "Original" + thisIsModifiedVersion: "{name} utiliza uma versão modificada do Misskey original." + translation: "Traduza o Misskey" + donate: "Doe para o Misskey" + morePatrons: "Nós apreciamos o apoio de vários outros apoiadores não listados aqui. Obrigado! 🥰" + patrons: "Apoiadores" + projectMembers: "Membros do projeto" +_displayOfSensitiveMedia: + respect: "Esconder mÃdia marcada como sensÃvel" + ignore: "Exibir mÃdia marcada como sensÃvel" + force: "Esconder toda mÃdia" +_instanceTicker: + none: "Nunca mostrar" + remote: "Mostrar para usuários remotos" + always: "Sempre mostrar" +_serverDisconnectedBehavior: + reload: "Recarregar automaticamente" + dialog: "Exibir diálogo de aviso de conteúdo" + quiet: "Exibir aviso de conteúdo discreto" _channel: + create: "Criar canal" + edit: "Editar canal" + setBanner: "Definir banner" + removeBanner: "Remover banner" featured: "Destaques" + owned: "Autoral" following: "Seguindo" usersCount: "{n} usuários ativos" notesCount: "{n} notas" nameAndDescription: "Nome e descrição" + nameOnly: "Apenas o nome" + allowRenoteToExternal: "Permitir repostagens e citações de fora do canal" _menuDisplay: sideFull: "Exibir painel lateral inteiro" + sideIcon: "Lateral (Ãcones)" top: "Exibir barra superior" hide: "Ocultar" +_wordMute: + muteWords: "Palavras silenciadas" + muteWordsDescription: "Separe com espaços para uma condicional AND (&&) ou por linha para uma condicional OR (||)." + muteWordsDescription2: "Cercar palavras-chave com barras para usar expressões regulares (RegEx)." _instanceMute: instanceMuteDescription: "Todas as notas e repostagens do servidor configurado serão silenciados, incluindo respostas aos usuários do servidor mutado." + instanceMuteDescription2: "Separar por linha" + title: "Esconder notas das instâncias listadas. " + heading: "Lista de instâncias a serem silenciadas" _theme: + explore: "Explorar Temas" + install: "Instalar um tema" + manage: "Gerenciar temas" + code: "Código do tema" description: "Descrição" + installed: "{name} foi instalado" + installedThemes: "Temas instalados" + builtinThemes: "Temas nativos" + alreadyInstalled: "Esse tema já foi instalado" + invalid: "O formato desse tema é invalido" + make: "Fazer um tema" + base: "Base" + addConstant: "Adicionar constante" + constant: "Constante" + defaultValue: "Valor padrão" + color: "Cor" + refProp: "Referenciar uma propriedade" + refConst: "Referenciar uma constante" + key: "Chave" + func: "Funções" + funcKind: "Tipo de função" + argument: "Argumento" + basedProp: "Propriedade referenciada" alpha: "Opacidade" + darken: "Escurecer" + lighten: "Esclarecer" + inputConstantName: "Insira um nome para essa constante" + importInfo: "Se você inserir o código do tema aqui, você pode importá-lo no editor de temas" deleteConstantConfirm: "Confirma a exclusão da constante {const}?" keys: + accent: "Cor de destaque" + bg: "Plano de fundo" + fg: "Texto" + focus: "Foco" + indicator: "Indicador" + panel: "Painel" + shadow: "Sombra" + header: "Cabeçalho" + navBg: "Plano de fundo da barra lateral" + navFg: "Texto da barra lateral" + navHoverFg: "Texto da coluna lateral (Selecionado)" + navActive: "Texto da coluna lateral (Ativa)" + navIndicator: "Indicador da coluna lateral" + link: "Link" + hashtag: "Hashtag" mention: "Menção" + mentionMe: "Menciona (a mim)" renote: "Repostar" + modalBg: "Plano de fundo modal" divider: "Separador" + scrollbarHandle: "Alça da barra de rolagem (Selecionada)" + scrollbarHandleHover: "Alça da barra de rolagem (Selecionada)" + dateLabelFg: "Texto do rótulo de data" + infoBg: "Plano de fundo de informações" + infoFg: "Texto de informações" + infoWarnBg: "Plano de fundo de avisos" + infoWarnFg: "Texto de avisos" + toastBg: "Plano de fundo de notificações" + toastFg: "Texto da notificação" + 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" + messageBg: "Plano de fundo do chat" + accentDarken: "Cor de destaque (Escurecida)" + accentLighten: "Cor de destaque (Esclarecida)" + fgHighlighted: "Texto Destacado" _sfx: note: "Posts" + noteMy: "Própria nota" notification: "Notificações" + reaction: "Ao selecionar uma reação" +_soundSettings: + driveFile: "Usar um arquivo de áudio do Drive." + driveFileWarn: "Selecione um arquivo de áudio do Drive." + driveFileTypeWarn: "Esse arquivo não é compatÃvel" + driveFileTypeWarnDescription: "Selecione um arquivo de áudio" + driveFileDurationWarn: "O áudio é muito longo." + driveFileDurationWarnDescription: "Ãudios longos podem atrapalhar o funcionamento do Misskey. Deseja continuar?" + driveFileError: "Não foi possÃvel carregar o som. Por favor, altere a configuração." _ago: + future: "Futuro" + justNow: "Agora mesmo" + secondsAgo: "{n}s atrás" + minutesAgo: "{n}m atrás" + hoursAgo: "{n}h atrás" + daysAgo: "{n}d atrás" + weeksAgo: "{n} semanas atrás" + monthsAgo: "{n} meses atrás" + yearsAgo: "{n} anos atrás" invalid: "Não há nada aqui" +_timeIn: + seconds: "Em {n}s" + minutes: "Em {n}m" + hours: "Em {n}h" + days: "Em {n}d" + weeks: "Em {n} semanas" + months: "Em {n} meses" + years: "Em {n} anos" +_time: + second: "Segundo(s)" + minute: "Minuto(s)" + hour: "Hora(s)" + day: "Dia(s)" _2fa: + alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores." + registerTOTP: "Cadastrar aplicativo autenticador" + step1: "Inicialmente, instale um aplicativo autenticador (como {a} ou {b}) em seu dispositivo." + step2: "Então, escaneie o código QR exibido na tela." + step2Uri: "Acesse o seguinte URI se você estiver utilizando um aplicativo no computador" + step3Title: "Insira o código de autenticação" + step3: "Insira o código de autenticação (token) providenciado pelo seu aplicativo para terminar a configuração." + setupCompleted: "Configuração completa" + step4: "De agora em diante, quaisquer solicitações de entrada pedirão pelo código." + securityKeyNotSupported: "O seu navegador não é compatÃvel com chaves de segurança." + registerTOTPBeforeKey: "Por favor, configure um aplicativo autenticador para registrar uma chave de segurança." securityKeyInfo: "Além da autenticação por impressão digital ou PIN, você também pode configurar a autenticação por chaves de segurança de hardware compatÃvel com FIDO2 para proteger ainda mais a sua conta." + registerSecurityKey: "Registre um código de segurança" + securityKeyName: "Insira um nome para a chave" + tapSecurityKey: "Por favor, siga as instruções do navegador para registrar o código de segurança" + removeKey: "Remover código de segurança" removeKeyConfirm: "Deseja excluir {name}?" + whyTOTPOnlyRenew: "O autenticador não pode ser removido enquanto há códigos de segurança registrados." + renewTOTP: "Reconfigurar autenticador" + renewTOTPConfirm: "Isso interromperá o funcionamento dos códigos de aplicativos anteriores " + renewTOTPOk: "Reconfigurar" renewTOTPCancel: "Não, obrigado" + checkBackupCodesBeforeCloseThisWizard: "Antes de fechar essa janela, anote os códigos de backup a seguir." + backupCodes: "Códigos de backup" + backupCodesDescription: "Você pode utilizar esses códigos para ganhar acesso à conta caso sua autenticação de dois fatores esteja indisponÃvel. Cada código pode ser utilizado apenas uma vez. Por favor, guarde-os em um local seguro." + backupCodeUsedWarning: "Um código de backup foi utilizado. Por favor, reconfigure a autenticação de dois fatores o quanto antes, caso não consiga utilizá-la." + backupCodesExhaustedWarning: "Todos os códigos de backup foram utilizados. Caso perca acesso à autenticação de dois fatores, você perderá o acesso à conta. Por favor, reconfigure a autenticação de dois fatores." + moreDetailedGuideHere: "Aqui está um guia detalhado" _permissions: "read:account": "Visualizar informações da conta" "write:account": "Editar informações da conta" @@ -1364,6 +2055,82 @@ _permissions: "write:gallery": "Editar sua galeria" "read:gallery-likes": "Visualizar a sua lista de curtidas da galeria" "write:gallery-likes": "Editar a sua lista de curtidas da galeria" + "read:flash": "Ver Play" + "write:flash": "Editar Plays" + "read:flash-likes": "Ver lista de Plays curtidas" + "write:flash-likes": "Editar lista de Plays curtidas" + "read:admin:abuse-user-reports": "Ver relatórios de usuário" + "write:admin:delete-account": "Excluir conta de usuário" + "write:admin:delete-all-files-of-a-user": "Excluir todos os arquivos de um usuário" + "read:admin:index-stats": "Ver estatÃsticas do Ãndice do banco de dados" + "read:admin:table-stats": "Ver estatÃsticas da tabela do banco de dados" + "read:admin:user-ips": "Ver endereços IP do usuário" + "read:admin:meta": "Ver metadados da instância" + "write:admin:reset-password": "Mudar a senha do usuário" + "write:admin:resolve-abuse-user-report": "Resolver relatório de usuário" + "write:admin:send-email": "Enviar email" + "read:admin:server-info": "Ver informações do servidor" + "read:admin:show-moderation-log": "Ver log de moderação" + "read:admin:show-user": "Ver informações privadas do usuário" + "write:admin:suspend-user": "Suspender usuário" + "write:admin:unset-user-avatar": "Remover avatar do usuário" + "write:admin:unset-user-banner": "Remover banner do usuário" + "write:admin:unsuspend-user": "Cancelar a suspensão do usuário" + "write:admin:meta": "Gerenciar os metadados da instância" + "write:admin:user-note": "Gerenciar a nota de moderação" + "write:admin:roles": "Gerenciar cargos" + "read:admin:roles": "Ver cargos" + "write:admin:relays": "Gerenciar relays" + "read:admin:relays": "Ver relays" + "write:admin:invite-codes": "Gerenciar códigos de convite" + "read:admin:invite-codes": "Ver códigos de convite" + "write:admin:announcements": "Gerenciar anúncios" + "read:admin:announcements": "Ver anúncios" + "write:admin:avatar-decorations": "Gerenciar decorações de avatar" + "read:admin:avatar-decorations": "Ver decorações de avatar" + "write:admin:federation": "Gerenciar dados de federação" + "write:admin:account": "Gerenciar conta de usuário" + "read:admin:account": "Ver conta de usuário" + "write:admin:emoji": "Gerenciar emoji" + "read:admin:emoji": "Ver emoji" + "write:admin:queue": "Gerenciar trabalhos pendentes" + "read:admin:queue": "Ver informações de trabalhos pendentes" + "write:admin:promo": "Gerenciar notas de promoção" + "write:admin:drive": "Gerenciar Drive de usuário" + "read:admin:drive": "Ver informações de Drive de usuário" + "read:admin:stream": "Utilizar WebSocket API para Admin" + "write:admin:ad": "Gerenciar propagandas" + "read:admin:ad": "Ver propagandas" + "write:invite-codes": "Criar códigos de convite" + "read:invite-codes": "Obter códigos de convite" + "write:clip-favorite": "Gerenciar clipes favoritados" + "read:clip-favorite": "Ver Clipes favoritados" + "read:federation": "Ver dados de federação" + "write:report-abuse": "Reportar violação" +_auth: + shareAccessTitle: "Conceder permissões do aplicativo" + shareAccess: "Você gostaria de autorizar \"{name}\" para acessar essa conta?" + shareAccessAsk: "Você tem certeza de que gostaria de conceder ao aplicativo o acesso à conta?" + permission: "{name} solicita as seguintes permissões" + permissionAsk: "O aplicativo solicita as seguintes permissões" + pleaseGoBack: "Por favor, volte ao aplicativo" + callback: "Retornando ao aplicativo" + denied: "Acesso negado" + pleaseLogin: "Por favor, entre para autorizar aplicativos." +_antennaSources: + all: "Todas as notas" + homeTimeline: "Notas de usuários seguidos" + users: "Notas de usuários especÃficos" + userList: "Notas de uma lista especÃfica de usuários" + userBlacklist: "Todas as notas, exceto as de um ou mais usuários especÃficos" +_weekday: + sunday: "Domingo" + monday: "Segunda-feira" + tuesday: "Terça-feira" + wednesday: "Quarta-feira" + thursday: "Quinta-feira" + friday: "Sexta-feira" + saturday: "Sábado" _widgets: profile: "Perfil" instanceInfo: "Informações da instância" @@ -1394,29 +2161,112 @@ _widgets: _userList: chooseList: "Selecione uma lista" clicker: "Clicker" + birthdayFollowings: "Usuários de aniversário hoje" _cw: + hide: "Esconder" show: "Carregar mais" + chars: "{count} caracteres" + files: "{count} arquivo(s)" _poll: + noOnlyOneChoice: "São necessárias, no mÃnimo, duas escolhas" + choiceN: "Escolha {n}" + noMore: "Você não pode adicionar mais escolhas" canMultipleVote: "Permitir múltipla seleção" + expiration: "Encerrar enquete" + infinite: "Nunca" + at: "Terminar em..." + after: "Terminar após..." + deadlineDate: "Data de término" + deadlineTime: "Tempo" + duration: "Duração" + votesCount: "{n} votos" + totalVotes: "{n} votos totais" vote: "Votar em enquetes" + showResult: "Ver resultados" + voted: "Votada" + closed: "Encerrada" + remainingDays: "{d} dia(s) {h} hora(s) restantes" + remainingHours: "{h} hora(s) {m} minuto(s) restantes" + remainingMinutes: "{m} minuto(s) {s} segundo(s) restantes" + remainingSeconds: "{s} segundo(s) restantes" _visibility: + public: "Público" + publicDescription: "Sua nota será visÃvel para todos os usuários" home: "InÃcio" + homeDescription: "Publicar apenas na linha do tempo InÃcio" followers: "Seguidores" followersDescription: "Tornar visÃvel apenas para os meus seguidores" + specified: "Mensagem Direta" + specifiedDescription: "Tornar visÃvel apenas para usuários especÃficos" + disableFederation: "Defederar" + disableFederationDescription: "Não transmitir à s outras instâncias" +_postForm: + replyPlaceholder: "Responder a essa nota..." + quotePlaceholder: "Citar essa nota..." + channelPlaceholder: "Postar em canal..." + _placeholders: + a: "Como vão as coisas?" + b: "O que está rolando por aÃ?" + c: "No que está pensando?" + d: "Do que você quer falar?" + e: "Comece a digitar..." + f: "Esperando você digitar..." _profile: name: "Nome" username: "Nome de usuário" + description: "Bio" + youCanIncludeHashtags: "Você pode incluir hashtags em sua bio." + metadata: "Informações Adicionais" + metadataEdit: "Editar informações adicionais" + metadataDescription: "Aqui, você pode exibir campos adicionais de informação no seu perfil." + metadataLabel: "Rótulo" + metadataContent: "Conteúdo" + changeAvatar: "Mudar avatar" + changeBanner: "Mudar banner" + verifiedLinkDescription: "Ao inserir um URL que contém um link para essa conta, um Ãcone de verificação será exibido ao lado do campo" + avatarDecorationMax: "Você pode adicionar até {max} decorações." _exportOrImport: + allNotes: "Todas as notas" favoritedNotes: "Notas nos favoritos" clips: "Clipe" followingList: "Seguindo" muteList: "Silenciar" blockingList: "Bloquear" userLists: "Listas" + excludeMutingUsers: "Excluir usuários silenciados" + excludeInactiveUsers: "Excluir usuários inativos" + withReplies: "Incluir respostas de usuários importados na linha do tempo" _charts: federation: "União" + apRequest: "Solicitações" + usersIncDec: "Diferença no número de usuários" + usersTotal: "Número total de usuários" + activeUsers: "Usuários ativos" + notesIncDec: "Diferença no número de notas" + localNotesIncDec: "Diferença no número de notas locais" + remoteNotesIncDec: "Diferença no número de notas remotas" + notesTotal: "Número total de notas" + filesIncDec: "Diferença no número de arquivos" + filesTotal: "Número total de arquivos" + storageUsageIncDec: "Diferença no uso de armazenamento" + storageUsageTotal: "Uso total de armazenamento" +_instanceCharts: + requests: "Solicitações" + users: "Diferença no número de usuários" + usersTotal: "Número cumulativo de usuários" + notes: "Diferença no número de notas" + notesTotal: "Número cumulativo de notas" + ff: "Diferença entre número de usuários seguidos/seguidores" + ffTotal: "Número cumulativo de usuários seguidos/seguidores" + cacheSize: "Diferença do tamanho do cache" + cacheSizeTotal: "Tamanho cumulativo do cache" + files: "Diferença no número de arquivos" + filesTotal: "Número cumulativo de arquivos" _timelines: home: "InÃcio" + local: "Local" + social: "Social" + global: "Global" _play: new: "Criar Play" edit: "Editar Play" @@ -1425,18 +2275,66 @@ _play: deleted: "Play foi excluÃdo" pageSetting: "Configurações do Play" editThisPage: "Editar este Play" + viewSource: "Ver fonte" my: "Meus Plays" liked: "Plays curtidos" + featured: "Popular" + title: "TÃtulo" script: "Script" summary: "Descrição" + visibilityDescription: "Pôr em privado significa que ele não será visÃvel no perfil, mas qualquer um com o URL poderá acessar" _pages: + newPage: "Criar uma Página" + editPage: "Editar essa Página" + readPage: "Ver a fonte dessa Página" + created: "Página criada com sucesso" + updated: "Página atualizada com sucesso" deleted: "Página excluÃda com sucesso" + pageSetting: "Configurações da página" + nameAlreadyExists: "O URL de Página especificado já existe" + invalidNameTitle: "O URL de Página especificado é inválido" + invalidNameText: "Confira se o tÃtulo da Página não está vazio" + editThisPage: "Editar essa Página" + viewSource: "Ver código-fonte" viewPage: "Visualizar as suas páginas" like: "Curtir" unlike: "Remover curtida" + my: "Minhas Páginas" liked: "Páginas curtidas" + featured: "Populares" + inspector: "Inspetor" + contents: "Conteúdo" + content: "Bloco da Página" + variables: "Variáveis" + title: "TÃtulo" + url: "URL da Página" + summary: "Resumo da página" + alignCenter: "Centralizar elementos" + hideTitleWhenPinned: "Esconder tÃtulo da Página quando fixado em perfil" + font: "Fonte" + fontSerif: "Serif" + fontSansSerif: "Sans Serif" + eyeCatchingImageSet: "Escolher miniatura" + eyeCatchingImageRemove: "Excluir miniatura" + chooseBlock: "Adicionar bloco" + enterSectionTitle: "Insira um tÃtulo à seção" + selectType: "Selecionar um tipo" + contentBlocks: "Conteúdo" + inputBlocks: "Inserir" + specialBlocks: "Especial" blocks: + text: "Texto" + textarea: "Ãrea do texto" + section: "Seção" image: "imagem" + button: "Botão" + dynamic: "Blocos Dinâmicos" + dynamicDescription: "Esse bloco foi abolido. Por favor, use {play} de agora em diante." + note: "Nota embutida" + _note: + id: "ID da nota" + idDescription: "Você também pode colar o URL da nota aqui." + detailed: "Visão detalhada" _relayStatus: requesting: "Pendente" accepted: "Aprovado" @@ -1451,18 +2349,34 @@ _notification: youReceivedFollowRequest: "Você recebeu um pedido de seguidor" yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito" pollEnded: "Os resultados da enquete agora estão disponÃveis" + newNote: "Nova nota" + unreadAntennaNote: "Antena {name}" + roleAssigned: "Cargo dado" emptyPushNotificationMessage: "As notificações de alerta foram atualizadas" + achievementEarned: "Conquista desbloqueada" + testNotification: "Notificação teste" + checkNotificationBehavior: "Verificar aparência da notificação" + sendTestNotification: "Enviar notificação de teste" + notificationWillBeDisplayedLikeThis: "Notificações se parecem com isso" + reactedBySomeUsers: "{n} usuários reagiram" + likedBySomeUsers: "{n} usuários gostaram da nota" + renotedBySomeUsers: "{n} usuários repostaram a nota" + followedBySomeUsers: "{n} usuários te seguiram" + flushNotification: "Limpar notificações" _types: all: "Todas" + note: "Novas notas" follow: "Seguindo" mention: "Menção" reply: "Respostas" renote: "Repostar" - quote: "Citar" + quote: "Citações" reaction: "Reações" pollEnded: "Enquetes terminando" receiveFollowRequest: "Recebeu pedidos de seguidor" followRequestAccepted: "Aceitou pedidos de seguidor" + roleAssigned: "Cargo dado" + achievementEarned: "Conquista desbloqueada" app: "Notificações de aplicativos conectados" _actions: followBack: "te seguiu de volta" @@ -1472,13 +2386,23 @@ _deck: alwaysShowMainColumn: "Sempre mostrar a coluna principal" columnAlign: "Alinhar colunas" addColumn: "Adicionar coluna" + newNoteNotificationSettings: "Opções de notificação para novas notas" + configureColumn: "Configurar coluna" swapLeft: "Trocar de posição com a coluna à esquerda" swapRight: "Trocar de posição com a coluna à direita" swapUp: "Trocar de posição com a coluna acima" swapDown: "Trocar de posição com a coluna abaixo" + stackLeft: "Empilhar na coluna à esquerda" popRight: "Acoplar coluna à direita" profile: "Perfil" + newProfile: "Novo perfil" deleteProfile: "Remover perfil" + introduction: "Crie a interface perfeita para você arranjando as colunas livremente!" + introduction2: "Clique no + à direita da tela para adicionar novas colunas quando quiser." + widgetsIntroduction: "Por favor, selecione \"Editar widgets\" no menu em coluna e adicione um widget." + useSimpleUiForNonRootPages: "Usar UI simples para páginas navegadas" + usedAsMinWidthWhenFlexible: "A largura mÃnima será usada para isso quando o \"Ajuste automático da largura\" estiver ativado" + flexible: "Ajuste automático da largura" _columns: main: "Principal" widgets: "Widgets" @@ -1490,18 +2414,230 @@ _deck: mentions: "Menções" direct: "Notas diretas" roleTimeline: "Linha do tempo do cargo" +_dialog: + charactersExceeded: "Você excedeu o limite de caracteres! Atualmente em {current} de {max}." + charactersBelow: "Você está abaixo do limite mÃnimo de caracteres! Atualmente em {current} of {min}." +_disabledTimeline: + title: "Linha do tempo desabilitada" + description: "Você não pode acessar essa linha do tempo sob o seu cargo atual." _drivecleaner: orderBySizeDesc: "Tamanho descendente" orderByCreatedAtAsc: "Data ascendente" _webhookSettings: + createWebhook: "Criar Webhook" + modifyWebhook: "Modificar Webhook" name: "Nome" + secret: "Segredo" + trigger: "Gatilho" active: "Ativado" _events: follow: "Quando seguindo um usuário" followed: "Quando sendo seguido" + note: "Ao postar uma nota" + reply: "Quando receber uma resposta" renote: "Quando repostado" + reaction: "Quando receber uma reação" + mention: "Quando for mencionado" + _systemEvents: + abuseReport: "Quando receber um relatório de abuso" + abuseReportResolved: "Quando relatórios de abuso forem resolvidos " + userCreated: "Quando um usuário é criado" + deleteConfirm: "Você tem certeza de que deseja excluir o Webhook?" +_abuseReport: + _notificationRecipient: + createRecipient: "Adicionar destinatário para relatórios de abuso" + modifyRecipient: "Editar destinatários para relatórios de abuso" + recipientType: "TIpo de notificação" + _recipientType: + mail: "E-mail" + webhook: "Webhook" + _captions: + mail: "Enviar o email aos endereços dos moderadores ao receber relatório de abuso." + webhook: "Enviar uma notificação ao SystemWebhook quando você receber um resolver um relatório de abuso." + keywords: "Palavras-chave" + notifiedUser: "Usuários para notificar" + notifiedWebhook: "Webhook usado" + deleteConfirm: "Você tem certeza de que quer excluir o destinatário da notificação?" _moderationLogTypes: + createRole: "Cargo criado" + deleteRole: "Cargo excluÃdo" + updateRole: "Cargo atualizado" + assignRole: "Cargo atribuÃdo" + unassignRole: "Cargo removido" suspend: "Suspender" + unsuspend: "Suspensão cancelada" + addCustomEmoji: "Emoji personalizado adicionado" + updateCustomEmoji: "Emoji personalizado atualizado" + deleteCustomEmoji: "Emoji personalizado removido" + updateServerSettings: "Configurações de servidor atualizadas" + updateUserNote: "Nota de moderação atualizada" + deleteDriveFile: "Arquivo excluÃdo" + deleteNote: "Nota excluÃda" + createGlobalAnnouncement: "Anúncio global criado" + createUserAnnouncement: "Anúncio de usuário criado" + updateGlobalAnnouncement: "Anúncio global atualizado" + updateUserAnnouncement: "Anúncio de usuário atualizado" + deleteGlobalAnnouncement: "Anúncio global excluÃdo" + deleteUserAnnouncement: "Anúncio de usuário excluÃdo" resetPassword: "Redefinir senha" + suspendRemoteInstance: "Instância remota suspensa" + unsuspendRemoteInstance: "Suspensão de instância remota removida" + updateRemoteInstanceNote: "Nota de moderação atualizada para instância remota." + markSensitiveDriveFile: "Arquivo marcado como sensÃvel" + unmarkSensitiveDriveFile: "Arquivo desmarcado como sensÃvel" + resolveAbuseReport: "Relatório resolvido" + createInvitation: "Convite gerado" + createAd: "Propaganda criada" + deleteAd: "Propaganda excluÃda" + updateAd: "Propaganda atualizada" + createAvatarDecoration: "Decoração de avatar criada" + updateAvatarDecoration: "Decoração de avatar atualizada" + deleteAvatarDecoration: "Decoração de avatar removida" + unsetUserAvatar: "Remover avatar de usuário" + unsetUserBanner: "Remover banner de usuário" + createSystemWebhook: "Criar SystemWebhook" + updateSystemWebhook: "Atualizar SystemWebhook" + deleteSystemWebhook: "Remover SystemWebhook" + createAbuseReportNotificationRecipient: "Criar um destinatário para relatórios de abuso" + updateAbuseReportNotificationRecipient: "Atualizar destinatários para relatórios de abuso" + deleteAbuseReportNotificationRecipient: "Remover um destinatário para relatórios de abuso" + deleteAccount: "Remover conta" + deletePage: "Remover página" + deleteFlash: "Remover Play" + deleteGalleryPost: "Remover a publicação da galeria" +_fileViewer: + title: "Detalhes do arquivo" + type: "Tipo de arquivo" + size: "Tamanho do arquivo" + url: "URL" + uploadedAt: "Adicionado em" + attachedNotes: "Notas anexadas" + thisPageCanBeSeenFromTheAuthor: "Essa página só pode ser vista pelo usuário que enviou esse arquivo." +_externalResourceInstaller: + title: "Instalar de site externo" + checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação." + _plugin: + title: "Deseja instalar esse plugin?" + metaTitle: "Informações do plugin" + _theme: + title: "Deseja instalar esse tema?" + metaTitle: "Informações do tema" + _meta: + base: "Paleta de cores base" + _vendorInfo: + title: "Informações do distribuidor" + endpoint: "Endpoint referenciado" + hashVerify: "Verificação de hashes" + _errors: + _invalidParams: + title: "Parâmetros inválidos" + description: "Não há informações suficientes para carregar dados do site externo. Por favor, confirme o URL inserido." + _resourceTypeNotSupported: + title: "Esse recurso externo é incompatÃvel" + description: "Esse tipo de recuso externo é incompatÃvel. Por favor, comunique o administrador do site." + _failedToFetch: + title: "Não foi possÃvel obter dados" + fetchErrorDescription: "Houve um erro ao comunicar com o site externo. Se tentar novamente não resolver o problema, contate o administrador do site." + parseErrorDescription: "Houve um erro processando os dados do site externo. Por favor, contate o administrador do site." + _hashUnmatched: + title: "Verificação de dados falhou" + description: "Houve um erro verificando a integridade do conteúdo obtido. Como medida de segurança, a instalação foi interrompida. Por favor, contate o administrador do site." + _pluginParseFailed: + title: "Erro AiScript" + description: "Os dados solicitados foram obtidos com sucesso, mas houve um erro na leitura do AiScript. Por favor, contate o autor do plugin. Detalhes de erro podem ser vistos no console Javascript." + _pluginInstallFailed: + title: "A instalação do plugin falhou." + description: "Houve um problema na instalação do plugin. Por favor, tente novamente. Detalhes de erro podem ser vistos no console Javascript." + _themeParseFailed: + title: "Erro na leitura do tema" + description: "Os dados solicitados foram obtidos com sucesso, mas houve um erro na leitura do tema. Por favor, contate o autor do tema. Detalhes de erro podem ser vistos no console Javascript." + _themeInstallFailed: + title: "Falha ao instalar tema" + description: "Houve um problema na instalação do tema. Por favor, tente novamente. Detalhes do erro podem ser vistos no console Javascript." +_dataSaver: + _media: + title: "Carregando mÃdia" + description: "Previne que mÃdia seja carregada automaticamente. MÃdias escondidas serão carregadas quando selecionadas." + _avatar: + title: "Imagem do avatar" + description: "Parar animação de avatares. Imagens animadas podem ter um arquivo mais pesado do que imagens normais, potencialmente levando a reduções no tráfego de dados." + _urlPreview: + title: "Miniaturas na prévia de URLs" + description: "Miniaturas na prévia de URLs não serão mais carregadas." + _code: + title: "Destaque de código" + description: "Se as notações de formatação de código forem utilizadas em MFM, elas não irão carregar até serem selecionadas. Destaque de código exige baixar arquivos de alta definição para cada linguagem de programação. Logo, desabilitar o carregamento automático desses arquivos diminui a quantidade de informação comunicada." +_hemisphere: + N: "Hemisfério Norte" + S: "Hemisfério Sul" + caption: "Utilizado em algumas configurações de aplicativo para determinar a estação do ano." _reversi: + reversi: "Reversi" + gameSettings: "Configurações de jogo" + chooseBoard: "Escolha um tabuleiro" + blackOrWhite: "Preto/Branco" + blackIs: "{name} é as peças Pretas" + rules: "Regras" + thisGameIsStartedSoon: "O jogo começará em breve" + waitingForOther: "Esperando o turno do oponente" + waitingForMe: "Esperando o seu turno" + waitingBoth: "Prepare-se" + ready: "Pronto" + cancelReady: "Não pronto" + opponentTurn: "Turno do oponente" + myTurn: "Seu turno" + turnOf: "É o turno de {name}" + pastTurnOf: "Turno de {name}" + surrender: "Desistir" + surrendered: "Desistiu" + timeout: "Fim do tempo" + drawn: "Empate" + won: "{name} venceu" + black: "Preto" + white: "Branco" total: "Total" + turnCount: "Turno {count}" + myGames: "Meus jogos" + allGames: "Todos os jogos" + ended: "Terminado" + playing: "Atualmente jogando" + isLlotheo: "Aquele com menos pedras vence (Llotheo)" + loopedMap: "Mapa em ‘loop’" + canPutEverywhere: "É possÃvel pôr em qualquer lugar" + timeLimitForEachTurn: "Tempo limite por turno" + freeMatch: "Partida Livre" + lookingForPlayer: "À procura de adversários..." + gameCanceled: "A partida foi cancelada." + shareToTlTheGameWhenStart: "Compartilhar jogo na linha do tempo ao iniciar" + iStartedAGame: "O jogo começou! #MisskeyReversi" + opponentHasSettingsChanged: "O oponente alterou as configurações dele" + allowIrregularRules: "Regras irregulares (completamente livre)" + disallowIrregularRules: "Sem regras irregulares" + showBoardLabels: "Exibir numeração de linha e coluna no tabuleiro" + useAvatarAsStone: "Utilizar avatares de usuário como as pedras" +_offlineScreen: + title: "Offline - não foi possÃvel conectar ao servidor" + header: "Não foi possÃvel conectar ao servidor" +_urlPreviewSetting: + title: "Configurações da prévia de URL" + enable: "Habilitar prévia de URL" + timeout: "Tempo máximo para obter a prévia (ms)" + timeoutDescription: "Se demorar mais que esse valor para obter uma prévia, ela não será gerada." + maximumContentLength: "Content-Length máximo (em bytes)" + maximumContentLengthDescription: "Se o Content-Length for maior que esse valor, a prévia não será gerada." + requireContentLength: "Gerar previu apenas se houver cabeçalho Content-Length disponÃvel na solicitação" + requireContentLengthDescription: "Se o outro servidor não retornar um cabeçalho Content-Length, a prévia não será gerada." + userAgent: "User-Agent" + userAgentDescription: "Define o User-Agent a ser usado ao gerar prévias. Se for deixado em branco, será usado o User-Agent padrão." + summaryProxy: "Endpoints do Proxy que geram prévias" + summaryProxyDescription: "Fora do Misskey, gerar prévias usando o Sumally Proxy." + summaryProxyDescription2: "Os parâmetros a seguir são vinculados ao proxy como um 'query string'. Se o proxy não os suportar, os valores serão ignorados." +_mediaControls: + pip: "Picture-in-Picture" + playbackRate: "Velocidade de Reprodução" + loop: "Reprodução em Loop" +_contextMenu: + title: "Menu de contexto" + app: "Aplicativo" + appWithShift: "Aplicativo com a tecla shift" + native: "Nativo" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index 88424cbbfb5754c30afc9a926a209414264b4e3c..95c1e1650827a7bc6b0e76024c94a703b123206c 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -728,6 +728,10 @@ _deck: mentions: "MenÈ›iuni" _webhookSettings: name: "Nume" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Email" _moderationLogTypes: suspend: "Suspendă" resetPassword: "Resetează parola" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 71f5cad601b5550dae2813d930673e24deec94ea..88f59155d656da1e3f2fb7e24a9aa8ebac14e79a 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1612,8 +1612,6 @@ _sfx: note: "Заметки" noteMy: "СобÑтвенные заметки" notification: "УведомлениÑ" - antenna: "Ðнтенна" - channel: "Канал" _ago: future: "Из будущего" justNow: "Только что" @@ -1983,6 +1981,10 @@ _webhookSettings: createWebhook: "Создать вебхук" name: "Ðазвание" active: "Вкл." +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "ÐÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð°" _moderationLogTypes: suspend: "Заморозить" addCustomEmoji: "Добавлено Ñмодзи" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 6ae90395ab896ecd7b1b42d797deb8ad6c114711..409d682af7a4587b16fe0148b861a717f62798b7 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1124,8 +1124,6 @@ _sfx: note: "Poznámky" noteMy: "Vlastná poznámka" notification: "Oznámenia" - antenna: "Antény" - channel: "Upozornenia kanála" _ago: future: "BudúcnosÅ¥" justNow: "Teraz" @@ -1447,6 +1445,10 @@ _deck: _webhookSettings: name: "Názov" active: "Zapnuté" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Email" _moderationLogTypes: suspend: "ZmraziÅ¥" resetPassword: "ResetovaÅ¥ heslo" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 11b2b36499dbe122c28e137b56ba2115d300bd63..c98782450f163d0b8ad9d2d88f382dcdb84bc91c 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -512,7 +512,6 @@ _theme: _sfx: note: "Noter" notification: "Notifikationer" - antenna: "Antenner" _2fa: renewTOTPCancel: "Nej tack" _antennaSources: @@ -577,6 +576,10 @@ _deck: _webhookSettings: name: "Namn" active: "Aktiverad" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "E-post" _moderationLogTypes: suspend: "Suspendera" resetPassword: "Ã…terställ Lösenord" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 01510cc03c79b41da67c23eb2824163a165c6469..508ca38949a456a881773c138bca7edddc769047 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -2,10 +2,10 @@ _lang_: "ภาษาไทย" headlineMisskey: "เชื่à¸à¸¡à¸•à¹ˆà¸à¹€à¸„รืà¸à¸‚่ายโดยโน้ต" introMisskey: "ยินดีต้à¸à¸™à¸£à¸±à¸šà¸—ุà¸à¸„นจ้า! Misskey คืภซà¸à¸Ÿà¸•à¹Œà¹à¸§à¸£à¹Œà¹‚à¸à¹€à¸žà¸™à¸‹à¸à¸£à¹Œà¸ªà¸ªà¸³à¸«à¸£à¸±à¸šà¸šà¸£à¸´à¸à¸²à¸£à¹„มโครบล็à¸à¸à¸à¸´à¹‰à¸‡ (MicroBlogging) à¹à¸šà¸šà¸à¸£à¸°à¸ˆà¸²à¸¢à¸¨à¸¹à¸™à¸¢à¹Œà¸à¸³à¸™à¸²à¸ˆ (Decentralized) \n\nเขียน “โน้ต (Note)†เพื่à¸à¸ªà¹ˆà¸‡à¸•à¹ˆà¸à¹€à¸£à¸·à¹ˆà¸à¸‡à¸£à¸²à¸§à¸‚à¸à¸‡à¸„ุณให้ทั้งโลà¸à¹„ด้รับรู้📡\nà¹à¸¥à¸°à¸à¸¢à¹ˆà¸²à¸¥à¸·à¸¡à¸—ี่จะ “รีà¹à¸à¸„ชั่น†à¸à¸±à¸šà¹€à¸£à¸·à¹ˆà¸à¸‡à¸£à¸²à¸§à¸‚à¸à¸‡à¸„นà¸à¸·à¹ˆà¸™ ๆ ด้วยนะ! ðŸ‘\n\nท่à¸à¸‡à¸ªà¸³à¸£à¸§à¸ˆà¹‚ลà¸à¹ƒà¸šà¹ƒà¸«à¸¡à¹ˆà¸à¸±à¸™à¹€à¸–à¸à¸°ðŸš€" -poweredByMisskeyDescription: "{name} เป็นส่วนหนึ่งในบริà¸à¸²à¸£à¸—ี่ถูà¸à¸‚ับเคลื่à¸à¸™à¹‚ดยà¹à¸žà¸¥à¸•à¸Ÿà¸à¸£à¹Œà¸¡à¹‚à¸à¹€à¸žà¹ˆà¸™à¸‹à¸à¸£à¹Œà¸ª <b>Misskey</b> (เรียà¸à¸§à¹ˆà¸² \"à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ Misskey\")" +poweredByMisskeyDescription: "{name} เป็นหนึ่งในเซิร์ฟเวà¸à¸£à¹Œà¸‚à¸à¸‡à¹à¸žà¸¥à¸•à¸Ÿà¸à¸£à¹Œà¸¡à¹‚à¸à¹€à¸žà¹ˆà¸™à¸‹à¸à¸£à¹Œà¸ª <b>Misskey</b>" monthAndDay: "{month}/{day}" search: "ค้นหา" -notifications: "à¸à¸²à¸£à¹€à¹€à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" +notifications: "เเจ้งเตืà¸à¸™" username: "ชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" password: "รหัสผ่าน" forgotPassword: "ลืมรหัสผ่าน" @@ -18,7 +18,7 @@ enterUsername: "à¸à¸£à¸à¸à¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" renotedBy: "รีโน้ตโดย {user}" noNotes: "ไม่มีโน้ต" noNotifications: "ไม่มีà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" -instance: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" +instance: "เซิร์ฟเวà¸à¸£à¹Œ" settings: "à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่า" notificationSettings: "ตั้งค่าà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" basicSettings: "à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าพื้นà¸à¸²à¸™" @@ -48,7 +48,7 @@ copyLink: "คัดลà¸à¸à¸¥à¸´à¸‡à¸à¹Œ" copyLinkRenote: "คัดลà¸à¸à¸¥à¸´à¸‡à¸à¹Œà¸£à¸µà¹‚น้ต" delete: "ลบ" deleteAndEdit: "ลบà¹à¸¥à¸°à¹à¸à¹‰à¹„ข" -deleteAndEditConfirm: "คุณต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¹‚น้ตนี้à¹à¸¥à¸°à¹à¸à¹‰à¹„ขใหม่ใช่ไหม? รีà¹à¸à¸„ชั่น รีโน้ต à¹à¸¥à¸°à¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸•à¹ˆà¸à¹‚น้ตนี้ทั้งหมดจะถูà¸à¸¥à¸šà¸à¸à¸à¸”้วย" +deleteAndEditConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¹‚น้ตนี้à¹à¸¥à¸°à¹à¸à¹‰à¹„ขใหม่ใช่ไหม? รีà¹à¸à¸„ชั่น รีโน้ต à¹à¸¥à¸°à¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸•à¹ˆà¸à¹‚น้ตนี้ทั้งหมดจะถูà¸à¸¥à¸šà¸à¸à¸à¸”้วย" addToList: "เพิ่มลงรายชื่à¸" addToAntenna: "เพิ่มไปยังเสาà¸à¸²à¸à¸²à¸¨" sendMessage: "ส่งข้à¸à¸„วาม" @@ -60,6 +60,7 @@ copyFileId: "คัดลà¸à¸à¹„ฟล์ ID" copyFolderId: "คัดลà¸à¸à¹‚ฟลเดà¸à¸£à¹Œ ID" copyProfileUrl: "คัดลà¸à¸à¹‚ปรไฟล์ URL" searchUser: "ค้นหาผู้ใช้" +searchThisUsersNotes: "ค้นหาโน้ตขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" reply: "ตà¸à¸šà¸à¸¥à¸±à¸š" loadMore: "à¹à¸ªà¸”งเพิ่มเติม" showMore: "à¹à¸ªà¸”งเพิ่มเติม" @@ -68,7 +69,7 @@ youGotNewFollower: "ได้ติดตามคุณ" receiveFollowRequest: "มีคำขà¸à¸•à¸´à¸”ตามส่งมาหา" followRequestAccepted: "à¸à¸²à¸£à¸•à¸´à¸”ตามได้รับà¸à¸²à¸£à¸à¸™à¸¸à¸¡à¸±à¸•à¸´à¹à¸¥à¹‰à¸§" mention: "à¸à¸¥à¹ˆà¸²à¸§à¸–ึง" -mentions: "พูดถึง" +mentions: "à¸à¸¥à¹ˆà¸²à¸§à¸–ึงคุณ" directNotes: "โพสต์à¹à¸šà¸šà¹„ดเร็à¸à¸•à¹Œ" importAndExport: "นำเข้า / ส่งà¸à¸à¸" import: "นำเข้า" @@ -92,7 +93,7 @@ error: "ผิดพลาด!" somethingHappened: "à¸à¸¸à¹Šà¸¢ ! มีà¸à¸°à¹„รบางà¸à¸¢à¹ˆà¸²à¸‡à¸œà¸´à¸”พลาด" retry: "ลà¸à¸‡à¹ƒà¸«à¸¡à¹ˆà¸à¸µà¸à¸„รั้ง" pageLoadError: "เà¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาดในà¸à¸²à¸£à¹‚หลดหน้านี้" -pageLoadErrorDescription: "โดยปà¸à¸•à¸´à¹à¸¥à¹‰à¸§à¸¡à¸±à¸à¸ˆà¸°à¹€à¸à¸´à¸”จาà¸à¸‚้à¸à¸œà¸´à¸”พลาดขà¸à¸‡à¹€à¸„รืà¸à¸‚่ายหรืà¸à¹à¸„ชขà¸à¸‡à¹€à¸šà¸£à¸²à¸§à¹Œà¹€à¸‹à¸à¸£à¹Œ ลà¸à¸‡à¸¥à¹‰à¸²à¸‡à¹à¸„ชà¹à¸¥à¹‰à¸§à¸¥à¸à¸‡à¹ƒà¸«à¸¡à¹ˆà¸à¸µà¸à¸„รั้งหลังจาà¸à¸£à¸à¸ªà¸±à¸à¸„รู่ " +pageLoadErrorDescription: "ปัà¸à¸«à¸²à¸™à¸µà¹‰à¸¡à¸±à¸à¹€à¸à¸´à¸”จาà¸à¹à¸„ชขà¸à¸‡à¹€à¸„รืà¸à¸‚่ายหรืà¸à¹€à¸šà¸£à¸²à¸§à¹Œà¹€à¸‹à¸à¸£à¹Œ ควรล้างà¹à¸„ช, รà¸à¸ªà¸±à¸à¸„รู่ à¹à¸¥à¹‰à¸§à¸¥à¸à¸‡à¹ƒà¸«à¸¡à¹ˆà¸à¸µà¸à¸„รั้ง" serverIsDead: "เซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰à¹„ม่มีà¸à¸²à¸£à¸•à¸à¸šà¸ªà¸™à¸à¸‡ โปรดà¸à¸£à¸¸à¸“ารà¸à¸ªà¸±à¸à¸„รู่à¹à¸¥à¹‰à¸§à¸¥à¸à¸‡à¹ƒà¸«à¸¡à¹ˆà¸à¸µà¸à¸„รั้ง" youShouldUpgradeClient: "หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¸”ูหน้านี้ à¸à¸£à¸¸à¸“าโหลดหน้าใหม่เพื่à¸à¸à¸±à¸›à¹€à¸”ตไคลเà¸à¹‡à¸™à¸•à¹Œà¸‚à¸à¸‡à¸„ุณ" enterListName: "ป้à¸à¸™à¸™à¸²à¸¡à¹€à¸£à¸µà¸¢à¸à¸‚à¸à¸‡à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸à¸Šà¸¸à¸”นี้" @@ -108,11 +109,14 @@ enterEmoji: "พิมพ์เà¸à¹‚มจิ" renote: "รีโน้ต" unrenote: "เลิà¸à¸£à¸µà¹‚น้ต" renoted: "รีโน้ตà¹à¸¥à¹‰à¸§" +renotedToX: "รีโน้ตให้ {name} à¹à¸¥à¹‰à¸§" cantRenote: "โพสต์นี้ไม่สามารถรีโน้ตใหม่ได้" cantReRenote: "รีโน้ตไม่สามารถรีโน้ตซ้ำได้" quote: "à¸à¹‰à¸²à¸‡à¸à¸´à¸‡" inChannelRenote: "รีโน้ตในช่à¸à¸‡à¹€à¸—่านั้น" inChannelQuote: "à¸à¹‰à¸²à¸‡à¸à¸´à¸‡à¹ƒà¸™à¸Šà¹ˆà¸à¸‡à¹€à¸—่านั้น" +renoteToChannel: "รีโน้ตไปที่ช่à¸à¸‡" +renoteToOtherChannel: "รีโน้ตไปยังช่à¸à¸‡à¸à¸·à¹ˆà¸™" pinnedNote: "โน้ตที่ปัà¸à¸«à¸¡à¸¸à¸”ไว้" pinned: "ปัà¸à¸«à¸¡à¸¸à¸”" you: "คุณ" @@ -151,6 +155,7 @@ editList: "à¹à¸à¹‰à¹„ขรายชื่à¸" selectChannel: "เลืà¸à¸à¸Šà¹ˆà¸à¸‡" selectAntenna: "เลืà¸à¸à¹€à¸ªà¸²à¸à¸²à¸à¸²à¸¨" editAntenna: "à¹à¸à¹‰à¹„ขเสาà¸à¸²à¸à¸²à¸¨" +createAntenna: "สร้างเสาà¸à¸²à¸à¸²à¸¨" selectWidget: "เลืà¸à¸à¸§à¸´à¸”เจ็ต" editWidgets: "à¹à¸à¹‰à¹„ขวิดเจ็ต" editWidgetsExit: "เรียบร้à¸à¸¢" @@ -163,20 +168,24 @@ addEmoji: "à¹à¸—รà¸à¹€à¸à¹‚มจิ" settingGuide: "à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าที่à¹à¸™à¸°à¸™à¸³" cacheRemoteFiles: "à¹à¸„ชไฟล์ระยะไà¸à¸¥" cacheRemoteFilesDescription: "หาà¸à¹€à¸›à¸´à¸”ใช้งาน ไฟล์ระยะไà¸à¸¥à¸ˆà¸°à¸–ูà¸à¹à¸„ชไว้ ทำให้à¹à¸ªà¸”งภาพเร็วขึ้น à¹à¸•à¹ˆà¸à¹‡à¹ƒà¸Šà¹‰à¸žà¸·à¹‰à¸™à¸—ี่เà¸à¹‡à¸šà¸‚้à¸à¸¡à¸¹à¸¥à¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸¡à¸²à¸à¸‚ึ้นเช่นà¸à¸±à¸™ สำหรับขีดจำà¸à¸±à¸”ที่ผู้ใช้ระยะไà¸à¸¥à¸–ูà¸à¹à¸„ชไว้จะขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¸„วามจุไดรฟ์ตามบทบาทขà¸à¸‡à¹€à¸‚า เมื่à¸à¹€à¸à¸´à¸™à¹à¸¥à¹‰à¸§à¹„ฟล์เà¸à¹ˆà¸²à¸ˆà¸°à¸–ูà¸à¸¥à¸šà¸à¸à¸à¹à¸¥à¸°à¹€à¸à¹‡à¸šà¹€à¸›à¹‡à¸™à¸¥à¸´à¸‡à¸à¹Œà¹à¸—น หาà¸à¸›à¸´à¸”ใช้งาน ไฟล์ระยะไà¸à¸¥à¸ˆà¸°à¸–ูà¸à¹€à¸à¹‡à¸šà¹€à¸›à¹‡à¸™à¸¥à¸´à¸‡à¸à¹Œà¸•à¸±à¹‰à¸‡à¹à¸•à¹ˆà¸•à¹‰à¸™ เราà¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¸•à¸±à¹‰à¸‡à¸„่า proxyRemoteFiles ใน default.yml เป็น true เพื่à¸à¸ªà¸£à¹‰à¸²à¸‡à¸˜à¸±à¸¡à¸šà¹Œà¹€à¸™à¸¥à¹à¸¥à¸°à¸›à¸à¸›à¹‰à¸à¸‡à¸„วามเป็นส่วนตัวขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" -youCanCleanRemoteFilesCache: "คุณสามารถล้างà¹à¸„ชได้โดยคลิà¸à¸—ี่ปุ่ม ðŸ—‘ï¸ à¹ƒà¸™à¸¡à¸¸à¸¡à¸¡à¸à¸‡à¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¹„ฟล์" +youCanCleanRemoteFilesCache: "สามารถลบà¹à¸„ชทั้งหมดได้โดยใช้ปุ่ม ðŸ—‘ï¸ à¹ƒà¸™à¸«à¸™à¹‰à¸²à¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¹„ฟล์" cacheRemoteSensitiveFiles: "à¹à¸„ชไฟล์ระยะไà¸à¸¥à¸—ี่มีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" -cacheRemoteSensitiveFilesDescription: "เมื่à¸à¸›à¸´à¸”à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่านี้ ไฟล์ระยะไà¸à¸¥à¸—ี่มีเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸§à¹ˆà¸²à¸¡à¸µà¹€à¸™à¸·à¹‰à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™à¸™à¸±à¹‰à¸™à¸ˆà¸°à¸–ูà¸à¹‚หลดโดยตรงจาà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹‚ดยที่ไม่มีà¸à¸²à¸£à¹à¸„ช" +cacheRemoteSensitiveFilesDescription: "เมื่à¸à¸›à¸´à¸”à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่านี้ ไฟล์ระยะไà¸à¸¥à¸—ี่มีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™à¸ˆà¸°à¸–ูà¸à¹‚หลดโดยตรงจาà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹‚ดยไม่มีà¸à¸²à¸£à¹à¸„ช" flagAsBot: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸šà¸à¸à¸§à¹ˆà¸²à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¹€à¸›à¹‡à¸™à¸šà¸à¸•" -flagAsBotDescription: "à¸à¸²à¸£à¹€à¸›à¸´à¸”ใช้งานตัวเลืà¸à¸à¸™à¸µà¹‰à¸«à¸²à¸à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¸„วบคุมโดยนัà¸à¹€à¸‚ียนโปรà¹à¸à¸£à¸¡ หรืภถ้าหาà¸à¹€à¸›à¸´à¸”ใช้งาน มันจะทำหน้าที่เป็นà¹à¸Ÿà¸¥à¹‡à¸à¸ªà¸³à¸«à¸£à¸±à¸šà¸™à¸±à¸à¸žà¸±à¸’นารายà¸à¸·à¹ˆà¸™à¹† à¹à¸¥à¸°à¹€à¸žà¸·à¹ˆà¸à¸›à¹‰à¸à¸‡à¸à¸±à¸™à¸à¸²à¸£à¹‚ต้ตà¸à¸šà¹à¸šà¸šà¹„ม่มีที่สิ้นสุดà¸à¸±à¸šà¸šà¸à¸—ตัวà¸à¸·à¹ˆà¸™à¹† à¹à¸¥à¸°à¸¢à¸±à¸‡à¸ªà¸²à¸¡à¸²à¸£à¸–ปรับเปลี่ยนระบบภายในขà¸à¸‡ Misskey เพื่à¸à¸›à¸à¸´à¸šà¸±à¸•à¸´à¸•à¹ˆà¸à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¹€à¸›à¹‡à¸™à¸šà¸à¸—" +flagAsBotDescription: "เปิดใช้งานตัวเลืà¸à¸à¸™à¸µà¹‰à¸«à¸²à¸à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¸„วบคุมโดยโปรà¹à¸à¸£à¸¡ เมื่à¸à¹€à¸›à¸´à¸”ใช้งาน มันจะทำหน้าที่เป็นà¹à¸Ÿà¸¥à¹‡à¸à¸ªà¸³à¸«à¸£à¸±à¸šà¸™à¸±à¸à¸žà¸±à¸’นารายà¸à¸·à¹ˆà¸™à¹ƒà¸™à¸à¸²à¸£à¸›à¹‰à¸à¸‡à¸à¸±à¸™à¸à¸²à¸£à¸ªà¸£à¹‰à¸²à¸‡à¸«à¹ˆà¸§à¸‡à¹‚ซ่à¸à¸²à¸£à¹‚ต้ตà¸à¸šà¹à¸šà¸šà¸à¸™à¸±à¸™à¸•à¹Œà¸à¸±à¸šà¸šà¸à¸•à¸•à¸±à¸§à¸à¸·à¹ˆà¸™ à¹à¸¥à¸°à¸›à¸£à¸±à¸šà¸£à¸°à¸šà¸šà¸ ายในขà¸à¸‡ Misskey เพื่à¸à¸ˆà¸±à¸”à¸à¸²à¸£à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¹ƒà¸™à¸à¸²à¸™à¸°à¸šà¸à¸•" flagAsCat: "เมี้ยววววววววววววววว!!!!!!!!!!!" flagAsCatDescription: "เหมียวเหมียวเมี้ยว??" -flagShowTimelineReplies: "à¹à¸ªà¸”งตà¸à¸šà¸à¸¥à¸±à¸š ในไทม์ไลน์" -flagShowTimelineRepliesDescription: "à¹à¸ªà¸”งà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¹„ปยังโน้ตขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸£à¸²à¸¢à¸à¸·à¹ˆà¸™à¹†à¹ƒà¸™à¹„ทม์ไลน์หาà¸à¹„ด้เปิดเà¸à¸²à¹„ว้" -autoAcceptFollowed: "à¸à¸™à¸¸à¸¡à¸±à¸•à¸´à¸„ำขà¸à¸•à¸´à¸”ตามโดยà¸à¸±à¸•à¹‚นมัติทันที จาà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸—ี่คุณà¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตาม" +flagShowTimelineReplies: "à¹à¸ªà¸”งตà¸à¸šà¸à¸¥à¸±à¸šà¹‚น้ตลงไทม์ไลน์" +flagShowTimelineRepliesDescription: "เมื่à¸à¹€à¸›à¸´à¸”ใช้งาน จะà¹à¸ªà¸”งà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸„นนั้นต่à¸à¹‚น้ตà¸à¸·à¹ˆà¸™à¹† ในไทม์ไลน์ด้วย" +autoAcceptFollowed: "à¸à¸™à¸¸à¸¡à¸±à¸•à¸´à¸„ำขà¸à¸•à¸´à¸”ตามจาà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่คุณติดตามà¸à¸¢à¸¹à¹ˆà¹‚ดยà¸à¸±à¸•à¹‚นมัติ" addAccount: "เพิ่มบัà¸à¸Šà¸µ" reloadAccountsList: "รีโหลดรายà¸à¸²à¸£à¸šà¸±à¸à¸Šà¸µà¹ƒà¸«à¸¡à¹ˆ" loginFailed: "à¸à¸²à¸£à¹€à¸‚้าสู่ระบบไม่สำเร็จ" -showOnRemote: "ดูบนà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" +showOnRemote: "ดูบนเซิร์ฟเวà¸à¸£à¹Œà¸à¸±à¹ˆà¸‡à¸£à¸°à¸¢à¸°à¹„à¸à¸¥" +continueOnRemote: "ดำเนินà¸à¸²à¸£à¸•à¹ˆà¸à¸šà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸à¸±à¹ˆà¸‡à¸£à¸°à¸¢à¸°à¹„à¸à¸¥" +chooseServerOnMisskeyHub: "เลืà¸à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸ˆà¸²à¸ Misskey Hub" +specifyServerHost: "ระบุโดเมนขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¹‚ดยตรง" +inputHostName: "โปรดป้à¸à¸™à¹‚ดเมน" general: "ทั่วไป" wallpaper: "ภาพพื้นหลัง" setWallpaper: "ตั้งค่าภาพพื้นหลัง" @@ -185,23 +194,25 @@ searchWith: "ค้นหา: {q}" youHaveNoLists: "คุณไม่มีรายชื่à¸à¹ƒà¸”ๆ " followConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตาม {name} ใช่ไหม?" proxyAccount: "บัà¸à¸Šà¸µà¸žà¸£à¹‡à¸à¸à¸‹à¸µà¹ˆ" -proxyAccountDescription: "บัà¸à¸Šà¸µà¸žà¸£à¹‡à¸à¸à¸‹à¸µà¹ˆ คืภบัà¸à¸Šà¸µà¸—ี่จะทำหน้าที่เป็นผู้ติดตามระยะไà¸à¸¥à¸ªà¸³à¸«à¸£à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸—ี่à¸à¸¢à¸¹à¹ˆà¸ ายใต้ด้วยเงื่à¸à¸™à¹„ขบางà¸à¸¢à¹ˆà¸²à¸‡ ยà¸à¸•à¸±à¸§à¸à¸¢à¹ˆà¸²à¸‡ เช่น เมื่à¸à¸¡à¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸™à¸±à¹‰à¸™à¹„ด้เพิ่มผู้ใช้งานจาà¸à¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¸¥à¸‡à¹ƒà¸™à¸£à¸²à¸¢à¸à¸²à¸£ à¹à¸•à¹ˆà¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹ƒà¸™à¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¸™à¸±à¹‰à¸™à¸ˆà¸°à¹„ม่ถูà¸à¸ªà¹ˆà¸‡à¹„ปยังà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸«à¸²à¸à¹„ม่มีผู้ใช้งานในพื้นที่ติดตามผู้ใช้รายนั้น ดังนั้นบัà¸à¸Šà¸µà¸žà¸£à¹‡à¸à¸à¸‹à¸µà¸™à¸µà¹‰à¸ˆà¸°à¸•à¸´à¸”ตามà¹à¸—น" +proxyAccountDescription: "บัà¸à¸Šà¸µà¸žà¸£à¹‡à¸à¸à¸‹à¸µ คืภบัà¸à¸Šà¸µà¸—ี่ทำหน้าที่ติดตาม(ผู้ใช้)ระยะไà¸à¸¥à¸ ายใต้เงื่à¸à¸™à¹„ขบางประà¸à¸²à¸£ ตัวà¸à¸¢à¹ˆà¸²à¸‡à¹€à¸Šà¹ˆà¸™ เมื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—้à¸à¸‡à¸–ิ่นเพิ่มผู้ใช้ระยะไà¸à¸¥à¸¥à¸‡à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸ หาà¸à¹„ม่มีใครติดตามผู้ใช้ระยะไà¸à¸¥à¹ƒà¸™à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸à¸™à¸±à¹‰à¸™ à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸à¹‡à¸ˆà¸°à¹„ม่ถูà¸à¸ªà¹ˆà¸‡à¸¡à¸²à¸¢à¸±à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ ดังนั้นจึงมีบัà¸à¸Šà¸µà¸žà¸£à¹‡à¸à¸à¸‹à¸µà¹„ว้ติดตามผู้ใช้ระยะไà¸à¸¥à¹€à¸«à¸¥à¹ˆà¸²à¸™à¸±à¹‰à¸™" host: "โฮสต์" +selectSelf: "เลืà¸à¸à¸•à¸±à¸§à¹€à¸à¸‡" selectUser: "เลืà¸à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" recipient: "ผู้รับ" annotation: "หมายเหตุประà¸à¸à¸š" federation: "สหพันธ์" -instances: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" +instances: "เซิร์ฟเวà¸à¸£à¹Œ" registeredAt: "วันที่ลงทะเบียน" latestRequestReceivedAt: "คำขà¸à¸¥à¹ˆà¸²à¸ªà¸¸à¸”ที่ได้รับ" latestStatus: "สถานะล่าสุด" storageUsage: "พื้นที่จัดเà¸à¹‡à¸šà¸‚้à¸à¸¡à¸¹à¸¥à¸—ี่ใช้ไป" -charts: "โดดเด่น" -perHour: "ทุà¸à¸Šà¸±à¹ˆà¸§à¹‚มง" +charts: "à¹à¸œà¸™à¸ ูมิ" +perHour: "ต่à¸à¸Šà¸±à¹ˆà¸§à¹‚มง" perDay: "ต่à¸à¸§à¸±à¸™" stopActivityDelivery: "หยุดส่งà¸à¸´à¸ˆà¸à¸£à¸£à¸¡" -blockThisInstance: "บล็à¸à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰" -silenceThisInstance: "ปิดปาà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰" +blockThisInstance: "บล็à¸à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰" +silenceThisInstance: "ปิดปาà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰" +mediaSilenceThisInstance: "ปิดปาà¸à¸ªà¸·à¹ˆà¸à¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰" operations: "ดำเนินà¸à¸²à¸£" software: "ซà¸à¸Ÿà¸•à¹Œà¹à¸§à¸£à¹Œ" version: "เวà¸à¸£à¹Œà¸Šà¸±à¹ˆà¸™" @@ -212,17 +223,19 @@ jobQueue: "คิวงาน" cpuAndMemory: "ซีพียู à¹à¸¥à¸° หน่วยความจำ" network: "เครืà¸à¸‚่าย" disk: "ดิสà¸à¹Œ" -instanceInfo: "ข้à¸à¸¡à¸¹à¸¥à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" +instanceInfo: "ข้à¸à¸¡à¸¹à¸¥à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" statistics: "สถิติà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" clearQueue: "ล้างคิว" clearQueueConfirmTitle: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¹‰à¸²à¸‡à¸„ิวใช่ไหม?" clearQueueConfirmText: "โพสต์ที่ยังค้างในคิวจะไม่ถูà¸à¸ˆà¸±à¸”ส่งà¸à¸µà¸à¸•à¹ˆà¸à¹„ป โดยปà¸à¸•à¸´à¹à¸¥à¹‰à¸§à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¹„ม่จำเป็น" clearCachedFiles: "ล้างà¹à¸„ช" clearCachedFilesConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¹„ฟล์ระยะไà¸à¸¥à¸—ี่à¹à¸„ชไว้ทั้งหมดใช่ไหม?" -blockedInstances: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" -blockedInstancesDescription: "ระบุชื่à¸à¹‚ฮสต์ขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่คุณต้à¸à¸‡à¸à¸²à¸£à¸šà¸¥à¹‡à¸à¸ à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸²à¸¢à¸à¸²à¸£à¸™à¸±à¹‰à¸™à¸ˆà¸°à¹„ม่สามารถพูดคุยà¸à¸±à¸šà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹„ด้à¸à¸µà¸à¸•à¹ˆà¸à¹„ป" -silencedInstances: "ปิดปาà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹à¸¥à¹‰à¸§" -silencedInstancesDescription: "ตั้งค่ารายชื่à¸à¹‚ฮสต์ขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่คุณต้à¸à¸‡à¸à¸²à¸£à¸›à¸´à¸”ปาภบัà¸à¸Šà¸µà¸—ั้งหมดขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸à¸™à¸±à¹‰à¸™à¹† จะถืà¸à¸§à¹ˆà¸²à¸–ูà¸à¸›à¸´à¸”ปาà¸à¹€à¸Šà¹ˆà¸™à¸à¸±à¸™ ทำได้เฉพาะคำขà¸à¸•à¸´à¸”ตามเท่านั้น à¹à¸¥à¸°à¹„ม่สามารถà¸à¸¥à¹ˆà¸²à¸§à¸–ึงบัà¸à¸Šà¸µà¹ƒà¸™à¸žà¸·à¹‰à¸™à¸—ี่ได้หาà¸à¹„ม่ได้ติดตาม | สิ่งนี้ไม่มีผลต่à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" +blockedInstances: "เซิร์ฟเวà¸à¸£à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" +blockedInstancesDescription: "ระบุโฮสต์ขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่ต้à¸à¸‡à¸à¸²à¸£à¸šà¸¥à¹‡à¸à¸ คั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่ เซิร์ฟเวà¸à¸£à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸à¸ˆà¸°à¹„ม่สามารถติดต่à¸à¸à¸±à¸šà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹„ด้" +silencedInstances: "ปิดปาà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰à¹à¸¥à¹‰à¸§" +silencedInstancesDescription: "ระบุโฮสต์ขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่ต้à¸à¸‡à¸à¸²à¸£à¸›à¸´à¸”ปาภคั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่, บัà¸à¸Šà¸µà¸—ั้งหมดขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸”ังà¸à¸¥à¹ˆà¸²à¸§à¸ˆà¸°à¸–ืà¸à¸§à¹ˆà¸²à¸–ูà¸à¸›à¸´à¸”ปาà¸à¹€à¸Šà¹ˆà¸™à¸à¸±à¸™ ทำได้เฉพาะคำขà¸à¸•à¸´à¸”ตามเท่านั้น à¹à¸¥à¸°à¹„ม่สามารถà¸à¸¥à¹ˆà¸²à¸§à¸–ึงบัà¸à¸Šà¸µà¹ƒà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰à¹„ด้หาà¸à¹„ม่ได้ถูà¸à¸•à¸´à¸”ตามà¸à¸¥à¸±à¸š | สิ่งนี้ไม่มีผลต่à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" +mediaSilencedInstances: "เซิร์ฟเวà¸à¸£à¹Œà¸—ี่ถูà¸à¸›à¸´à¸”ปาà¸à¸ªà¸·à¹ˆà¸" +mediaSilencedInstancesDescription: "ระบุโฮสต์ขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่ต้à¸à¸‡à¸à¸²à¸£à¸›à¸´à¸”ปาà¸à¸ªà¸·à¹ˆà¸ คั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่, ไฟล์ที่ถูà¸à¸ªà¹ˆà¸‡à¸ˆà¸²à¸à¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸”ังà¸à¸¥à¹ˆà¸²à¸§à¸ˆà¸°à¸–ืà¸à¸§à¹ˆà¸²à¸–ูà¸à¸›à¸´à¸”ปาภà¹à¸¥à¹‰à¸§à¸ˆà¸°à¸–ูà¸à¸•à¸´à¸”เครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸§à¹ˆà¸²à¸¡à¸µà¹€à¸™à¸·à¹‰à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™ à¹à¸¥à¸°à¹€à¸à¹‚มจิà¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เà¸à¸‡à¸à¹‡à¸ˆà¸°à¹ƒà¸Šà¹‰à¹„ม่ได้ด้วย | สิ่งนี้ไม่มีผลต่à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" muteAndBlock: "ปิดเสียงà¹à¸¥à¸°à¸šà¸¥à¹‡à¸à¸" mutedUsers: "ผู้ใช้ที่ถูà¸à¸›à¸´à¸”เสียง" blockedUsers: "ผู้ใช้ที่ถูà¸à¸šà¸¥à¹‡à¸à¸" @@ -240,14 +253,14 @@ noCustomEmojis: "ไม่มีเà¸à¹‚มจิ" noJobs: "ไม่มีงาน" federating: "สหพันธ์" blocked: "ถูà¸à¸šà¸¥à¹‡à¸à¸" -suspended: "ถูà¸à¸£à¸°à¸‡à¸±à¸š" +suspended: "ระงับà¸à¸²à¸£à¸ªà¹ˆà¸‡" all: "ทั้งหมด" -subscribing: "สมัครà¹à¸¥à¹‰à¸§" +subscribing: "à¸à¸³à¸¥à¸±à¸‡à¸ªà¸¡à¸±à¸„รสมาชิà¸" publishing: "à¸à¸³à¸¥à¸±à¸‡à¹€à¸œà¸¢à¹à¸žà¸£à¹ˆ" notResponding: "ไม่มีà¸à¸²à¸£à¸•à¸à¸šà¸ªà¸™à¸à¸‡" -instanceFollowing: "à¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตามบนà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" -instanceFollowers: "ผู้ติดตามขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" -instanceUsers: "ผู้ใช้งานขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰" +instanceFollowing: "à¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตามบนเซิร์ฟเวà¸à¸£à¹Œ" +instanceFollowers: "ผู้ติดตามขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" +instanceUsers: "ผู้ใช้ขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰" changePassword: "เปลี่ยนรหัสผ่าน" security: "ความปลà¸à¸”ภัย" retypedNotMatch: "ทั้งสà¸à¸‡à¸›à¹‰à¸à¸™à¸‚้à¸à¸¡à¸¹à¸¥à¹„ม่สà¸à¸”คล้à¸à¸‡à¸à¸±à¸™" @@ -284,20 +297,20 @@ messageRead: "à¸à¹ˆà¸²à¸™à¹à¸¥à¹‰à¸§" noMoreHistory: "ไม่มีประวัติเพิ่มเติม" startMessaging: "เริ่มà¸à¸²à¸£à¸ªà¸™à¸—นา" nUsersRead: "à¸à¹ˆà¸²à¸™à¹‚ดย {n}" -agreeTo: "ฉันยà¸à¸¡à¸£à¸±à¸šà¸—ี่จะ {0}" +agreeTo: "ฉันยà¸à¸¡à¸£à¸±à¸š {0}" agree: "ยà¸à¸¡à¸£à¸±à¸š" -agreeBelow: "ฉันยà¸à¸¡à¸£à¸±à¸šà¸–ึงด้านล่าง" +agreeBelow: "ยà¸à¸¡à¸£à¸±à¸šà¸•à¸²à¸¡à¸—ี่ระบุด้านล่าง" basicNotesBeforeCreateAccount: "หมายเหตุสำคัà¸" termsOfService: "เงื่à¸à¸™à¹„ขà¸à¸²à¸£à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£" start: "เริ่ม" -home: "หน้าà¹à¸£à¸" -remoteUserCaution: "ข้à¸à¸¡à¸¹à¸¥à¸à¸²à¸ˆà¹„ม่สมบูรณ์เนื่à¸à¸‡à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸£à¸²à¸¢à¸™à¸µà¹‰à¸¡à¸²à¸ˆà¸²à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" +home: "หน้าหลัà¸" +remoteUserCaution: "ข้à¸à¸¡à¸¹à¸¥à¸à¸²à¸ˆà¹„ม่สมบูรณ์เนื่à¸à¸‡à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸£à¸²à¸¢à¸™à¸µà¹‰à¸¡à¸²à¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" activity: "à¸à¸´à¸ˆà¸à¸£à¸£à¸¡" images: "รูปภาพ" image: "รูปภาพ" birthday: "วันเà¸à¸´à¸”" yearsOld: "{age} ปี" -registeredDate: "วันที่สมัครสมาชิà¸" +registeredDate: "วันที่ลงทะเบียน" location: "ตำà¹à¸«à¸™à¹ˆà¸‡à¸—ี่ตั้ง" theme: "ธีม" themeForLightMode: "ธีมที่จะใช้ในโหมดสว่าง" @@ -313,6 +326,7 @@ selectFile: "เลืà¸à¸à¹„ฟล์" selectFiles: "เลืà¸à¸à¹„ฟล์" selectFolder: "เลืà¸à¸à¹‚ฟลเดà¸à¸£à¹Œ" selectFolders: "เลืà¸à¸à¹‚ฟลเดà¸à¸£à¹Œ" +fileNotSelected: "ยังไม่ได้เลืà¸à¸à¹„ฟล์" renameFile: "เปลี่ยนชื่à¸à¹„ฟล์" folderName: "ชื่à¸à¹‚ฟลเดà¸à¸£à¹Œ" createFolder: "สร้างโฟลเดà¸à¸£à¹Œ" @@ -336,15 +350,15 @@ displayOfSensitiveMedia: "à¹à¸ªà¸”งสื่à¸à¸—ี่มีเนื้ภwhenServerDisconnected: "เมื่à¸à¸ªà¸¹à¸à¹€à¸ªà¸µà¸¢à¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸à¸à¸±à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" disconnectedFromServer: "à¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸–ูà¸à¸•à¸±à¸”" reload: "รีโหลด" -doNothing: "เมิน" +doNothing: "ช่างมัน" reloadConfirm: "รีโหลดเลยไหม?" -watch: "ดู" -unwatch: "หยุดดู" +watch: "เพ่งเล็ง" +unwatch: "เลิà¸à¹€à¸žà¹ˆà¸‡à¹€à¸¥à¹‡à¸‡" accept: "ยà¸à¸¡à¸£à¸±à¸š" reject: "ปà¸à¸´à¹€à¸ªà¸˜" normal: "ปà¸à¸•à¸´" -instanceName: "ชื่à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" -instanceDescription: "คำà¸à¸˜à¸´à¸šà¸²à¸¢à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" +instanceName: "ชื่à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" +instanceDescription: "คำà¸à¸˜à¸´à¸šà¸²à¸¢à¹à¸™à¸°à¸™à¸³à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" maintainerName: "ผู้ดูà¹à¸¥" maintainerEmail: "à¸à¸µà¹€à¸¡à¸¥à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸š" tosUrl: "URL เงื่à¸à¸™à¹„ขà¸à¸²à¸£à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£" @@ -355,16 +369,16 @@ dayX: "{day}" monthX: "เดืà¸à¸™ {month}" yearX: "{year}" pages: "หน้าเพจ" -integration: "รวบรวม" +integration: "เชื่à¸à¸¡à¹‚ยง" connectService: "เชื่à¸à¸¡à¸•à¹ˆà¸" disconnectService: "ตัดà¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸" -enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ในพื้นที่" +enableLocalTimeline: "เปิดใช้งานไทม์ไลน์ท้à¸à¸‡à¸–ิ่น" enableGlobalTimeline: "เปิดใช้งานไทม์ไลน์ทั่วโลà¸" disablingTimelinesInfo: "ผู้ดูà¹à¸¥à¸£à¸°à¸šà¸šà¹à¸¥à¸°à¸œà¸¹à¹‰à¸„วบคุมจะสามารถเข้าถึงไทม์ไลน์ทั้งหมด ถึงà¹à¸¡à¹‰à¸§à¹ˆà¸²à¸ˆà¸°à¹„ม่ได้เปิดใช้งานà¸à¹‡à¸•à¸²à¸¡" registration: "ลงทะเบียน" enableRegistration: "เปิดใช้งานà¸à¸²à¸£à¸¥à¸‡à¸—ะเบียนผู้ใช้ใหม่" invite: "คำเชิà¸" -driveCapacityPerLocalAccount: "ความจุขà¸à¸‡à¹„ดรฟ์ต่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸ ายในเครื่à¸à¸‡" +driveCapacityPerLocalAccount: "ความจุขà¸à¸‡à¹„ดรฟ์ต่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—้à¸à¸‡à¸–ิ่น" driveCapacityPerRemoteAccount: "ความจุขà¸à¸‡à¹„ดรฟ์ต่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸£à¸°à¸¢à¸°à¹„à¸à¸¥" inMb: "เป็นเมà¸à¸°à¹„บต์" bannerUrl: "URL รูปภาพà¹à¸šà¸™à¹€à¸™à¸à¸£à¹Œ" @@ -373,7 +387,7 @@ basicInfo: "ข้à¸à¸¡à¸¹à¸¥à¹€à¸šà¸·à¹‰à¸à¸‡à¸•à¹‰à¸™" pinnedUsers: "ผู้ใช้ที่ถูà¸à¸›à¸±à¸à¸«à¸¡à¸¸à¸”" pinnedUsersDescription: "ป้à¸à¸™à¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่คุณต้à¸à¸‡à¸à¸²à¸£à¸›à¸±à¸à¸«à¸¡à¸¸à¸”ในหน้า “ค้นพบ†ฯลฯ คั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่" pinnedPages: "หน้าเพจที่ปัà¸à¸«à¸¡à¸¸à¸”" -pinnedPagesDescription: "ป้à¸à¸™à¹€à¸ªà¹‰à¸™à¸—างขà¸à¸‡à¸«à¸™à¹‰à¸²à¹€à¸žà¸ˆà¸—ี่คุณต้à¸à¸‡à¸à¸²à¸£à¸›à¸±à¸à¸«à¸¡à¸¸à¸”ไว้ที่หน้าà¹à¸£à¸à¸‚à¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰ คั่นด้วยขึ้นบรรทัดใหม่" +pinnedPagesDescription: "ป้à¸à¸™à¹€à¸ªà¹‰à¸™à¸—างขà¸à¸‡à¸«à¸™à¹‰à¸²à¹€à¸žà¸ˆà¸—ี่คุณต้à¸à¸‡à¸à¸²à¸£à¸›à¸±à¸à¸«à¸¡à¸¸à¸”ไว้ที่หน้าà¹à¸£à¸à¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰ คั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่" pinnedClipId: "ID ขà¸à¸‡à¸„ลิปที่จะปัà¸à¸«à¸¡à¸¸à¸”" pinnedNotes: "โน้ตที่ปัà¸à¸«à¸¡à¸¸à¸”ไว้" hcaptcha: "hCaptcha" @@ -389,11 +403,11 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "เปิดใช้ reCAPTCHA" recaptchaSiteKey: "คีย์ไซต์" recaptchaSecretKey: "คีย์ลับ" -turnstile: "เทิร์น'สไทล" -enableTurnstile: "เปิดใช้งาน เทิร์น'สไทล" +turnstile: "Turnstile" +enableTurnstile: "เปิดใช้งาน Turnstile" turnstileSiteKey: "คีย์ไซต์" turnstileSecretKey: "คีย์ลับ" -avoidMultiCaptchaConfirm: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸£à¸°à¸šà¸š Captcha หลายระบบà¸à¸²à¸ˆà¸—ำให้เà¸à¸´à¸”à¸à¸²à¸£à¸£à¸šà¸à¸§à¸™à¸«à¸£à¸·à¸à¸à¸²à¸ˆà¸ˆà¸°à¹€à¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาดได้ หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¸—ี่จะปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸£à¸°à¸šà¸š Captcha à¸à¸·à¹ˆà¸™ ๆ à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¸›à¸´à¸”ตัวà¸à¸·à¹ˆà¸™à¹†à¸à¹ˆà¸à¸™ ถ้าหาà¸à¸„ุณต้à¸à¸‡à¸à¸²à¸£à¹ƒà¸«à¹‰à¹€à¸›à¸´à¸”ใช้งานต่à¸à¹„ป ให้ à¸à¸” ยà¸à¹€à¸¥à¸´à¸" +avoidMultiCaptchaConfirm: "à¸à¸²à¸£à¹ƒà¸Šà¹‰ Captcha หลายตัวà¸à¸²à¸ˆà¸—ำให้เà¸à¸´à¸”à¸à¸²à¸£à¸£à¸šà¸à¸§à¸™à¸«à¸£à¸·à¸à¸‚้à¸à¸œà¸´à¸”พลาดได้ ต้à¸à¸‡à¸à¸²à¸£à¸—ี่จะปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ Captcha ตัวà¸à¸·à¹ˆà¸™à¹€à¸¥à¸¢à¹„หม? หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¹ƒà¸«à¹‰à¹€à¸›à¸´à¸”ใช้งานต่à¸à¹„ป ให้à¸à¸”ยà¸à¹€à¸¥à¸´à¸" antennas: "เสาà¸à¸²à¸à¸²à¸¨" manageAntennas: "จัดà¸à¸²à¸£à¹€à¸ªà¸²à¸à¸²à¸à¸²à¸¨" name: "ชื่à¸" @@ -401,7 +415,7 @@ antennaSource: "à¹à¸«à¸¥à¹ˆà¸‡à¹€à¸ªà¸²à¸à¸²à¸à¸²à¸¨" antennaKeywords: "คีย์เวิร์ดที่ควรฟัง" antennaExcludeKeywords: "คีย์เวิร์ดที่จะยà¸à¹€à¸§à¹‰à¸™" antennaExcludeBots: "ยà¸à¹€à¸§à¹‰à¸™à¸šà¸±à¸à¸Šà¸µà¸šà¸à¸•" -antennaKeywordsDescription: "คั่นด้วยช่à¸à¸‡à¸§à¹ˆà¸²à¸‡à¸ªà¸³à¸«à¸£à¸±à¸šà¹€à¸‡à¸·à¹ˆà¸à¸™à¹„ข AND หรืà¸à¸”้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่สำหรับเงื่à¸à¸™à¹„ข OR" +antennaKeywordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่à¸à¸™à¹„ข AND, หรืà¸à¸‚ึ้นบรรทัดใหม่สำหรับเงื่à¸à¸™à¹„ข OR" notifyAntenna: "à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¹‚น้ตใหม่" withFileAntenna: "เฉพาะโน้ตที่มีไฟล์" enableServiceworker: "เปิดใช้งานà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Šà¹„ปยังเบราว์เซà¸à¸£à¹Œà¸‚à¸à¸‡à¸„ุณ" @@ -418,7 +432,7 @@ unsilenceConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¹€à¸¥à¸´à¸à¸›à¸´à¸”ปาà¸à¸œà¸¹à¹‰ popularUsers: "ผู้ใช้ที่เป็นที่นิยม" recentlyUpdatedUsers: "ผู้ใช้ที่เพิ่งใช้งานล่าสุด" recentlyRegisteredUsers: "ผู้ใช้ที่เข้าร่วมใหม่" -recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบใหม่" +recentlyDiscoveredUsers: "ผู้ใช้ที่เพิ่งค้นพบล่าสุด" exploreUsersCount: "มีผู้ใช้ {count} ราย" exploreFediverse: "สำรวจสหพันธ์" popularTags: "à¹à¸—็à¸à¸¢à¸à¸”นิยม" @@ -435,7 +449,7 @@ moderator: "ผู้ควบคุม" moderation: "à¸à¸²à¸£à¸à¸¥à¸±à¹ˆà¸™à¸à¸£à¸à¸‡" moderationNote: "โน้ตà¸à¸²à¸£à¸à¸¥à¸±à¹ˆà¸™à¸à¸£à¸à¸‡" addModerationNote: "เพิ่มโน้ตà¸à¸²à¸£à¸à¸¥à¸±à¹ˆà¸™à¸à¸£à¸à¸‡" -moderationLogs: "ปูมà¸à¸²à¸£à¹à¸à¹‰à¹„ข" +moderationLogs: "ปูมà¸à¸²à¸£à¸„วบคุมดูà¹à¸¥" nUsersMentioned: "à¸à¸¥à¹ˆà¸²à¸§à¸–ึงโดยผู้ใช้ {n} ราย" securityKeyAndPasskey: "ความปลà¸à¸”ภัยà¹à¸¥à¸°à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™" securityKey: "à¸à¸¸à¸à¹à¸ˆà¸„วามปลà¸à¸”ภัย" @@ -468,44 +482,46 @@ retype: "พิมพ์รหัสà¸à¸µà¸à¸„รั้ง" noteOf: "โน้ตขà¸à¸‡ {user}" quoteAttached: "à¸à¹‰à¸²à¸‡à¸à¸´à¸‡" quoteQuestion: "ต้à¸à¸‡à¸à¸²à¸£à¸—ี่จะà¹à¸™à¸šà¸¡à¸±à¸™à¹€à¸žà¸·à¹ˆà¸à¸à¹‰à¸²à¸‡à¸à¸´à¸‡à¹ƒà¸Šà¹ˆà¹„หม?" +attachAsFileQuestion: "ข้à¸à¸„วามในคลิปบà¸à¸£à¹Œà¸”ยาวเà¸à¸´à¸™à¹„ป คุณต้à¸à¸‡à¸à¸²à¸£à¹à¸™à¸šà¹€à¸›à¹‡à¸™à¹„ฟล์ข้à¸à¸„วามหรืà¸à¹„ม่?" noMessagesYet: "ยังไม่มีข้à¸à¸„วาม" newMessageExists: "คุณมีข้à¸à¸„วามใหม่" onlyOneFileCanBeAttached: "สามารถà¹à¸™à¸šà¹„ฟล์ได้เพียงไฟล์เดียวต่ภ1 ข้à¸à¸„วาม" -signinRequired: "à¸à¸£à¸¸à¸“าลงทะเบียนหรืà¸à¸¥à¸‡à¸Šà¸·à¹ˆà¸à¹€à¸‚้าใช้à¸à¹ˆà¸à¸™à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸" +signinRequired: "à¸à¹ˆà¸à¸™à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸ à¸à¸£à¸¸à¸“าลงทะเบียนหรืà¸à¹€à¸‚้าสู่ระบบ" +signinOrContinueOnRemote: "เพื่à¸à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸à¹„ด้ คุณต้à¸à¸‡à¹„ปที่เซิร์ฟเวà¸à¸£à¹Œà¸—ี่คุณใช้งานà¸à¸¢à¸¹à¹ˆ หรืà¸à¸¥à¸‡à¸—ะเบียน/เข้าสู่ระบบเซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰" invitations: "คำเชิà¸" invitationCode: "รหัสเชิà¸" checking: "Checking" available: "พร้à¸à¸¡à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" unavailable: "ไม่พร้à¸à¸¡à¹ƒà¸Šà¹‰" -usernameInvalidFormat: "คุณสามารถใช้à¸à¸±à¸à¸©à¸£à¸•à¸±à¸§à¸žà¸´à¸¡à¸žà¹Œà¹ƒà¸«à¸à¹ˆà¹à¸¥à¸°à¸•à¸±à¸§à¸žà¸´à¸¡à¸žà¹Œà¹€à¸¥à¹‡à¸ ตัวเลข à¹à¸¥à¸°à¸‚ีดล่างได้นะ ( a-z , A-Z , 0-9 , รวมไปถึงà¸à¸±à¸à¸©à¸£à¸žà¸´à¹€à¸¨à¸©à¹€à¸Šà¹ˆà¸™ + * / , . - à¸à¸·à¹ˆà¸™à¹†à¹€à¸›à¹‡à¸™à¸•à¹‰à¸™ )" +usernameInvalidFormat: "สามารถใช้ a~z A~Z 0~9 à¹à¸¥à¸° _ ได้" tooShort: "สั้นเà¸à¸´à¸™à¹„ปนะ" tooLong: "ยาวเà¸à¸´à¸™à¹„ปนะ" -weakPassword: "รหัสผ่าน à¹à¸¢à¹ˆà¸¡à¸²à¸" +weakPassword: "รหัสผ่านà¹à¸¢à¹ˆà¸¡à¸²à¸" normalPassword: "รหัสผ่านปà¸à¸•à¸´" strongPassword: "รหัสผ่านรัดà¸à¸¸à¸¡à¸¡à¸²à¸" passwordMatched: "ถูà¸à¸•à¹‰à¸à¸‡!" passwordNotMatched: "ไม่ถูà¸à¸•à¹‰à¸à¸‡" -signinWith: "ลงชื่à¸à¹€à¸‚้าใช้ด้วย {x}" -signinFailed: "ไม่สามารถลงชื่à¸à¸œà¸¹à¹‰à¹€à¸‚้าใช้ได้ เนื่à¸à¸‡à¸ˆà¸²à¸ ชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸«à¸£à¸·à¸à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™à¸—ี่คุณป้à¸à¸™à¸™à¸±à¹‰à¸™à¹„ม่ถูà¸à¸•à¹‰à¸à¸‡à¸™à¸°" +signinWith: "เข้าสู่ระบบด้วย {x}" +signinFailed: "ไม่สามารถเข้าสู่ระบบได้ à¸à¸£à¸¸à¸“าตรวจสà¸à¸šà¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹à¸¥à¸°à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™" or: "หรืà¸" language: "ภาษา" uiLanguage: "ภาษาà¸à¸´à¸™à¹€à¸—à¸à¸£à¹Œà¹€à¸Ÿà¸‹à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" aboutX: "เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸š {x}" -emojiStyle: "สไตล์เà¸à¹‚มจิ" +emojiStyle: "สไตล์ขà¸à¸‡à¹€à¸à¹‚มจิ" native: "ภาษาà¹à¸¡à¹ˆ" -disableDrawer: "à¸à¸¢à¹ˆà¸²à¹ƒà¸Šà¹‰à¸¥à¸´à¹‰à¸™à¸Šà¸±à¸à¸ªà¹„ตล์เมนู" -showNoteActionsOnlyHover: "à¹à¸ªà¸”งà¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¹€à¸‰à¸žà¸²à¸°à¹‚น้ตเมื่à¸à¹‚ฮเวà¸à¸£à¹Œ" +disableDrawer: "ไม่à¹à¸ªà¸”งเมนูในรูปà¹à¸šà¸šà¸¥à¸´à¹‰à¸™à¸Šà¸±à¸" +showNoteActionsOnlyHover: "à¹à¸ªà¸”งà¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¹‚น้ตเมื่à¸à¹‚ฮเวà¸à¸£à¹Œ(วางเมาส์เหนืà¸)เท่านั้น" showReactionsCount: "à¹à¸ªà¸”งจำนวนรีà¹à¸à¸à¸Šà¸±à¹ˆà¸™à¹ƒà¸™à¹‚น้ต" noHistory: "ไม่มีประวัติ" signinHistory: "ประวัติà¸à¸²à¸£à¹€à¸‚้าสู่ระบบ" enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง" -enableAnimatedMfm: "เปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ MFM ด้วยà¹à¸à¸™à¸´à¹€à¸¡à¸Šà¸±à¹ˆà¸™" +enableAnimatedMfm: "เปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ MFM à¹à¸šà¸šà¹€à¸„ลื่à¸à¸™à¹„หว" doing: "à¸à¸³à¸¥à¸±à¸‡à¸›à¸£à¸°à¸¡à¸§à¸¥à¸œà¸¥......" category: "หมวดหมู่" tags: "นามà¹à¸à¸‡" docSource: "ที่มาขà¸à¸‡à¹€à¸à¸à¸ªà¸²à¸£à¸™à¸µà¹‰" createAccount: "สร้างบัà¸à¸Šà¸µ" -existingAccount: "บัà¸à¸Šà¸µà¸—ี่มีà¸à¸¢à¸¹à¹ˆ" +existingAccount: "บัà¸à¸Šà¸µà¸—ี่มีà¸à¸¢à¸¹à¹ˆà¹à¸¥à¹‰à¸§" regenerate: "สร้างà¸à¸µà¸à¸„รั้ง" fontSize: "ขนาดตัวà¸à¸±à¸à¸©à¸£" mediaListWithOneImageAppearance: "ความสูงขà¸à¸‡à¸£à¸²à¸¢à¸à¸²à¸£à¸ªà¸·à¹ˆà¸à¸—ี่มีเพียงรูปเดียว" @@ -513,11 +529,11 @@ limitTo: "จำà¸à¸±à¸”ไว้ที่ {x}" noFollowRequests: "คุณไม่มีคำขà¸à¸•à¸´à¸”ตามที่รà¸à¸”ำเนินà¸à¸²à¸£" openImageInNewTab: "เปิดรูปภาพในà¹à¸—็บใหม่" dashboard: "หน้าà¸à¸£à¸°à¸”านหลัà¸" -local: "ในพื้นที่" +local: "ท้à¸à¸‡à¸–ิ่น" remote: "ระยะไà¸à¸¥" total: "รวมทั้งหมด" -weekOverWeekChanges: "เปลี่ยนà¹à¸›à¸¥à¸‡à¹„ปเมื่à¸à¸ªà¸±à¸›à¸”าห์ที่à¹à¸¥à¹‰à¸§" -dayOverDayChanges: "เปลี่ยนà¹à¸›à¸¥à¸‡à¹„ปเมื่à¸à¸§à¸²à¸™à¸™à¸µà¹‰" +weekOverWeekChanges: "เทียบà¸à¸±à¸šà¸ªà¸±à¸›à¸”าห์à¸à¹ˆà¸à¸™" +dayOverDayChanges: "เทียบà¸à¸±à¸šà¹€à¸¡à¸·à¹ˆà¸à¸§à¸²à¸™" appearance: "ภาพลัà¸à¸©à¸“์" clientSettings: "à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าไคลเà¸à¸™à¸•à¹Œ" accountSettings: "ตั้งค่าบัà¸à¸Šà¸µ" @@ -531,19 +547,19 @@ useObjectStorage: "ใช้à¸à¸²à¸£à¸ˆà¸±à¸”เà¸à¹‡à¸šà¹ƒà¸™à¸£à¸¹à¸›à¹à¸š objectStorageBaseUrl: "Base URL" objectStorageBaseUrlDesc: "URL ที่ใช้เป็นข้à¸à¸¡à¸¹à¸¥à¸à¹‰à¸²à¸‡à¸à¸´à¸‡ ระบุ URL ขà¸à¸‡ CDN หรืภProxy ถ้าหาà¸à¸„ุณใช้à¸à¸¢à¹ˆà¸²à¸‡à¹ƒà¸”à¸à¸¢à¹ˆà¸²à¸‡à¸«à¸™à¸¶à¹ˆà¸‡\n สำหรับà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ S3 'https://<bucket>.s3.amazonaws.com' à¹à¸¥à¸°à¸ªà¸³à¸«à¸£à¸±à¸š GCS หรืà¸à¸šà¸£à¸´à¸à¸²à¸£à¸—ี่เทียบเท่าใช้ 'https://storage.googleapis.com/<bucket>', เป็นต้น" objectStorageBucket: "Bucket" -objectStorageBucketDesc: "โปรดระบุชื่à¸à¸—ี่เà¸à¹‡à¸šà¸‚้à¸à¸¡à¸¹à¸¥à¸—ี่ใช้à¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£à¸‚à¸à¸‡à¸„ุณ" +objectStorageBucketDesc: "โปรดระบุชื่à¸à¸šà¸±à¸„เà¸à¹‡à¸•à¸‚à¸à¸‡à¸šà¸£à¸´à¸à¸²à¸£à¸—ี่ใช้à¸à¸¢à¸¹à¹ˆ" objectStoragePrefix: "คำนำหน้า" objectStoragePrefixDesc: "ไฟล์ทั้งหมดจะถูà¸à¹€à¸à¹‡à¸šà¹„ว้ภายใต้ไดเร็à¸à¸—à¸à¸£à¸µà¸—ี่มีคำนำหน้านี้" objectStorageEndpoint: "ปลายทาง" objectStorageEndpointDesc: "เว้นว่างไว้หาà¸à¸„ุณใช้ AWS S3 หรืà¸à¸£à¸°à¸šà¸¸à¸›à¸¥à¸²à¸¢à¸—างเป็น '<host>' หรืภ'<host>:<port>' ทั้งนี้ขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£à¸—ี่คุณใช้à¸à¸¢à¸¹à¹ˆà¸”้วย" objectStorageRegion: "ภูมิภาค" -objectStorageRegionDesc: "ระบุภูมิภาค เช่น 'xx-east-1' ถ้าหาà¸à¸šà¸£à¸´à¸à¸²à¸£à¸‚à¸à¸‡à¸„ุณไม่ได้à¹à¸¢à¸à¸„วามà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸£à¸°à¸«à¸§à¹ˆà¸²à¸‡à¸ ูมิภาคà¸à¹‡à¹ƒà¸«à¹‰ เว้นว่างไว้หรืà¸à¸›à¹‰à¸à¸™ 'us-east-1'" +objectStorageRegionDesc: "ระบุภูมิภาค เช่น ‘xx-east-1’ หาà¸à¸šà¸£à¸´à¸à¸²à¸£à¸‚à¸à¸‡à¸„ุณไม่à¹à¸¢à¸à¸ ูมิภาค ให้ระบุเป็น ‘us-east-1’ หรืà¸à¹€à¸§à¹‰à¸™à¸§à¸²à¸‡à¹„ว้หาà¸à¹ƒà¸Šà¹‰ AWS configuration files / environment variables" objectStorageUseSSL: "ใช้ SSL" objectStorageUseSSLDesc: "ปิดà¸à¸²à¸£à¸—ำงานนี้ไว้ ถ้าหาà¸à¸„ุณจะไม่ใช้ HTTPS สำหรับà¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸ API" objectStorageUseProxy: "เชื่à¸à¸¡à¸•à¹ˆà¸à¸œà¹ˆà¸²à¸™à¸žà¸£à¹‡à¸à¸à¸‹à¸µ" objectStorageUseProxyDesc: "ปิดสิ่งนี้ไว้ถ้าหาà¸à¸„ุณจะไม่ใช้ Proxy สำหรับà¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸à¸¡à¸•à¹ˆà¸ API" objectStorageSetPublicRead: "ตั้งค่าเป็น “public-read†เมื่à¸à¸à¸±à¸›à¹‚หลด" -s3ForcePathStyleDesc: "ถ้าหาà¸à¹€à¸›à¸´à¸”ใช้งาน s3ForcePathStyle ชื่à¸à¸šà¸±à¸„เà¸à¹‡à¸•à¸™à¸±à¹‰à¸™à¸à¸²à¸ˆà¸ˆà¸°à¸•à¹‰à¸à¸‡à¸£à¸§à¸¡à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¹€à¸ªà¹‰à¸™à¸—างขà¸à¸‡ URL ซึ่งตรงข้ามà¸à¸±à¸šà¸Šà¸·à¹ˆà¸à¹‚ฮสต์ขà¸à¸‡ URL คุณà¸à¸²à¸ˆà¸ˆà¸°à¸•à¹‰à¸à¸‡à¹€à¸›à¸´à¸”ใช้งานà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่านี้เมื่à¸à¹ƒà¸Šà¹‰à¸šà¸£à¸´à¸à¸²à¸£à¸•à¹ˆà¸²à¸‡à¹† เช่น à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ Minio ที่โฮสต์เà¸à¸‡à¸™à¸°" +s3ForcePathStyleDesc: "เมื่à¸à¹€à¸›à¸´à¸”ใช้งาน s3ForcePathStyle จะบังคับให้ ระบุชื่à¸à¸šà¸±à¸„เà¸à¹‡à¸•à¹€à¸›à¹‡à¸™à¸ªà¹ˆà¸§à¸™à¸«à¸™à¸¶à¹ˆà¸‡à¸‚à¸à¸‡à¸žà¸²à¸˜ à¹à¸—นที่จะเป็นชื่à¸à¹‚ฮสต์ใน URL, à¸à¸²à¸ˆà¸ˆà¸³à¹€à¸›à¹‡à¸™à¸•à¹‰à¸à¸‡à¹€à¸›à¸´à¸”ใช้งานตัวเลืà¸à¸à¸™à¸µà¹‰à¹€à¸¡à¸·à¹ˆà¸à¹ƒà¸Šà¹‰à¸à¸±à¸š Minio ที่โฮสต์เà¸à¸‡à¸«à¸£à¸·à¸à¸šà¸£à¸´à¸à¸²à¸£à¸—ี่คล้ายà¸à¸±à¸™" serverLogs: "ปูมขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" deleteAll: "ลบทั้งหมด" showFixedPostForm: "à¹à¸ªà¸”งà¹à¸šà¸šà¸Ÿà¸à¸£à¹Œà¸¡à¸à¸²à¸£à¹‚พสต์ที่ด้านบนสุดขà¸à¸‡à¹„ทม์ไลน์" @@ -575,7 +591,7 @@ sort: "เรียงลำดับ" ascendingOrder: "เรียงลำดับขึ้น" descendingOrder: "เรียงลำดับลง" scratchpad: "Scratchpad" -scratchpadDescription: "Scratchpad เป็นà¸à¸²à¸£à¸ˆà¸±à¸”เตรียมสภาพà¹à¸§à¸”ล้à¸à¸¡à¸ªà¸³à¸«à¸£à¸±à¸šà¸à¸²à¸£à¸—ดลà¸à¸‡ AiScript à¹à¸•à¹ˆà¸„ุณสามารถเขียน ดำเนินà¸à¸²à¸£ à¹à¸¥à¸°à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸œà¸¥à¸¥à¸±à¸žà¸˜à¹Œà¸‚à¸à¸‡à¸à¸²à¸£à¹‚ต้ตà¸à¸šà¸à¸±à¸š Misskey มันได้ด้วยนะ" +scratchpadDescription: "Scratchpad ให้สภาพà¹à¸§à¸”ล้à¸à¸¡à¸ªà¸³à¸«à¸£à¸±à¸šà¸à¸²à¸£à¸—ดลà¸à¸‡ AiScript คุณสามารถเขียนโค้ด/สั่งดำเนินà¸à¸²à¸£/ตรวจสà¸à¸šà¸œà¸¥à¸¥à¸±à¸žà¸˜à¹Œ ขà¸à¸‡à¸à¸²à¸£à¹‚ต้ตà¸à¸šà¸à¸±à¸š Misskey ได้" output: "เà¸à¸²à¸—์พุต" script: "สคริปต์" disablePagesScript: "ปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ AiScript บนเพจ" @@ -587,15 +603,15 @@ unsetUserBannerConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¹€à¸¥à¸´à¸à¸•à¸±à¹‰à¸‡à¹à¸šà¸™ deleteAllFiles: "ลบไฟล์ทั้งหมด" deleteAllFilesConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¹„ฟล์ทั้งหมดใช่ไหม?" removeAllFollowing: "เลิà¸à¸•à¸´à¸”ตามผู้ใช้ที่ติดตามทั้งหมด" -removeAllFollowingDescription: "เลิà¸à¸•à¸´à¸”ตามทั้งหมดจาภ{host} โปรดเรียà¸à¹ƒà¸Šà¹‰à¸ªà¸´à¹ˆà¸‡à¸™à¸µà¹‰à¹€à¸¡à¸·à¹ˆà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸”ังà¸à¸¥à¹ˆà¸²à¸§à¹„ด้สูà¸à¸«à¸²à¸¢à¸•à¸²à¸¢à¸ˆà¸²à¸à¹„ปà¹à¸¥à¹‰à¸§" +removeAllFollowingDescription: "จะเลิà¸à¸•à¸´à¸”ตามทั้งหมดจาภ{host} โปรดดำเนินà¸à¸²à¸£à¸ªà¸´à¹ˆà¸‡à¸™à¸µà¹‰à¹€à¸¡à¸·à¹ˆà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸”ังà¸à¸¥à¹ˆà¸²à¸§à¹„ด้สูà¸à¸«à¸²à¸¢à¸•à¸²à¸¢à¸ˆà¸²à¸à¹„ปà¹à¸¥à¹‰à¸§" userSuspended: "ผู้ใช้รายนี้ถูà¸à¸£à¸°à¸‡à¸±à¸šà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" userSilenced: "ผู้ใช้รายนี้ถูà¸à¸›à¸´à¸”ปาà¸à¸à¸¢à¸¹à¹ˆ" yourAccountSuspendedTitle: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸™à¸±à¹‰à¸™à¸–ูà¸à¸£à¸°à¸‡à¸±à¸š" yourAccountSuspendedDescription: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¸£à¸°à¸‡à¸±à¸š เนื่à¸à¸‡à¸ˆà¸²à¸à¸¥à¸°à¹€à¸¡à¸´à¸”ข้à¸à¸à¸³à¸«à¸™à¸”ในà¸à¸²à¸£à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£à¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸«à¸£à¸·à¸à¸à¸²à¸ˆà¸ˆà¸°à¸¥à¸°à¹€à¸¡à¸´à¸”หลัà¸à¹€à¸à¸“ฑ์ชุมชน หรืภà¸à¸²à¸ˆà¸ˆà¸°à¹‚ดนร้à¸à¸‡à¹€à¸£à¸µà¸¢à¸™à¹€à¸£à¸·à¹ˆà¸à¸‡à¸à¸²à¸£à¸¥à¸°à¹€à¸¡à¸´à¸”ลิขสิทธิ์à¹à¸¥à¸°à¸à¸·à¹ˆà¸™à¹†à¸à¸¢à¹ˆà¸²à¸‡à¸•à¹ˆà¸à¹€à¸™à¸·à¹ˆà¸à¸‡à¸‹à¹‰à¸³à¹† หาà¸à¸„ุณคิดว่าไม่ได้ทำผิดจริงๆหรืà¸à¸•à¸±à¸”สินผิดพลาด ได้โปรดà¸à¸£à¸¸à¸“าติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¸«à¸²à¸à¸„ุณต้à¸à¸‡à¸à¸²à¸£à¸—ราบเหตุผลโดยละเà¸à¸µà¸¢à¸”เพิ่มเติม à¹à¸¥à¸°à¸‚à¸à¸„วามà¸à¸£à¸¸à¸“าà¸à¸¢à¹ˆà¸²à¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µà¹ƒà¸«à¸¡à¹ˆ" tokenRevoked: "โทเค็นไม่ถูà¸à¸•à¹‰à¸à¸‡" -tokenRevokedDescription: "โทเค็นนี้หมดà¸à¸²à¸¢à¸¸à¹à¸¥à¹‰à¸§à¸™à¸°à¸„่ะà¸à¸£à¸¸à¸“าเข้าสู่ระบบà¸à¸µà¸à¸„รั้งนะ" +tokenRevokedDescription: "โทเค็นà¸à¸²à¸£à¹€à¸‚้าสู่ระบบหมดà¸à¸²à¸¢à¸¸ à¸à¸£à¸¸à¸“าเข้าสู่ระบบใหม่à¸à¸µà¸à¸„รั้ง" accountDeleted: "ลบบัà¸à¸Šà¸µà¹à¸¥à¹‰à¸§" -accountDeletedDescription: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¸¥à¸šà¹„ปà¹à¸¥à¹‰à¸§à¸™à¸°" +accountDeletedDescription: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¸¥à¸šà¹à¸¥à¹‰à¸§" menu: "เมนู" divider: "ตัวà¹à¸šà¹ˆà¸‡" addItem: "เพิ่มรายà¸à¸²à¸£" @@ -615,14 +631,14 @@ enablePlayer: "เปิดเครื่à¸à¸‡à¹€à¸¥à¹ˆà¸™à¸§à¸´à¸”ีโà¸" disablePlayer: "ปิดเครื่à¸à¸‡à¹€à¸¥à¹ˆà¸™à¸§à¸´à¸”ีโà¸" expandTweet: "ขยายทวีต" themeEditor: "ตัวà¹à¸à¹‰à¹„ขธีม" -description: "รายละเà¸à¸µà¸¢à¸”" +description: "คำà¸à¸˜à¸´à¸šà¸²à¸¢" describeFile: "เพิ่มà¹à¸„ปชั่น" enterFileDescription: "ใส่à¹à¸„ปชั่น" author: "ผู้เขียน" leaveConfirm: "มีà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ี่ยังไม่ได้บันทึภต้à¸à¸‡à¸à¸²à¸£à¸¥à¸°à¸—ิ้งมันใช่ไหม?" manage: "à¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£" plugins: "ปลั๊à¸à¸à¸´à¸™" -preferencesBackups: "ตั้งค่าà¸à¸²à¸£à¸ªà¸³à¸£à¸à¸‡à¸‚้à¸à¸¡à¸¹à¸¥" +preferencesBackups: "สำรà¸à¸‡à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่า" deck: "เด็ค" undeck: "à¸à¸à¸à¸ˆà¸²à¸à¹€à¸”็ค" useBlurEffectForModal: "ใช้เà¸à¸Ÿà¹€à¸Ÿà¸à¸•à¹Œà¹€à¸šà¸¥à¸à¸ªà¸³à¸«à¸£à¸±à¸šà¹‚มดà¸à¸¥" @@ -632,21 +648,21 @@ height: "ความสูง" large: "ใหà¸à¹ˆ" medium: "ปานà¸à¸¥à¸²à¸‡" small: "เล็à¸" -generateAccessToken: "สร้างà¸à¸²à¸£à¹€à¸‚้าถึงโทเค็น" -permission: "à¸à¸²à¸£à¸à¸™à¸¸à¸à¸²à¸•" +generateAccessToken: "สร้างโทเค็นà¸à¸²à¸£à¹€à¸‚้าถึง" +permission: "สิทธิ์" adminPermission: "สิทธิ์ขà¸à¸‡à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸š" enableAll: "เปิดใช้งานทั้งหมด" disableAll: "ปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸—ั้งหมด" tokenRequested: "ให้สิทธิ์à¸à¸²à¸£à¹€à¸‚้าถึงบัà¸à¸Šà¸µ" -pluginTokenRequestedDescription: "ปลั๊à¸à¸à¸´à¸™à¸™à¸µà¹‰à¸ˆà¸°à¸ªà¸²à¸¡à¸²à¸£à¸–ใช้à¸à¸²à¸£à¸à¸™à¸¸à¸à¸²à¸•à¸—ี่ตั้งค่าไว้ที่นี่นะ" +pluginTokenRequestedDescription: "ปลั๊à¸à¸à¸´à¸™à¸™à¸µà¹‰à¸ˆà¸°à¹ƒà¸Šà¹‰à¸ªà¸´à¸—ธิ์ตามที่ตั้งค่าไว้ที่นี่" notificationType: "ประเภทà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" edit: "à¹à¸à¹‰à¹„ข" -emailServer: "à¸à¸µà¹€à¸¡à¸¥à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" +emailServer: "เซิร์ฟเวà¸à¸£à¹Œà¸‚à¸à¸‡à¸à¸µà¹€à¸¡à¸¥" enableEmail: "เปิดใช้งานà¸à¸²à¸£à¸à¸£à¸°à¸ˆà¸²à¸¢à¸à¸µà¹€à¸¡à¸¥" -emailConfigInfo: "ใช้เพื่à¸à¸¢à¸·à¸™à¸¢à¸±à¸™à¸à¸µà¹€à¸¡à¸¥à¸‚à¸à¸‡à¸„ุณระหว่างà¸à¸²à¸£à¸ªà¸¡à¸±à¸„รหรืà¸à¸–้าหาà¸à¸„ุณลืมรหัสผ่าน" +emailConfigInfo: "ใช้สำหรับà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸à¸µà¹€à¸¡à¸¥à¸«à¸£à¸·à¸à¸à¸²à¸£à¸£à¸µà¹€à¸‹à¹‡à¸•à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™" email: "à¸à¸µà¹€à¸¡à¸¥" emailAddress: "ที่à¸à¸¢à¸¹à¹ˆà¸à¸µà¹€à¸¡à¸¥" -smtpConfig: "à¸à¸³à¸«à¸™à¸”ค่าเซิร์ฟเวà¸à¸£à¹Œ SMTP" +smtpConfig: "ตั้งค่าเซิร์ฟเวà¸à¸£à¹Œ SMTP" smtpHost: "โฮสต์" smtpPort: "พà¸à¸£à¹Œà¸•" smtpUser: "ชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" @@ -656,10 +672,10 @@ smtpSecure: "ใช้โดยนัย SSL/TLS สำหรับà¸à¸²à¸£à¹€ smtpSecureInfo: "ปิดสิ่งนี้เมื่à¸à¹ƒà¸Šà¹‰ STARTTLS" testEmail: "ทดสà¸à¸šà¸à¸²à¸£à¸ªà¹ˆà¸‡à¸à¸µà¹€à¸¡à¸¥" wordMute: "ปิดเสียงคำ" -hardWordMute: "ปิดเสียงคำยาà¸" -regexpError: "ข้à¸à¸œà¸´à¸”พลาดขà¸à¸‡à¸™à¸´à¸žà¸ˆà¸™à¹Œà¸—ั่วไป" -regexpErrorDescription: "เà¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาดในนิพจน์ทั่วไปในบรรทัดที่ {line} ขà¸à¸‡à¸à¸²à¸£à¸›à¸´à¸”เสียงคำ {tab} ขà¸à¸‡à¸„ุณ:" -instanceMute: "ปิดเสียง à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" +hardWordMute: "ปิดเสียงคำà¹à¸šà¸šà¹à¸‚็งโป๊à¸" +regexpError: "เà¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาดใน regular expression" +regexpErrorDescription: "เà¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาดใน regular expression บรรทัดที่ {line} ขà¸à¸‡à¸à¸²à¸£à¸›à¸´à¸”เสียงคำ {tab} :" +instanceMute: "ปิดเสียงเซิร์ฟเวà¸à¸£à¹Œ" userSaysSomething: "{name} พูดà¸à¸°à¹„รบางà¸à¸¢à¹ˆà¸²à¸‡" makeActive: "เปิดใช้งาน" display: "à¹à¸ªà¸”งผล" @@ -690,17 +706,17 @@ reportAbuseOf: "รายงาน {name}" fillAbuseReportDescription: "à¸à¸£à¸¸à¸“าà¸à¸£à¸à¸à¸£à¸²à¸¢à¸¥à¸°à¹€à¸à¸µà¸¢à¸”เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸£à¸²à¸¢à¸‡à¸²à¸™à¸™à¸µà¹‰ หาà¸à¹€à¸›à¹‡à¸™à¹€à¸£à¸·à¹ˆà¸à¸‡à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¹‚น้ตโดยเฉพาะ ได้โปรดระบุ URL" abuseReported: "เราได้ส่งรายงานขà¸à¸‡à¸„ุณไปà¹à¸¥à¹‰à¸§ ขà¸à¸šà¸„ุณมาà¸à¹†à¸™à¸°" reporter: "ผู้รายงาน" -reporteeOrigin: "รายงานต้นทาง" +reporteeOrigin: "ปลายทางรายงาน" reporterOrigin: "à¹à¸«à¸¥à¹ˆà¸‡à¸œà¸¹à¹‰à¸£à¸²à¸¢à¸‡à¸²à¸™" -forwardReport: "ส่งต่à¸à¸£à¸²à¸¢à¸‡à¸²à¸™à¹„ปยังà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" -forwardReportIsAnonymous: "ข้à¸à¸¡à¸¹à¸¥à¸‚à¸à¸‡à¸„ุณจะไม่ปราà¸à¸à¸šà¸™à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹à¸¥à¸°à¸›à¸£à¸²à¸à¸à¹€à¸›à¹‡à¸™à¸šà¸±à¸à¸Šà¸µà¸£à¸°à¸šà¸šà¸—ี่ไม่ระบุชื่à¸" +forwardReport: "ส่งต่à¸à¸£à¸²à¸¢à¸‡à¸²à¸™à¹„ปยังเซิร์ฟเวà¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" +forwardReportIsAnonymous: "ข้à¸à¸¡à¸¹à¸¥à¸‚à¸à¸‡à¸„ุณจะไม่ปราà¸à¸à¸šà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹à¸¥à¸°à¸›à¸£à¸²à¸à¸à¹€à¸›à¹‡à¸™à¸šà¸±à¸à¸Šà¸µà¸£à¸°à¸šà¸šà¸—ี่ไม่ระบุชื่à¸" send: "ส่ง" abuseMarkAsResolved: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸£à¸²à¸¢à¸‡à¸²à¸™à¸§à¹ˆà¸²à¹à¸à¹‰à¹„ขà¹à¸¥à¹‰à¸§" openInNewTab: "เปิดในà¹à¸—็บใหม่" openInSideView: "เปิดในมุมมà¸à¸‡à¸”้านข้าง" defaultNavigationBehaviour: "พฤติà¸à¸£à¸£à¸¡à¸à¸²à¸£à¸™à¸³à¸—างที่เป็นค่าเริ่มต้น" editTheseSettingsMayBreakAccount: "à¸à¸²à¸£à¹à¸à¹‰à¹„ขà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าเหล่านี้à¸à¸²à¸ˆà¸—ำให้บัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณเสียหายนะ" -instanceTicker: "ข้à¸à¸¡à¸¹à¸¥à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸‚à¸à¸‡à¹‚น้ต" +instanceTicker: "ข้à¸à¸¡à¸¹à¸¥à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸‚à¸à¸‡à¹‚น้ต" waitingFor: "à¸à¸³à¸¥à¸±à¸‡à¸£à¸ {x}" random: "สุ่มค่า" system: "ระบบ" @@ -739,7 +755,7 @@ alwaysMarkSensitive: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸§à¹ˆà¸²à¸¡à¸µà¹€ loadRawImages: "โหลดภาพต้นฉบับà¹à¸—นà¸à¸²à¸£à¹à¸ªà¸”งภาพขนาดย่à¸" disableShowingAnimatedImages: "ไม่ต้à¸à¸‡à¹€à¸¥à¹ˆà¸™à¸ าพเคลื่à¸à¸™à¹„หว" highlightSensitiveMedia: "ไฮไลท์สื่à¸à¸—ี่มีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" -verificationEmailSent: "ส่งà¸à¸µà¹€à¸¡à¸¥à¸¢à¸·à¸™à¸¢à¸±à¸™à¹à¸¥à¹‰à¸§à¸™à¸° ได้โปรดà¸à¸£à¸¸à¸“าไปที่ลิงà¸à¹Œà¸—ี่รวมไว้เพื่à¸à¸—ำà¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¹ƒà¸«à¹‰à¹€à¸ªà¸£à¹‡à¸ˆà¸ªà¸´à¹‰à¸™" +verificationEmailSent: "ได้ส่งà¸à¸µà¹€à¸¡à¸¥à¸¢à¸·à¸™à¸¢à¸±à¸™à¹à¸¥à¹‰à¸§ à¸à¸£à¸¸à¸“าเข้าลิงà¸à¹Œà¸—ี่ระบุในà¸à¸µà¹€à¸¡à¸¥à¹€à¸žà¸·à¹ˆà¸à¸—ำà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าให้เสร็จสิ้น" notSet: "ไม่ได้ตั้งค่า" emailVerified: "à¸à¸µà¹€à¸¡à¸¥à¹„ด้รับà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¹à¸¥à¹‰à¸§" noteFavoritesCount: "จำนวนโน้ตที่ชื่นชà¸à¸š" @@ -750,7 +766,7 @@ useSystemFont: "ใช้ฟà¸à¸™à¸•à¹Œà¹€à¸£à¸´à¹ˆà¸¡à¸•à¹‰à¸™à¸‚à¸à¸‡à¸£à¸° clips: "คลิป" experimentalFeatures: "ฟังà¸à¹Œà¸Šà¸±à¹ˆà¸™à¸—ดสà¸à¸š" experimental: "ทดลà¸à¸‡" -thisIsExperimentalFeature: "นี่คืà¸à¸Ÿà¸µà¹€à¸ˆà¸à¸£à¹Œà¸—ดลà¸à¸‡à¸™à¸°à¸„่ะ ฟังà¸à¹Œà¸Šà¸±à¸™à¸à¸²à¸£à¸—ำงานบางà¸à¸¢à¹ˆà¸²à¸‡à¸à¸²à¸ˆà¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¹„ด้ à¹à¸¥à¸°à¸à¸²à¸ˆà¹„ม่ทำงานหรืà¸à¹„ม่เสถียรตามที่ตั้งใจไว้นะ" +thisIsExperimentalFeature: "นี่เป็นฟีเจà¸à¸£à¹Œà¸—ดลà¸à¸‡ ซึ่งà¸à¸²à¸ˆà¸¡à¸µà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸à¸²à¸£à¸—ำงาน à¹à¸¥à¸°à¸à¸²à¸ˆà¹„ม่ทำงานตามที่ตั้งใจไว้" developer: "สำหรับนัà¸à¸žà¸±à¸’นา" makeExplorable: "ทำให้บัà¸à¸Šà¸µà¸¡à¸à¸‡à¹€à¸«à¹‡à¸™à¹ƒà¸™ “สำรวจâ€" makeExplorableDescription: "ถ้าหาà¸à¸„ุณปิดà¸à¸²à¸£à¸—ำงานนี้ บัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณนั้นจะไม่à¹à¸ªà¸”งในส่วน “สำรวจâ€" @@ -761,14 +777,14 @@ center: "à¸à¸¶à¹ˆà¸‡à¸à¸¥à¸²à¸‡" wide: "à¸à¸§à¹‰à¸²à¸‡" narrow: "ชิด" reloadToApplySetting: "à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่านี้จะมีผลหลังจาà¸à¹‚หลดหน้าซ้ำเท่านั้น ต้à¸à¸‡à¸à¸²à¸£à¸—ี่จะโหลดใหม่เลยไหม?" -needReloadToApply: "จำเป็นต้à¸à¸‡à¹‚หลดซ้ำถึงจะมีผลนะ" +needReloadToApply: "ต้à¸à¸‡à¸£à¸µà¹‚หลดเพื่à¸à¹ƒà¸«à¹‰à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸¡à¸µà¸œà¸¥" showTitlebar: "à¹à¸ªà¸”งà¹à¸–บชื่à¸" clearCache: "ล้างà¹à¸„ช" -onlineUsersCount: "{n} ผู้ใช้คนนี้à¸à¸³à¸¥à¸±à¸‡à¸à¸à¸™à¹„ลน์" +onlineUsersCount: "{n} รายà¸à¸³à¸¥à¸±à¸‡à¸à¸à¸™à¹„ลน์" nUsers: "{n} ผู้ใช้งาน" nNotes: "{n} โน้ต" -sendErrorReports: "ส่งรายงานว่าข้à¸à¸œà¸´à¸”พลาด" -sendErrorReportsDescription: "เมื่à¸à¹€à¸›à¸´à¸”ใช้งาน ข้à¸à¸¡à¸¹à¸¥à¸‚้à¸à¸œà¸´à¸”พลาดโดยรายละเà¸à¸µà¸¢à¸”นั้นจะถูà¸à¹à¸Šà¸£à¹Œà¹ƒà¸«à¹‰à¸à¸±à¸š Misskey เมื่à¸à¹€à¸à¸´à¸”ปัà¸à¸«à¸² ซึ่งช่วยปรับปรุงคุณภาพขà¸à¸‡ Misskey\nซึ่งจะรวมถึงข้à¸à¸¡à¸¹à¸¥ เช่น เวà¸à¸£à¹Œà¸Šà¸±à¹ˆà¸™à¸‚à¸à¸‡à¸£à¸°à¸šà¸šà¸›à¸à¸´à¸šà¸±à¸•à¸´à¸à¸²à¸£ เบราว์เซà¸à¸£à¹Œà¸—ี่คุณใช้ à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸‚à¸à¸‡à¸„ุณใน Misskey เป็นต้น" +sendErrorReports: "ส่งรายงานข้à¸à¸œà¸´à¸”พลาด" +sendErrorReportsDescription: "เมื่à¸à¹€à¸›à¸´à¸”ใช้งาน à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¸‚้à¸à¸œà¸´à¸”พลาดจะถูà¸à¹à¸Šà¸£à¹Œà¸à¸±à¸š Misskey เมื่à¸à¹€à¸à¸´à¸”ปัà¸à¸«à¸² ซึ่งช่วยในà¸à¸²à¸£à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¸„ุณภาพขà¸à¸‡à¸‹à¸à¸Ÿà¸•à¹Œà¹à¸§à¸£à¹Œ ข้à¸à¸¡à¸¹à¸¥à¸‚้à¸à¸œà¸´à¸”พลาดà¸à¸²à¸ˆà¸£à¸§à¸¡à¸–ึงเวà¸à¸£à¹Œà¸Šà¸±à¸™à¸‚à¸à¸‡à¸£à¸°à¸šà¸šà¸›à¸à¸´à¸šà¸±à¸•à¸´à¸à¸²à¸£ ประเภทขà¸à¸‡à¹€à¸šà¸£à¸²à¸§à¹Œà¹€à¸‹à¸à¸£à¹Œ à¹à¸¥à¸°à¸›à¸£à¸°à¸§à¸±à¸•à¸´à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ ฯลฯ" myTheme: "ธีมขà¸à¸‡à¸‰à¸±à¸™" backgroundColor: "สีพื้นหลัง" accentColor: "สีหลัà¸" @@ -780,7 +796,7 @@ value: "ค่า" createdAt: "สร้างเมื่à¸" updatedAt: "à¸à¸±à¸›à¹€à¸”ตล่าสุด" saveConfirm: "บันทึà¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸¡à¸±à¹‰à¸¢?" -deleteConfirm: "ลบจริงๆเหรà¸?" +deleteConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¹ƒà¸Šà¹ˆà¹„หม?" invalidValue: "ค่านี้ไม่ถูà¸à¸•à¹‰à¸à¸‡" registry: "ทะเบียน" closeAccount: "ปิด บัà¸à¸Šà¸µ" @@ -793,7 +809,7 @@ capacity: "ความจุ" inUse: "ใช้à¹à¸¥à¹‰à¸§" editCode: "à¹à¸à¹‰à¹„ขโค้ด" apply: "นำไปใช้" -receiveAnnouncementFromInstance: "รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸ˆà¸²à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰" +receiveAnnouncementFromInstance: "รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰" emailNotification: "à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸—างà¸à¸µà¹€à¸¡à¸¥" publish: "เผยà¹à¸žà¸£à¹ˆ" inChannelSearch: "ค้นหาในช่à¸à¸‡" @@ -821,7 +837,7 @@ active: "ใช้งานà¸à¸¢à¸¹à¹ˆ" offline: "à¸à¸à¸Ÿà¹„ลน์" notRecommended: "ไม่à¹à¸™à¸°à¸™à¸³" botProtection: "à¸à¸²à¸£à¸›à¹‰à¸à¸‡à¸à¸±à¸™ Bot" -instanceBlocking: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" +instanceBlocking: "เซิร์ฟเวà¸à¸£à¹Œà¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸/ปิดปาà¸" selectAccount: "เลืà¸à¸à¸šà¸±à¸à¸Šà¸µ" switchAccount: "สลับบัà¸à¸Šà¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" enabled: "เปิดใช้งาน" @@ -831,9 +847,10 @@ user: "ผู้ใช้" administration: "à¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£" accounts: "บัà¸à¸Šà¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" switch: "สลับ" -noMaintainerInformationWarning: "ข้à¸à¸¡à¸¹à¸¥à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¹„ม่ได้รับà¸à¸²à¸£à¸à¸³à¸«à¸™à¸”ค่านะ" -noBotProtectionWarning: "ไม่ได้à¸à¸³à¸«à¸™à¸”ค่าà¸à¸²à¸£à¸›à¹‰à¸à¸‡à¸à¸±à¸™à¸šà¸à¸—นะ" -configure: "à¸à¸³à¸«à¸™à¸”ค่า" +noMaintainerInformationWarning: "ยังไม่ได้ตั้งค่าข้à¸à¸¡à¸¹à¸¥à¸‚à¸à¸‡à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸š" +noInquiryUrlWarning: "ยังไม่ได้ตั้งค่า URL สำหรับà¸à¸²à¸£à¸•à¸´à¸”ต่à¸à¸ªà¸à¸šà¸–าม" +noBotProtectionWarning: "ยังไม่ได้ตั้งค่าà¸à¸²à¸£à¸›à¹‰à¸à¸‡à¸à¸±à¸™à¸šà¸à¸•" +configure: "ตั้งค่า" postToGallery: "สร้างโพสต์à¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¹ˆà¹ƒà¸«à¸¡à¹ˆ" postToHashtag: "โพสต์ไปที่à¹à¸®à¸Šà¹à¸—็à¸à¸™à¸µà¹‰" gallery: "à¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¹ˆ" @@ -870,7 +887,7 @@ accountDeletionInProgress: "à¸à¸³à¸¥à¸±à¸‡à¸”ำเนินà¸à¸²à¸£à¸¥à¸šà¸š usernameInfo: "ชื่à¸à¸—ี่ระบุบัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณจาà¸à¸œà¸¹à¹‰à¸à¸·à¹ˆà¸™à¹ƒà¸™à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰ คุณสามารถใช้ตัวà¸à¸±à¸à¸©à¸£ (a~z, A~Z), ตัวเลข (0~9) หรืà¸à¸‚ีดล่าง (_) ชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹„ม่สามารถเปลี่ยนà¹à¸›à¸¥à¸‡à¹„ด้ในภายหลัง" aiChanMode: "โหมด Ai " devMode: "โหมดนัà¸à¸žà¸±à¸’นา" -keepCw: "เà¸à¹‡à¸šà¸„ำเตืà¸à¸™à¹€à¸™à¸·à¹‰à¸à¸«à¸²" +keepCw: "คงà¸à¸²à¸£à¹€à¸•à¸·à¸à¸™à¹€à¸™à¸·à¹‰à¸à¸«à¸²à¹„ว้" pubSub: "บัà¸à¸Šà¸µ Pub/Sub" lastCommunication: "à¸à¸²à¸£à¸ªà¸·à¹ˆà¸à¸ªà¸²à¸£à¸„รั้งสุดท้ายล่าสุด" resolved: "คลี่คลายà¹à¸¥à¹‰à¸§" @@ -909,8 +926,8 @@ themeColor: "สีธีม" size: "ขนาด" numberOfColumn: "จำนวนคà¸à¸¥à¸±à¸¡à¸™à¹Œ" searchByGoogle: "ค้นหา" -instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" -instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" +instanceDefaultLightTheme: "ธีมสว่างตามค่าเริ่มต้นขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" +instanceDefaultDarkTheme: "ธีมมืดตามค่าเริ่มต้นขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" instanceDefaultThemeDescription: "ป้à¸à¸™à¸£à¸«à¸±à¸ªà¸˜à¸µà¸¡à¹ƒà¸™à¸£à¸¹à¸›à¹à¸šà¸šà¸à¸à¸šà¹€à¸ˆà¹‡à¸à¸•à¹Œ" mutePeriod: "ระยะเวลาปิดเสียง" period: "ระยะเวลา" @@ -930,7 +947,7 @@ cropNo: "ใช้ตามที่เป็นà¸à¸¢à¸¹à¹ˆ" file: "ไฟล์" recentNHours: "ล่าสุด {n} ชั่วโมงที่à¹à¸¥à¹‰à¸§" recentNDays: "ล่าสุด {n} วันที่à¹à¸¥à¹‰à¸§" -noEmailServerWarning: "ไม่ได้à¸à¸³à¸«à¸™à¸”ค่าเซิร์ฟเวà¸à¸£à¹Œà¸à¸µà¹€à¸¡à¸¥à¸™à¸µà¹‰" +noEmailServerWarning: "ยังไม่ได้ตั้งค่าเซิร์ฟเวà¸à¸£à¹Œà¸‚à¸à¸‡à¸à¸µà¹€à¸¡à¸¥" thereIsUnresolvedAbuseReportWarning: "มีรายงานที่ยังไม่ได้à¹à¸à¹‰à¹„ข" recommended: "à¹à¸™à¸°à¸™à¸³" check: "ตรวจสà¸à¸š" @@ -965,7 +982,7 @@ cannotUploadBecauseExceedsFileSizeLimit: "ไม่สามารถà¸à¸±à¸› beta: "เบต้า" enableAutoSensitive: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸§à¹ˆà¸²à¸¡à¸µà¹€à¸™à¸·à¹‰à¸à¸«à¸²à¸—ี่ละเà¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™à¹‚ดยà¸à¸±à¸•à¹‚นมัติ" enableAutoSensitiveDescription: "à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¸•à¸£à¸§à¸ˆà¸«à¸²à¹à¸¥à¸°à¸—ำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸ªà¸·à¹ˆà¸à¸§à¹ˆà¸²à¸¡à¸µà¹€à¸™à¸·à¹‰à¸à¸«à¸²à¹‚ดยละเà¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™à¹‚ดยà¸à¸±à¸•à¹‚นมัติ ผ่าน Machine Learning หาà¸à¹€à¸›à¹‡à¸™à¹„ปได้ à¹à¸¡à¹‰à¸§à¹ˆà¸²à¸„ุณจะปิดคุณสมบัตินี้ à¸à¹‡à¸à¸²à¸ˆà¸–ูà¸à¸•à¸±à¹‰à¸‡à¸„่าโดยà¸à¸±à¸•à¹‚นมัติ ทั้งนี้ขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" -activeEmailValidationDescription: "เปิดใช้งานà¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸—ี่à¸à¸¢à¸¹à¹ˆà¸à¸µà¹€à¸¡à¸¥à¹ƒà¸«à¹‰à¸¡à¸µà¸„วามเข้มงวดยิ่งขึ้น ซึ่งà¸à¸²à¸ˆà¸ˆà¸°à¸£à¸§à¸¡à¹„ปถึงà¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸—ี่à¸à¸¢à¸¹à¹ˆà¸à¸µà¹€à¸¡à¸¥à¹Œà¸—ี่ใช้à¹à¸¥à¹‰à¸§à¸—ิ้งà¹à¸¥à¸°à¹‚ดยให้พิจารณาว่าสามารถสื่à¸à¸ªà¸²à¸£à¸”้วยได้หรืà¸à¹„ม่ เมื่à¸à¹„ม่เลืà¸à¸à¸£à¸°à¸šà¸šà¸ˆà¸°à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¹€à¸‰à¸žà¸²à¸°à¸£à¸¹à¸›à¹à¸šà¸šà¸‚à¸à¸‡à¸à¸µà¹€à¸¡à¸¥à¹€à¸—่านั้น" +activeEmailValidationDescription: "à¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸à¸µà¹€à¸¡à¸¥à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸ˆà¸°à¹€à¸‚้มงวดมาà¸à¸‚ึ้น โดยพิจารณาว่าเป็นà¸à¸µà¹€à¸¡à¸¥à¸Šà¸±à¹ˆà¸§à¸„ราวหรืà¸à¹„ม่ à¹à¸¥à¸°à¸ªà¸²à¸¡à¸²à¸£à¸–ติดต่à¸à¹„ด้จริงหรืà¸à¹„ม่ หาà¸à¸›à¸´à¸”à¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸™à¸µà¹‰ จะตรวจสà¸à¸šà¹€à¸žà¸µà¸¢à¸‡à¸§à¹ˆà¸²à¸£à¸¹à¸›à¹à¸šà¸šà¸à¸µà¹€à¸¡à¸¥à¸—ี่ถูà¸à¸•à¹‰à¸à¸‡à¸«à¸£à¸·à¸à¹„ม่เท่านั้น" navbar: "à¹à¸–บนำทาง" shuffle: "สลับ" account: "บัà¸à¸Šà¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" @@ -974,7 +991,7 @@ pushNotification: "à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Š" subscribePushNotification: "เปิดà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Š" unsubscribePushNotification: "ปิดà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Š" pushNotificationAlreadySubscribed: "à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Šà¹„ด้เปิดใช้งานà¹à¸¥à¹‰à¸§" -pushNotificationNotSupported: "เบราว์เซà¸à¸£à¹Œà¸«à¸£à¸·à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸‚à¸à¸‡à¸„ุณนั้นไม่รà¸à¸‡à¸£à¸±à¸šà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Š" +pushNotificationNotSupported: "เบราว์เซà¸à¸£à¹Œà¸«à¸£à¸·à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¹„ม่รà¸à¸‡à¸£à¸±à¸šà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Š" sendPushNotificationReadMessage: "ลบà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Šà¹€à¸¡à¸·à¹ˆà¸à¸à¹ˆà¸²à¸™à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸«à¸£à¸·à¸à¸‚้à¸à¸„วามที่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้à¸à¸‡à¹à¸¥à¹‰à¸§" sendPushNotificationReadMessageCaption: "à¸à¸²à¸ˆà¸—ำให้à¸à¸¸à¸›à¸à¸£à¸“์ขà¸à¸‡à¸„ุณใช้พลังงานมาà¸à¸‚ึ้น" windowMaximize: "ขยายใหà¸à¹ˆà¸ªà¸¸à¸”" @@ -1017,36 +1034,37 @@ achievements: "ความสำเร็จ" gotInvalidResponseError: "à¸à¸²à¸£à¸•à¸à¸šà¸ªà¸™à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¹„ม่ถูà¸à¸•à¹‰à¸à¸‡" gotInvalidResponseErrorDescription: "เซิร์ฟเวà¸à¸£à¹Œà¸à¸²à¸ˆà¹„ม่สามารถเข้าถึงได้หรืà¸à¸à¸²à¸ˆà¸ˆà¸°à¸à¸³à¸¥à¸±à¸‡à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸°à¸«à¸§à¹ˆà¸²à¸‡à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡ à¸à¸£à¸¸à¸“าลà¸à¸‡à¹ƒà¸«à¸¡à¹ˆà¸à¸µà¸à¸„รั้งในภายหลังนะคะ" thisPostMayBeAnnoying: "โน้ตนี้à¸à¸²à¸ˆà¸ˆà¸°à¹€à¸›à¹‡à¸™à¸à¸²à¸£à¸£à¸šà¸à¸§à¸™à¸œà¸¹à¹‰à¸à¸·à¹ˆà¸™à¸™à¸°à¸„ะ" -thisPostMayBeAnnoyingHome: "โพสต์ไปยังไทม์ไลน์หน้าà¹à¸£à¸" -thisPostMayBeAnnoyingCancel: "เลิà¸" -thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงà¸à¹‡à¹à¸¥à¹‰à¸§à¹à¸•à¹ˆ" +thisPostMayBeAnnoyingHome: "โพสต์ลงไทม์ไลน์หลัà¸à¹€à¸—่านั้น" +thisPostMayBeAnnoyingCancel: "ยà¸à¹€à¸¥à¸´à¸" +thisPostMayBeAnnoyingIgnore: "โพสต์ไปเลย ไม่ต้à¸à¸‡à¸›à¸£à¸±à¸šà¸à¸²à¸£à¸¡à¸à¸‡à¹€à¸«à¹‡à¸™" collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นà¹à¸¥à¹‰à¸§" +collapseRenotesDescription: "พับย่à¸à¹‚น้ตที่เคยตà¸à¸šà¸ªà¸™à¸à¸‡à¸«à¸£à¸·à¸à¸£à¸µà¹‚น้ตà¹à¸¥à¹‰à¸§" internalServerError: "เซิร์ฟเวà¸à¸£à¹Œà¸ ายในเà¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาด" -internalServerErrorDescription: "เซิร์ฟเวà¸à¸£à¹Œà¸£à¸±à¸™à¸„้นพบข้à¸à¸œà¸´à¸”พลาดที่ไม่คาดคิด" +internalServerErrorDescription: "เà¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาดที่ไม่คาดคิดภายในเซิร์ฟเวà¸à¸£à¹Œ" copyErrorInfo: "คัดลà¸à¸à¸£à¸²à¸¢à¸¥à¸°à¹€à¸à¸µà¸¢à¸”ข้à¸à¸œà¸´à¸”พลาด" -joinThisServer: "ลงชื่à¸à¸ªà¸¡à¸±à¸„รใช้ในà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰" -exploreOtherServers: "มà¸à¸‡à¸«à¸²à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸à¸·à¹ˆà¸™" +joinThisServer: "ลงทะเบียนในเซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰" +exploreOtherServers: "มà¸à¸‡à¸«à¸²à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸à¸·à¹ˆà¸™" letsLookAtTimeline: "มาดูไทม์ไลน์à¸à¸±à¸™" -disableFederationConfirm: "ปิดใช้งานสหพันธ์จริงๆหรà¸à¹à¸™à¹ˆà¹ƒà¸ˆà¹à¸¥à¹‰à¸§à¸™à¸°?" +disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?" disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่à¸à¹„ป เว้นà¹à¸•à¹ˆà¸ˆà¸°à¸•à¸±à¹‰à¸‡à¸„่าเป็นà¸à¸¢à¹ˆà¸²à¸‡à¸à¸·à¹ˆà¸™" disableFederationOk: "ปิดà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" -invitationRequiredToRegister: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹€à¸›à¹‡à¸™à¹à¸šà¸šà¸£à¸±à¸šà¹€à¸Šà¸´à¸ เฉพาะผู้ที่มีรหัสเชิà¸à¹€à¸—่านั้นที่สามารถลงทะเบียนได้" -emailNotSupported: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹„ม่รà¸à¸‡à¸£à¸±à¸šà¸à¸²à¸£à¸ªà¹ˆà¸‡à¸à¸µà¹€à¸¡à¸¥" +invitationRequiredToRegister: "เซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰à¹€à¸›à¹‡à¸™à¹à¸šà¸šà¸£à¸±à¸šà¹€à¸Šà¸´à¸ เฉพาะผู้มีรหัสเชิà¸à¹€à¸—่านั้นถึงสามารถลงทะเบียนได้" +emailNotSupported: "เซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰à¹„ม่รà¸à¸‡à¸£à¸±à¸šà¸à¸²à¸£à¸ªà¹ˆà¸‡à¸à¸µà¹€à¸¡à¸¥" postToTheChannel: "โพสต์ลงช่à¸à¸‡" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนà¹à¸›à¸¥à¸‡à¹„ด้ในภายหลังนะ" reactionAcceptance: "à¸à¸²à¸£à¸¢à¸à¸¡à¸£à¸±à¸šà¸£à¸µà¹à¸à¸„ชั่น" likeOnly: "ที่ถูà¸à¹ƒà¸ˆà¹€à¸—่านั้น" -likeOnlyForRemote: "ทั้งหมด (เฉพาะà¸à¸²à¸£à¸–ูà¸à¹ƒà¸ˆà¸ˆà¸²à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥)" +likeOnlyForRemote: "ทั้งหมด (เฉพาะà¸à¸²à¸£à¸–ูà¸à¹ƒà¸ˆà¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥)" nonSensitiveOnly: "เฉพาะไม่มีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" nonSensitiveOnlyForLocalLikeOnlyForRemote: "เฉพาะไม่มีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™ (เฉพาะà¸à¸²à¸£à¸–ูà¸à¹ƒà¸ˆà¸ˆà¸²à¸à¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹€à¸—่านั้น)" rolesAssignedToMe: "บทบาทที่ได้รับมà¸à¸šà¸«à¸¡à¸²à¸¢à¹ƒà¸«à¹‰à¸‰à¸±à¸™" -resetPasswordConfirm: "รีเซ็ตรหัสผ่านขà¸à¸‡à¸„ุณจริงๆหรà¸?" +resetPasswordConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸£à¸µà¹€à¸‹à¹‡à¸•à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™à¹ƒà¸Šà¹ˆà¹„หม?" sensitiveWords: "คำที่มีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" -sensitiveWordsDescription: "à¸à¸²à¸£à¹€à¸›à¸´à¸”เผยโน้ตทั้งหมดที่มีคำที่à¸à¸³à¸«à¸™à¸”ค่าไว้จะถูà¸à¸•à¸±à¹‰à¸‡à¸„่าเป็น \"หน้าà¹à¸£à¸\" โดยà¸à¸±à¸•à¹‚นมัติ คุณยังสามารถà¹à¸ªà¸”งหลายรายà¸à¸²à¸£à¹„ด้โดยà¹à¸¢à¸à¸£à¸²à¸¢à¸à¸²à¸£à¹‚ดยใช้ตัวà¹à¸šà¹ˆà¸‡à¸šà¸£à¸£à¸—ัดได้นะ" -sensitiveWordsDescription2: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸Šà¹ˆà¸à¸‡à¸§à¹ˆà¸²à¸‡à¸™à¸±à¹‰à¸™à¸à¸²à¸ˆà¸ˆà¸°à¸ªà¸£à¹‰à¸²à¸‡à¸™à¸´à¸žà¸ˆà¸™à¹Œ AND à¹à¸¥à¸°à¸„ำหลัà¸à¸—ี่มีเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸—ับล้à¸à¸¡à¸£à¸à¸šà¸ˆà¸°à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹€à¸›à¹‡à¸™à¸™à¸´à¸žà¸ˆà¸™à¹Œà¸—ั่วไปนะ" +sensitiveWordsDescription: "โน้ตที่มีคำที่ระบุไว้จะถูà¸à¸•à¸±à¹‰à¸‡à¸„่าà¸à¸²à¸£à¸¡à¸à¸‡à¹€à¸«à¹‡à¸™à¸‚à¸à¸‡à¹ƒà¸«à¹‰à¹à¸ªà¸”งเฉพาะในหน้าหลัà¸à¹€à¸—่านั้น คั่นคำด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่" +sensitiveWordsDescription2: "ถ้าà¹à¸¢à¸à¸”้วยเว้นวรรคจะเป็นà¸à¸²à¸£à¸£à¸°à¸šà¸¸ AND à¹à¸¥à¸°à¸–้าล้à¸à¸¡à¸„ำด้วยสà¹à¸¥à¸Š (/) จะเป็นà¸à¸²à¸£à¹ƒà¸Šà¹‰ regular expression" prohibitedWords: "คำต้à¸à¸‡à¸«à¹‰à¸²à¸¡" prohibitedWordsDescription: "จะà¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸§à¹ˆà¸²à¹€à¸à¸´à¸”ข้à¸à¸œà¸´à¸”พลาดเมื่à¸à¸žà¸¢à¸²à¸¢à¸²à¸¡à¹‚พสต์โน้ตที่มีคำที่à¸à¸³à¸«à¸™à¸”ไว้ สามารถตั้งได้หลายคำด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่" -prohibitedWordsDescription2: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸Šà¹ˆà¸à¸‡à¸§à¹ˆà¸²à¸‡à¸™à¸±à¹‰à¸™à¸à¸²à¸ˆà¸ˆà¸°à¸ªà¸£à¹‰à¸²à¸‡à¸™à¸´à¸žà¸ˆà¸™à¹Œ AND à¹à¸¥à¸°à¸„ำหลัà¸à¸—ี่มีเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸—ับล้à¸à¸¡à¸£à¸à¸šà¸ˆà¸°à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹€à¸›à¹‡à¸™à¸™à¸´à¸žà¸ˆà¸™à¹Œà¸—ั่วไปนะ" +prohibitedWordsDescription2: "ถ้าà¹à¸¢à¸à¸”้วยเว้นวรรคจะเป็นà¸à¸²à¸£à¸£à¸°à¸šà¸¸ AND à¹à¸¥à¸°à¸–้าล้à¸à¸¡à¸„ำด้วยสà¹à¸¥à¸Š (/) จะเป็นà¸à¸²à¸£à¹ƒà¸Šà¹‰ regular expression" hiddenTags: "à¹à¸®à¸Šà¹à¸—็à¸à¸—ี่ซ่à¸à¸™à¸à¸¢à¸¹à¹ˆ" hiddenTagsDescription: "เลืà¸à¸à¹à¸—็à¸à¸—ี่จะไม่à¹à¸ªà¸”งในรายà¸à¸²à¸£à¹€à¸—รนด์ สามารถลงทะเบียนหลายà¹à¸—็à¸à¹„ด้โดยขึ้นบรรทัดใหม่" notesSearchNotAvailable: "à¸à¸²à¸£à¸„้นหาโน้ตไม่พร้à¸à¸¡à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" @@ -1058,7 +1076,7 @@ retryAllQueuesNow: "ลà¸à¸‡à¹€à¸£à¸µà¸¢à¸à¹ƒà¸Šà¹‰à¸„ิวทั้งหม retryAllQueuesConfirmTitle: "ลà¸à¸‡à¹ƒà¸«à¸¡à¹ˆà¸—ั้งหมดจริงๆหรà¸à¹à¸™à¹ˆà¹ƒà¸ˆà¸™à¸°?" retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มà¸à¸²à¸£à¹‚หลดเซิร์ฟเวà¸à¸£à¹Œà¸Šà¸±à¹ˆà¸§à¸„ราวนะ" enableChartsForRemoteUser: "สร้างà¹à¸œà¸™à¸ ูมิข้à¸à¸¡à¸¹à¸¥à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸£à¸°à¸¢à¸°à¹„à¸à¸¥" -enableChartsForFederatedInstances: "สร้างà¹à¸œà¸™à¸ ูมิข้à¸à¸¡à¸¹à¸¥à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" +enableChartsForFederatedInstances: "สร้างà¹à¸œà¸™à¸ ูมิขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" showClipButtonInNoteFooter: "เพิ่ม “คลิป†ไปยังเมนูสั่งà¸à¸²à¸£à¸‚à¸à¸‡à¹‚น้ต" reactionsDisplaySize: "ขนาดขà¸à¸‡à¸£à¸µà¹à¸à¸„ชั่น" limitWidthOfReaction: "จำà¸à¸±à¸”ความà¸à¸§à¹‰à¸²à¸‡à¸ªà¸¹à¸‡à¸ªà¸¸à¸”ขà¸à¸‡à¸£à¸µà¹à¸à¸„ชั่นà¹à¸¥à¸°à¹à¸ªà¸”งให้เล็à¸à¸¥à¸‡" @@ -1077,7 +1095,7 @@ addMemo: "เพิ่มเมโม" editMemo: "à¹à¸à¹‰à¹„ขเมโม" reactionsList: "รายà¸à¸²à¸£à¸£à¸µà¹à¸à¸„ชั่น" renotesList: "รายà¸à¸²à¸£à¸£à¸µà¹‚น้ต" -notificationDisplay: "à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" +notificationDisplay: "à¸à¸²à¸£à¹à¸ªà¸”งà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" leftTop: "บนซ้าย" rightTop: "บนขวา" leftBottom: "ล่างซ้าย" @@ -1087,13 +1105,15 @@ vertical: "à¹à¸™à¸§à¸•à¸±à¹‰à¸‡" horizontal: "à¹à¸™à¸§à¸™à¸à¸™" position: "ตำà¹à¸«à¸™à¹ˆà¸‡" serverRules: "à¸à¸Žà¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" -pleaseConfirmBelowBeforeSignup: "โปรดยืนยันที่ด้านล่างà¸à¹ˆà¸à¸™à¸ªà¸¡à¸±à¸„รใช้งาน" +pleaseConfirmBelowBeforeSignup: "หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¸¥à¸‡à¸—ะเบียนในเซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰ คุณต้à¸à¸‡à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¹à¸¥à¸°à¸¢à¸à¸¡à¸£à¸±à¸šà¸ªà¸´à¹ˆà¸‡à¸•à¹ˆà¸à¹„ปนี้" pleaseAgreeAllToContinue: "คุณต้à¸à¸‡à¸¢à¸à¸¡à¸£à¸±à¸šà¸—ุà¸à¸Šà¹ˆà¸à¸‡à¸•à¸£à¸‡à¸”้านบนเพื่à¸à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸à¸„่ะ" continue: "ดำเนินà¸à¸²à¸£à¸•à¹ˆà¸" preservedUsernames: "ชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่สงวนไว้" preservedUsernamesDescription: "ระบุชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่จะสงวนชื่à¸à¹„ว้ คั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่ ชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ระบุที่นี่จะไม่สามารถใช้งานได้à¸à¸µà¸à¸•à¹ˆà¸à¹„ปเมื่à¸à¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µà¹ƒà¸«à¸¡à¹ˆ ยà¸à¹€à¸§à¹‰à¸™à¹€à¸¡à¸·à¹ˆà¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µ นà¸à¸à¸ˆà¸²à¸à¸™à¸µà¹‰ บัà¸à¸Šà¸µà¸—ี่มีà¸à¸¢à¸¹à¹ˆà¹à¸¥à¹‰à¸§à¸ˆà¸°à¹„ม่ได้รับผลà¸à¸£à¸°à¸—บ" createNoteFromTheFile: "เรียบเรียงโน้ตจาà¸à¹„ฟล์นี้" archive: "เà¸à¹‡à¸šà¸–าวร" +archived: "เà¸à¹‡à¸šà¸–าวรà¹à¸¥à¹‰à¸§" +unarchive: "เลิà¸à¸à¸²à¸£à¹€à¸à¹‡à¸šà¸–าวร" channelArchiveConfirmTitle: "ต้à¸à¸‡à¸à¸²à¸£à¹€à¸à¹‡à¸šà¸–าวรเจ้า {name} ใช่ไหม?" channelArchiveConfirmDescription: "เมื่à¸à¹€à¸à¹‡à¸šà¸–าวรà¹à¸¥à¹‰à¸§ จะไม่ปราà¸à¸à¹ƒà¸™à¸£à¸²à¸¢à¸à¸²à¸£à¸Šà¹ˆà¸à¸‡à¸«à¸£à¸·à¸à¸œà¸¥à¸à¸²à¸£à¸„้นหาà¸à¸µà¸à¸•à¹ˆà¸à¹„ป à¹à¸¥à¸°à¸ˆà¸°à¹„ม่สามารถโพสต์ใหม่ได้à¸à¸µà¸à¸•à¹ˆà¸à¹„ป" thisChannelArchived: "ช่à¸à¸‡à¸™à¸µà¹‰à¸–ูà¸à¹€à¸à¹‡à¸šà¸–าวรà¹à¸¥à¹‰à¸§à¸™à¸°" @@ -1104,6 +1124,9 @@ preventAiLearning: "ปà¸à¸´à¹€à¸ªà¸˜à¸à¸²à¸£à¹€à¸£à¸µà¸¢à¸™à¸£à¸¹à¹‰à¸”้ว preventAiLearningDescription: "ส่งคำร้à¸à¸‡à¸‚à¸à¹„ม่ให้ใช้ ข้à¸à¸„วามในโน้ตที่โพสต์, หรืà¸à¹€à¸™à¸·à¹‰à¸à¸«à¸²à¸£à¸¹à¸›à¸ าพ ฯลฯ ในà¸à¸²à¸£à¹€à¸£à¸µà¸¢à¸™à¸£à¸¹à¹‰à¸‚à¸à¸‡à¹€à¸„รื่à¸à¸‡(machine learning) / Predictive AI / Generative AI โดยà¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¹à¸Ÿà¸¥à¹‡à¸ “noai†ลง HTML-Response ให้à¸à¸±à¸šà¹€à¸™à¸·à¹‰à¸à¸«à¸²à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้à¸à¸‡ à¹à¸•à¹ˆà¸—ั้งนี้ ไม่ได้ป้à¸à¸‡à¸à¸±à¸™ AI จาà¸à¸à¸²à¸£à¹€à¸£à¸µà¸¢à¸™à¸£à¸¹à¹‰à¹„ด้à¸à¸¢à¹ˆà¸²à¸‡à¸ªà¸¡à¸šà¸¹à¸£à¸“์ เนื่à¸à¸‡à¸ˆà¸²à¸à¸¡à¸µ AI บางตัวเท่านั้นที่จะเคารพคำขà¸à¸”ังà¸à¸¥à¹ˆà¸²à¸§" options: "ตัวเลืà¸à¸à¸šà¸—บาท" specifyUser: "ผู้ใช้เฉพาะ" +lookupConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¹€à¸£à¸µà¸¢à¸à¸”ูข้à¸à¸¡à¸¹à¸¥à¹ƒà¸Šà¹ˆà¹„หม?" +openTagPageConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¹€à¸›à¸´à¸”หน้าà¹à¸®à¸Šà¹à¸—็à¸à¹ƒà¸Šà¹ˆà¹„หม?" +specifyHost: "ระบุโฮสต์" failedToPreviewUrl: "ไม่สามารถดูตัวà¸à¸¢à¹ˆà¸²à¸‡à¹„ด้" update: "à¸à¸±à¸›à¹€à¸”ต" rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เà¸à¹‚มจินี้เป็นรีà¹à¸à¸„ชั่นได้" @@ -1157,7 +1180,7 @@ notifyNotes: "à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¹‚พสต unnotifyNotes: "หยุดà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¹‚น้ตใหม่" authentication: "à¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸ªà¸´à¸—ธิ์" authenticationRequiredToContinue: "à¸à¸£à¸¸à¸“ายืนยันตัวตนทางà¸à¸´à¹€à¸¥à¹‡à¸à¸—รà¸à¸™à¸´à¸à¸ªà¹Œà¹€à¸žà¸·à¹ˆà¸à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸" -dateAndTime: "เวลาประทับ" +dateAndTime: "วันเวลา" showRenotes: "à¹à¸ªà¸”งรีโน้ต" edited: "à¹à¸à¹‰à¹„ขà¹à¸¥à¹‰à¸§" notificationRecieveConfig: "à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" @@ -1168,11 +1191,11 @@ showRepliesToOthersInTimeline: "à¹à¸ªà¸”งà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹ hideRepliesToOthersInTimeline: "ไม่à¹à¸ªà¸”งà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¸à¸·à¹ˆà¸™à¸¥à¸‡à¹ƒà¸™à¹„ทม์ไลน์" showRepliesToOthersInTimelineAll: "รวมตà¸à¸šà¸à¸¥à¸±à¸šà¸ˆà¸²à¸à¸—ุà¸à¸„นที่คุณติดตามไว้ในไทม์ไลน์ขà¸à¸‡à¸„ุณ" hideRepliesToOthersInTimelineAll: "ซ่à¸à¸™à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸ˆà¸²à¸à¸—ุà¸à¸„นที่คุณติดตามไปจาà¸à¹„ทม์ไลน์ขà¸à¸‡à¸„ุณ" -confirmShowRepliesAll: "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¹„ม่สามารถย้à¸à¸™à¸à¸¥à¸±à¸šà¹„ด้ คุณต้à¸à¸‡à¸à¸²à¸£à¹à¸ªà¸”งà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¸à¸·à¹ˆà¸™à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ุà¸à¸„นที่คุณติดตามà¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¹„ทม์ไลน์ขà¸à¸‡à¸„ุณหรืà¸à¹„ม่?" -confirmHideRepliesAll: "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¹„ม่สามารถย้à¸à¸™à¸à¸¥à¸±à¸šà¹„ด้ คุณต้à¸à¸‡à¸à¸²à¸£à¸‹à¹ˆà¸à¸™à¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¸à¸·à¹ˆà¸™à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ุà¸à¸„นที่คุณติดตามà¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¹„ทม์ไลน์ขà¸à¸‡à¸„ุณหรืà¸à¹„ม่?" +confirmShowRepliesAll: "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¹„ม่สามารถย้à¸à¸™à¸à¸¥à¸±à¸šà¹„ด้ คุณต้à¸à¸‡à¸à¸²à¸£à¹à¸ªà¸”งà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¸à¸·à¹ˆà¸™à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ุà¸à¸„นที่คุณติดตามà¸à¸¢à¸¹à¹ˆ ใส่ลงไทม์ไลน์ใช่ไหม?" +confirmHideRepliesAll: "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¹„ม่สามารถย้à¸à¸™à¸à¸¥à¸±à¸šà¹„ด้ คุณต้à¸à¸‡à¸à¸²à¸£à¸‹à¹ˆà¸à¸™à¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¸à¸·à¹ˆà¸™à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ุà¸à¸„นที่คุณติดตามà¸à¸¢à¸¹à¹ˆ ไปจาà¸à¹„ทม์ไลน์ใช่ไหม?" externalServices: "บริà¸à¸²à¸£à¸ ายนà¸à¸" sourceCode: "ซà¸à¸£à¹Œà¸ªà¹‚ค้ด" -sourceCodeIsNotYetProvided: "ซà¸à¸£à¹Œà¸ªà¹‚ค้ดยังไม่พร้à¸à¸¡à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ โปรดติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¸‚à¸à¸‡à¸„ุณเพื่à¸à¹à¸à¹‰à¹„ขปัà¸à¸«à¸²à¸™à¸µà¹‰" +sourceCodeIsNotYetProvided: "ซà¸à¸£à¹Œà¸ªà¹‚ค้ดยังไม่พร้à¸à¸¡à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ โปรดติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¹€à¸žà¸·à¹ˆà¸à¹à¸à¹‰à¹„ขปัà¸à¸«à¸²à¸™à¸µà¹‰" repositoryUrl: "URL ขà¸à¸‡ repository" repositoryUrlDescription: "หาà¸à¸¡à¸µà¸—ี่เà¸à¹‡à¸šà¸‹à¸à¸£à¹Œà¸ªà¹‚ค้ดที่เปิดเผยต่à¸à¸ªà¸²à¸˜à¸²à¸£à¸“ะ ให้ป้à¸à¸™ URL ที่เà¸à¹‡à¸šà¸‹à¸à¸£à¹Œà¸ªà¹‚ค้ดนั้น à¹à¸•à¹ˆà¸«à¸²à¸à¸„ุณใช้ Misskey ตามต้นฉบับ (ไม่มีà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸‹à¸à¸£à¹Œà¸ªà¹‚ค้ด) ให้ป้à¸à¸™ https://github.com/misskey-dev/misskey" repositoryUrlOrTarballRequired: "หาà¸à¸„ุณไม่มี repository สาธารณะ คุณจะต้à¸à¸‡à¸ˆà¸±à¸”เตรียม tarball à¹à¸—น ดู .config/example.yml สำหรับรายละเà¸à¸µà¸¢à¸”" @@ -1227,7 +1250,7 @@ surrender: "ยà¸à¸¡à¹à¸žà¹‰" gameRetry: "เริ่มเà¸à¸¡à¹ƒà¸«à¸¡à¹ˆ" notUsePleaseLeaveBlank: "หาà¸à¹„ม่ได้ใช้à¸à¸£à¸¸à¸“าเว้นว่างไว้" useTotp: "ใช้รหัสผ่านà¹à¸šà¸šà¹ƒà¸Šà¹‰à¸„รั้งเดียว (TOTP)" -useBackupCode: "ใช้รหัสสำรà¸à¸‡" +useBackupCode: "ใช้รหัสà¹à¸šà¹Šà¸à¸à¸±à¸›" launchApp: "เริ่มà¹à¸à¸›" useNativeUIForVideoAudioPlayer: "ใช้ UI ขà¸à¸‡à¹€à¸šà¸£à¸²à¸§à¹Œà¹€à¸‹à¸à¸£à¹Œà¹€à¸žà¸·à¹ˆà¸à¹€à¸¥à¹ˆà¸™à¸§à¸´à¸”ีโà¸/เสียง" keepOriginalFilename: "คงชื่à¸à¹„ฟล์เดิมไว้" @@ -1235,13 +1258,23 @@ keepOriginalFilenameDescription: "หาà¸à¸›à¸´à¸”à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่ noDescription: "ไม่มีข้à¸à¸„วามà¸à¸˜à¸´à¸šà¸²à¸¢" alwaysConfirmFollow: "à¹à¸ªà¸”งข้à¸à¸„วามยืนยันเมื่à¸à¸à¸”ติดตาม" inquiry: "ติดต่à¸à¹€à¸£à¸²" +tryAgain: "โปรดลà¸à¸‡à¸à¸µà¸à¸„รั้ง" +confirmWhenRevealingSensitiveMedia: "ตรวจสà¸à¸šà¸à¹ˆà¸à¸™à¹à¸ªà¸”งสื่à¸à¸—ี่มีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" +sensitiveMediaRevealConfirm: "สื่à¸à¸™à¸µà¹‰à¸¡à¸µà¹€à¸™à¸·à¹‰à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™, ต้à¸à¸‡à¸à¸²à¸£à¹à¸ªà¸”งใช่ไหม?" +createdLists: "รายชื่à¸à¸—ี่ถูà¸à¸ªà¸£à¹‰à¸²à¸‡" +createdAntennas: "เสาà¸à¸²à¸à¸²à¸¨à¸—ี่ถูà¸à¸ªà¸£à¹‰à¸²à¸‡" _delivery: - stop: "ถูà¸à¸£à¸°à¸‡à¸±à¸š" + status: "สถานะà¸à¸²à¸£à¸ˆà¸±à¸”ส่ง" + stop: "ระงับà¸à¸²à¸£à¸ªà¹ˆà¸‡" + resume: "จัดส่งต่à¸" _type: none: "à¸à¸³à¸¥à¸±à¸‡à¹€à¸œà¸¢à¹à¸žà¸£à¹ˆ" + manuallySuspended: "หยุดชั่วคราวด้วยตนเà¸à¸‡" + goneSuspended: "เซิร์ฟเวà¸à¸£à¹Œà¸–ูà¸à¸£à¸°à¸‡à¸±à¸šà¹€à¸™à¸·à¹ˆà¸à¸‡à¸ˆà¸²à¸à¸¡à¸µà¸à¸²à¸£à¸¥à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰" + autoSuspendedForNotResponding: "เซิร์ฟเวà¸à¸£à¹Œà¸–ูà¸à¸£à¸°à¸‡à¸±à¸šà¹€à¸™à¸·à¹ˆà¸à¸‡à¸ˆà¸²à¸à¹„ม่ตà¸à¸šà¸ªà¸™à¸à¸‡" _bubbleGame: howToPlay: "วิธีเล่น" - hold: "หยุดชั่วคราว" + hold: "ถืà¸à¹„ว้" _score: score: "คะà¹à¸™à¸™" scoreYen: "จำนวนเงินที่ได้รับ" @@ -1255,27 +1288,27 @@ _bubbleGame: section2: "เมื่à¸à¸§à¸±à¸•à¸–ุประเภทเดียวà¸à¸±à¸™à¸¡à¸²à¸£à¸§à¸¡à¸à¸±à¸™ พวà¸à¸¡à¸±à¸™à¸ˆà¸°à¸à¸¥à¸²à¸¢à¹€à¸›à¹‡à¸™à¸§à¸±à¸•à¸–ุใหม่à¹à¸¥à¸°à¸„ุณจะได้รับคะà¹à¸™à¸™" section3: "หาà¸à¸§à¸±à¸•à¸–ุล้นà¸à¸à¸à¸¡à¸²à¸ˆà¸²à¸à¸à¸¥à¹ˆà¸à¸‡ เà¸à¸¡à¸à¹‡à¸ˆà¸°à¸ˆà¸šà¸¥à¸‡ ตั้งเป้าทำคะà¹à¸™à¸™à¹ƒà¸«à¹‰à¸ªà¸¹à¸‡à¸”้วยà¸à¸²à¸£à¸«à¸¥à¸à¸¡à¸§à¸±à¸•à¸–ุต่าง ๆ โดยไม่ทำให้ล้นà¸à¸¥à¹ˆà¸à¸‡!" _announcement: - forExistingUsers: "ผู้ใช้งานที่มีà¸à¸¢à¸¹à¹ˆà¹€à¸—่านั้น" - forExistingUsersDescription: "à¸à¸²à¸£à¸›à¸£à¸°à¸à¸²à¸¨à¸™à¸µà¹‰à¸ˆà¸°à¹à¸ªà¸”งต่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่มีà¸à¸¢à¸¹à¹ˆ ณ จุดที่เผยà¹à¸žà¸£à¹ˆà¸™à¸±à¹‰à¸™à¹†à¸–้าหาà¸à¹€à¸›à¸´à¸”ใช้งาน ถ้าหาà¸à¸›à¸´à¸”ใช้งานผู้ที่à¸à¸³à¸¥à¸±à¸‡à¸ªà¸¡à¸±à¸„รใหม่หลังจาà¸à¹‚พสต์à¹à¸¥à¹‰à¸§à¸™à¸±à¹‰à¸™à¸à¹‡à¸ˆà¸°à¹€à¸«à¹‡à¸™à¹€à¸Šà¹ˆà¸™à¸à¸±à¸™" + forExistingUsers: "ผู้ใช้งานที่มีà¸à¸¢à¸¹à¹ˆà¸•à¸à¸™à¸™à¸µà¹‰à¹€à¸—่านั้น" + forExistingUsersDescription: "หาà¸à¹€à¸›à¸´à¸”ใช้งาน à¸à¸²à¸£à¸›à¸£à¸°à¸à¸²à¸¨à¸™à¸µà¹‰à¸ˆà¸°à¹à¸ªà¸”งเฉพาะà¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่สร้างบัà¸à¸Šà¸µà¸à¹ˆà¸à¸™/ที่มีà¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸‚ณะที่สร้างประà¸à¸²à¸¨à¸™à¸µà¹‰à¹€à¸—่านั้น หาà¸à¸›à¸´à¸”ใช้งาน à¸à¸²à¸£à¸›à¸£à¸°à¸à¸²à¸¨à¸™à¸µà¹‰à¸ˆà¸°à¹à¸ªà¸”งà¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่สร้างบัà¸à¸Šà¸µà¸«à¸¥à¸±à¸‡à¸ˆà¸²à¸à¸ªà¸£à¹‰à¸²à¸‡à¸›à¸£à¸°à¸à¸²à¸¨à¸™à¸µà¹‰à¸”้วย" needConfirmationToRead: "จำเป็นต้à¸à¸‡à¸¢à¸·à¸™à¸¢à¸±à¸™à¸§à¹ˆà¸²à¸à¹ˆà¸²à¸™à¹à¸¥à¹‰à¸§" needConfirmationToReadDescription: "à¸à¸¥à¹ˆà¸à¸‡à¹‚ต้ตà¸à¸šà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸ˆà¸°à¸›à¸£à¸²à¸à¸à¸‚ึ้นเมื่à¸à¸ˆà¸°à¸—ำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸§à¹ˆà¸²à¸à¹ˆà¸²à¸™à¹à¸¥à¹‰à¸§ นà¸à¸à¸ˆà¸²à¸à¸™à¸µà¹‰à¸¢à¸±à¸‡à¸—ำให้ประà¸à¸²à¸¨à¸™à¸µà¹‰à¸¢à¸±à¸‡à¹„ม่ถูà¸à¸à¹ˆà¸²à¸™à¹€à¸¡à¸·à¹ˆà¸à¹ƒà¸Šà¹‰à¸Ÿà¸±à¸‡à¸à¹Œà¸Šà¸±à¹ˆà¸™ “ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸¯ ทั้งหมดว่าà¸à¹ˆà¸²à¸™à¹à¸¥à¹‰à¸§â€" end: "เà¸à¹‡à¸šà¸›à¸£à¸°à¸à¸²à¸¨" - tooManyActiveAnnouncementDescription: "à¸à¸²à¸£à¸¡à¸µà¸›à¸£à¸°à¸à¸²à¸¨à¸—ี่ใช้งานมาà¸à¹€à¸à¸´à¸™à¹„ปนั้นà¸à¸²à¸ˆà¸ˆà¸°à¸—ำให้ประสบà¸à¸²à¸£à¸“์ขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸™à¸±à¹‰à¸™à¸”ูà¹à¸¢à¹ˆà¸¥à¸‡ โปรดà¸à¸£à¸¸à¸“าพิจารณาà¸à¸²à¸£à¹€à¸à¹‡à¸šà¸›à¸£à¸°à¸à¸²à¸¨à¸—ี่ล้าสมัยด้วยนะค่ะ" + tooManyActiveAnnouncementDescription: "เนื่à¸à¸‡à¸ˆà¸²à¸à¸¡à¸µà¸à¸²à¸£à¸›à¸£à¸°à¸à¸²à¸¨à¸—ี่ยังใช้งานà¸à¸¢à¸¹à¹ˆà¸ˆà¸³à¸™à¸§à¸™à¸¡à¸²à¸ à¸à¸²à¸ˆà¸—ำให้ UX ลดลง à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¸žà¸´à¸ˆà¸²à¸£à¸“าà¸à¸²à¸£à¹€à¸à¹‡à¸šà¸›à¸£à¸°à¸à¸²à¸¨à¸—ี่สิ้นสุดไปà¹à¸¥à¹‰à¸§" readConfirmTitle: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸§à¹ˆà¸²à¸à¹ˆà¸²à¸™à¹à¸¥à¹‰à¸§à¹€à¸¥à¸¢à¹„หม?" readConfirmText: "จะทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¹ƒà¸ªà¹ˆ “{title}†ว่าà¸à¹ˆà¸²à¸™à¹à¸¥à¹‰à¸§" - shouldNotBeUsedToPresentPermanentInfo: "เราขà¸à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹ƒà¸Šà¹‰à¸›à¸£à¸°à¸à¸²à¸¨à¹€à¸žà¸·à¹ˆà¸à¹‚พสต์ข้à¸à¸¡à¸¹à¸¥à¹à¸šà¸š flow มาà¸à¸à¸§à¹ˆà¸²à¸‚้à¸à¸¡à¸¹à¸¥à¹à¸šà¸š stock เนื่à¸à¸‡à¸ˆà¸²à¸à¸¡à¸µà¹à¸™à¸§à¹‚น้มที่จะส่งผลเสียต่ภUX โดยเฉพาะสำหรับผู้ใช้ใหม่" + shouldNotBeUsedToPresentPermanentInfo: "เนื่à¸à¸‡à¸ˆà¸²à¸à¸¡à¸µà¸„วามเป็นไปได้สูงที่จะส่งผลเสียต่à¸à¸‡ UX ขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹ƒà¸«à¸¡à¹ˆ จึงขà¸à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹ƒà¸Šà¹‰à¸›à¸£à¸°à¸à¸²à¸¨à¸ªà¸³à¸«à¸£à¸±à¸šà¸‚้à¸à¸¡à¸¹à¸¥à¸—ี่ต้à¸à¸‡à¸à¸²à¸£à¸à¸²à¸£à¸•à¸à¸šà¸ªà¸™à¸à¸‡à¹ƒà¸™à¸—ันที ไม่ใช่ข้à¸à¸¡à¸¹à¸¥à¸—ี่ต้à¸à¸‡à¸à¸²à¸£à¹à¸ªà¸”งตลà¸à¸”เวลา" dialogAnnouncementUxWarn: "เราขà¸à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹ƒà¸Šà¹‰à¸”้วยความระมัดระวัง เนื่à¸à¸‡à¸ˆà¸²à¸à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸à¸¥à¹ˆà¸à¸‡à¹‚ต้ตà¸à¸šà¸•à¸±à¹‰à¸‡à¹à¸•à¹ˆ 2 รายà¸à¸²à¸£à¸‚ึ้นไปพร้à¸à¸¡à¸à¸±à¸™à¸à¸²à¸ˆà¸ªà¹ˆà¸‡à¸œà¸¥à¹€à¸ªà¸µà¸¢à¸•à¹ˆà¸ UX ได้à¸à¸¢à¹ˆà¸²à¸‡à¸¡à¸²à¸" silence: "ไม่มีà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" silenceDescription: "หาà¸à¹€à¸›à¸´à¸”ใช้งาน จะไม่มีà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸›à¸£à¸°à¸à¸²à¸¨à¸™à¸µà¹‰ à¹à¸¥à¸°à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸ˆà¸°à¹„ม่จำเป็นต้à¸à¸‡à¸—ำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸§à¹ˆà¸²à¸à¹ˆà¸²à¸™à¹à¸¥à¹‰à¸§" _initialAccountSetting: - accountCreated: "คุณได้สร้างบัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณสำเร็จเรียบร้à¸à¸¢à¹à¸¥à¹‰à¸§!" + accountCreated: "สร้างบัà¸à¸Šà¸µà¹€à¸ªà¸£à¹‡à¸ˆà¸ªà¸¡à¸šà¸¹à¸£à¸“์!" letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ขà¸à¸‡à¸„ุณà¸à¸±à¸™à¹€à¸–à¸à¸°" letsFillYourProfile: "à¸à¹ˆà¸à¸™à¸à¸·à¹ˆà¸™à¸¡à¸²à¸•à¸±à¹‰à¸‡à¸„่าโปรไฟล์ขà¸à¸‡à¸„ุณ" profileSetting: "ตั้งค่าโปรไฟล์" privacySetting: "ตั้งค่าความเป็นส่วนตัว" theseSettingsCanEditLater: "คุณสามารถเปลี่ยนà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าเหล่านี้ได้ในภายหลังได้ตลà¸à¸”เวลานะ" - youCanEditMoreSettingsInSettingsPageLater: "ยังมีà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าà¸à¸·à¹ˆà¸™à¹† à¸à¸µà¸à¸¡à¸²à¸à¸¡à¸²à¸¢à¸—ี่คุณนั้นสามารถà¸à¸³à¸«à¸™à¸”ค่าได้จาภ\"à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่า\" เพื่à¸à¹ƒà¸«à¹‰à¹à¸™à¹ˆà¹ƒà¸ˆà¸§à¹ˆà¸²à¹„ด้เยี่ยมชมมันได้ภายหลังนะ" - followUsers: "ลà¸à¸‡à¸•à¸´à¸”ตามผู้ใช้บางคนที่คุณà¸à¸²à¸ˆà¸ˆà¸°à¸ªà¸™à¹ƒà¸ˆà¹€à¸žà¸·à¹ˆà¸à¸ªà¸£à¹‰à¸²à¸‡à¹„ทม์ไลน์ขà¸à¸‡à¸„ุณสิ !" + youCanEditMoreSettingsInSettingsPageLater: "สามารถตั้งค่าเพิ่มเติมได้ที่หน้า “à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่า†à¸à¸¢à¹ˆà¸²à¸¥à¸·à¸¡à¹„ปเยี่ยมชมภายหลังด้วย" + followUsers: "ลà¸à¸‡à¸•à¸´à¸”ตามผู้ใช้ที่สนใจเพื่à¸à¸ªà¸£à¹‰à¸²à¸‡à¹„ทม์ไลน์ดูสิ" pushNotificationDescription: "à¸à¸³à¸¥à¸±à¸‡à¹€à¸›à¸´à¸”ใช้งานà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹à¸šà¸šà¸žà¸¸à¸Šà¸ˆà¸°à¸Šà¹ˆà¸§à¸¢à¹ƒà¸«à¹‰à¸„ุณได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸ˆà¸²à¸ {name} โดยตรงบนà¸à¸¸à¸›à¸à¸£à¸“์ขà¸à¸‡à¸„ุณนะ" initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์à¹à¸¥à¹‰à¸§!" haveFun: "ขà¸à¹ƒà¸«à¹‰à¸ªà¸™à¸¸à¸à¸à¸±à¸š {name}!" @@ -1310,7 +1343,7 @@ _initialTutorial: description1: "Misskey มีหลายไทม์ไลน์ขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¸§à¸´à¸˜à¸µà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸‚à¸à¸‡à¸„ุณ (บางไทม์ไลน์à¸à¸²à¸ˆà¹„ม่สามารถใช้ได้ขึ้นà¸à¸¢à¸¹à¹ˆà¸à¸±à¸šà¸™à¹‚ยบายขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ)" home: "คุณสามารถดูโพสต์จาà¸à¸šà¸±à¸à¸Šà¸µà¸—ี่คุณติดตามได้" local: "คุณสามารถดูโพสต์จาà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ั้งหมดบนเซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰" - social: "โพสต์จาà¸à¸—ั้งไทม์ไลน์หน้าà¹à¸£à¸à¹à¸¥à¸°à¹„ทม์ไลน์ในพื้นที่ขà¸à¸‡à¸„ุณจะปราà¸à¸à¸‚ึ้น" + social: "จะà¹à¸ªà¸”งโพสต์ทั้งจาà¸à¹„ทม์ไลน์หลัà¸à¹à¸¥à¸°à¹„ทม์ไลน์ท้à¸à¸‡à¸–ิ่น" global: "คุณสามารถดูโพสต์จาà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่เชื่à¸à¸¡à¸•à¹ˆà¸à¸à¸·à¹ˆà¸™à¹† ทั้งหมดได้" description2: "คุณสามารถสลับระหว่างà¹à¸•à¹ˆà¸¥à¸°à¹„ทม์ไลน์ได้ตลà¸à¸”เวลาได้ที่บริเวณด้านบนขà¸à¸‡à¸«à¸™à¹‰à¸²à¸ˆà¸" description3: "นà¸à¸à¸ˆà¸²à¸à¸™à¸µà¹‰à¸¢à¸±à¸‡à¸¡à¸µà¸£à¸²à¸¢à¸à¸²à¸£à¹„ทม์ไลน์ ไทม์ไลน์ขà¸à¸‡à¸Šà¹ˆà¸à¸‡ ฯลฯ โปรดดู {link} สำหรับรายละเà¸à¸µà¸¢à¸”เพิ่มเติม" @@ -1320,7 +1353,7 @@ _initialTutorial: _visibility: description: "คุณสามารถจำà¸à¸±à¸”ผู้ที่สามารถดูโน้ตขà¸à¸‡à¸„ุณได้นะ" public: "โน้ตขà¸à¸‡à¸„ุณนั้นจะปราà¸à¸à¹à¸à¹ˆà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸—ุà¸à¸„น" - home: "เผยà¹à¸žà¸£à¹ˆà¸šà¸™à¹„ทม์ไลน์หน้าà¹à¸£à¸à¹€à¸—่านั้น ผู้คนที่เข้าชมโปรไฟล์ขà¸à¸‡à¸„ุณ ผ่านผู้ติดตาม à¹à¸¥à¸°à¸œà¹ˆà¸²à¸™à¸à¸²à¸£à¸£à¸µà¹‚น้ตสามารถเห็นได้" + home: "เผยà¹à¸žà¸£à¹ˆà¸šà¸™à¹„ทม์ไลน์หลัà¸à¹€à¸—่านั้น à¹à¸•à¹ˆà¸œà¸¹à¹‰à¸•à¸´à¸”ตาม ผู้ที่เข้ามาดูโปรไฟล์ à¹à¸¥à¸°à¸œà¸¹à¹‰à¸—ี่เห็นจาà¸à¸£à¸µà¹‚น้ตยังสามารถดูโพสต์นี้ได้" followers: "มà¸à¸‡à¹€à¸«à¹‡à¸™à¹„ด้เฉพาะผู้ติดตามเท่านั้น ไม่มีใครà¸à¸·à¹ˆà¸™à¸™à¸à¸à¸ˆà¸²à¸à¸•à¸±à¸§à¸„ุณเà¸à¸‡à¸—ี่สามารถรีโน้ตได้ à¹à¸¥à¸°à¸¡à¸µà¹€à¸žà¸µà¸¢à¸‡à¸œà¸¹à¹‰à¸•à¸´à¸”ตามขà¸à¸‡à¸„ุณเท่านั้นที่สามารถดูได้" direct: "เปิดให้เห็นเฉพาะผู้ใช้ที่ระบุเท่านั้น à¹à¸¥à¸°à¸žà¸§à¸à¹€à¸‚าจะได้รับà¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸”้วย คุณสามารถใช้มันà¹à¸—นข้à¸à¸„วามโดยตรง (dm)" doNotSendConfidencialOnDirect1: "โปรดใช้ความระมัดระวังในà¸à¸²à¸£à¸ªà¹ˆà¸‡à¸‚้à¸à¸¡à¸¹à¸¥à¸—ี่ละเà¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" @@ -1328,9 +1361,9 @@ _initialTutorial: localOnly: "à¸à¸²à¸£à¹‚พสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวà¸à¸£à¹Œà¸à¸·à¹ˆà¸™ ผู้ใช้บนเซิร์ฟเวà¸à¸£à¹Œà¸à¸·à¹ˆà¸™à¸ˆà¸°à¹„ม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าà¸à¸²à¸£à¹à¸ªà¸”งผลข้างต้น" _cw: title: "คำเตืà¸à¸™à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¹€à¸™à¸·à¹‰à¸à¸«à¸²" - description: "เนื้à¸à¸«à¸²à¸—ี่เขียนด้วย “คำà¸à¸˜à¸´à¸šà¸²à¸¢à¸›à¸£à¸°à¸à¸à¸šâ€ จะà¹à¸ªà¸”งà¹à¸—นข้à¸à¸„วามหลัภคลิภ“ดูเพิ่มเติม†เพื่à¸à¹à¸ªà¸”งข้à¸à¸„วามเต็ม" + description: "เนื้à¸à¸«à¸²à¸—ี่เขียนใน “คำà¸à¸˜à¸´à¸šà¸²à¸¢à¸›à¸£à¸°à¸à¸à¸šâ€ จะà¹à¸ªà¸”งà¹à¸—นเนื้à¸à¸«à¸²à¸«à¸¥à¸±à¸ ต้à¸à¸‡à¸„ลิภ“ดูเพิ่มเติม†เพื่à¸à¹ƒà¸«à¹‰à¹€à¸™à¸·à¹‰à¸à¸«à¸²à¸«à¸¥à¸±à¸à¹à¸ªà¸”ง" _exampleNote: - cw: "นี่à¸à¸²à¸ˆà¸ˆà¸°à¸—ำให้คุณหิวà¸à¸¢à¹ˆà¸²à¸‡à¹à¸™à¹ˆà¸™à¸à¸™!" + cw: " ห้ามดู ระวังหิว" note: "เพิ่งไปà¸à¸´à¸™à¹‚ดนัทเคลืà¸à¸šà¸Šà¹‡à¸à¸„โà¸à¹à¸¥à¸•à¸¡à¸² ðŸ©ðŸ˜‹" useCases: "ใช้สิ่งนี้เพื่à¸à¸£à¸°à¸šà¸¸à¹‚น้ตที่ต้à¸à¸‡à¸•à¸²à¸¡à¹à¸™à¸§à¸—างปà¸à¸´à¸šà¸±à¸•à¸´à¸‚à¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ หรืà¸à¹€à¸žà¸·à¹ˆà¸à¸„วบคุมà¸à¸²à¸£à¸ªà¸›à¸à¸¢à¸¥à¹Œà¹à¸¥à¸°à¸‚้à¸à¸„วามที่ละเà¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™à¸”้วยตนเà¸à¸‡" _howToMakeAttachmentsSensitive: @@ -1346,17 +1379,17 @@ _initialTutorial: title: "บทเรียนจบลงà¹à¸¥à¹‰à¸§à¸ˆà¹‰à¸² เย่เย่เย่ 🎉" description: "คุณสมบัติที่à¹à¸™à¸°à¸™à¸³à¹ƒà¸™à¸—ี่นี่เป็นเพียงบางส่วนเท่านั้น หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¹€à¸£à¸µà¸¢à¸™à¸£à¸¹à¹‰à¹€à¸žà¸´à¹ˆà¸¡à¹€à¸•à¸´à¸¡à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸§à¸´à¸˜à¸µà¹ƒà¸Šà¹‰ Misskey โปรดไปที่ {link}" _timelineDescription: - home: "บนไทม์ไลน์หน้าà¹à¸£à¸ คุณสามารถดูโพสต์จาà¸à¸šà¸±à¸à¸Šà¸µà¸—ี่คุณติดตามได้" - local: "ไทม์ไลน์ในพื้นที่ช่วยให้คุณเห็นโพสต์จาà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ั้งหมดบนเซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰" - social: "ไทม์ไลน์โซเชียลจะà¹à¸ªà¸”งโพสต์จาà¸à¸—ั้งไทม์ไลน์หน้าà¹à¸£à¸à¹à¸¥à¸°à¹„ทม์ไลน์ในพื้นที่" + home: "บนไทม์ไลน์หลัภคุณสามารถดูโพสต์จาà¸à¸šà¸±à¸à¸Šà¸µà¸—ี่ติดตามà¸à¸¢à¸¹à¹ˆà¹„ด้" + local: "ไทม์ไลน์ท้à¸à¸‡à¸–ิ่นช่วยให้เห็นโพสต์จาà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ั้งหมดบนเซิร์ฟเวà¸à¸£à¹Œà¸™à¸µà¹‰" + social: "ไทม์ไลน์โซเชียลจะà¹à¸ªà¸”งโพสต์จาà¸à¸—ั้งไทม์ไลน์หลัà¸à¹à¸¥à¸°à¹„ทม์ไลน์ท้à¸à¸‡à¸–ิ่น" global: "ในไทม์ไลน์ทั่วโลภคุณสามารถดูโน้ตจาà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่เชื่à¸à¸¡à¸•à¹ˆà¸à¸—ั้งหมดได้" _serverRules: description: "ชุดขà¸à¸‡à¸à¸Žà¸—ี่จะà¹à¸ªà¸”งà¸à¹ˆà¸à¸™à¸à¸²à¸£à¸¥à¸‡à¸—ะเบียนเราขà¸à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¸•à¸±à¹‰à¸‡à¸„่าสรุปข้à¸à¸à¸³à¸«à¸™à¸”ในà¸à¸²à¸£à¹ƒà¸«à¹‰à¸šà¸£à¸´à¸à¸²à¸£" _serverSettings: iconUrl: "URL ไà¸à¸„à¸à¸™" appIconDescription: "ระบุไà¸à¸„à¸à¸™à¸—ี่จะใช้เมื่ภ{host} à¹à¸ªà¸”งเป็นà¹à¸à¸›" - appIconUsageExample: "E.g. เป็น PWA หรืà¸à¹€à¸¡à¸·à¹ˆà¸à¹à¸ªà¸”งผลเป็นบุ๊à¸à¸¡à¸²à¸£à¹Œà¸à¸«à¸™à¹‰à¸²à¸ˆà¸à¸«à¸¥à¸±à¸à¸šà¸™à¹‚ทรศัพท์" - appIconStyleRecommendation: "เนื่à¸à¸‡à¸ˆà¸²à¸à¹„à¸à¸„à¸à¸™à¸à¸²à¸ˆà¸–ูà¸à¸„รà¸à¸šà¸•à¸±à¸”เป็นสี่เหลี่ยมจัตุรัสหรืà¸à¸§à¸‡à¸à¸¥à¸¡ จึงà¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹ƒà¸Šà¹‰à¹„à¸à¸„à¸à¸™à¸—ี่มีขà¸à¸šà¸ªà¸µà¸£à¸à¸šà¹† เนื้à¸à¸«à¸²" + appIconUsageExample: "ตัวà¸à¸¢à¹ˆà¸²à¸‡à¹€à¸Šà¹ˆà¸™ เมื่à¸à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡à¹€à¸›à¹‡à¸™ PWA หรืà¸à¸šà¸¸à¹Šà¸à¸¡à¸²à¸£à¹Œà¸à¸šà¸™à¸«à¸™à¹‰à¸²à¸ˆà¸à¸«à¸¥à¸±à¸à¹ƒà¸™à¸ªà¸¡à¸²à¸£à¹Œà¸—โฟน" + appIconStyleRecommendation: "เนื่à¸à¸‡à¸ˆà¸²à¸à¸à¸²à¸ˆà¸–ูà¸à¸„รà¸à¸šà¸•à¸±à¸”เป็นสี่เหลี่ยมหรืà¸à¸§à¸‡à¸à¸¥à¸¡ จึงà¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹ƒà¸Šà¹‰à¸ าพที่เผื่à¸à¸žà¸·à¹‰à¸™à¸—ี่รà¸à¸šà¹† ตัวโลโà¸à¹‰à¹„à¸à¸„à¸à¸™à¹„ว้" appIconResolutionMustBe: "ความละเà¸à¸µà¸¢à¸”ขั้นต่ำไว้คืภ{resolution}." manifestJsonOverride: "เขียนทับ manifest.json" shortName: "ชื่à¸à¸¢à¹ˆà¸" @@ -1364,27 +1397,29 @@ _serverSettings: fanoutTimelineDescription: "เพิ่มประสิทธิภาพà¸à¸²à¸£à¸”ึงข้à¸à¸¡à¸¹à¸¥à¹„ทม์ไลน์à¸à¸¢à¹ˆà¸²à¸‡à¸¡à¸²à¸ à¹à¸¥à¸°à¸¥à¸”ภาระในà¸à¸²à¸™à¸‚้à¸à¸¡à¸¹à¸¥à¹€à¸¡à¸·à¹ˆà¸à¹€à¸›à¸´à¸”ใช้งาน ในทางà¸à¸¥à¸±à¸šà¸à¸±à¸™ à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸«à¸™à¹ˆà¸§à¸¢à¸„วามจำขà¸à¸‡ Redis จะเพิ่มขึ้น ลà¸à¸‡à¸›à¸´à¸”à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸™à¸µà¹‰à¹ƒà¸™à¸à¸£à¸“ีที่หน่วยความจำเซิร์ฟเวà¸à¸£à¹Œà¹€à¸«à¸¥à¸·à¸à¸™à¹‰à¸à¸¢à¸«à¸£à¸·à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¹„ม่เสถียร" fanoutTimelineDbFallback: "ฟà¸à¸¥à¹à¸šà¹Šà¸à¸à¸¥à¸±à¸šà¸à¸²à¸™à¸‚้à¸à¸¡à¸¹à¸¥" fanoutTimelineDbFallbackDescription: "เมื่à¸à¹€à¸›à¸´à¸”ใช้งาน หาà¸à¹„ม่ได้à¹à¸„ชไทม์ไลน์ ไทม์ไลน์จะฟà¸à¸¥à¹à¸šà¹Šà¸à¹„ปยังà¸à¸²à¸™à¸‚้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸«à¸£à¸±à¸šà¸à¸²à¸£ query เพิ่มเติม à¸à¸²à¸£à¸›à¸´à¸”ใช้งานจะช่วยลดภาระขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸”้วยà¸à¸²à¸£à¸à¸³à¸ˆà¸±à¸”à¸à¸£à¸°à¸šà¸§à¸™à¸Ÿà¸à¸¥à¹à¸šà¹Šà¸ à¹à¸•à¹ˆà¸¡à¸±à¸™à¸à¹‡à¸ˆà¸°à¸ˆà¸³à¸à¸±à¸”ช่วงเวลาไทม์ไลน์ที่สามารถดึงข้à¸à¸¡à¸¹à¸¥à¹„ด้" + inquiryUrl: "URL สำหรับà¸à¸²à¸£à¸•à¸´à¸”ต่à¸à¸ªà¸à¸šà¸–าม" + inquiryUrlDescription: "ระบุ URL ขà¸à¸‡à¸«à¸™à¹‰à¸²à¹€à¸§à¹‡à¸šà¸—ี่มีà¹à¸šà¸šà¸Ÿà¸à¸£à¹Œà¸¡à¸ªà¸³à¸«à¸£à¸±à¸šà¸•à¸´à¸”ต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ หรืà¸à¸‚้à¸à¸¡à¸¹à¸¥à¸à¸²à¸£à¸•à¸´à¸”ต่à¸à¸‚à¸à¸‡à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" _accountMigration: - moveFrom: "ย้ายข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µà¸à¸·à¹ˆà¸™à¹„ปยังà¸à¸µà¸à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¸«à¸™à¸¶à¹ˆà¸‡" + moveFrom: "ย้ายจาà¸à¸šà¸±à¸à¸Šà¸µà¸à¸·à¹ˆà¸™à¸¡à¸²à¸—ี่บัà¸à¸Šà¸µà¸™à¸µà¹‰" moveFromSub: "สร้างนามà¹à¸à¸‡à¹„ปยังบัà¸à¸Šà¸µà¸à¸·à¹ˆà¸™" moveFromLabel: "บัà¸à¸Šà¸µà¸—ี่จะย้ายจาภ#{n}" - moveFromDescription: "ถ้าหาà¸à¸„ุณต้à¸à¸‡à¸à¸²à¸£à¹‚à¸à¸™à¸‚้à¸à¸¡à¸¹à¸¥ คุณจำเป็นต้à¸à¸‡à¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µà¸ªà¸³à¸£à¸à¸‡à¸ªà¸³à¸«à¸£à¸±à¸šà¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸šà¸±à¸à¸Šà¸µ หลังจาà¸à¸™à¸±à¹‰à¸™à¸›à¹‰à¸à¸™à¸šà¸±à¸à¸Šà¸µà¸—ี่จะย้ายไปในรูปà¹à¸šà¸šà¸•à¹ˆà¸à¹„ปนี้: @person@instance.com" - moveTo: "ย้ายข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¹„ปยังบัà¸à¸Šà¸µà¸à¸µà¸à¸«à¸™à¸¶à¹ˆà¸‡" + moveFromDescription: "หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¹‚à¸à¸™à¸‚้à¸à¸¡à¸¹à¸¥à¸ˆà¸²à¸à¸šà¸±à¸à¸Šà¸µà¸à¸·à¹ˆà¸™à¸¡à¸²à¸¢à¸±à¸‡à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰ จำเป็นต้à¸à¸‡à¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µà¸™à¸²à¸¡à¹à¸à¸‡ (alias) ไว้ที่นี่ด้วย\nà¸à¸£à¸¸à¸“าà¸à¸£à¸à¸à¸šà¸±à¸à¸Šà¸µà¹€à¸”ิมในรูปà¹à¸šà¸š: @username@server.example.com\nหาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¸¥à¸š alias, ให้เว้นว่างไว้à¹à¸¥à¹‰à¸§à¸šà¸±à¸™à¸—ึภ(ไม่à¹à¸™à¸°à¸™à¸³)" + moveTo: "ย้ายบัà¸à¸Šà¸µà¸™à¸µà¹‰à¹„ปยังบัà¸à¸Šà¸µà¹ƒà¸«à¸¡à¹ˆ" moveToLabel: "บัà¸à¸Šà¸µà¸—ี่จะย้ายไปที่:" moveCannotBeUndone: "ไม่สามารถยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¹‚à¸à¸™à¸¢à¹‰à¸²à¸¢à¸šà¸±à¸à¸Šà¸µà¹„ด้" moveAccountDescription: "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¸ˆà¸°à¸¢à¹‰à¸²à¸¢à¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณไปยังบัà¸à¸Šà¸µà¸à¸·à¹ˆà¸™\n・ผู้ที่à¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตามคุณจาà¸à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¸ˆà¸°à¸–ูà¸à¸¢à¹‰à¸²à¸¢à¹„ปยังบัà¸à¸Šà¸µà¹ƒà¸«à¸¡à¹ˆà¹‚ดยà¸à¸±à¸•à¹‚นมัติ\n・บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸ˆà¸°à¹€à¸¥à¸´à¸à¸•à¸´à¸”ตามผู้ใช้ทั้งหมดที่à¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตามà¸à¸¢à¸¹à¹ˆ\n・คุณจะไม่สามารถสร้างโน้ต ฯลฯ ในบัà¸à¸Šà¸µà¸™à¸µà¹‰à¹„ด้\n\nà¹à¸¡à¹‰à¸§à¹ˆà¸²à¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸œà¸¹à¹‰à¸—ี่ติดตามคุณจะเป็นไปโดยà¸à¸±à¸•à¹‚นมัติ à¹à¸•à¹ˆà¸„ุณต้à¸à¸‡à¹€à¸•à¸£à¸µà¸¢à¸¡à¸‚ั้นตà¸à¸™à¸šà¸²à¸‡à¸à¸¢à¹ˆà¸²à¸‡à¸”้วยตนเà¸à¸‡ เพื่à¸à¸¢à¹‰à¸²à¸¢à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่คุณà¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตาม โดยดำเนินà¸à¸²à¸£à¸ªà¹ˆà¸‡à¸à¸à¸à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸à¹à¸¥à¹‰à¸§à¸„่à¸à¸¢à¸™à¸³à¹€à¸‚้ามาภายหลังในเมนูà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าขà¸à¸‡à¸šà¸±à¸à¸Šà¸µà¹ƒà¸«à¸¡à¹ˆ ใช้ขั้นตà¸à¸™à¹€à¸”ียวà¸à¸±à¸™à¸™à¸µà¹‰à¹ƒà¸Šà¹‰à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸›à¸´à¸”เสียงà¹à¸¥à¸°à¸–ูà¸à¸šà¸¥à¹‡à¸à¸\n\n(คำà¸à¸˜à¸´à¸šà¸²à¸¢à¸™à¸µà¹‰à¹ƒà¸Šà¹‰à¸à¸±à¸š Misskey v13.12.0 ขึ้นไป, ซà¸à¸Ÿà¸•à¹Œà¹à¸§à¸£à¹Œ ActivityPub à¸à¸·à¹ˆà¸™à¹† เช่น Mastodon à¸à¸²à¸ˆà¸—ำงานà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸à¸à¸à¹„ป)" - moveAccountHowTo: "หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸‚้à¸à¸¡à¸¹à¸¥à¸à¹ˆà¸à¸™à¸à¸·à¹ˆà¸™à¹ƒà¸«à¹‰à¸ªà¸£à¹‰à¸²à¸‡à¸Šà¸·à¹ˆà¸à¹à¸—นสำหรับบัà¸à¸Šà¸µà¸™à¸µà¹‰ ในบัà¸à¸Šà¸µà¸—ี่จะต้à¸à¸‡à¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¹„ป\nหลังจาà¸à¸—ี่คุณสร้างนามà¹à¸à¸‡à¸™à¸±à¹‰à¸™à¹à¸¥à¹‰à¸§ ให้ป้à¸à¸™à¸šà¸±à¸à¸Šà¸µà¸—ี่ต้à¸à¸‡à¸à¸²à¸£à¸ˆà¸°à¸¢à¹‰à¸²à¸¢à¹„ปในรูปà¹à¸šà¸šà¸”ังต่à¸à¹„ปนี้: @username@server.example.com" + moveAccountHowTo: "à¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸šà¸±à¸à¸Šà¸µà¸ˆà¸°à¹€à¸£à¸´à¹ˆà¸¡à¸•à¹‰à¸™à¹‚ดยà¸à¸²à¸£à¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µà¸™à¸²à¸¡à¹à¸à¸‡ (alias) ขà¸à¸‡à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰ ณ บัà¸à¸Šà¸µà¸—ี่เป็นปลายทาง หลังจาà¸à¸ªà¸£à¹‰à¸²à¸‡à¸™à¸²à¸¡à¹à¸à¸‡à¹à¸¥à¹‰à¸§ ให้ป้à¸à¸™à¸šà¸±à¸à¸Šà¸µà¸›à¸¥à¸²à¸¢à¸—างในรูปà¹à¸šà¸šà¸”ังนี้: @username@server.example.com" startMigration: "โà¸à¸™à¸¢à¹‰à¸²à¸¢" migrationConfirm: "ยืนยันà¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸‚้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¹„ปที่ {account} เมื่à¸à¹€à¸£à¸´à¹ˆà¸¡à¹à¸¥à¹‰à¸§à¸ˆà¸°à¹„ม่สามารถหยุดหรืà¸à¸™à¸³à¸à¸¥à¸±à¸šà¸„ืนมาได้ à¹à¸¥à¸°à¸„ุณจะไม่สามารถใช้บัà¸à¸Šà¸µà¸™à¸µà¹‰à¹ƒà¸™à¸ªà¸–านะดั้งเดิมได้à¸à¸µà¸à¸•à¹ˆà¸à¹„ป\n\nนà¸à¸à¸ˆà¸²à¸à¸™à¸µà¹‰ คุณจำเป็นต้à¸à¸‡à¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µà¸ªà¸³à¸£à¸à¸‡à¸ªà¸³à¸«à¸£à¸±à¸šà¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸šà¸±à¸à¸Šà¸µ" - movedAndCannotBeUndone: "\nบัà¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¹‚à¸à¸™à¸¢à¹‰à¸²à¸¢à¹„ปà¹à¸¥à¹‰à¸§\nไม่สามารถย้à¸à¸™à¸à¸¥à¸±à¸šà¹‚à¸à¸™à¸¢à¹‰à¸²à¸¢à¸‚้à¸à¸¡à¸¹à¸¥à¹„ด้" - postMigrationNote: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸ˆà¸°à¸–ูà¸à¹€à¸¥à¸´à¸à¸•à¸´à¸”ตามบัà¸à¸Šà¸µà¸—ั้งหมดที่à¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตามภายใน 24 ชั่วโมงหลังจาà¸à¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸‚้à¸à¸¡à¸¹à¸¥à¸™à¸±à¹‰à¸™à¹€à¸ªà¸£à¹‡à¸ˆà¸ªà¸´à¹‰à¸™ ทั้งจำนวนผู้ติดตามà¹à¸¥à¸°à¸œà¸¹à¹‰à¸•à¸´à¸”ตามนั้นจะà¸à¸¥à¸²à¸¢à¹€à¸›à¹‡à¸™à¸¨à¸¹à¸™à¸¢à¹Œ เพื่à¸à¸«à¸¥à¸µà¸à¹€à¸¥à¸µà¹ˆà¸¢à¸‡à¸›à¹‰à¸à¸‡à¸à¸±à¸™à¹„ม่ให้ผู้ติดตามขà¸à¸‡à¸„ุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามขà¸à¸‡à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¹„ด้ à¹à¸•à¹ˆà¸à¸¢à¹ˆà¸²à¸‡à¹„รà¸à¹‡à¸•à¸²à¸¡à¹à¸¥à¹‰à¸§à¸žà¸§à¸à¹€à¸‚าจะยังคงติดตามบัà¸à¸Šà¸µà¸™à¸µà¹‰à¸•à¹ˆà¸à¹„ป" - movedTo: "บัà¸à¸Šà¸µà¸—ี่จะย้ายไปที่:" + movedAndCannotBeUndone: "\nบัà¸à¸Šà¸µà¸™à¸µà¹‰à¸–ูà¸à¹‚à¸à¸™à¸¢à¹‰à¸²à¸¢à¹„ปà¹à¸¥à¹‰à¸§\nไม่สามารถยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¹‚à¸à¸™à¸¢à¹‰à¸²à¸¢à¹„ด้" + postMigrationNote: "บัà¸à¸Šà¸µà¸™à¸µà¹‰à¸ˆà¸°à¸”ำเนินà¸à¸²à¸£à¸¢à¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸•à¸´à¸”ตามทั้งหมดหลังจาà¸à¸à¸²à¸£à¸¢à¹‰à¸²à¸¢à¸‚้à¸à¸¡à¸¹à¸¥à¹„ปà¹à¸¥à¹‰à¸§ 24 ชั่วโมง จำนวนà¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตามà¹à¸¥à¸°à¸ˆà¸³à¸™à¸§à¸™à¸œà¸¹à¹‰à¸•à¸´à¸”ตามขà¸à¸‡à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¸ˆà¸°à¹€à¸›à¹‡à¸™ 0 à¹à¸¥à¸°à¹€à¸žà¸·à¹ˆà¸à¸«à¸¥à¸µà¸à¹€à¸¥à¸µà¹ˆà¸¢à¸‡à¹„ม่ให้ผู้ติดตามคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามฯได้ à¸à¸²à¸£à¸¢à¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸•à¸´à¸”ตามจะไม่à¸à¸£à¸°à¸—บà¸à¸±à¸šà¸œà¸¹à¹‰à¸•à¸´à¸”ตามคุณ ดังนั้นผู้ติดตามคุณยังคงสามารถดูโพสต์ขà¸à¸‡à¸šà¸±à¸à¸Šà¸µà¸™à¸µà¹‰à¹„ด้" + movedTo: "บัà¸à¸Šà¸µà¸—ี่จะย้ายไป:" _achievements: earnedAt: "ได้รับเมื่à¸" _types: _notes1: title: "just setting up my shonk" - description: "โพสต์โน้ตà¹à¸£à¸à¸‚à¸à¸‡à¸„ุณ" + description: "โพสต์โน้ตเป็นครั้งà¹à¸£à¸" flavor: "ขà¸à¹ƒà¸«à¹‰à¸¡à¸µà¸Šà¹ˆà¸§à¸‡à¹€à¸§à¸¥à¸²à¸—ี่ดีà¸à¸±à¸š Misskey นะคะ!" _notes10: title: "โน้ตไม่à¸à¸µà¹ˆà¸Šà¸´à¹‰à¸™" @@ -1444,15 +1479,15 @@ _achievements: title: "มืà¸à¹ƒà¸«à¸¡à¹ˆ III" description: "เข้าสู่ระบบเป็นเวลารวม 15 วัน" _login30: - title: "มิสคิสท์ I" + title: "มิสคิสต์ I" description: "เข้าสู่ระบบเป็นเวลารวม 30 วัน" _login60: - title: "มิสคิสท์ II" + title: "มิสคิสต์ II" description: "เข้าสู่ระบบเป็นเวลารวม 60 วัน" _login100: - title: "มิสคิสท์ III" + title: "มิสคิสต์ III" description: "เข้าสู่ระบบเป็นเวลารวม 100 วัน" - flavor: "มิสคิสต์หัวรุนà¹à¸£à¸‡" + flavor: "Violent Misskist (ทำไมเหมืà¸à¸™à¸Šà¸·à¹ˆà¸à¸«à¸™à¸±à¸‡à¸ªà¸±à¸à¹€à¸£à¸·à¹ˆà¸à¸‡à¸ˆà¸±à¸‡à¹€à¸¥à¸¢à¸™à¸°)" _login200: title: "ลูà¸à¸„้าประจำ I" description: "เข้าสู่ระบบเป็นเวลารวม 200 วัน" @@ -1484,19 +1519,19 @@ _achievements: flavor: "ขà¸à¸šà¸„ุณที่ใช้ Misskey นะ !" _noteClipped1: title: "à¸à¸”ไม่ได้ที่จะต้à¸à¸‡à¸„ลิปมันเà¸à¸²à¹„ว้" - description: "คลิปโน้ตตัวà¹à¸£à¸à¸‚à¸à¸‡à¸„ุณ" + description: "คลิปโน้ตเป็นครั้งà¹à¸£à¸" _noteFavorited1: title: "สตาร์เà¸à¹€à¸‹à¸à¸£à¹Œ" - description: "ชื่นชà¸à¸šà¹‚น้ตà¹à¸£à¸à¸‚à¸à¸‡à¸„ุณ" + description: "ใส่โน้ตเป็นรายà¸à¸²à¸£à¹‚ปรดเป็นครั้งà¹à¸£à¸" _myNoteFavorited1: title: "à¹à¸ªà¸§à¸‡à¸«à¸²à¸”วงดาว" - description: "มีคนà¸à¸·à¹ˆà¸™à¹†à¸—ี่ชื่นชà¸à¸šà¸«à¸™à¸¶à¹ˆà¸‡à¹ƒà¸™à¹‚น้ตขà¸à¸‡à¸„ุณ" + description: "โน้ตตัวเà¸à¸‡à¸–ูà¸à¸„นà¸à¸·à¹ˆà¸™à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸‡à¸£à¸²à¸¢à¸à¸²à¸£à¹‚ปรดขà¸à¸‡à¹€à¸‚า" _profileFilled: title: "เตรียมตัวà¸à¸¢à¹ˆà¸²à¸‡à¸”ี" - description: "ตั้งค่าโปรไฟล์ขà¸à¸‡à¸„ุณ" + description: "ตั้งค่าโปรไฟล์" _markedAsCat: title: "ฉันเป็นà¹à¸¡à¸§" - description: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณว่าเป็นà¹à¸¡à¸§" + description: "ตั้งค่าบัà¸à¸Šà¸µà¹€à¸›à¹‡à¸™à¹à¸¡à¸§à¹€à¸¡à¸µà¹‰à¸¢à¸§à¹€à¸¡à¸µà¹‰à¸¢à¸§" flavor: "à¹à¸¡à¸§à¸™à¹‰à¸à¸¢à¹„ร้ชื่à¸" _following1: title: "à¸à¹‰à¸²à¸§à¹à¸£à¸à¸ªà¸¹à¹ˆ...à¸à¸”ติดตาม" @@ -1539,7 +1574,7 @@ _achievements: description: "ได้รับความสำเร็จ 30 ครั้ง" _viewAchievements3min: title: "ชà¸à¸šà¸šà¸£à¸£à¸¥à¸¸à¸„วามสà¹à¸²à¹€à¸£à¹‡à¸ˆ" - description: "มà¸à¸‡à¸”ูรายà¸à¸²à¸£à¸„วามสำเร็จขà¸à¸‡à¸„ุณเป็นเวลาà¸à¸¢à¹ˆà¸²à¸‡à¸™à¹‰à¸à¸¢ 3 นาที" + description: "มà¸à¸‡à¸”ูรายà¸à¸²à¸£à¸„วามสำเร็จเป็นเวลานานà¸à¸§à¹ˆà¸² 3 นาที" _iLoveMisskey: title: "ฉันรัภMisskey" description: "โพสต์ “I ⤠#Misskeyâ€" @@ -1566,13 +1601,13 @@ _achievements: flavor: "โป๊ะ โป๊ะ โป๊ะ ปิ้งงงงง" _selfQuote: title: "à¸à¹‰à¸²à¸‡à¸à¸´à¸‡à¸•à¸™à¹€à¸à¸‡" - description: "à¸à¹‰à¸²à¸‡à¹‚น้ตขà¸à¸‡à¸„ุณเà¸à¸‡" + description: "à¸à¹‰à¸²à¸‡à¸à¸´à¸‡à¹‚น้ตตัวเà¸à¸‡" _htl20npm: title: "ไทม์ไลน์ไหล" - description: "มีà¸à¸²à¸£à¸—ำความเร็วขà¸à¸‡à¹„ทม์ไลน์หน้าà¹à¸£à¸à¹€à¸à¸´à¸™ 20 npm (โน้ตต่à¸à¸™à¸²à¸—ี)" + description: "มีà¸à¸²à¸£à¸—ำความเร็วขà¸à¸‡à¹„ทม์ไลน์หลัà¸à¹€à¸à¸´à¸™ 20 npm (โน้ตต่à¸à¸™à¸²à¸—ี)" _viewInstanceChart: title: "วิเคราะห์" - description: "ดูà¹à¸œà¸™à¸ ูมิà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸‚à¸à¸‡à¸„ุณ" + description: "ดูà¹à¸œà¸™à¸ ูมิขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" _outputHelloWorldOnScratchpad: title: "หวัดดีชาวโลà¸!" description: "เà¸à¸²à¸žà¸¸à¸• \"hello world\" ใน Scratchpad" @@ -1593,16 +1628,16 @@ _achievements: description: "มีโà¸à¸à¸²à¸ªà¸—ี่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุภๆ 10 วินาที" _setNameToSyuilo: title: "คà¸à¸¡à¹€à¸žà¸¥à¹‡à¸à¸‹à¹Œà¸‚à¸à¸‡à¸žà¸£à¸°à¹€à¸ˆà¹‰à¸²" - description: "ตั้งชื่à¸à¸‚à¸à¸‡à¸„ุณเป็น “syuiloâ€" + description: "ตั้งชื่à¸à¹€à¸›à¹‡à¸™ “syuiloâ€" _passedSinceAccountCreated1: title: "ครบรà¸à¸šà¸«à¸™à¸¶à¹ˆà¸‡à¸›à¸µ" - description: "ผ่านไปหนึ่งปีà¹à¸¥à¹‰à¸§à¸™à¸°à¸•à¸±à¹‰à¸‡à¹à¸•à¹ˆà¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณถูà¸à¸ªà¸£à¹‰à¸²à¸‡à¸‚ึ้นมาน่ะ" + description: "ผ่านไป 1 ปีนับตั้งà¹à¸•à¹ˆà¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µ" _passedSinceAccountCreated2: title: "ครบรà¸à¸šà¸ªà¸à¸‡à¸›à¸µ" - description: "ผ่านไปสà¸à¸‡à¸›à¸µà¹à¸¥à¹‰à¸§à¸™à¸°à¸•à¸±à¹‰à¸‡à¹à¸•à¹ˆà¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณถูà¸à¸ªà¸£à¹‰à¸²à¸‡à¸‚ึ้นมาน่ะ" + description: "ผ่านไป 2 ปีนับตั้งà¹à¸•à¹ˆà¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µ" _passedSinceAccountCreated3: title: "ครบรà¸à¸šà¸ªà¸²à¸¡à¸›à¸µ" - description: "ผ่านไปสามปีà¹à¸¥à¹‰à¸§à¸™à¸°à¸•à¸±à¹‰à¸‡à¹à¸•à¹ˆà¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณถูà¸à¸ªà¸£à¹‰à¸²à¸‡à¸‚ึ้นมาน่ะ" + description: "ผ่านไป 3 ปีนับตั้งà¹à¸•à¹ˆà¸ªà¸£à¹‰à¸²à¸‡à¸šà¸±à¸à¸Šà¸µ" _loggedInOnBirthday: title: "สุขสันต์วันเà¸à¸´à¸”" description: "เข้าสู่ระบบในวันเà¸à¸´à¸”ขà¸à¸‡à¸„ุณ" @@ -1637,7 +1672,7 @@ _role: name: "ชื่à¸à¸šà¸—บาท" description: "คำà¸à¸˜à¸´à¸šà¸²à¸¢à¸šà¸—บาท" permission: "สิทธิ์ตามบทบาท" - descriptionOfPermission: "<b>ผู้ควบคุม</b> สามารถดำเนินà¸à¸²à¸£à¸”ูà¹à¸¥à¸‚ั้นพื้นà¸à¸²à¸™à¹„ด้\n<b>ผู้ดูà¹à¸¥à¸£à¸°à¸šà¸š</b> สามารถเปลี่ยนà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าทั้งหมดขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¹„ด้" + descriptionOfPermission: "<b>ผู้ควบคุม</b> สามารถดำเนินà¸à¸²à¸£à¸”ูà¹à¸¥à¸‚ั้นพื้นà¸à¸²à¸™à¹„ด้\n<b>ผู้ดูà¹à¸¥à¸£à¸°à¸šà¸š</b> สามารถเปลี่ยนà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าทั้งหมดขà¸à¸‡à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¹„ด้" assignTarget: "มà¸à¸šà¸«à¸¡à¸²à¸¢" descriptionOfAssignTarget: "à¹à¸šà¸š<b>ปรับเà¸à¸‡</b> เพิ่มถà¸à¸™à¸šà¸—บาทนี้à¹à¸à¹ˆà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸”้วยตัวเà¸à¸‡\nà¹à¸šà¸š<b>มีเงื่à¸à¸™à¹„ข</b> เพิ่มถà¸à¸™à¸šà¸—บาทนี้à¹à¸à¹ˆà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹‚ดยà¸à¸±à¸•à¹‚นมัติหาà¸à¹€à¸‚้าเงื่à¸à¸™à¹„ขใดต่à¸à¹„ปนี้" manual: "ปรับเà¸à¸‡" @@ -1650,8 +1685,8 @@ _role: descriptionOfIsPublic: "บทบาทจะปราà¸à¸à¸šà¸™à¹‚ปรไฟล์ขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹à¸¥à¸°à¹€à¸›à¸´à¸”เผยต่à¸à¸ªà¸²à¸˜à¸²à¸£à¸“ะ (ทุà¸à¸„นสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)" options: "ตัวเลืà¸à¸à¸šà¸—บาท" policies: "นโยบาย" - baseRole: "เทมเพลตบทบาท" - useBaseValue: "ใช้ตามเทมเพลตบทบาท" + baseRole: "à¹à¸¡à¹ˆà¹à¸šà¸šà¸šà¸—บาท" + useBaseValue: "ใช้ตามà¹à¸¡à¹ˆà¹à¸šà¸šà¸šà¸—บาท" chooseRoleToAssign: "เลืà¸à¸à¸šà¸—บาทที่ต้à¸à¸‡à¸à¸²à¸£à¸à¸³à¸«à¸™à¸”" iconUrl: "URL ไà¸à¸„à¸à¸™" asBadge: "à¹à¸ªà¸”งเป็นตรา" @@ -1668,11 +1703,11 @@ _role: middle: "ปานà¸à¸¥à¸²à¸‡" high: "สูง" _options: - gtlAvailable: "à¸à¸²à¸£à¸”ูไทม์ไลน์ทั่วโลà¸" - ltlAvailable: "à¸à¸²à¸£à¸”ูไทม์ไลน์ในท้à¸à¸‡à¸–ิ่น" + gtlAvailable: "สามารถดูไทม์ไลน์ทั่วโลà¸à¹„ด้" + ltlAvailable: "สามารถดูไทม์ไลน์ท้à¸à¸‡à¸–ิ่นได้" canPublicNote: "สามารถโพสต์à¹à¸šà¸šà¸ªà¸²à¸˜à¸²à¸£à¸“ะ" mentionMax: "จำนวนà¸à¸²à¸£à¸à¸¥à¹ˆà¸²à¸§à¸–ึงสูงสุดต่à¸à¹‚น้ต" - canInvite: "สร้างรหัสเชิà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" + canInvite: "สร้างรหัสเชิà¸à¹€à¸‚้าเซิร์ฟเวà¸à¸£à¹Œ" inviteLimit: "จำà¸à¸±à¸”à¸à¸²à¸£à¹€à¸Šà¸´à¸" inviteLimitCycle: "คูลดาวน์ในà¸à¸²à¸£à¹€à¸Šà¸´à¸" inviteExpirationTime: "วันหมดà¸à¸²à¸¢à¸¸à¸‚à¸à¸‡à¸£à¸«à¸±à¸ªà¸à¸²à¸£à¹€à¸Šà¸´à¸" @@ -1680,6 +1715,7 @@ _role: canManageAvatarDecorations: "จัดà¸à¸²à¸£à¸•à¸à¹à¸•à¹ˆà¸‡à¸à¸§à¸•à¸²à¸£" driveCapacity: "ความจุขà¸à¸‡à¹„ดรฟ์" alwaysMarkNsfw: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¹„ฟล์ว่าเป็น NSFW เสมà¸" + canUpdateBioMedia: "à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹„à¸à¸„à¸à¸™à¹à¸¥à¸°à¹à¸šà¸™à¹€à¸™à¸à¸£à¹Œ" pinMax: "จà¹à¸²à¸™à¸§à¸™à¸ªà¸¹à¸‡à¸ªà¸¸à¸”ขà¸à¸‡à¹‚น้ตที่ปัà¸à¸«à¸¡à¸¸à¸”ไว้" antennaMax: "จำนวนสูงสุดขà¸à¸‡à¹€à¸ªà¸²à¸à¸²à¸à¸²à¸¨" wordMuteMax: "จำนวนà¸à¸±à¸à¸‚ระสูงสุดที่à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸™à¸à¸²à¸£à¸›à¸´à¸”เสียงคำ" @@ -1696,7 +1732,7 @@ _role: avatarDecorationLimit: "จำนวนà¸à¸²à¸£à¸•à¸à¹à¸•à¹ˆà¸‡à¹„à¸à¸„à¸à¸™à¸ªà¸¹à¸‡à¸ªà¸¸à¸”ที่สามารถติดตั้งได้" _condition: roleAssignedTo: "มà¸à¸šà¸«à¸¡à¸²à¸¢à¹ƒà¸«à¹‰à¸¡à¸µà¸šà¸—บาทà¹à¸šà¸šà¸—ำมืà¸" - isLocal: "ผู้ใช้ในพื้นที่" + isLocal: "ผู้ใช้ท้à¸à¸‡à¸–ิ่น" isRemote: "ผู้ใช้ระยะไà¸à¸¥" isCat: "ผู้ใช้ที่เป็นà¹à¸¡à¸§" isBot: "ผู้ใช้ที่เป็นบà¸à¸•" @@ -1755,8 +1791,8 @@ _ad: adsTooClose: "เนื่à¸à¸‡à¸ˆà¸²à¸à¸Šà¹ˆà¸§à¸‡à¹€à¸§à¸¥à¸²à¸à¸²à¸£à¹à¸ªà¸”งโฆษณาสั้นมาภประสบà¸à¸²à¸£à¸“์ผู้ใช้จึงà¸à¸²à¸ˆà¸¥à¸”ลงà¸à¸¢à¹ˆà¸²à¸‡à¸¡à¸²à¸" _forgotPassword: enterEmail: "ป้à¸à¸™à¸—ี่à¸à¸¢à¸¹à¹ˆà¸à¸µà¹€à¸¡à¸¥à¸—ี่คุณเคยใช้ในà¸à¸²à¸£à¸¥à¸‡à¸—ะเบียนไว้ ลิงà¸à¹Œà¸—ี่คุณสามารถรีเซ็ตรหัสผ่านได้นั้นจะถูà¸à¸ªà¹ˆà¸‡à¹„ปนะ" - ifNoEmail: "ถ้าหาà¸à¸„ุณไม่ได้ใช้à¸à¸µà¹€à¸¡à¸¥à¸£à¸°à¸«à¸§à¹ˆà¸²à¸‡à¸à¸²à¸£à¸¥à¸‡à¸—ะเบียน à¸à¸£à¸¸à¸“าติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¹à¸—นนะ" - contactAdmin: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸™à¸µà¹‰à¹„ม่รà¸à¸‡à¸£à¸±à¸šà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸—ี่à¸à¸¢à¸¹à¹ˆà¸à¸µà¹€à¸¡à¸¥à¸™à¸µà¹‰ à¸à¸£à¸¸à¸“าติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¹€à¸žà¸·à¹ˆà¸à¸£à¸µà¹€à¸‹à¹‡à¸•à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™à¸‚à¸à¸‡à¸„ุณà¹à¸—น" + ifNoEmail: "หาà¸à¸¥à¸‡à¸—ะเบียนà¹à¸šà¸šà¹„ม่ใช้à¸à¸µà¹€à¸¡à¸¥ โปรดติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸š" + contactAdmin: "เนื่à¸à¸‡à¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸™à¸µà¹‰à¹„ม่รà¸à¸‡à¸£à¸±à¸šà¸à¸²à¸£à¸ªà¹ˆà¸‡à¸à¸µà¹€à¸¡à¸¥ หาà¸à¸•à¹‰à¸à¸‡à¸à¸²à¸£à¸£à¸µà¹€à¸‹à¹‡à¸•à¸£à¸«à¸±à¸ªà¸œà¹ˆà¸²à¸™ à¸à¸£à¸¸à¸“าติดต่à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸š" _gallery: my: "à¹à¸à¸¥à¸¥à¸à¸£à¸µà¹ˆà¸‚à¸à¸‡à¸‰à¸±à¸™" liked: "โพสต์ที่ถูà¸à¹ƒà¸ˆ" @@ -1774,23 +1810,23 @@ _plugin: viewSource: "ดูต้นฉบับ" viewLog: "à¹à¸ªà¸”งปูม" _preferencesBackups: - list: "สร้างà¸à¸²à¸£à¸ªà¸³à¸£à¸à¸‡à¸‚้à¸à¸¡à¸¹à¸¥" - saveNew: "บันทึà¸à¸‚้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡à¹ƒà¸«à¸¡à¹ˆ" + list: "à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าที่สำรà¸à¸‡à¹„ว้" + saveNew: "บันทึà¸à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าสำรà¸à¸‡à¹ƒà¸«à¸¡à¹ˆ" loadFile: "โหลดจาà¸à¹„ฟล์" apply: "นำไปใช้à¸à¸±à¸šà¸à¸¸à¸›à¸à¸£à¸“์นี้" save: "บันทึà¸" - inputName: "à¸à¸£à¸¸à¸“าป้à¸à¸™à¸Šà¸·à¹ˆà¸à¸ªà¸³à¸«à¸£à¸±à¸šà¸‚้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡à¸™à¸µà¹‰" + inputName: "à¸à¸£à¸¸à¸“าป้à¸à¸™à¸Šà¸·à¹ˆà¸à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าสำรà¸à¸‡à¸™à¸µà¹‰" cannotSave: "à¸à¸²à¸£à¸šà¸±à¸™à¸—ึà¸à¸¥à¹‰à¸¡à¹€à¸«à¸¥à¸§" - nameAlreadyExists: "มีข้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡à¸Šà¸·à¹ˆà¸ \"{name}\" นี้à¸à¸¢à¸¹à¹ˆà¹à¸¥à¹‰à¸§ à¸à¸£à¸¸à¸“าป้à¸à¸™à¸Šà¸·à¹ˆà¸à¸à¸·à¹ˆà¸™à¸™à¸°" - applyConfirm: "คุณต้à¸à¸‡à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‚้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡ \"{name}\" à¸à¸±à¸šà¸à¸¸à¸›à¸à¸£à¸“์นี้à¸à¸¢à¹ˆà¸²à¸‡à¸‡à¸±à¹‰à¸™à¸ˆà¸£à¸´à¸‡à¸«à¸£à¸ à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าที่มีà¸à¸¢à¸¹à¹ˆà¸‚à¸à¸‡à¸à¸¸à¸›à¸à¸£à¸“์นี้จะถูà¸à¹€à¸‚ียนทับนะ" - saveConfirm: "บันทึà¸à¸‚้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡à¹€à¸›à¹‡à¸™ {name} มั้ย?" - deleteConfirm: "ลบข้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡ {name} มั้ย?" - renameConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¸Šà¸·à¹ˆà¸à¸‚้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡à¸ˆà¸²à¸ “{old}†เป็น “{new}†ใช่ไหม?" - noBackups: "ไม่มีข้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡ สามารถบันทึà¸à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าไคลเà¸à¸™à¸•à¹Œà¸›à¸±à¸ˆà¸ˆà¸¸à¸šà¸±à¸™à¹„ปยังเซิร์ฟเวà¸à¸£à¹Œà¸”้วย “บันทึà¸à¸‚้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡à¹ƒà¸«à¸¡à¹ˆâ€" + nameAlreadyExists: "มีà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าสำรà¸à¸‡à¸Šà¸·à¹ˆà¸ “{name}†à¸à¸¢à¸¹à¹ˆà¹à¸¥à¹‰à¸§ à¸à¸£à¸¸à¸“าป้à¸à¸™à¸Šà¸·à¹ˆà¸à¸à¸·à¹ˆà¸™" + applyConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าสำรà¸à¸‡ “{name}†à¸à¸±à¸šà¸à¸¸à¸›à¸à¸£à¸“์นี้ใช่ไหม? à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าที่มีà¸à¸¢à¸¹à¹ˆà¸šà¸™à¸à¸¸à¸›à¸à¸£à¸“์นี้จะถูà¸à¹€à¸‚ียนทับ" + saveConfirm: "บันทึà¸à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าสำรà¸à¸‡à¹€à¸›à¹‡à¸™ {name} ใช่ไหม?" + deleteConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¸š {name} ใช่ไหม?" + renameConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¸Šà¸·à¹ˆà¸à¸ˆà¸²à¸ “{old}†เป็น “{new}†ใช่ไหม?" + noBackups: "ไม่มีà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าสำรà¸à¸‡ สามารถบันทึà¸à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าไคลเà¸à¸™à¸•à¹Œà¸›à¸±à¸ˆà¸ˆà¸¸à¸šà¸±à¸™à¹„ปยังเซิร์ฟเวà¸à¸£à¹Œà¸”้วย “บันทึà¸à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าสำรà¸à¸‡à¹ƒà¸«à¸¡à¹ˆâ€" createdAt: "สร้างเมื่à¸: {date} {time}" updatedAt: "à¸à¸±à¸›à¹€à¸”ตเมื่à¸: {date} {time}" cannotLoad: "à¸à¸²à¸£à¹‚หลดล้มเหลว" - invalidFile: "รูปà¹à¸šà¸šà¹„ฟล์ไม่ถูà¸à¸•à¹‰à¸à¸‡à¸™à¸°" + invalidFile: "รูปà¹à¸šà¸šà¹„ฟล์ไม่ถูà¸à¸•à¹‰à¸à¸‡" _registry: scope: "สโคป" key: "คีย์" @@ -1841,13 +1877,13 @@ _menuDisplay: hide: "ซ่à¸à¸™" _wordMute: muteWords: "ปิดเสียงคำ" - muteWordsDescription: "คั่นด้วยช่à¸à¸‡à¸§à¹ˆà¸²à¸‡à¸ªà¸³à¸«à¸£à¸±à¸šà¹€à¸‡à¸·à¹ˆà¸à¸™à¹„ข AND หรืà¸à¸”้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่สำหรับเงื่à¸à¸™à¹„ข OR นะ" + muteWordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่à¸à¸™à¹„ข AND, หรืà¸à¸‚ึ้นบรรทัดใหม่สำหรับเงื่à¸à¸™à¹„ข OR" muteWordsDescription2: "ล้à¸à¸¡à¸£à¸à¸šà¸„ีย์เวิร์ดด้วยเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¸—ับเพื่à¸à¹ƒà¸Šà¹‰à¸™à¸´à¸žà¸ˆà¸™à¹Œà¸—ั่วไป" _instanceMute: - instanceMuteDescription: "à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸™à¸µà¹‰à¸ˆà¸°à¸›à¸´à¸”เสียง\"โน้ต/รีโน้ต\"จาà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่à¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸²à¸¢à¸à¸²à¸£ รวมถึงบันทึà¸à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ตà¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸ˆà¸²à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ปิดเสียง" + instanceMuteDescription: "ปิดเสียง “โน้ต/รีโน้ต†ทั้งหมดจาà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่ระบุไว้ รวมถึงโน้ตขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ตà¸à¸šà¸à¸¥à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸ˆà¸²à¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่ถูà¸à¸›à¸´à¸”เสียง" instanceMuteDescription2: "คั่นด้วยà¸à¸²à¸£à¸‚ึ้นบรรทัดใหม่" - title: "ซ่à¸à¸™à¹‚น้ตจาà¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่มีà¸à¸¢à¸¹à¹ˆà¹ƒà¸™à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸" - heading: "รายชื่à¸à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸—ี่ถูà¸à¸›à¸´à¸”เสียง" + title: "ซ่à¸à¸™à¹‚น้ตจาà¸à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸—ี่มีระบุไว้" + heading: "เซิร์ฟเวà¸à¸£à¹Œà¸—ี่ถูà¸à¸›à¸´à¸”เสียง" _theme: explore: "สำรวจธีม" install: "ติดตั้งธีม" @@ -1923,8 +1959,6 @@ _sfx: note: "โน้ต" noteMy: "โน้ตขà¸à¸‡à¸•à¸±à¸§à¹€à¸à¸‡" notification: "à¸à¸²à¸£à¹€à¹€à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" - antenna: "เสาà¸à¸²à¸à¸²à¸¨" - channel: "à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸Šà¹ˆà¸à¸‡" reaction: "เมื่à¸à¹€à¸¥à¸·à¸à¸à¸£à¸µà¹à¸à¸„ชั่น" _soundSettings: driveFile: "ใช้เสียงจาà¸à¹„ดรฟ์" @@ -1932,7 +1966,8 @@ _soundSettings: driveFileTypeWarn: "ไม่รà¸à¸‡à¸£à¸±à¸šà¹„ฟล์นี้" driveFileTypeWarnDescription: "à¸à¸£à¸¸à¸“าเลืà¸à¸à¹„ฟล์เสียง" driveFileDurationWarn: "เสียงยาวเà¸à¸´à¸™à¹„ป" - driveFileDurationWarnDescription: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸ªà¸µà¸¢à¸‡à¸—ี่ยาวà¸à¸²à¸ˆà¸£à¸šà¸à¸§à¸™à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ Misskey, ต้à¸à¸‡à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸à¸«à¸£à¸·à¸à¹„ม่?" + driveFileDurationWarnDescription: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸ªà¸µà¸¢à¸‡à¸—ี่ยาว à¸à¸²à¸ˆà¸£à¸šà¸à¸§à¸™à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ Misskey, ต้à¸à¸‡à¸à¸²à¸£à¸”ำเนินà¸à¸²à¸£à¸•à¹ˆà¸à¹ƒà¸Šà¹ˆà¹„หม?" + driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ à¸à¸£à¸¸à¸“าเปลี่ยนà¹à¸›à¸¥à¸‡à¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่า" _ago: future: "à¸à¸™à¸²à¸„ต" justNow: "เมื่à¸à¸à¸µà¹Šà¸™à¸µà¹‰" @@ -1976,49 +2011,49 @@ _2fa: removeKey: "ลบคีย์ความปลà¸à¸”ภัยà¸à¸à¸" removeKeyConfirm: "ลบข้à¸à¸¡à¸¹à¸¥à¸ªà¸³à¸£à¸à¸‡ {name} มั้ย?" whyTOTPOnlyRenew: "ไม่สามารถลบà¹à¸à¸›à¸•à¸±à¸§à¸£à¸±à¸šà¸£à¸à¸‡à¸„วามถูà¸à¸•à¹‰à¸à¸‡à¹„ด้ตราบใดที่มีà¸à¸²à¸£à¸¥à¸‡à¸—ะเบียนคีย์ความปลà¸à¸”ภัยไว้à¹à¸¥à¹‰à¸§" - renewTOTP: "à¸à¸³à¸«à¸™à¸”ค่าà¹à¸à¸žà¸•à¸±à¸§à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸ªà¸´à¸—ธิ์ใหม่" + renewTOTP: "ตั้งค่าà¹à¸à¸›à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•à¸±à¸§à¸•à¸™" renewTOTPConfirm: "วิธีà¸à¸²à¸£à¹à¸šà¸šà¸™à¸µà¹‰à¸ˆà¸°à¸—à¹à¸²à¹ƒà¸«à¹‰à¸£à¸«à¸±à¸ªà¸¢à¸·à¸™à¸¢à¸±à¸™à¸ˆà¸²à¸à¹à¸à¸žà¸à¹ˆà¸à¸™à¸«à¸™à¹‰à¸²à¸‚à¸à¸‡à¸„ุณหยุดทà¹à¸²à¸‡à¸²à¸™à¹€à¸¥à¸¢à¸™à¸°" renewTOTPOk: "ตั้งค่าคà¸à¸™à¸Ÿà¸´à¸à¹ƒà¸«à¸¡à¹ˆ" renewTOTPCancel: "ไม่เป็นไร" - checkBackupCodesBeforeCloseThisWizard: "โปรดตรวจสà¸à¸šà¸£à¸«à¸±à¸ªà¸ªà¸³à¸£à¸à¸‡à¸”้านล่างà¸à¹ˆà¸à¸™à¸—ี่จะปิดวิซาร์ดนี้" - backupCodes: "รหัสสำรà¸à¸‡à¸‚้à¸à¸¡à¸¹à¸¥" + checkBackupCodesBeforeCloseThisWizard: "โปรดตรวจสà¸à¸šà¸£à¸«à¸±à¸ªà¹à¸šà¹Šà¸à¸à¸±à¸›à¸”้านล่างà¸à¹ˆà¸à¸™à¸—ี่จะปิดวิซาร์ดนี้" + backupCodes: "รหัสà¹à¸šà¹Šà¸à¸à¸±à¸›" backupCodesDescription: "หาà¸à¹à¸à¸›à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•à¸±à¸§à¸•à¸™à¸‚à¸à¸‡à¸„ุณไม่พร้à¸à¸¡à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ คุณสามารถใช้รหัสสำรà¸à¸‡à¸”้านล่างเพื่à¸à¹€à¸‚้าถึงบัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณได้ à¸à¸¢à¹ˆà¸²à¸¥à¸·à¸¡à¹€à¸à¹‡à¸šà¸£à¸«à¸±à¸ªà¹€à¸«à¸¥à¹ˆà¸²à¸™à¸µà¹‰à¹„ว้ในที่ปลà¸à¸”ภัย à¹à¸•à¹ˆà¸¥à¸°à¸£à¸«à¸±à¸ªà¸ªà¸²à¸¡à¸²à¸£à¸–ใช้ได้เพียงครั้งเดียวเท่านั้น" - backupCodeUsedWarning: "มีà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸£à¸«à¸±à¸ªà¸ªà¸³à¸£à¸à¸‡à¹à¸¥à¹‰à¸§ โปรดà¸à¸£à¸¸à¸“าà¸à¸³à¸«à¸™à¸”ค่าà¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸ªà¸´à¸—ธิ์à¹à¸šà¸šà¸ªà¸à¸‡à¸›à¸±à¸ˆà¸ˆà¸±à¸¢à¹‚ดยเร็วที่สุดถ้าหาà¸à¸„ุณยังไม่สามารถใช้งานได้à¸à¸µà¸" - backupCodesExhaustedWarning: "รหัสสำรà¸à¸‡à¸—ั้งหมดถูà¸à¹ƒà¸Šà¹‰à¹à¸¥à¹‰à¸§ ถ้าหาà¸à¸„ุณยังสูà¸à¹€à¸ªà¸µà¸¢à¸à¸²à¸£à¹€à¸‚้าถึงà¹à¸à¸›à¸à¸²à¸£à¸•à¸£à¸§à¸ˆà¸ªà¸à¸šà¸ªà¸´à¸—ธิ์à¹à¸šà¸šà¸ªà¸à¸‡à¸›à¸±à¸ˆà¸ˆà¸±à¸¢à¸„ุณจะยังไม่สามารถเข้าถึงบัà¸à¸Šà¸µà¸™à¸µà¹‰à¹„ด้ à¸à¸£à¸¸à¸“าà¸à¸³à¸«à¸™à¸”ค่าà¸à¸²à¸£à¸£à¸±à¸šà¸£à¸à¸‡à¸„วามถูà¸à¸•à¹‰à¸à¸‡à¸”้วยà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸ªà¸à¸‡à¸Šà¸±à¹‰à¸™" + backupCodeUsedWarning: "รหัสà¹à¸šà¹Šà¸à¸à¸±à¸›à¸–ูà¸à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¹à¸¥à¹‰à¸§ หาà¸à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชันà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•à¸±à¸§à¸•à¸™à¹„ม่สามารถใช้งานได้ ให้รีบทำà¸à¸²à¸£à¸•à¸±à¹‰à¸‡à¸„่าà¹à¸à¸›à¸¯à¹ƒà¸«à¸¡à¹ˆà¹‚ดยเร็วที่สุด" + backupCodesExhaustedWarning: "รหัสà¹à¸šà¹Šà¸à¸à¸±à¸›à¸—ั้งหมดถูà¸à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¹à¸¥à¹‰à¸§ หาà¸à¸¢à¸±à¸‡à¹„ม่สามารถใช้à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชันà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•à¸±à¸§à¸•à¸™à¹„ด้à¸à¹‡à¸ˆà¸°à¹„ม่สามารถเข้าถึงบัà¸à¸Šà¸µà¸™à¸µà¹‰à¹„ด้à¸à¸µà¸à¸•à¹ˆà¸à¹„ป à¸à¸£à¸¸à¸“าลงทะเบียนà¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชันà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•à¸±à¸§à¸•à¸™à¹ƒà¸«à¸¡à¹ˆ" moreDetailedGuideHere: "คลิà¸à¸—ี่นี่เพื่à¸à¸”ูคำà¹à¸™à¸°à¸™à¸³à¹‚ดยละเà¸à¸µà¸¢à¸”" _permissions: - "read:account": "ดูข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณ" - "write:account": "à¹à¸à¹‰à¹„ขข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณ" - "read:blocks": "ดูรายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸à¸‚à¸à¸‡à¸„ุณ" - "write:blocks": "à¹à¸à¹‰à¹„ขรายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸à¸‚à¸à¸‡à¸„ุณ" - "read:drive": "เข้าถึงไฟล์à¹à¸¥à¸°à¹‚ฟลเดà¸à¸£à¹Œà¹ƒà¸™à¹„ดรฟ์ขà¸à¸‡à¸„ุณ" - "write:drive": "à¹à¸à¹‰à¹„ขหรืà¸à¸¥à¸šà¹„ฟล์à¹à¸¥à¸°à¹‚ฟลเดà¸à¸£à¹Œà¹ƒà¸™à¹„ดรฟ์ขà¸à¸‡à¸„ุณ" + "read:account": "ดูข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µ" + "write:account": "à¹à¸à¹‰à¹„ขข้à¸à¸¡à¸¹à¸¥à¸šà¸±à¸à¸Šà¸µ" + "read:blocks": "ดูรายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" + "write:blocks": "à¹à¸à¹‰à¹„ขรายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸šà¸¥à¹‡à¸à¸" + "read:drive": "เข้าถึงไดรฟ์" + "write:drive": "จัดà¸à¸²à¸£à¹„ดรฟ์" "read:favorites": "ดูรายà¸à¸²à¸£à¹‚ปรด" "write:favorites": "à¹à¸à¹‰à¹„ขรายà¸à¸²à¸£à¹‚ปรด" "read:following": "ดูข้à¸à¸¡à¸¹à¸¥à¸§à¹ˆà¸²à¹ƒà¸„รที่คุณติดตาม" "write:following": "ติดตามหรืà¸à¹€à¸¥à¸´à¸à¸•à¸´à¸”ตามบัà¸à¸Šà¸µà¸à¸·à¹ˆà¸™" - "read:messaging": "ดูà¹à¸Šà¸—ขà¸à¸‡à¸„ุณ" + "read:messaging": "ดูà¹à¸Šà¸—" "write:messaging": "เขียนหรืà¸à¸¥à¸šà¸‚้à¸à¸„วามà¹à¸Šà¸—" - "read:mutes": "ดูรายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ปิดเสียงขà¸à¸‡à¸„ุณ" + "read:mutes": "ดูรายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸›à¸´à¸”เสียง" "write:mutes": "à¹à¸à¹‰à¹„ขรายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸›à¸´à¸”เสียง" "write:notes": "เขียนหรืà¸à¸¥à¸šà¹‚น้ต" - "read:notifications": "ดูà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸‚à¸à¸‡à¸„ุณ" - "write:notifications": "จัดà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸‚à¸à¸‡à¸„ุณ" - "read:reactions": "ดูรีà¹à¸à¸„ชั่นขà¸à¸‡à¸„ุณ" - "write:reactions": "à¹à¸à¹‰à¹„ขรีà¹à¸à¸„ชั่นขà¸à¸‡à¸„ุณ" + "read:notifications": "ดูà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" + "write:notifications": "จัดà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" + "read:reactions": "ดูรีà¹à¸à¸„ชั่น" + "write:reactions": "à¹à¸à¹‰à¹„ขรีà¹à¸à¸„ชั่น" "write:votes": "โหวตบนสำรวจความคิดเห็น" "read:pages": "ดูหน้าเพจ" - "write:pages": "à¹à¸à¹‰à¹„ขหรืà¸à¸¥à¸šà¹€à¸žà¸ˆà¸‚à¸à¸‡à¸„ุณ" + "write:pages": "à¹à¸à¹‰à¹„ขหรืà¸à¸¥à¸šà¹€à¸žà¸ˆ" "read:page-likes": "ดูรายà¸à¸²à¸£à¹€à¸žà¸ˆà¸—ี่ถูà¸à¹ƒà¸ˆà¹„ว้" "write:page-likes": "à¹à¸à¹‰à¹„ขรายà¸à¸²à¸£à¹€à¸žà¸ˆà¸—ี่ถูà¸à¹ƒà¸ˆ" - "read:user-groups": "ดูà¸à¸¥à¸¸à¹ˆà¸¡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‚à¸à¸‡à¸„ุณ" - "write:user-groups": "à¹à¸à¹‰à¹„ขหรืà¸à¸¥à¸šà¸à¸¥à¸¸à¹ˆà¸¡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‚à¸à¸‡à¸„ุณ" - "read:channels": "ดูà¹à¸Šà¸™à¹à¸™à¸¥à¸‚à¸à¸‡à¸„ุณ" - "write:channels": "à¹à¸à¹‰à¹„ขà¹à¸Šà¸™à¹à¸™à¸¥à¸‚à¸à¸‡à¸„ุณ" + "read:user-groups": "ดูà¸à¸¥à¸¸à¹ˆà¸¡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" + "write:user-groups": "à¹à¸à¹‰à¹„ขหรืà¸à¸¥à¸šà¸à¸¥à¸¸à¹ˆà¸¡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" + "read:channels": "ดูช่à¸à¸‡" + "write:channels": "à¹à¸à¹‰à¹„ขช่à¸à¸‡" "read:gallery": "ดูà¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¹ˆ" - "write:gallery": "à¹à¸à¹‰à¹„ขà¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¹ˆà¸‚à¸à¸‡à¸„ุณ" - "read:gallery-likes": "ดูรายà¸à¸²à¸£à¹‚พสต์à¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¸—ี่ถูà¸à¹ƒà¸ˆà¹„ว้" - "write:gallery-likes": "à¹à¸à¹‰à¹„ขรายà¸à¸²à¸£à¹‚พสต์à¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¸—ี่ถูà¸à¹ƒà¸ˆà¹„ว้" + "write:gallery": "à¹à¸à¹‰à¹„ขà¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µ" + "read:gallery-likes": "ดูà¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¸—ี่ถูà¸à¹ƒà¸ˆà¹„ว้" + "write:gallery-likes": "จัดà¸à¸²à¸£à¹à¸à¸¥à¹€à¸¥à¸à¸£à¸µà¸—ี่ถูà¸à¹ƒà¸ˆà¹„ว้" "read:flash": "ดู Play" "write:flash": "à¹à¸à¹‰à¹„ข Play" "read:flash-likes": "ดูรายà¸à¸²à¸£ play ที่ถูà¸à¹ƒà¸ˆà¹„ว้" @@ -2027,20 +2062,20 @@ _permissions: "write:admin:delete-account": "ลบบัà¸à¸Šà¸µà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" "write:admin:delete-all-files-of-a-user": "ลบไฟล์ทั้งหมดขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" "read:admin:index-stats": "ดูข้à¸à¸¡à¸¹à¸¥à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸”ัชนีà¸à¸²à¸™à¸‚้à¸à¸¡à¸¹à¸¥" - "read:admin:table-stats": "ดูข้à¸à¸¡à¸¹à¸¥à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸•à¸²à¸£à¸²à¸‡à¸à¸²à¸™à¸‚้à¸à¸¡à¸¹à¸¥" + "read:admin:table-stats": "ดูข้à¸à¸¡à¸¹à¸¥à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸•à¸²à¸£à¸²à¸‡à¹ƒà¸™à¸à¸²à¸™à¸‚้à¸à¸¡à¸¹à¸¥" "read:admin:user-ips": "ดูที่à¸à¸¢à¸¹à¹ˆ IP ขà¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" - "read:admin:meta": "ดูข้à¸à¸¡à¸¹à¸¥à¹€à¸¡à¸•à¸²à¸‚à¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" + "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-moderation-log": "ดูปูมà¸à¸²à¸£à¸„วบคุมดูà¹à¸¥" "read:admin:show-user": "ดูข้à¸à¸¡à¸¹à¸¥à¸ªà¹ˆà¸§à¸™à¸•à¸±à¸§à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" "write:admin:suspend-user": "ระงับผู้ใช้" "write:admin:unset-user-avatar": "ลบà¸à¸§à¸•à¸²à¸£à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" "write:admin:unset-user-banner": "ลบà¹à¸šà¸™à¹€à¸™à¸à¸£à¹Œà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" "write:admin:unsuspend-user": "ยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸£à¸°à¸‡à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" - "write:admin:meta": "จัดà¸à¸²à¸£à¸‚้à¸à¸¡à¸¹à¸¥à¹€à¸¡à¸•à¸²à¸‚à¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" + "write:admin:meta": "จัดà¸à¸²à¸£à¸‚้à¸à¸¡à¸¹à¸¥à¸à¸ ิพันธุ์ขà¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" "write:admin:user-note": "จัดà¸à¸²à¸£à¹‚น้ตà¸à¸²à¸£à¸à¸¥à¸±à¹ˆà¸™à¸à¸£à¸à¸‡" "write:admin:roles": "จัดà¸à¸²à¸£à¸šà¸—บาท" "read:admin:roles": "ดูบทบาท" @@ -2067,14 +2102,14 @@ _permissions: "read:admin:ad": "ดูโฆษณา" "write:invite-codes": "สร้างรหัสเชิà¸" "read:invite-codes": "รับรหัสเชิà¸" - "write:clip-favorite": "ควบคุมà¸à¸²à¸£à¸–ูà¸à¹ƒà¸ˆà¸‚à¸à¸‡à¸„ลิป" - "read:clip-favorite": "ดูà¸à¸²à¸£à¸–ูà¸à¹ƒà¸ˆà¸‚à¸à¸‡à¸„ลิป" + "write:clip-favorite": "จัดà¸à¸²à¸£à¸„ลิปที่ถูà¸à¹ƒà¸ˆ" + "read:clip-favorite": "ดูคลิปที่ถูà¸à¹ƒà¸ˆ" "read:federation": "รับข้à¸à¸¡à¸¹à¸¥à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸ªà¸«à¸žà¸±à¸™à¸˜à¹Œ" "write:report-abuse": "รายงานà¸à¸²à¸£à¸¥à¸°à¹€à¸¡à¸´à¸”" _auth: shareAccessTitle: "à¸à¸²à¸£à¹ƒà¸«à¹‰à¸ªà¸´à¸—ธิ์à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชัน" shareAccess: "คุณต้à¸à¸‡à¸à¸²à¸£à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰ \"{name}\" เข้าถึงบัà¸à¸Šà¸µà¸™à¸µà¹‰à¹€à¸¥à¸¢à¸¡à¸±à¹‰à¸¢?" - shareAccessAsk: "ต้à¸à¸‡à¸à¸²à¸£à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชันนี้เข้าถึงบัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณหรืà¸à¹„ม่?" + shareAccessAsk: "ต้à¸à¸‡à¸à¸²à¸£à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชันนี้เข้าถึงบัà¸à¸Šà¸µà¸‚à¸à¸‡à¸„ุณใช่ไหม?" permission: "{name} ได้ขà¸à¸ªà¸´à¸—ธิ์à¸à¸²à¸£à¹€à¸‚้าถึงดังต่à¸à¹„ปนี้" permissionAsk: "à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชันนี้ขà¸à¸ªà¸´à¸—ธิ์ดังต่à¸à¹„ปนี้" pleaseGoBack: "à¸à¸£à¸¸à¸“าà¸à¸¥à¸±à¸šà¹„ปที่à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชัน" @@ -2097,7 +2132,7 @@ _weekday: saturday: "วันเสาร์" _widgets: profile: "โปรไฟล์" - instanceInfo: "ข้à¸à¸¡à¸¹à¸¥ à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œ" + instanceInfo: "ข้à¸à¸¡à¸¹à¸¥à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" memo: "โน้ตà¹à¸›à¸°" notifications: "à¸à¸²à¸£à¹€à¹€à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" timeline: "ไทม์ไลน์" @@ -2111,7 +2146,7 @@ _widgets: digitalClock: "นาฬิà¸à¸²à¸”ิจิตà¸à¸¥" unixClock: "นาฬิà¸à¸² UNIX" federation: "สหพันธ์" - instanceCloud: "à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸„ลาวด์" + instanceCloud: "à¸à¸¥à¸¸à¹ˆà¸¡à¹€à¸¡à¸†à¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œ" postForm: "à¹à¸šà¸šà¸Ÿà¸à¸£à¹Œà¸¡à¸à¸²à¸£à¹‚พสต์" slideshow: "à¹à¸ªà¸”งภาพนิ่ง" button: "ปุ่ม" @@ -2120,7 +2155,7 @@ _widgets: serverMetric: "ตัวชี้วัดเซิร์ฟเวà¸à¸£à¹Œ" aiscript: " คà¸à¸™à¹‚ซล AiScript" aiscriptApp: "à¹à¸à¸› AiScript" - aichan: "ไà¸" + aichan: "è— (ไà¸)" userList: "รายชื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" _userList: chooseList: "เลืà¸à¸à¸£à¸²à¸¢à¸Šà¸·à¹ˆà¸" @@ -2144,7 +2179,7 @@ _poll: deadlineTime: "เวลา" duration: "ระยะเวลา" votesCount: "{n} คะà¹à¸™à¸™à¹€à¸ªà¸µà¸¢à¸‡" - totalVotes: "{n} คะà¹à¸™à¸™à¹€à¸ªà¸µà¸¢à¸‡à¸—ั้งหมด" + totalVotes: "ทั้งหมด {n} คะà¹à¸™à¸™à¹€à¸ªà¸µà¸¢à¸‡" vote: "โหวต" showResult: "ดูผลลัพธ์" voted: "โหวตà¹à¸¥à¹‰à¸§" @@ -2156,14 +2191,14 @@ _poll: _visibility: public: "สาธารณะ" publicDescription: "โน้ตขà¸à¸‡à¸„ุณจะปราà¸à¸à¹à¸à¹ˆà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ุà¸à¸„น" - home: "หน้าà¹à¸£à¸" - homeDescription: "โพสลงไทม์ไลน์ที่บ้านเท่านั้น" + home: "หน้าหลัà¸" + homeDescription: "โพสต์ลงไทม์ไลน์หลัà¸à¹€à¸—่านั้น" followers: "ผู้ติดตาม" followersDescription: "เฉพาะผู้ติดตามเท่านั้นที่มà¸à¸‡à¹€à¸«à¹‡à¸™à¹„ด้" specified: "ไดเร็ค" specifiedDescription: "ทำให้มà¸à¸‡à¹€à¸«à¹‡à¸™à¹„ด้เฉพาะผู้ใช้ที่ระบุเท่านั้น" - disableFederation: "ไม่มีสหพันธ์" - disableFederationDescription: "à¸à¸¢à¹ˆà¸²à¸ªà¹ˆà¸‡à¹„ปยังà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸à¸·à¹ˆà¸™" + disableFederation: "à¸à¸²à¸£à¸›à¸´à¸”ใช้งานสหพันธ์" + disableFederationDescription: "à¸à¸¢à¹ˆà¸²à¸ªà¹ˆà¸‡à¸‚้à¸à¸¡à¸¹à¸¥à¹„ปยังเซิร์ฟเวà¸à¸£à¹Œà¸à¸·à¹ˆà¸™" _postForm: replyPlaceholder: "ตà¸à¸šà¸à¸¥à¸±à¸šà¹‚น้ตนี้..." quotePlaceholder: "à¸à¹‰à¸²à¸‡à¹‚น้ตนี้..." @@ -2199,37 +2234,37 @@ _exportOrImport: userLists: "รายชื่à¸" excludeMutingUsers: "ยà¸à¹€à¸§à¹‰à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ปิดเสียง" excludeInactiveUsers: "ยà¸à¹€à¸§à¹‰à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ไม่ได้ใช้งาน" - withReplies: "รวมà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่นำเข้าไว้ในไทม์ไลน์" + withReplies: "รวมà¸à¸²à¸£à¸•à¸à¸šà¸à¸¥à¸±à¸šà¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ถูà¸à¸™à¸³à¹€à¸‚้า ลงไทม์ไลน์" _charts: federation: "สหพันธ์" apRequest: "คำขà¸" - usersIncDec: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" + usersIncDec: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" usersTotal: "จำนวนผู้ใช้งานทั้งหมด" activeUsers: "จำนวนผู้ใช้งานที่ยังมีความเคลื่à¸à¸™à¹„หวà¸à¸¢à¸¹à¹ˆ" - notesIncDec: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ต" - localNotesIncDec: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ตท้à¸à¸‡à¸–ิ่น" - remoteNotesIncDec: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ตระยะไà¸à¸¥" + notesIncDec: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ต" + localNotesIncDec: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ตท้à¸à¸‡à¸–ิ่น" + remoteNotesIncDec: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ตระยะไà¸à¸¥" notesTotal: "จำนวนโน้ตทั้งหมด" - filesIncDec: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹„ฟล์" + filesIncDec: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹„ฟล์" filesTotal: "จำนวนไฟล์ทั้งหมด" - storageUsageIncDec: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¹ƒà¸™à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸žà¸·à¹‰à¸™à¸—ี่เà¸à¹‡à¸šà¸‚้à¸à¸¡à¸¹à¸¥" + storageUsageIncDec: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ในà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸žà¸·à¹‰à¸™à¸—ี่เà¸à¹‡à¸šà¸‚้à¸à¸¡à¸¹à¸¥" storageUsageTotal: "à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸žà¸·à¹‰à¸™à¸—ี่เà¸à¹‡à¸šà¸‚้à¸à¸¡à¸¹à¸¥à¸—ั้งหมด" _instanceCharts: requests: "คำขà¸" - users: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" + users: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™" usersTotal: "จำนวนผู้ใช้งานสะสม" - notes: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ต" + notes: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹‚น้ต" notesTotal: "จำนวนโน้ตสะสม" - ff: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸—ี่ติดตาม / ผู้ติดตาม" - ffTotal: "จำนวนผู้ใช้งานที่ติดตามสะสม / ผู้ติดตาม" - cacheSize: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¹ƒà¸™à¸‚นาดขà¸à¸‡à¹à¸„ช" - cacheSizeTotal: "ขนาดà¹à¸„ชรวมที่สะสม" - files: "ความà¹à¸•à¸à¸•à¹ˆà¸²à¸‡à¸‚à¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹„ฟล์" + ff: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตาม/ผู้ติดตาม" + ffTotal: "จำนวนสะสมขà¸à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตาม/ผู้ติดตาม" + cacheSize: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขนาดขà¸à¸‡à¹à¸„ช" + cacheSizeTotal: "ขนาดà¹à¸„ชสะสม" + files: "à¸à¸²à¸£à¹€à¸žà¸´à¹ˆà¸¡à¸¥à¸”ขà¸à¸‡à¸ˆà¸³à¸™à¸§à¸™à¹„ฟล์" filesTotal: "จำนวนไฟล์สะสม" _timelines: - home: "หน้าà¹à¸£à¸" - local: "ในพื้นที่" - social: "โซเชี่ยล" + home: "หน้าหลัà¸" + local: "ท้à¸à¸‡à¸–ิ่น" + social: "โซเชียล" global: "ทั่วโลà¸" _play: new: "สร้าง Play" @@ -2245,7 +2280,7 @@ _play: featured: "เป็นที่นิยม" title: "หัวข้à¸" script: "สคริปต์" - summary: "รายละเà¸à¸µà¸¢à¸”" + summary: "คำà¸à¸˜à¸´à¸šà¸²à¸¢" visibilityDescription: "หาà¸à¸•à¸±à¹‰à¸‡à¸„่าเป็นส่วนตัว มันจะไม่ปราà¸à¸à¹ƒà¸™à¹‚ปรไฟล์à¸à¸µà¸à¸•à¹ˆà¸à¹„ป à¹à¸•à¹ˆà¸œà¸¹à¹‰à¸—ี่ทราบ URL ขà¸à¸‡à¸¡à¸±à¸™à¸ˆà¸°à¸¢à¸±à¸‡à¸ªà¸²à¸¡à¸²à¸£à¸–เข้าถึงได้" _pages: newPage: "สร้างหน้าเพจใหม่" @@ -2333,7 +2368,7 @@ _notification: mention: "à¸à¸¥à¹ˆà¸²à¸§à¸–ึง" reply: "ตà¸à¸šà¸à¸¥à¸±à¸š" renote: "รีโน้ต" - quote: "à¸à¹‰à¸²à¸‡à¸„ำพูด" + quote: "à¸à¹‰à¸²à¸‡à¸à¸´à¸‡" reaction: "รีà¹à¸à¸„ชั่น" pollEnded: "โพลสิ้นสุดà¹à¸¥à¹‰à¸§" receiveFollowRequest: "ได้รับคำร้à¸à¸‡à¸‚à¸à¸•à¸´à¸”ตาม" @@ -2349,6 +2384,7 @@ _deck: alwaysShowMainColumn: "à¹à¸ªà¸”งคà¸à¸¥à¸±à¸¡à¸™à¹Œà¸«à¸¥à¸±à¸à¹€à¸ªà¸¡à¸" columnAlign: "จัดà¹à¸™à¸§à¸„à¸à¸¥à¸±à¸¡à¸™à¹Œ" addColumn: "เพิ่มคà¸à¸¥à¸±à¸¡à¸™à¹Œ" + newNoteNotificationSettings: "ตั้งค่าà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹€à¸¡à¸·à¹ˆà¸à¸¡à¸µà¹‚น้ตใหม่" configureColumn: "ตั้งค่าคà¸à¸¥à¸±à¸¡à¸™à¹Œ" swapLeft: "ขยับไปทางซ้าย" swapRight: "ขยับไปทางขวา" @@ -2373,7 +2409,7 @@ _deck: antenna: "เสาà¸à¸²à¸à¸²à¸¨" list: "รายà¸à¸²à¸£" channel: "ช่à¸à¸‡" - mentions: "พูดถึง" + mentions: "à¸à¸¥à¹ˆà¸²à¸§à¸–ึงคุณ" direct: "ไดเร็à¸à¸•à¹Œ" roleTimeline: "บทบาทไทม์ไลน์" _dialog: @@ -2387,9 +2423,10 @@ _drivecleaner: orderByCreatedAtAsc: "วันที่จาà¸à¸™à¹‰à¸à¸¢à¹„ปหามาà¸" _webhookSettings: createWebhook: "สร้าง Webhook" + modifyWebhook: "à¹à¸à¹‰à¹„ข Webhook" name: "ชื่à¸" secret: "ความลับ" - events: "à¸à¸µà¹€à¸§à¹‰à¸™à¸—์ Webhook" + trigger: "ทริà¸à¹€à¸à¸à¸£à¹Œ" active: "เปิดใช้งาน" _events: follow: "เมื่à¸à¸à¸³à¸¥à¸±à¸‡à¸•à¸´à¸”ตามผู้ใช้" @@ -2399,6 +2436,26 @@ _webhookSettings: renote: "รีโน้ตà¹à¸¥à¹‰à¸§à¹€à¸¡à¸·à¹ˆà¸" reaction: "เมื่à¸à¹„ด้รับรีà¹à¸à¸„ชั่น" mention: "เมื่à¸à¸à¸³à¸¥à¸±à¸‡à¸–ูà¸à¸à¸¥à¹ˆà¸²à¸§à¸–ึง" + _systemEvents: + abuseReport: "เมื่à¸à¸¡à¸µà¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" + abuseReportResolved: "เมื่à¸à¸¡à¸µà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸à¸±à¸šà¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™à¸ˆà¸²à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" + userCreated: "เมื่à¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸–ูà¸à¸ªà¸£à¹‰à¸²à¸‡à¸‚ึ้น" + deleteConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¸š Webhook ใช่ไหม?" +_abuseReport: + _notificationRecipient: + createRecipient: "เพิ่มปลายทางà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™" + modifyRecipient: "à¹à¸à¹‰à¹„ขปลายทางà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™" + recipientType: "ประเภทขà¸à¸‡à¸›à¸¥à¸²à¸¢à¸—างà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™\n" + _recipientType: + mail: "à¸à¸µà¹€à¸¡à¸¥" + webhook: "Webhook" + _captions: + mail: "ส่งà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹„ปยังที่à¸à¸¢à¸¹à¹ˆà¸à¸µà¹€à¸¡à¸¥à¸‚à¸à¸‡à¸œà¸¹à¹‰à¸„วบคุม (เฉพาะเมื่à¸à¹„ด้รับà¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™)" + webhook: "ส่งà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹„ปยัง SystemWebhook ที่à¸à¸³à¸«à¸™à¸” (จะส่งเมื่à¸à¹„ด้รับà¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™à¹à¸¥à¸°à¹€à¸¡à¸·à¹ˆà¸à¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™à¹„ด้รับà¸à¸²à¸£à¹à¸à¹‰à¹„ข)" + keywords: "คีย์เวิร์ด" + notifiedUser: "ผู้ใช้ที่ได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™" + notifiedWebhook: "Webhook ที่ใช้" + deleteConfirm: "ต้à¸à¸‡à¸à¸²à¸£à¸¥à¸šà¸›à¸¥à¸²à¸¢à¸—างà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¹ƒà¸Šà¹ˆà¹„หม?" _moderationLogTypes: createRole: "สร้างบทบาทà¹à¸¥à¹‰à¸§" deleteRole: "ลบบทบาทà¹à¸¥à¹‰à¸§" @@ -2421,9 +2478,9 @@ _moderationLogTypes: deleteGlobalAnnouncement: "ลบประà¸à¸²à¸¨à¸—ั่วโลà¸à¸à¸à¸à¹à¸¥à¹‰à¸§" deleteUserAnnouncement: "ลบประà¸à¸²à¸¨à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸à¸à¸à¹à¸¥à¹‰à¸§" resetPassword: "รีเซ็ตรหัสผ่าน" - suspendRemoteInstance: "ระงับà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" - unsuspendRemoteInstance: "เลิà¸à¸£à¸°à¸‡à¸±à¸šà¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" - updateRemoteInstanceNote: "à¸à¸±à¸›à¹€à¸”ตโน้ตà¸à¸²à¸£à¸à¸¥à¸±à¹ˆà¸™à¸à¸£à¸à¸‡à¸‚à¸à¸‡à¸à¸´à¸™à¸ªà¹à¸•à¸™à¸‹à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹à¸¥à¹‰à¸§" + suspendRemoteInstance: "ระงับเซิร์ฟเวà¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" + unsuspendRemoteInstance: "เลิà¸à¸£à¸°à¸‡à¸±à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥" + updateRemoteInstanceNote: "à¸à¸±à¸›à¹€à¸”ตโน้ตà¸à¸²à¸£à¸à¸¥à¸±à¹ˆà¸™à¸à¸£à¸à¸‡à¸ªà¸³à¸«à¸£à¸±à¸šà¹€à¸‹à¸´à¸£à¹Œà¸Ÿà¹€à¸§à¸à¸£à¹Œà¸£à¸°à¸¢à¸°à¹„à¸à¸¥à¹à¸¥à¹‰à¸§" markSensitiveDriveFile: "ทำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¹„ฟล์ว่ามีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" unmarkSensitiveDriveFile: "ยà¸à¹€à¸¥à¸´à¸à¸—ำเครื่à¸à¸‡à¸«à¸¡à¸²à¸¢à¹„ฟล์ว่ามีเนื้à¸à¸«à¸²à¸¥à¸°à¹€à¸à¸µà¸¢à¸”à¸à¹ˆà¸à¸™" resolveAbuseReport: "รายงานได้รับà¸à¸²à¸£à¹à¸à¹‰à¹„ขà¹à¸¥à¹‰à¸§" @@ -2436,6 +2493,12 @@ _moderationLogTypes: deleteAvatarDecoration: "ลบà¸à¸²à¸£à¸•à¸à¹à¸•à¹ˆà¸‡à¹„à¸à¸„à¸à¸™à¹à¸¥à¹‰à¸§" unsetUserAvatar: "ลบไà¸à¸„à¸à¸™à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" unsetUserBanner: "ลบà¹à¸šà¸™à¹€à¸™à¸à¸£à¹Œà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰" + createSystemWebhook: "สร้าง SystemWebhook" + updateSystemWebhook: "à¸à¸±à¸›à¹€à¸”ต SystemWebhook" + deleteSystemWebhook: "ลบ SystemWebhook" + createAbuseReportNotificationRecipient: "สร้างปลายทางà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™" + updateAbuseReportNotificationRecipient: "à¸à¸±à¸›à¹€à¸”ตปลายทางà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™" + deleteAbuseReportNotificationRecipient: "ลบปลายทางà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•à¸·à¸à¸™à¸à¸²à¸£à¸£à¸²à¸¢à¸‡à¸²à¸™" _fileViewer: title: "รายละเà¸à¸µà¸¢à¸”ไฟล์" type: "ประเภทไฟล์" @@ -2448,10 +2511,10 @@ _externalResourceInstaller: title: "ติดตั้งจาà¸à¹„ซต์ภายนà¸à¸" checkVendorBeforeInstall: "โปรดตรวจสà¸à¸šà¹ƒà¸«à¹‰à¹à¸™à¹ˆà¹ƒà¸ˆà¸§à¹ˆà¸²à¹à¸«à¸¥à¹ˆà¸‡à¹à¸ˆà¸à¸«à¸™à¹ˆà¸²à¸¢à¸¡à¸µà¸„วามน่าเชื่à¸à¸–ืà¸à¸à¹ˆà¸à¸™à¸—ำà¸à¸²à¸£à¸•à¸´à¸”ตั้ง" _plugin: - title: "ต้à¸à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตั้งปลั๊à¸à¸à¸´à¸™à¸™à¸µà¹‰à¸«à¸£à¸·à¸à¹„ม่?" + title: "ต้à¸à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตั้งปลั๊à¸à¸à¸´à¸™à¸™à¸µà¹‰à¹ƒà¸Šà¹ˆà¹„หม?" metaTitle: "ข้à¸à¸¡à¸¹à¸¥à¸ªà¹ˆà¸§à¸™à¹€à¸ªà¸£à¸´à¸¡" _theme: - title: "ต้à¸à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตั้งธีมนี้หรืà¸à¹„ม่?" + title: "ต้à¸à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตั้งธีมนี้ใช่ไหม?" metaTitle: "ข้à¸à¸¡à¸¹à¸¥à¸˜à¸µà¸¡" _meta: base: "โทนสีพื้นà¸à¸²à¸™" @@ -2487,7 +2550,7 @@ _externalResourceInstaller: description: "เà¸à¸´à¸”ปัà¸à¸«à¸²à¸£à¸°à¸«à¸§à¹ˆà¸²à¸‡à¸à¸²à¸£à¸•à¸´à¸”ตั้งธีม à¸à¸£à¸¸à¸“าลà¸à¸‡à¸à¸µà¸à¸„รั้ง. รายละเà¸à¸µà¸¢à¸”ข้à¸à¸œà¸´à¸”พลาดสามารถดูได้ในคà¸à¸™à¹‚ซล Javascript" _dataSaver: _media: - title: "โหลดมีเดีย" + title: "โหลดสื่à¸" description: "à¸à¸±à¸™à¹„ม่ให้ภาพà¹à¸¥à¸°à¸§à¸´à¸”ีโà¸à¹‚หลดโดยà¸à¸±à¸•à¹‚นมัติ à¹à¸•à¸°à¸£à¸¹à¸›à¸ าพ/วิดีโà¸à¸—ี่ซ่à¸à¸™à¸à¸¢à¸¹à¹ˆà¹€à¸žà¸·à¹ˆà¸à¹‚หลด" _avatar: title: "รูปไà¸à¸„à¸à¸™" @@ -2567,3 +2630,8 @@ _mediaControls: pip: "รูปภาพในรูปภาม" playbackRate: "ความเร็วในà¸à¸²à¸£à¹€à¸¥à¹ˆà¸™" loop: "เล่นวนซ้ำ" +_contextMenu: + title: "เมนูเนื้à¸à¸«à¸²" + app: "à¹à¸à¸›à¸žà¸¥à¸´à¹€à¸„ชัน" + appWithShift: "à¹à¸à¸›à¸Ÿà¸¥à¸´à¹€à¸„ชันด้วยปุ่มยà¸à¹à¸„ร่ (Shift)" + native: "UI ขà¸à¸‡à¹€à¸šà¸£à¸²à¸§à¹Œà¹€à¸‹à¸à¸£à¹Œ" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index cf6729a81d8e66cbf65ef32eb04722e010ac1402..266fb3161c606ee4de0a7d6daef14815419d45b0 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ı" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 661ecf19d7d9ebb4db02afd44e1483febaa66da7..36d741d30e4a92e97fb1c25ae2992baf7a6545a3 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1318,8 +1318,6 @@ _sfx: note: "Ðотатки" noteMy: "Мої нотатки" notification: "СповіщеннÑ" - antenna: "Прийом антени" - channel: "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ°Ð½Ð°Ð»Ñƒ" _ago: future: "Майбутнє" justNow: "Щойно" @@ -1622,6 +1620,10 @@ _deck: _webhookSettings: name: "Ім'Ñ" active: "Увімкнено" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "E-mail" _moderationLogTypes: suspend: "Призупинити" resetPassword: "Скинути пароль" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index 306705e42ed29e09d098df9ee8747ff172bfb57c..2023771104bfc30eebb6afeeb99b31069dd8abe1 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" @@ -1089,6 +1089,10 @@ _webhookSettings: _events: renote: "Qayta qayd qilinganda" mention: "Eslanganda" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Email" _moderationLogTypes: suspend: "To'xtatish" resetPassword: "Parolni tiklash" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index cf6eafd69f5947c7829f11e8b5a56ad9d29761de..87b4403c27aa33e29b8c0d0bf08bf8c4c0fc9b03 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -376,6 +376,7 @@ mcaptcha: "mCaptcha" enableMcaptcha: "Báºt mCaptcha" mcaptchaSiteKey: "Khóa của trang" mcaptchaSecretKey: "Khóa bà máºt" +mcaptchaInstanceUrl: "URL mCaptcha máy chủ" recaptcha: "reCAPTCHA" enableRecaptcha: "Báºt reCAPTCHA" recaptchaSiteKey: "Khóa của trang" @@ -426,6 +427,7 @@ moderator: "Kiểm duyệt viên" moderation: "Kiểm duyệt" moderationNote: "Ghi chú kiểm duyệt" addModerationNote: "Thêm ghi chú kiểm duyệt" +moderationLogs: "Nháºt kà quản trị" nUsersMentioned: "Dùng bởi {n} ngÆ°á»i" securityKeyAndPasskey: "Mã bảo máºt・Passkey" securityKey: "Khóa bảo máºt" @@ -458,6 +460,7 @@ retype: "Nháºp lại" noteOf: "Tút của {user}" quoteAttached: "TrÃch dẫn" quoteQuestion: "TrÃch dẫn lại?" +attachAsFileQuestion: "Văn bản ở trong bá»™ nhá»› tạm rất dà i. Bạn có muốn đăng nó dÆ°á»›i dạng má»™t tệp văn bản không?" noMessagesYet: "ChÆ°a có tin nhắn" newMessageExists: "Bạn có tin nhắn má»›i" onlyOneFileCanBeAttached: "Bạn chỉ có thể Ä‘Ãnh kèm má»™t táºp tin" @@ -1285,7 +1288,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" @@ -1326,7 +1329,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" @@ -1480,7 +1483,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" @@ -1559,8 +1562,6 @@ _sfx: note: "Tút" noteMy: "Tút của tôi" notification: "Thông báo" - antenna: "Trạm phát sóng" - channel: "Kênh" _ago: future: "TÆ°Æ¡ng lai" justNow: "Vừa xong" @@ -1917,11 +1918,14 @@ _webhookSettings: createWebhook: "Tạo Webhook" name: "Tên" secret: "Mã bà máºt" - events: "Sá»± kiện Webhook" active: "Äã báºt" _events: reaction: "Khi nháºn được sá»± kiện" mention: "Khi có ngÆ°á»i nhắc tá»›i bạn" +_abuseReport: + _notificationRecipient: + _recipientType: + mail: "Email" _moderationLogTypes: suspend: "Vô hiệu hóa" resetPassword: "Äặt lại máºt khẩu" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 7d1148909ff37c66dbc47628732a45dd8a88083d..1bc1df7930d9910c4395d7ee05ef1a6d87a319dc 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -60,6 +60,7 @@ copyFileId: "å¤åˆ¶æ–‡ä»¶ID" copyFolderId: "å¤åˆ¶æ–‡ä»¶å¤¹ID" copyProfileUrl: "å¤åˆ¶ä¸ªäººèµ„æ–™URL" searchUser: "æœç´¢ç”¨æˆ·" +searchThisUsersNotes: "æœç´¢ç”¨æˆ·å¸–å" reply: "回å¤" loadMore: "查看更多" showMore: "查看更多" @@ -154,6 +155,7 @@ editList: "编辑列表" selectChannel: "选择频é“" selectAntenna: "选择天线" editAntenna: "编辑天线" +createAntenna: "创建天线" selectWidget: "选择å°å·¥å…·" editWidgets: "编辑部件" editWidgetsExit: "完æˆç¼–辑" @@ -180,6 +182,10 @@ addAccount: "æ·»åŠ è´¦æˆ·" reloadAccountsList: "更新账户列表" loginFailed: "登录失败" showOnRemote: "转到所在æœåŠ¡å™¨æ˜¾ç¤º" +continueOnRemote: "转到所在æœåŠ¡å™¨ç»§ç»" +chooseServerOnMisskeyHub: "从 Misskey Hub 选择æœåŠ¡å™¨" +specifyServerHost: "直接输入æœåŠ¡å™¨åŸŸå" +inputHostName: "请输入域å" general: "常规设置" wallpaper: "å£çº¸" setWallpaper: "设置å£çº¸" @@ -190,6 +196,7 @@ followConfirm: "ä½ ç¡®å®šè¦å…³æ³¨ {name} å—?" proxyAccount: "代ç†è´¦æˆ·" proxyAccountDescription: "代ç†è´¦æˆ·æ˜¯åœ¨æŸäº›æƒ…况下替代用户进行远程关注用的账户。 例如说,当用户将一ä½è¿œç¨‹ç”¨æˆ·æ”¾å…¥ä¸€ä¸ªåˆ—表ä¸æ—¶ï¼Œå¦‚果本地æœåŠ¡å™¨ä¸Šæ²¡æœ‰ä»»ä½•äººå…³æ³¨è¿™ä½è¿œç¨‹ç”¨æˆ·ï¼Œåˆ™è¿™ä½è¿œç¨‹ç”¨æˆ·çš„账户活动将ä¸ä¼šè¢«é€åˆ°æœ¬åœ°æœåŠ¡å™¨ä¸Šã€‚作为替代,æ¤æ—¶å°†ä½¿ç”¨ä»£ç†è´¦æˆ·è¿›è¡Œå…³æ³¨ã€‚" host: "主机å" +selectSelf: "选择自己" selectUser: "选择用户" recipient: "收件人" annotation: "注解" @@ -205,6 +212,7 @@ perDay: "æ¯å¤©" stopActivityDelivery: "åœæ¢å‘é€æ´»åŠ¨" blockThisInstance: "阻æ¢æ¤æœåŠ¡å™¨å‘本æœåŠ¡å™¨æŽ¨æµ" silenceThisInstance: "使æœåŠ¡å™¨é™éŸ³" +mediaSilenceThisInstance: "éšè—æ¤æœåŠ¡å™¨çš„媒体文件" operations: "æ“作" software: "软件" version: "版本" @@ -223,9 +231,11 @@ clearQueueConfirmText: "未é€è¾¾çš„帖åå°†ä¸ä¼šè¢«æŠ•é€’。 é€šå¸¸æ— éœ€æ‰§ clearCachedFiles: "清除缓å˜" clearCachedFilesConfirm: "确定è¦æ¸…除所有缓å˜çš„远程文件?" blockedInstances: "被å°é”çš„æœåŠ¡å™¨" -blockedInstancesDescription: "设定è¦å°é”çš„æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œæ¥è¿›è¡Œåˆ†å‰²ã€‚被å°é”çš„æœåŠ¡å™¨å°†æ— 法与本æœåŠ¡å™¨è¿›è¡Œäº¤æ¢é€šè®¯ã€‚å域å也åŒæ ·ä¼šè¢«å°é”。" +blockedInstancesDescription: "设定è¦å°é”çš„æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œåˆ†éš”。被å°é”çš„æœåŠ¡å™¨å°†æ— 法与本æœåŠ¡å™¨è¿›è¡Œäº¤æ¢é€šè®¯ã€‚å域å也åŒæ ·ä¼šè¢«å°é”。" silencedInstances: "被é™éŸ³çš„æœåŠ¡å™¨" -silencedInstancesDescription: "设置è¦é™éŸ³çš„æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œç¬¦åˆ†éš”。被é™éŸ³çš„æœåŠ¡å™¨å†…所有的账户将默认处于「é™éŸ³ã€çŠ¶æ€ï¼Œä»…能å‘é€å…³æ³¨è¯·æ±‚,并且在未关注状æ€ä¸‹æ— 法æåŠæœ¬åœ°è´¦æˆ·ã€‚被阻æ¢çš„实例ä¸å—å½±å“。" +silencedInstancesDescription: "设置è¦é™éŸ³çš„æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œåˆ†éš”。被é™éŸ³çš„æœåŠ¡å™¨å†…所有的账户将默认处于「é™éŸ³ã€çŠ¶æ€ï¼Œä»…能å‘é€å…³æ³¨è¯·æ±‚,并且在未关注状æ€ä¸‹æ— 法æåŠæœ¬åœ°è´¦æˆ·ã€‚被阻æ¢çš„实例ä¸å—å½±å“。" +mediaSilencedInstances: "å·²éšè—媒体文件的æœåŠ¡å™¨" +mediaSilencedInstancesDescription: "设置è¦éšè—媒体文件的æœåŠ¡å™¨ï¼Œä»¥æ¢è¡Œåˆ†éš”。被设置为éšè—媒体文件æœåŠ¡å™¨å†…所有账å·çš„文件å‡æŒ‰ç…§ã€Œæ•æ„Ÿå†…容ã€å¤„ç†ï¼Œä¸”å°†æ— æ³•ä½¿ç”¨è‡ªå®šä¹‰è¡¨æƒ…ç¬¦å·ã€‚被阻æ¢çš„实例ä¸å—å½±å“。" muteAndBlock: "é™éŸ³/拉黑" mutedUsers: "å·²é™éŸ³ç”¨æˆ·" blockedUsers: "已拉黑的用户" @@ -351,7 +361,7 @@ instanceName: "æœåŠ¡å™¨å称" instanceDescription: "æœåŠ¡å™¨ç®€ä»‹" maintainerName: "管ç†å‘˜å称" maintainerEmail: "管ç†å‘˜ç”µå邮箱" -tosUrl: "æœåŠ¡æ¡æ¬¾ URL" +tosUrl: "æœåŠ¡æ¡æ¬¾åœ°å€" thisYear: "今年" thisMonth: "本月" today: "今天" @@ -433,8 +443,8 @@ administrator: "管ç†å‘˜" token: "Token (令牌)" 2fa: "åŒå› ç´ è®¤è¯" setupOf2fa: "设置åŒå› ç´ è®¤è¯" -totp: "身份验è¯åº”用" -totpDescription: "使用认è¯åº”用输入一次性密ç 。" +totp: "验è¯å™¨" +totpDescription: "使用验è¯å™¨è¾“入一次性密ç " moderator: "监察员" moderation: "管ç†" moderationNote: "管ç†ç¬”è®°" @@ -477,6 +487,7 @@ noMessagesYet: "现在没有新的èŠå¤©" newMessageExists: "æ–°ä¿¡æ¯" onlyOneFileCanBeAttached: "åªèƒ½æ·»åŠ 一个附件" signinRequired: "请先登录" +signinOrContinueOnRemote: "è‹¥è¦ç»§ç»ï¼Œéœ€è¦è½¬åˆ°æ‚¨æ‰€ä½¿ç”¨çš„实例,或者在æ¤æœåŠ¡å™¨ä¸Šæ³¨å†Œæˆ–登录。" invitations: "邀请" invitationCode: "邀请ç " checking: "æ£åœ¨ç¡®è®¤" @@ -837,6 +848,7 @@ administration: "管ç†" accounts: "账户" switch: "切æ¢" noMaintainerInformationWarning: "管ç†äººå‘˜ä¿¡æ¯æœªè®¾ç½®ã€‚" +noInquiryUrlWarning: "尚未设置è”络地å€ã€‚" noBotProtectionWarning: "Bot 防御未设置。" configure: "设置" postToGallery: "å‘é€åˆ°å›¾åº“" @@ -848,7 +860,7 @@ shareWithNote: "在帖åä¸åˆ†äº«" ads: "广告" expiration: "截æ¢æ—¶é—´" startingperiod: "开始时间" -memo: "便笺" +memo: "备注" priority: "优先级" high: "高" middle: "ä¸" @@ -1100,6 +1112,8 @@ preservedUsernames: "ä¿ç•™çš„用户å" preservedUsernamesDescription: "列出需è¦ä¿ç•™çš„用户å,使用æ¢è¡Œæ¥ä½œä¸ºåˆ†å‰²ã€‚被指定的用户ååœ¨å»ºç«‹è´¦æˆ·æ—¶æ— æ³•ä½¿ç”¨ï¼Œä½†ç”±ç®¡ç†å‘˜æ‰€åˆ›å»ºçš„账户ä¸å—该é™åˆ¶ã€‚æ¤å¤–,现有的账户也ä¸ä¼šå—到影å“。" createNoteFromTheFile: "从文件创建帖å" archive: "å½’æ¡£" +archived: "已归档" +unarchive: "å–消归档" channelArchiveConfirmTitle: "è¦å°† {name} å½’æ¡£å—?" channelArchiveConfirmDescription: "å½’æ¡£åŽï¼Œåœ¨é¢‘é“列表与æœç´¢ç»“æžœä¸ä¸ä¼šæ˜¾ç¤ºï¼Œä¹Ÿæ— 法å‘布新的贴文。" thisChannelArchived: "该频é“已被归档。" @@ -1110,6 +1124,9 @@ preventAiLearning: "æ‹’ç»æŽ¥å—生æˆå¼ AI çš„å¦ä¹ " preventAiLearningDescription: "è¦æ±‚æ–‡ç« ç”Ÿæˆ AI 或图åƒç”Ÿæˆ AI ä¸èƒ½å¤Ÿä»¥å‘布的帖å和图åƒç‰å†…容作为å¦ä¹ 对象。这是通过在 HTML å“应ä¸åŒ…å« noai æ ‡å¿—æ¥å®žçŽ°çš„,这ä¸èƒ½å®Œå…¨é˜»æ¢ AI å¦ä¹ ä½ çš„å‘布内容,并ä¸æ˜¯æ‰€æœ‰ AI 都会éµå®ˆè¿™ç±»è¯·æ±‚。" options: "选项" specifyUser: "用户指定" +lookupConfirm: "确定查询?" +openTagPageConfirm: "确定打开è¯é¢˜æ ‡ç¾é¡µé¢ï¼Ÿ" +specifyHost: "指定主机å" failedToPreviewUrl: "æ— æ³•é¢„è§ˆ" update: "æ›´æ–°" rolesThatCanBeUsedThisEmojiAsReaction: "å¯ä»¥ä½¿ç”¨è¡¨æƒ…作为回应的角色" @@ -1180,7 +1197,7 @@ externalServices: "外部æœåŠ¡" sourceCode: "æºä»£ç " sourceCodeIsNotYetProvided: "还未æä¾›æºä»£ç 。è¦è§£å†³æ¤é—®é¢˜è¯·è”系管ç†å‘˜ã€‚" repositoryUrl: "仓库地å€" -repositoryUrlDescription: "è‹¥æºä»£ç 所在的仓库是公开的,请填入对应的 URLã€‚è‹¥æ˜¯æŒ‰åŽŸæ ·ä½¿ç”¨ Misskeyï¼ˆå¹¶æœªè¿½åŠ æˆ–è€…ä¿®æ”¹ä»£ç )的情况请填入 https://github.com/misskey-dev/misskey。" +repositoryUrlDescription: "è‹¥æºä»£ç 所在的仓库是公开的,请填入对应的 URLã€‚è‹¥å¹¶æœªè¿½åŠ æˆ–è€…ä¿®æ”¹ Misskey 的代ç ,请填入 https://github.com/misskey-dev/misskey。" repositoryUrlOrTarballRequired: "若仓库并未公开,则需è¦æä¾› tarball 作为替代。详情请看 .config/example.yml。" feedback: "å馈" feedbackUrl: "å馈地å€" @@ -1241,6 +1258,11 @@ keepOriginalFilenameDescription: "若关é—æ¤è®¾ç½®ï¼Œä¸Šä¼ 文件时文件å noDescription: "没有æè¿°" alwaysConfirmFollow: "总是确认关注" inquiry: "è”系我们" +tryAgain: "请å†è¯•ä¸€æ¬¡" +confirmWhenRevealingSensitiveMedia: "显示æ•æ„Ÿå†…容å‰éœ€è¦ç¡®è®¤" +sensitiveMediaRevealConfirm: "这是æ•æ„Ÿå†…容。是å¦æ˜¾ç¤ºï¼Ÿ" +createdLists: "已创建的列表" +createdAntennas: "已创建的天线" _delivery: status: "投递状æ€" stop: "åœæ¢æŠ•é€’" @@ -1375,6 +1397,8 @@ _serverSettings: fanoutTimelineDescription: "当å¯ç”¨æ—¶ï¼Œå¯æ˜¾è‘—æ高获å–å„ç§æ—¶é—´çº¿æ—¶çš„性能,并å‡è½»æ•°æ®åº“çš„è´Ÿè·ã€‚但是相对的 Redis 的内å˜ä½¿ç”¨é‡å°†ä¼šå¢žåŠ 。如果æœåŠ¡å™¨çš„内å˜ä¸æ˜¯å¾ˆå¤§ï¼Œåˆæˆ–者è¿è¡Œä¸ç¨³å®šçš„è¯å¯ä»¥æŠŠå®ƒå…³æŽ‰ã€‚" fanoutTimelineDbFallback: "回退到数æ®åº“" fanoutTimelineDbFallbackDescription: "当å¯ç”¨æ—¶ï¼Œè‹¥æ—¶é—´çº¿æœªè¢«ç¼“å˜ï¼Œåˆ™å°†é¢å¤–查询数æ®åº“。ç¦ç”¨è¯¥åŠŸèƒ½å¯é€šè¿‡ä¸æ‰§è¡Œå›žé€€å¤„ç†è¿›ä¸€æ¥å‡å°‘æœåŠ¡å™¨è´Ÿè½½ï¼Œä½†ä¼šé™åˆ¶å¯æ£€ç´¢çš„时间线范围。" + inquiryUrl: "è”络地å€" + inquiryUrlDescription: "用æ¥æŒ‡å®šè¯¸å¦‚å‘æœåŠ¡è¿è¥å•†å’¨è¯¢çš„论å›åœ°å€ï¼Œæˆ–记载了è¿è¥å•†è”系方å¼ä¹‹ç±»çš„网页地å€ã€‚" _accountMigration: moveFrom: "从别的账å·è¿ç§»åˆ°æ¤è´¦æˆ·" moveFromSub: "为å¦ä¸€ä¸ªè´¦æˆ·å»ºç«‹åˆ«å" @@ -1641,6 +1665,7 @@ _achievements: _bubbleGameDoubleExplodingHead: title: "两个🤯" description: "ä½ åˆæˆå‡ºäº†2个游æˆé‡Œæœ€å¤§çš„Emoji" + flavor: "" _role: new: "创建角色" edit: "编辑角色" @@ -1670,8 +1695,8 @@ _role: descriptionOfIsExplorable: "打开åŽå°†å…¬å¼€è§’色时间线。如果角色ä¸æ˜¯å…¬å¼€çš„ï¼Œå°±æ— æ³•å…¬å¼€æ—¶é—´çº¿ã€‚" displayOrder: "显示顺åº" descriptionOfDisplayOrder: "æ•°å—越大,显示ä½ç½®è¶Šé å‰ã€‚" - canEditMembersByModerator: "å…许监察者编辑æˆå‘˜" - descriptionOfCanEditMembersByModerator: "如果选ä¸ï¼Œç›‘察者和管ç†å‘˜éƒ½èƒ½å¤Ÿä¸ºç”¨æˆ·åˆ†é…/å–消分é…角色。如果未选ä¸ï¼Œåˆ™åªæœ‰ç®¡ç†å‘˜å¯ä»¥æ‰§è¡Œæ¤æ“作。" + canEditMembersByModerator: "å…许监察员编辑æˆå‘˜" + descriptionOfCanEditMembersByModerator: "如果选ä¸ï¼Œç›‘察员和管ç†å‘˜éƒ½èƒ½å¤Ÿä¸ºç”¨æˆ·åˆ†é…/å–消分é…角色。如果未选ä¸ï¼Œåˆ™åªæœ‰ç®¡ç†å‘˜å¯ä»¥æ‰§è¡Œæ¤æ“作。" priority: "优先级" _priority: low: "低" @@ -1690,6 +1715,7 @@ _role: canManageAvatarDecorations: "管ç†å¤´åƒæŒ‚件" driveCapacity: "网盘容é‡" alwaysMarkNsfw: "æ€»æ˜¯å°†æ–‡ä»¶æ ‡è®°ä¸º NSFW" + canUpdateBioMedia: "å¯ä»¥æ›´æ–°å¤´åƒå’Œæ¨ªå¹…" pinMax: "帖å置顶数é‡é™åˆ¶" antennaMax: "å¯åˆ›å»ºçš„最大天线数é‡" wordMuteMax: "å±è”½è¯çš„å—æ•°é™åˆ¶" @@ -1933,8 +1959,6 @@ _sfx: note: "帖å" noteMy: "我的帖å" notification: "通知" - antenna: "天线接收" - channel: "频é“通知" reaction: "选择回应时" _soundSettings: driveFile: "使用网盘内的音频" @@ -1943,6 +1967,7 @@ _soundSettings: driveFileTypeWarnDescription: "请选择音频文件" driveFileDurationWarn: "音频过长" driveFileDurationWarnDescription: "使用长音频å¯èƒ½ä¼šå½±å“ Misskey 的使用。å³ä½¿è¿™æ ·ä¹Ÿè¦ç»§ç»å—?" + driveFileError: "æ— æ³•è¯»å–声音。请更改设置。" _ago: future: "未æ¥" justNow: "最近" @@ -1969,7 +1994,7 @@ _time: day: "æ—¥" _2fa: alreadyRegistered: "æ¤è®¾å¤‡å·²è¢«æ³¨å†Œ" - registerTOTP: "开始设置认è¯åº”用" + registerTOTP: "开始设置验è¯å™¨" step1: "首先,在您的设备上安装验è¯åº”用,例如 {a} 或 {b}。" step2: "然åŽï¼Œæ‰«æå±å¹•ä¸Šæ˜¾ç¤ºçš„二维ç 。" step2Uri: "如果使用桌é¢åº”用程åºçš„è¯ï¼Œè¯·è¾“入下é¢çš„ URI" @@ -1978,23 +2003,23 @@ _2fa: setupCompleted: "设置完æˆ" step4: "从现在开始,任何登录æ“作都将è¦æ±‚您æ供动æ€å£ä»¤ã€‚" securityKeyNotSupported: "您的æµè§ˆå™¨ä¸æ”¯æŒå®‰å…¨å¯†é’¥ã€‚" - registerTOTPBeforeKey: "è¦æ³¨å†Œå®‰å…¨å¯†é’¥æˆ– Passkey,请先设置验è¯å™¨åº”用程åºã€‚" + registerTOTPBeforeKey: "è¦æ³¨å†Œå®‰å…¨å¯†é’¥æˆ– Passkey,请先设置验è¯å™¨ã€‚" securityKeyInfo: "注册兼容 WebAuthn çš„å¯†é’¥ï¼Œä¾‹å¦‚æ”¯æŒ FIDO2 的硬件安全密钥ã€è®¾å¤‡ä¸Šçš„生物识别功能ã€PIN ç ä»¥åŠ Passkey ç‰ã€‚" registerSecurityKey: "注册安全密钥或 Passkey" securityKeyName: "输入密钥å称" tapSecurityKey: "请按照æµè§ˆå™¨è¯´æ˜Žæ“作æ¥æ³¨å†Œå®‰å…¨å¯†é’¥æˆ– Passkey。" removeKey: "åˆ é™¤å®‰å…¨å¯†é’¥" removeKeyConfirm: "您确定è¦åˆ 除 {name} å—?" - whyTOTPOnlyRenew: "å¦‚æžœæ³¨å†Œäº†å®‰å…¨å¯†é’¥ï¼Œåˆ™æ— æ³•å–消验è¯å™¨åº”用程åºä¸Šçš„设置。" - renewTOTP: "é‡ç½®éªŒè¯å™¨åº”用程åº" - renewTOTPConfirm: "当å‰éªŒè¯å™¨åº”用程åºçš„验è¯ç å°†ä¸å†æœ‰æ•ˆ" + whyTOTPOnlyRenew: "å½“æ³¨å†Œäº†å®‰å…¨å¯†é’¥æ—¶ï¼Œæ— æ³•å–消使用验è¯å™¨ã€‚" + renewTOTP: "é‡ç½®éªŒè¯å™¨" + renewTOTPConfirm: "当å‰éªŒè¯å™¨çš„验è¯ç åŠå¤‡ç”¨ä»£ç 已失效" renewTOTPOk: "é‡æ–°é…ç½®" renewTOTPCancel: "ä¸ç”¨ï¼Œè°¢è°¢" checkBackupCodesBeforeCloseThisWizard: "在关é—æ¤çª—å£å‰ï¼Œè¯·ç¡®è®¤ä¸‹é¢çš„备用代ç " backupCodes: "备用代ç " - backupCodesDescription: "å¦‚æžœæ— æ³•ä½¿ç”¨è®¤è¯åº”用,å¯ä»¥ä½¿ç”¨ä»¥ä¸‹çš„备用代ç æ¥è®¿é—®è´¦æˆ·ã€‚请务必将这些代ç ä¿å˜åœ¨å®‰å…¨çš„地方。æ¯ä¸ªä»£ç ä»…å¯ä½¿ç”¨ä¸€æ¬¡ã€‚" - backupCodeUsedWarning: "已使用备用代ç ã€‚å¦‚æžœæ— æ³•ä½¿ç”¨è®¤è¯åº”用,请尽快é‡æ–°è®¾å®šã€‚" - backupCodesExhaustedWarning: "已使用完所有的备用代ç ã€‚å¦‚æžœæ— æ³•ä½¿ç”¨è®¤è¯åº”ç”¨ï¼Œå°†æ— æ³•å†è®¿é—®æ‚¨çš„账户。请å†æ¬¡è®¾å®šè®¤è¯åº”用。" + backupCodesDescription: "å¦‚æžœæ— æ³•ä½¿ç”¨éªŒè¯å™¨ï¼Œå¯ä»¥ä½¿ç”¨ä»¥ä¸‹çš„备用代ç æ¥è®¿é—®è´¦æˆ·ã€‚请务必将这些代ç ä¿å˜åœ¨å®‰å…¨çš„地方。æ¯ä¸ªä»£ç ä»…å¯ä½¿ç”¨ä¸€æ¬¡ã€‚" + backupCodeUsedWarning: "已使用备用代ç 。若验è¯å™¨æ— 法使用,请尽快é‡ç½®éªŒè¯å™¨ã€‚" + backupCodesExhaustedWarning: "已使用完所有的备用代ç 。若验è¯å™¨æ— æ³•ä½¿ç”¨ï¼Œåˆ™æ— æ³•å†è®¿é—®æ‚¨çš„账户。请é‡ç½®éªŒè¯å™¨ã€‚" moreDetailedGuideHere: "æ¤å¤„为详细指å—" _permissions: "read:account": "查看账户信æ¯" @@ -2291,6 +2316,7 @@ _pages: eyeCatchingImageSet: "设置å°é¢å›¾ç‰‡" eyeCatchingImageRemove: "åˆ é™¤å°é¢å›¾ç‰‡" chooseBlock: "æ·»åŠ å—" + enterSectionTitle: "输入会è¯æ ‡é¢˜" selectType: "选择类型" contentBlocks: "内容" inputBlocks: "输入" @@ -2398,9 +2424,10 @@ _drivecleaner: orderByCreatedAtAsc: "æŒ‰æ·»åŠ æ—¥æœŸé™åºæŽ’列" _webhookSettings: createWebhook: "创建 Webhook" + modifyWebhook: "编辑 webhook" name: "å称" secret: "密钥" - events: "何时è¿è¡Œ Webhook" + trigger: "触å‘器" active: "å·²å¯ç”¨" _events: follow: "关注时" @@ -2410,6 +2437,26 @@ _webhookSettings: renote: "被转å‘æ—¶" reaction: "被回应时" mention: "被æåŠæ—¶" + _systemEvents: + abuseReport: "当收到举报时" + abuseReportResolved: "当举报被处ç†æ—¶" + userCreated: "当用户被创建时" + deleteConfirm: "è¦åˆ 除 webhook å—?" +_abuseReport: + _notificationRecipient: + createRecipient: "新建举报通知" + modifyRecipient: "编辑举报通知" + recipientType: "通知类型" + _recipientType: + mail: "邮箱" + webhook: "Webhook" + _captions: + mail: "当收到新举报时,å‘æŒæœ‰ç›‘察员æƒé™çš„用户å‘é€é€šçŸ¥é‚®ä»¶" + webhook: "当收到新举报åŠä¸¾æŠ¥è¢«å¤„ç†æ—¶ï¼Œä½¿ç”¨æŒ‡å®šçš„ SystemWebhook å‘é€é€šçŸ¥" + keywords: "关键å—" + notifiedUser: "通知的用户" + notifiedWebhook: "使用的 webhook" + deleteConfirm: "è¦åˆ 除通知å—?" _moderationLogTypes: createRole: "创建角色" deleteRole: "åˆ é™¤è§’è‰²" @@ -2447,6 +2494,16 @@ _moderationLogTypes: deleteAvatarDecoration: "åˆ é™¤å¤´åƒæŒ‚件" unsetUserAvatar: "清除用户头åƒ" unsetUserBanner: "清除用户横幅" + createSystemWebhook: "新建了 SystemWebhook" + updateSystemWebhook: "更新了 SystemWebhook" + deleteSystemWebhook: "åˆ é™¤äº† SystemWebhook" + createAbuseReportNotificationRecipient: "新建了举报通知" + updateAbuseReportNotificationRecipient: "更新了举报通知" + deleteAbuseReportNotificationRecipient: "åˆ é™¤äº†ä¸¾æŠ¥é€šçŸ¥" + deleteAccount: "åˆ é™¤äº†è´¦æˆ·" + deletePage: "åˆ é™¤äº†é¡µé¢" + deleteFlash: "åˆ é™¤äº† Play" + deleteGalleryPost: "åˆ é™¤äº†å›¾åº“ç¨¿ä»¶" _fileViewer: title: "文件信æ¯" type: "文件类型" @@ -2578,3 +2635,8 @@ _mediaControls: pip: "ç”»ä¸ç”»" playbackRate: "æ’放速度" loop: "循环æ’放" +_contextMenu: + title: "上下文èœå•" + app: "应用" + appWithShift: "Shift 键应用" + native: "æµè§ˆå™¨çš„用户界é¢" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 78116254bad87e393fe8c56084fd43ada50a53bd..80ad2eaf027c31a3589a028bb82d5d89ff990582 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -60,6 +60,7 @@ copyFileId: "複製檔案 ID" copyFolderId: "複製資料夾ID" copyProfileUrl: "複製個人資料網å€" searchUser: "æœå°‹ä½¿ç”¨è€…" +searchThisUsersNotes: "æœå°‹é€™å€‹ä½¿ç”¨è€…的貼文" reply: "回覆" loadMore: "載入更多" showMore: "載入更多" @@ -154,6 +155,7 @@ editList: "編輯清單" selectChannel: "é¸æ“‡é »é“" selectAntenna: "é¸æ“‡å¤©ç·š" editAntenna: "編輯天線" +createAntenna: "建立天線" selectWidget: "é¸æ“‡å°å·¥å…·" editWidgets: "編輯å°å·¥å…·" editWidgetsExit: "完æˆ" @@ -180,6 +182,10 @@ addAccount: "新增帳戶" reloadAccountsList: "更新帳戶清單的資訊" loginFailed: "登入失敗" showOnRemote: "轉到所在實例顯示" +continueOnRemote: "在é 端伺æœå™¨ç¹¼çºŒ" +chooseServerOnMisskeyHub: "從 Misskey Hub é¸æ“‡ä¼ºæœå™¨" +specifyServerHost: "直接指定伺æœå™¨ç¶²åŸŸ" +inputHostName: "請輸入域å" general: "一般" wallpaper: "桌布" setWallpaper: "è¨å®šæ¡Œå¸ƒ" @@ -190,6 +196,7 @@ followConfirm: "ä½ çœŸçš„è¦è¿½éš¨{name}嗎?" proxyAccount: "代ç†å¸³æˆ¶" proxyAccountDescription: "代ç†å¸³æˆ¶æ˜¯åœ¨ç‰¹å®šæ¢ä»¶ä¸‹å……當é 端追隨者的帳戶。例如,當使用者新增é 端使用者至其列表時,若沒有本地使用者追隨該é 端使用者,則其活動將ä¸æœƒå‚³é€è‡³ä¼ºæœå™¨ï¼Œæ¤æ™‚便會由代ç†å¸³æˆ¶ä»£ç‚ºè¿½éš¨ä»¥è§£æ±ºå•é¡Œã€‚" host: "主機" +selectSelf: "é¸æ“‡è‡ªå·±" selectUser: "é¸å–使用者" recipient: "收件人" annotation: "註解" @@ -205,6 +212,7 @@ perDay: "æ¯æ—¥" stopActivityDelivery: "åœæ¢ç™¼é€æ´»å‹•" blockThisInstance: "å°éŽ–æ¤ä¼ºæœå™¨" silenceThisInstance: "ç¦è¨€æ¤ä¼ºæœå™¨" +mediaSilenceThisInstance: "將這個伺æœå™¨çš„媒體è¨ç‚ºç¦è¨€" operations: "æ“作" software: "軟體" version: "版本" @@ -226,6 +234,8 @@ blockedInstances: "å·²å°éŽ–的伺æœå™¨" blockedInstancesDescription: "è«‹é€è¡Œè¼¸å…¥éœ€è¦å°éŽ–的伺æœå™¨ã€‚å·²å°éŽ–的伺æœå™¨å°‡ç„¡æ³•èˆ‡æœ¬ä¼ºæœå™¨é€²è¡Œé€šè¨Šã€‚" silencedInstances: "被ç¦è¨€çš„伺æœå™¨" silencedInstancesDescription: "è¨å®šè¦ç¦è¨€çš„伺æœå™¨ä¸»æ©Ÿå稱,以æ›è¡Œåˆ†éš”。隸屬於ç¦è¨€ä¼ºæœå™¨çš„所有帳戶都將被視為「ç¦è¨€å¸³æˆ¶ã€ï¼Œåªèƒ½ç™¼å‡ºã€Œè¿½éš¨è«‹æ±‚ã€ï¼Œè€Œä¸”無法æåŠæœªè¿½éš¨çš„本地帳戶。這ä¸æœƒå½±éŸ¿å·²å°éŽ–的實例。" +mediaSilencedInstances: "媒體被ç¦è¨€çš„伺æœå™¨" +mediaSilencedInstancesDescription: "è¨å®šæ‚¨æƒ³è¦å°åª’é«”è¨å®šç¦è¨€çš„伺æœå™¨ï¼Œä»¥æ›è¡Œç¬¦è™Ÿå€éš”。來自被媒體ç¦è¨€çš„伺æœå™¨æ‰€å±¬å¸³æˆ¶çš„所有檔案都會被視為æ•æ„Ÿæª”案,且自訂表情符號ä¸èƒ½ä½¿ç”¨ã€‚被å°éŽ–的伺æœå™¨ä¸å—影響。" muteAndBlock: "éœéŸ³å’Œå°éŽ–" mutedUsers: "被éœéŸ³çš„使用者" blockedUsers: "被å°éŽ–的使用者" @@ -243,10 +253,10 @@ noCustomEmojis: "沒有自訂的表情符號" noJobs: "沒有任務" federating: "è¯é‚¦é‹ä½œä¸" blocked: "å·²å°éŽ–" -suspended: "å·²å‡çµ" +suspended: "åœæ¢ç™¼é€" all: "全部" subscribing: "訂閱ä¸" -publishing: "ç›´æ’ä¸" +publishing: "發é€ä¸" notResponding: "沒有回應" instanceFollowing: "追隨的伺æœå™¨" instanceFollowers: "伺æœå™¨çš„追隨者" @@ -343,7 +353,7 @@ reload: "é‡æ–°æ•´ç†" doNothing: "無視" reloadConfirm: "確定è¦é‡æ–°æ•´ç†å—Žï¼Ÿ" watch: "關注" -unwatch: "å–消追隨" +unwatch: "å–消關注" accept: "接å—" reject: "拒絕" normal: "æ£å¸¸" @@ -477,6 +487,7 @@ noMessagesYet: "沒有訊æ¯" newMessageExists: "有新的訊æ¯" onlyOneFileCanBeAttached: "åªèƒ½åŠ 入一個附件" signinRequired: "請先登入" +signinOrContinueOnRemote: "è‹¥è¦ç¹¼çºŒï¼Œéœ€å‰å¾€æ‚¨æ‰€åœ¨çš„伺æœå™¨ï¼Œæˆ–者註冊並登入æ¤ä¼ºæœå™¨" invitations: "邀請" invitationCode: "邀請碼" checking: "確èªä¸" @@ -837,6 +848,7 @@ administration: "管ç†" accounts: "帳戶" switch: "切æ›" noMaintainerInformationWarning: "尚未è¨å®šç®¡ç†å“¡è¨Šæ¯ã€‚" +noInquiryUrlWarning: "尚未è¨å®šè¯çµ¡è¡¨å–®ç¶²å€ã€‚" noBotProtectionWarning: "尚未è¨å®š Bot 防è·ã€‚" configure: "è¨å®š" postToGallery: "發佈到相簿" @@ -1100,6 +1112,8 @@ preservedUsernames: "ä¿ç•™çš„使用者å稱" preservedUsernamesDescription: "æ›è¡Œåˆ—舉è¦ä¿ç•™çš„使用者å稱。æ¤è™•å‡ºç¾çš„å稱將在註冊時ç¦ç”¨ï¼Œä½†ç”±ç®¡ç†è€…建立帳戶則ä¸å—æ¤é™ã€‚æ¤å¤–,既有的帳戶也ä¸å—影響。" createNoteFromTheFile: "ç”±æ¤æª”案建立貼文" archive: "å°å˜" +archived: "å·²å°å˜" +unarchive: "å–消å°å˜" channelArchiveConfirmTitle: "è¦å°å˜{name}嗎?" channelArchiveConfirmDescription: "å°å˜å¾Œï¼Œå°‡ä¸æœƒåœ¨é »é“列表與æœå°‹çµæžœä¸é¡¯ç¤ºï¼Œä¹Ÿç„¡æ³•ç™¼ä½ˆæ–°è²¼æ–‡ã€‚" thisChannelArchived: "é€™å€‹é »é“已被å°å˜ã€‚" @@ -1110,6 +1124,9 @@ preventAiLearning: "拒絕接å—生æˆå¼AI的訓練" preventAiLearningDescription: "è¦æ±‚站外生æˆå¼ AI ä¸ä½¿ç”¨æ‚¨ç™¼ä½ˆçš„內容訓練模型。æ¤åŠŸèƒ½æœƒä½¿ä¼ºæœå™¨æ–¼ HTML 回應新增「noaiã€æ¨™ç±¤ï¼Œè€Œå› 為è¦è¦–乎 AI 會å¦éµå®ˆè©²æ¨™ç±¤ï¼Œæ‰€ä»¥æ¤åŠŸèƒ½ç„¡æ³•å®Œå…¨é˜»æ¢æ‰€æœ‰ AI 使用您的內容。" options: "é¸é …" specifyUser: "指定使用者" +lookupConfirm: "è¦æŸ¥è©¢å—Žï¼Ÿ" +openTagPageConfirm: "è¦é–‹å•Ÿæ¨™ç±¤çš„é é¢å—Žï¼Ÿ" +specifyHost: "指定主機" failedToPreviewUrl: "無法é 覽" update: "æ›´æ–°" rolesThatCanBeUsedThisEmojiAsReaction: "å¯ä»¥ä½¿ç”¨æ¤è¡¨æƒ…符號為å應的角色" @@ -1241,12 +1258,17 @@ keepOriginalFilenameDescription: "如果關閉æ¤è¨ç½®ï¼Œä¸Šå‚³æ™‚檔案å稱 noDescription: "沒有說明文å—" alwaysConfirmFollow: "點擊追隨時總是顯示確èªè¨Šæ¯" inquiry: "è¯çµ¡æˆ‘們" +tryAgain: "è«‹å†è©¦ä¸€æ¬¡ã€‚" +confirmWhenRevealingSensitiveMedia: "è¦é¡¯ç¤ºæ•æ„Ÿåª’體時需確èª" +sensitiveMediaRevealConfirm: "這是æ•æ„Ÿåª’體。確定è¦é¡¯ç¤ºå—Žï¼Ÿ" +createdLists: "已建立的清單" +createdAntennas: "已建立的天線" _delivery: status: "傳é€ç‹€æ…‹" - stop: "åœæ¢å‚³é€" - resume: "æ¢å¾©å‚³é€" + stop: "åœæ¢ç™¼é€" + resume: "æ¢å¾©ç™¼é€" _type: - none: "ç›´æ’ä¸" + none: "發é€ä¸" manuallySuspended: "手動暫åœä¸" goneSuspended: "å› ç‚ºä¼ºæœå™¨åˆªé™¤æ‰€ä»¥æš«åœä¸" autoSuspendedForNotResponding: "å› ç‚ºä¼ºæœå™¨æ²’有回應所以暫åœä¸" @@ -1376,7 +1398,7 @@ _serverSettings: fanoutTimelineDbFallback: "資料庫的回退" fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快å–的情æ³ä¸‹å°‡åŸ·è¡Œå›žé€€è™•ç†ä»¥é¡å¤–查詢資料庫。若åœç”¨ï¼Œå¯ä»¥é€éŽä¸åŸ·è¡Œå›žé€€è™•ç†ä¾†é€²ä¸€æ¥æ¸›å°‘伺æœå™¨çš„è² è·ï¼Œä½†æœƒé™åˆ¶å¯å–得的時間軸範åœã€‚" inquiryUrl: "è¯çµ¡è¡¨å–®ç¶²å€" - inquiryUrlDescription: "指定伺æœå™¨é‹ç‡Ÿè€…çš„è¯çµ¡è¡¨å–®ç¶²å€æˆ–包å«é‹ç‡Ÿè€…è¯çµ¡è³‡è¨Šç¶²é 的網å€ã€‚" + inquiryUrlDescription: "指定伺æœå™¨é‹ç‡Ÿè€…çš„è¯çµ¡è¡¨å–®ç¶²å€ï¼Œæˆ–包å«é‹ç‡Ÿè€…è¯çµ¡è³‡è¨Šç¶²é 的網å€ã€‚" _accountMigration: moveFrom: "從其他帳戶é·ç§»åˆ°é€™å€‹å¸³æˆ¶" moveFromSub: "為å¦ä¸€å€‹å¸³æˆ¶å»ºç«‹åˆ¥å" @@ -1693,6 +1715,7 @@ _role: canManageAvatarDecorations: "管ç†é åƒè£é£¾" driveCapacity: "雲端硬碟容é‡" alwaysMarkNsfw: "總是將檔案標記為NSFW" + canUpdateBioMedia: "å…許更新大é 貼和橫幅" pinMax: "ç½®é ‚è²¼æ–‡çš„æœ€å¤§æ•¸é‡" antennaMax: "å¯å»ºç«‹çš„天線數é‡" wordMuteMax: "éœéŸ³æ–‡å—的最大å—數" @@ -1936,8 +1959,6 @@ _sfx: note: "貼文" noteMy: "我的貼文" notification: "通知" - antenna: "天線接收" - channel: "é »é“通知" reaction: "é¸æ“‡å應時" _soundSettings: driveFile: "使用雲端硬碟的音效檔案" @@ -1946,6 +1967,7 @@ _soundSettings: driveFileTypeWarnDescription: "è«‹é¸æ“‡éŸ³æ•ˆæª”案" driveFileDurationWarn: "音效太長了" driveFileDurationWarnDescription: "使用長音效檔å¯èƒ½æœƒå½±éŸ¿ Misskey 的使用體驗。ä»è¦ä½¿ç”¨æ¤æª”案嗎?" + driveFileError: "無法載入語音。請變更è¨å®š" _ago: future: "未來" justNow: "剛剛" @@ -2217,7 +2239,7 @@ _charts: federation: "è¯é‚¦å®‡å®™" apRequest: "請求" usersIncDec: "使用者增減" - usersTotal: "使用者總數" + usersTotal: "使用者åˆè¨ˆ" activeUsers: "æ´»èºä½¿ç”¨è€…" notesIncDec: "貼文増減" localNotesIncDec: "本地貼文増減" @@ -2294,6 +2316,7 @@ _pages: eyeCatchingImageSet: "è¨å®šå°é¢å½±åƒ" eyeCatchingImageRemove: "刪除å°é¢å½±åƒ" chooseBlock: "新增方塊" + enterSectionTitle: "輸入å€æ®µçš„標題" selectType: "é¸æ“‡é¡žåž‹" contentBlocks: "內容" inputBlocks: "輸入" @@ -2401,9 +2424,10 @@ _drivecleaner: orderByCreatedAtAsc: "按新增日期é™åºæŽ’列" _webhookSettings: createWebhook: "建立 Webhook" + modifyWebhook: "編輯 Webhook" name: "åå—" secret: "密鑰" - events: "何時é‹è¡Œ Webhook" + trigger: "觸發器" active: "已啟用" _events: follow: "ç•¶ä½ è¿½éš¨æ™‚" @@ -2413,6 +2437,26 @@ _webhookSettings: renote: "當被轉發時" reaction: "當ç²å¾—å應時" mention: "當被æ到時" + _systemEvents: + abuseReport: "當使用者檢舉時" + abuseReportResolved: "當處ç†äº†ä½¿ç”¨è€…的檢舉時" + userCreated: "使用者被新增時" + deleteConfirm: "è«‹å•æ˜¯å¦è¦åˆªé™¤ Webhook?" +_abuseReport: + _notificationRecipient: + createRecipient: "新增接收檢舉的通知å°è±¡" + modifyRecipient: "編輯接收檢舉的通知å°è±¡" + recipientType: "通知å°è±¡çš„種類" + _recipientType: + mail: "é›»å郵件" + webhook: "Webhook" + _captions: + mail: "寄é€åˆ°æ“有監察員權é™çš„使用者電å郵件地å€ï¼ˆåƒ…在收到檢舉時)" + webhook: "å‘指定的 SystemWebhook 發é€é€šçŸ¥ï¼ˆåœ¨æ”¶åˆ°æª¢èˆ‰å’Œè§£æ±ºæª¢èˆ‰æ™‚發é€ï¼‰" + keywords: "é—œéµå—" + notifiedUser: "被通知的使用者" + notifiedWebhook: "使用的 Webhook" + deleteConfirm: "確定è¦åˆªé™¤é€šçŸ¥å°è±¡å—Žï¼Ÿ" _moderationLogTypes: createRole: "新增角色" deleteRole: "刪除角色 " @@ -2450,6 +2494,16 @@ _moderationLogTypes: deleteAvatarDecoration: "刪除é åƒè£é£¾" unsetUserAvatar: "移除使用者的大é è²¼" unsetUserBanner: "移除使用者的橫幅圖åƒ" + createSystemWebhook: "建立 SystemWebhook" + updateSystemWebhook: "æ›´æ–° SystemWebhook" + deleteSystemWebhook: "刪除 SystemWebhook" + createAbuseReportNotificationRecipient: "建立接收檢舉的通知å°è±¡" + updateAbuseReportNotificationRecipient: "更新接收檢舉的通知å°è±¡" + deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知å°è±¡" + deleteAccount: "刪除帳戶" + deletePage: "刪除é é¢" + deleteFlash: "刪除 Play" + deleteGalleryPost: "刪除相簿的貼文" _fileViewer: title: "檔案詳細資訊" type: "檔案類型 " @@ -2581,3 +2635,8 @@ _mediaControls: pip: "ç•«ä¸ç•«" playbackRate: "æ’放速度" loop: "循環æ’放" +_contextMenu: + title: "內容功能表" + app: "應用程å¼" + appWithShift: "Shift éµæ‡‰ç”¨ç¨‹å¼" + native: "ç€è¦½å™¨çš„使用者介é¢" diff --git a/misskey-assets b/misskey-assets deleted file mode 160000 index 0179793ec891856d6f37a3be16ba4c22f67a81b5..0000000000000000000000000000000000000000 --- a/misskey-assets +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5 diff --git a/package.json b/package.json index 93e5d97998fe009bbf45d7ac3d28ee7daae20f0f..1ffdcb091a40d800267e4c189d52166ffb32f912 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "sharkey", - "version": "2024.5.1", + "version": "2024.8.1", "codename": "shonk", "repository": { "type": "git", "url": "https://activitypub.software/TransFem-org/Sharkey.git" }, - "packageManager": "pnpm@9.0.6", + "packageManager": "pnpm@9.6.0", "workspaces": [ "packages/frontend", "packages/backend", @@ -21,8 +21,8 @@ "build-assets": "node ./scripts/build-assets.mjs", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", - "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", - "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", + "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", + "start": "pnpm check:connect && cd packages/backend && MK_WARNED_ABOUT_CONFIG=true node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "init": "pnpm migrate", "migrate": "cd packages/backend && pnpm migrate", @@ -51,23 +51,25 @@ "cssnano": "6.1.2", "execa": "8.0.1", "fast-glob": "3.3.2", - "ignore-walk": "6.0.4", + "ignore-walk": "6.0.5", "js-yaml": "4.1.0", - "postcss": "8.4.38", + "postcss": "8.4.40", "tar": "6.2.1", - "terser": "5.30.3", - "typescript": "5.4.5", - "esbuild": "0.20.2", - "glob": "10.3.12" + "terser": "5.31.3", + "typescript": "5.5.4", + "esbuild": "0.23.0", + "glob": "11.0.0" }, "devDependencies": { - "@types/node": "20.12.7", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", + "@misskey-dev/eslint-plugin": "2.0.3", + "@types/node": "20.14.12", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", "cross-env": "7.0.3", - "cypress": "13.7.3", - "eslint": "8.57.0", + "cypress": "13.13.1", + "eslint": "9.8.0", + "globals": "15.8.0", "ncp": "2.0.0", - "start-server-and-test": "2.0.3" + "start-server-and-test": "2.0.4" } } diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore deleted file mode 100644 index 790eb90145b40408e33a5cba44b4e8521c7c1d70..0000000000000000000000000000000000000000 --- a/packages/backend/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -/built -/.eslintrc.js -/@types/**/* diff --git a/packages/backend/.eslintrc.cjs b/packages/backend/.eslintrc.cjs deleted file mode 100644 index f9fe4814e6719d9ce841f630f2ff73856281c01d..0000000000000000000000000000000000000000 --- a/packages/backend/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json', './test/tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], - rules: { - 'import/order': ['warn', { - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'pathGroups': [ - { - 'pattern': '@/**', - 'group': 'external', - 'position': 'after' - } - ], - }], - 'no-restricted-globals': [ - 'error', - { - 'name': '__dirname', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - }, - { - 'name': '__filename', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - } - ] - }, -}; diff --git a/packages/backend/assets/api-doc.html b/packages/backend/assets/api-doc.html new file mode 100644 index 0000000000000000000000000000000000000000..19e0349d47d92d196fb1a83a57f66534d34a897d --- /dev/null +++ b/packages/backend/assets/api-doc.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + <head> + <title>Misskey API</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <style> + body { + margin: 0; + padding: 0; + } + </style> + </head> + <body> + <script + id="api-reference" + data-url="/api.json"></script> + <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script> + </body> +</html> diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg deleted file mode 100644 index 9d21137072a19c2da1fb41a98c9199c57c5d333c..0000000000000000000000000000000000000000 Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg and /dev/null differ diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf deleted file mode 100644 index a2601e0f1b00c4f2e72249911b911e48c55d6de1..0000000000000000000000000000000000000000 Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf and /dev/null differ diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff deleted file mode 100644 index d9f471fa35a861eb745fb136094ffd9c90b7922f..0000000000000000000000000000000000000000 Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff and /dev/null differ diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.svg b/packages/backend/assets/fonts/sharkey-icons/shark-font.svg new file mode 100644 index 0000000000000000000000000000000000000000..b67bd7f7d8119ae3bd9f3a0ed6557f1ca7a1f64a Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.svg differ diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf b/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0fdf04df082cc5724a7c94b19b299cfd5a317040 Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf differ diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.woff b/packages/backend/assets/fonts/sharkey-icons/shark-font.woff new file mode 100644 index 0000000000000000000000000000000000000000..993666bc3aacaf202da6002fbc9baa8e7a881581 Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.woff differ diff --git a/packages/backend/assets/fonts/sharkey-icons/style.css b/packages/backend/assets/fonts/sharkey-icons/style.css index 7fb0f9450470c649b09732630360c7a0bd521c3c..7168702e5a38a4c7edeee32b4df4728b6c0568de 100644 --- a/packages/backend/assets/fonts/sharkey-icons/style.css +++ b/packages/backend/assets/fonts/sharkey-icons/style.css @@ -1,31 +1,114 @@ -@charset "UTF-8"; - @font-face { - font-family: "custom-sharkey-icons"; - src: url("./custom-sharkey-icons.woff") format("woff"), - url("./custom-sharkey-icons.ttf") format("truetype"), - url("./custom-sharkey-icons.svg#custom-sharkey-icons") format("svg"); - font-weight: normal; + font-display: auto; + font-family: "shark-font"; font-style: normal; - font-display: block; + font-weight: normal; + + src: url("./shark-font.woff?1722899913909") format("woff"), url("./shark-font.ttf?1722899913909") format("truetype"), url("./shark-font.svg?1722899913909#shark-font") format("svg"); } .sk-icons { - font-family: "custom-sharkey-icons" !important; - font-style: normal; + display: inline-block; + font-family: "shark-font"; font-weight: normal; + font-style: normal; font-variant: normal; - text-transform: none; + text-rendering: auto; line-height: 1; - speak: none; - -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +.sk-icons-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -0.0667em; +} + +.sk-icons-xs { + font-size: 0.75em; +} + +.sk-icons-sm { + font-size: 0.875em; +} + +.sk-icons-1x { + font-size: 1em; +} + +.sk-icons-2x { + font-size: 2em; +} + +.sk-icons-3x { + font-size: 3em; +} + +.sk-icons-4x { + font-size: 4em; } -.sk-icons.sk-shark:before { - content: "\61"; +.sk-icons-5x { + font-size: 5em; } -.sk-icons.sk-misskey:before { - content: "\62"; +.sk-icons-6x { + font-size: 6em; } + +.sk-icons-7x { + font-size: 7em; +} + +.sk-icons-8x { + font-size: 8em; +} + +.sk-icons-9x { + font-size: 9em; +} + +.sk-icons-10x { + font-size: 10em; +} + +.sk-icons-fw { + text-align: center; + width: 1.25em; +} + +.sk-icons-border { + border: solid 0.08em #eee; + border-radius: 0.1em; + padding: 0.2em 0.25em 0.15em; +} + +.sk-icons-pull-left { + float: left; +} + +.sk-icons-pull-right { + float: right; +} + +.sk-icons.sk-icons-pull-left { + margin-right: 0.3em; +} + +.sk-icons.sk-icons-pull-right { + margin-left: 0.3em; +} + + +.sk-icons.sk-foldermove::before { + content: "\ea01"; +} + +.sk-icons.sk-misskey::before { + content: "\ea02"; +} + +.sk-icons.sk-shark::before { + content: "\ea03"; +} \ No newline at end of file diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html deleted file mode 100644 index 1b7ff9e0d2fda2910062ef25d7ada20f096f61a5..0000000000000000000000000000000000000000 --- a/packages/backend/assets/redoc.html +++ /dev/null @@ -1,24 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <title>Sharkey API</title> - <!-- needed for adaptive design --> - <meta charset="utf-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> - - <!-- - ReDoc doesn't change outer page styles - --> - <style> - body { - margin: 0; - padding: 0; - } - </style> - </head> - <body> - <redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc> - <script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script> - </body> -</html> diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..4fd9f0cd51b6819c6f0a9bb17c7b068027bf3626 --- /dev/null +++ b/packages/backend/eslint.config.js @@ -0,0 +1,46 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json', './test/tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'import/order': ['warn', { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + pathGroups: [{ + pattern: '@/**', + group: 'external', + position: 'after', + }], + }], + 'no-restricted-globals': ['error', { + name: '__dirname', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }, { + name: '__filename', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }], + }, + }, +]; diff --git a/packages/backend/migration/1713656541000-abuse-report-notification.js b/packages/backend/migration/1713656541000-abuse-report-notification.js new file mode 100644 index 0000000000000000000000000000000000000000..4a754f81e2f53b55574b45463f28b1803ef435d0 --- /dev/null +++ b/packages/backend/migration/1713656541000-abuse-report-notification.js @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AbuseReportNotification1713656541000 { + name = 'AbuseReportNotification1713656541000' + + async up(queryRunner) { + await queryRunner.query(` + CREATE TABLE "system_webhook" ( + "id" varchar(32) NOT NULL, + "isActive" boolean NOT NULL DEFAULT true, + "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "latestSentAt" timestamp with time zone NULL DEFAULT NULL, + "latestStatus" integer NULL DEFAULT NULL, + "name" varchar(255) NOT NULL, + "on" varchar(128) [] NOT NULL DEFAULT '{}'::character varying[], + "url" varchar(1024) NOT NULL, + "secret" varchar(1024) NOT NULL, + CONSTRAINT "PK_system_webhook_id" PRIMARY KEY ("id") + ); + CREATE INDEX "IDX_system_webhook_isActive" ON "system_webhook" ("isActive"); + CREATE INDEX "IDX_system_webhook_on" ON "system_webhook" USING gin ("on"); + + CREATE TABLE "abuse_report_notification_recipient" ( + "id" varchar(32) NOT NULL, + "isActive" boolean NOT NULL DEFAULT true, + "updatedAt" timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + "name" varchar(255) NOT NULL, + "method" varchar(64) NOT NULL, + "userId" varchar(32) NULL DEFAULT NULL, + "systemWebhookId" varchar(32) NULL DEFAULT NULL, + CONSTRAINT "PK_abuse_report_notification_recipient_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_abuse_report_notification_recipient_userId1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT "FK_abuse_report_notification_recipient_userId2" FOREIGN KEY ("userId") REFERENCES "user_profile"("userId") ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId" FOREIGN KEY ("systemWebhookId") REFERENCES "system_webhook"("id") ON DELETE CASCADE ON UPDATE NO ACTION + ); + CREATE INDEX "IDX_abuse_report_notification_recipient_isActive" ON "abuse_report_notification_recipient" ("isActive"); + CREATE INDEX "IDX_abuse_report_notification_recipient_method" ON "abuse_report_notification_recipient" ("method"); + CREATE INDEX "IDX_abuse_report_notification_recipient_userId" ON "abuse_report_notification_recipient" ("userId"); + CREATE INDEX "IDX_abuse_report_notification_recipient_systemWebhookId" ON "abuse_report_notification_recipient" ("systemWebhookId"); + `); + } + + async down(queryRunner) { + await queryRunner.query(` + ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId1"; + ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_userId2"; + ALTER TABLE "abuse_report_notification_recipient" DROP CONSTRAINT "FK_abuse_report_notification_recipient_systemWebhookId"; + DROP INDEX "IDX_abuse_report_notification_recipient_isActive"; + DROP INDEX "IDX_abuse_report_notification_recipient_method"; + DROP INDEX "IDX_abuse_report_notification_recipient_userId"; + DROP INDEX "IDX_abuse_report_notification_recipient_systemWebhookId"; + DROP TABLE "abuse_report_notification_recipient"; + + DROP INDEX "IDX_system_webhook_isActive"; + DROP INDEX "IDX_system_webhook_on"; + DROP TABLE "system_webhook"; + `); + } +} diff --git a/packages/backend/migration/1716197366117-MediaSilenceForHosts.js b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js new file mode 100644 index 0000000000000000000000000000000000000000..10bb7f025553516c805bf7159b3bef8e20914779 --- /dev/null +++ b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MediaSilenceForHosts1716197366117 { + name = 'MediaSilenceForHosts1716197366117' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`); + } +} diff --git a/packages/backend/migration/1721666053703-fixDriveUrl.js b/packages/backend/migration/1721666053703-fixDriveUrl.js new file mode 100644 index 0000000000000000000000000000000000000000..d8512fb8358d11c1ec54a79550049e624327390a --- /dev/null +++ b/packages/backend/migration/1721666053703-fixDriveUrl.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class FixDriveUrl1721666053703 { + name = 'FixDriveUrl1721666053703' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(1024), ALTER COLUMN "url" SET NOT NULL`); + await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`); + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(1024)`); + await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`); + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(512)`); + await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`); + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(512)`); + await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`); + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(512), ALTER COLUMN "url" SET NOT NULL`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index b05dc10c11a10f4ebfe3fbfe9851ff84aac60632..ff84beec6723b0f55a8fb2ccf4bd7818e5ff0b6e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": "^20.10.0" + "node": "^20.10.0 || ^22.0.0" }, "scripts": { "start": "node ./built/boot/entry.js", @@ -31,7 +31,7 @@ "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", "test-and-coverage": "pnpm jest-and-coverage", "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", - "generate-api-json": "pnpm build && node ./scripts/generate_api_json.js" + "generate-api-json": "node ./scripts/generate_api_json.js" }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", @@ -63,45 +63,45 @@ "utf-8-validate": "6.0.3" }, "dependencies": { - "@aws-sdk/client-s3": "3.412.0", - "@aws-sdk/lib-storage": "3.412.0", - "@bull-board/api": "5.17.0", - "@bull-board/fastify": "5.17.0", - "@bull-board/ui": "5.17.0", + "@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.2.0", - "@fastify/static": "7.0.3", + "@fastify/multipart": "8.3.0", + "@fastify/static": "7.0.4", "@fastify/view": "9.1.0", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.1.0", - "@napi-rs/canvas": "^0.1.52", - "@nestjs/common": "10.3.8", - "@nestjs/core": "10.3.8", - "@nestjs/testing": "10.3.8", + "@napi-rs/canvas": "^0.1.53", + "@nestjs/common": "10.3.10", + "@nestjs/core": "10.3.10", + "@nestjs/testing": "10.3.10", "@peertube/http-signature": "1.7.0", - "@sentry/node": "^8.5.0", - "@sentry/profiling-node": "^8.5.0", - "@simplewebauthn/server": "10.0.0", + "@sentry/node": "8.20.0", + "@sentry/profiling-node": "8.20.0", + "@simplewebauthn/server": "10.0.1", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.5.0", "@swc/cli": "0.3.12", - "@swc/core": "1.4.17", + "@swc/core": "1.6.6", "@transfem-org/sfm-js": "0.24.5", "@twemoji/parser": "15.1.1", "accepts": "1.3.8", - "ajv": "8.13.0", + "ajv": "8.17.1", "archiver": "7.0.1", "argon2": "^0.40.1", "async-mutex": "0.5.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "5.7.8", + "bullmq": "5.10.4", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.3.0", @@ -112,30 +112,31 @@ "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "4.26.2", + "fast-xml-parser": "^4.4.0", + "fastify": "4.28.1", "fastify-multer": "^2.0.3", "fastify-raw-body": "4.3.0", "feed": "4.2.2", - "file-type": "19.0.0", - "fluent-ffmpeg": "2.1.2", + "file-type": "19.3.0", + "fluent-ffmpeg": "2.1.3", "form-data": "4.0.0", "glob": "10.3.10", - "got": "14.2.1", - "happy-dom": "10.0.3", + "got": "14.4.2", + "happy-dom": "15.6.1", "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", "ioredis": "5.4.1", - "ip-cidr": "3.1.0", + "ip-cidr": "4.0.1", "ipaddr.js": "2.2.0", - "is-svg": "5.0.0", + "is-svg": "5.0.1", "js-yaml": "4.1.0", - "jsdom": "24.0.0", + "jsdom": "24.1.1", "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", "megalodon": "workspace:*", - "meilisearch": "0.38.0", + "meilisearch": "0.41.0", "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", @@ -144,23 +145,24 @@ "nanoid": "5.0.7", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "nodemailer": "6.9.13", + "nodemailer": "6.9.14", "oauth": "0.10.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", - "otpauth": "9.2.3", + "otpauth": "9.3.1", "parse5": "7.1.2", - "pg": "8.11.5", + "pg": "8.12.0", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", - "pug": "3.0.2", + "proxy-addr": "^2.0.7", + "pug": "3.0.3", "punycode": "2.3.1", "qrcode": "1.5.3", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.20.10", + "re2": "1.21.3", "redis-lock": "0.1.4", "reflect-metadata": "0.2.2", "rename": "1.0.4", @@ -168,28 +170,27 @@ "rxjs": "7.8.1", "sanitize-html": "2.13.0", "secure-json-parse": "2.7.0", - "sharp": "0.33.3", + "sharp": "0.33.4", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.22.7", + "systeminformation": "5.22.11", "tinycolor2": "1.6.0", "tmp": "0.2.3", - "tsc-alias": "1.8.8", + "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", "typeorm": "0.3.20", - "typescript": "5.4.5", + "typescript": "5.5.4", "ulid": "2.3.0", "uuid": "^9.0.1", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.17.0", + "ws": "8.18.0", "xev": "3.0.2" }, "devDependencies": { "@jest/globals": "29.7.0", - "@misskey-dev/eslint-plugin": "1.0.0", - "@nestjs/platform-express": "10.3.8", + "@nestjs/platform-express": "10.3.10", "@simplewebauthn/types": "10.0.0", "@swc/jest": "0.2.36", "@types/accepts": "1.3.7", @@ -199,22 +200,22 @@ "@types/color-convert": "2.0.3", "@types/content-disposition": "0.5.8", "@types/fluent-ffmpeg": "2.1.24", - "@types/htmlescape": "^1.1.3", - "@types/http-link-header": "1.0.5", + "@types/htmlescape": "1.1.3", + "@types/http-link-header": "1.0.7", "@types/jest": "29.5.12", "@types/js-yaml": "4.0.9", - "@types/jsdom": "21.1.6", - "@types/jsonld": "1.5.13", + "@types/jsdom": "21.1.7", + "@types/jsonld": "1.5.15", "@types/jsrsasign": "10.5.14", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.12.7", - "@types/node-fetch": "3.0.3", + "@types/node": "20.14.12", "@types/nodemailer": "6.4.15", - "@types/oauth": "0.9.4", + "@types/oauth": "0.9.5", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.5", + "@types/pg": "8.11.6", + "@types/proxy-addr": "^2.0.3", "@types/pug": "2.0.10", "@types/punycode": "2.1.4", "@types/qrcode": "1.5.5", @@ -230,19 +231,18 @@ "@types/uuid": "^9.0.4", "@types/vary": "1.1.3", "@types/web-push": "3.6.3", - "@types/ws": "8.5.10", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", - "aws-sdk-client-mock": "3.0.1", + "@types/ws": "8.5.11", + "@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": "8.57.0", "eslint-plugin-import": "2.29.1", - "execa": "8.0.1", - "fkill": "^9.0.0", + "execa": "9.3.0", + "fkill": "9.0.0", "jest": "29.7.0", "jest-mock": "29.7.0", - "nodemon": "3.1.0", + "nodemon": "3.1.4", "pid-port": "1.0.0", - "simple-oauth2": "5.0.0" + "simple-oauth2": "5.1.0" } } diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index ba25fd416c782cd376cb0e3ba37df6b3656e422f..d4bf4baf43cbf2048eda4db33ece2f82921718ee 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -5,11 +5,33 @@ import Redis from 'ioredis'; import { loadConfig } from '../built/config.js'; +import { createPostgresDataSource } from '../built/postgres.js'; const config = loadConfig(); -const redis = new Redis(config.redis); -redis.on('connect', () => redis.disconnect()); -redis.on('error', (e) => { - throw e; -}); +// 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 }); + + +// Connect to all redis servers +function connectToRedis(redisOptions) { + const redis = new Redis(redisOptions); + redis.on('connect', () => redis.disconnect()); + redis.on('error', (e) => { + throw e; + }); +} + +// 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); diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs index 2d0de0f91666b1ba7c8ba22c4d0f3ebefa9c3ad4..a3e0558abd9732b83e9360bfcb9f01b854191fbf 100644 --- a/packages/backend/scripts/dev.mjs +++ b/packages/backend/scripts/dev.mjs @@ -30,6 +30,7 @@ function execStart() { async function killProc() { if (backendProcess) { + backendProcess.catch(() => {}); // backendProcess.kill()ã«ã‚ˆã£ã¦ç™ºç”Ÿã™ã‚‹ä¾‹å¤–を無視ã™ã‚‹ãŸã‚ã«catch()を呼ã³å‡ºã™ backendProcess.kill(); await new Promise(resolve => backendProcess.on('exit', resolve)); backendProcess = undefined; @@ -46,6 +47,7 @@ async function killProc() { ], { stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], + serialization: "json", }) .on('message', async (message) => { if (message.type === 'exit') { diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js index b4769ef8012e75218a3c75e70eb720916f034b31..798e2430040ac97e92eb06381f7a22b0aef545f6 100644 --- a/packages/backend/scripts/generate_api_json.js +++ b/packages/backend/scripts/generate_api_json.js @@ -3,11 +3,34 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { loadConfig } from '../built/config.js' -import { genOpenapiSpec } from '../built/server/api/openapi/gen-spec.js' -import { writeFileSync } from "node:fs"; +import { execa } from 'execa'; +import { writeFileSync, existsSync } from "node:fs"; -const config = loadConfig(); -const spec = genOpenapiSpec(config, true); +async function main() { + if (!process.argv.includes('--no-build')) { + await execa('pnpm', ['run', 'build'], { + stdout: process.stdout, + stderr: process.stderr, + }); + } -writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); + if (!existsSync('./built')) { + throw new Error('`built` directory does not exist.'); + } + + /** @type {import('../src/config.js')} */ + const { loadConfig } = await import('../built/config.js'); + + /** @type {import('../src/server/api/openapi/gen-spec.js')} */ + const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js'); + + const config = loadConfig(); + const spec = genOpenapiSpec(config, true); + + writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8'); +} + +main().catch(e => { + console.error(e); + process.exit(1); +}); diff --git a/packages/backend/src/NestLogger.ts b/packages/backend/src/NestLogger.ts index 80f1f7a024d6a937f623b2cd58e52fb9052b6691..d0be19664f3168c81bfcb271e4a9e4b675fd5755 100644 --- a/packages/backend/src/NestLogger.ts +++ b/packages/backend/src/NestLogger.ts @@ -7,7 +7,7 @@ import { LoggerService } from '@nestjs/common'; import Logger from '@/logger.js'; const logger = new Logger('core', 'cyan'); -const nestLogger = logger.createSubLogger('nest', 'green', false); +const nestLogger = logger.createSubLogger('nest', 'green'); export class NestLogger implements LoggerService { /** diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index d1744b4b4bd7ee4b8b9d4f058fd35494f276da95..56128a7ab952dcfa779562284d5d6b854d046bf5 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -25,7 +25,7 @@ Error.stackTraceLimit = Infinity; EventEmitter.defaultMaxListeners = 128; const logger = new Logger('core', 'cyan'); -const clusterLogger = logger.createSubLogger('cluster', 'orange', false); +const clusterLogger = logger.createSubLogger('cluster', 'orange'); const ev = new Xev(); // We wrap this in a main function, that gets called, diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 303ba9420764b4cc79b8c5b8df42a3cc477115fb..3559816e9649b87bef7f5615832e46aaaadb859f 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -25,7 +25,7 @@ const _dirname = dirname(_filename); const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const logger = new Logger('core', 'cyan'); -const bootLogger = logger.createSubLogger('boot', 'magenta', false); +const bootLogger = logger.createSubLogger('boot', 'magenta'); const themeColor = chalk.hex('#86b300'); @@ -44,7 +44,7 @@ function greet() { //#endregion console.log(' Sharkey is an open-source decentralized microblogging platform.'); - console.log(chalk.rgb(255, 136, 0)(' If you like Sharkey, please donate to support development. https://ko-fi.com/transfem')); + console.log(chalk.rgb(255, 136, 0)(' If you like Sharkey, please donate to support development. https://opencollective.com/sharkey')); console.log(''); console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`); @@ -112,6 +112,11 @@ export async function masterMain() { await server(); } + if (config.clusterLimit === 0) { + bootLogger.error("Configuration error: we can't create workers, `config.clusterLimit` is 0 (if you don't want to use clustering, set the environment variable `MK_DISABLE_CLUSTERING` to a non-empty value instead)", null, true); + process.exit(1); + } + await spawnWorkers(config.clusterLimit); } @@ -180,7 +185,10 @@ async function connectDb(): Promise<void> { */ async function spawnWorkers(limit = 1) { - const workers = Math.min(limit, os.cpus().length); + const cpuCount = os.cpus().length; + // in some weird environments, node can't count the CPUs; we trust the config in those cases + const workers = cpuCount === 0 ? limit : Math.min(limit, cpuCount); + bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); await Promise.all([...Array(workers)].map(spawnWorker)); bootLogger.succ('All workers started'); diff --git a/packages/backend/src/boot/worker.ts b/packages/backend/src/boot/worker.ts index d4a7cd56e5cedfcdfc44919c5ddf6cd0d466c927..5d4a15b29f02001d8e5ecb8492c1babe45d8d570 100644 --- a/packages/backend/src/boot/worker.ts +++ b/packages/backend/src/boot/worker.ts @@ -4,13 +4,36 @@ */ import cluster from 'node:cluster'; +import * as Sentry from '@sentry/node'; +import { nodeProfilingIntegration } from '@sentry/profiling-node'; import { envOption } from '@/env.js'; +import { loadConfig } from '@/config.js'; import { jobQueue, server } from './common.js'; /** * Init worker process */ export async function workerMain() { + const config = loadConfig(); + + if (config.sentryForBackend) { + Sentry.init({ + integrations: [ + ...(config.sentryForBackend.enableNodeProfiling ? [nodeProfilingIntegration()] : []), + ], + + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + + // Set sampling rate for profiling - this is relative to tracesSampleRate + profilesSampleRate: 1.0, + + maxBreadcrumbs: 0, + + ...config.sentryForBackend.options, + }); + } + if (envOption.onlyServer) { await server(); } else if (envOption.onlyQueue) { diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 58c4d028aa3305283c842f70e5d3cbc124e2d52e..3f9967ad19ea6d1972aa2cf6e8bbde2d1b1cf98a 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -12,9 +12,10 @@ import * as Sentry from '@sentry/node'; import type { RedisOptions } from 'ioredis'; type RedisOptionsSource = Partial<RedisOptions> & { - host: string; - port: number; + host?: string; + port?: number; family?: number; + path?: string, pass: string; db?: number; prefix?: string; @@ -24,7 +25,7 @@ type RedisOptionsSource = Partial<RedisOptions> & { * è¨å®šãƒ•ã‚¡ã‚¤ãƒ«ã®åž‹ */ type Source = { - url: string; + url?: string; port?: number; socket?: string; chmodSocket?: string; @@ -32,9 +33,9 @@ type Source = { db: { host: string; port: number; - db: string; - user: string; - pass: string; + db?: string; + user?: string; + pass?: string; disableCache?: boolean; extra?: { [x: string]: string }; }; @@ -95,6 +96,7 @@ type Source = { customMOTD?: string[]; signToActivityPubGet?: boolean; + attachLdSignatureForRelays?: boolean; checkActivityPubGetSignature?: boolean; perChannelMaxNoteCacheCount?: number; @@ -161,6 +163,7 @@ export type Config = { proxyRemoteFiles: boolean | undefined; customMOTD: string[] | undefined; signToActivityPubGet: boolean; + attachLdSignatureForRelays: boolean; checkActivityPubGetSignature: boolean | undefined; version: string; @@ -221,8 +224,15 @@ export function loadConfig(): Config { JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; - const config = globSync(path).sort() - .map(path => fs.readFileSync(path, 'utf-8')) + const configFiles = globSync(path).sort(); + + if (configFiles.length === 0 + && !process.env['MK_WARNED_ABOUT_CONFIG']) { + console.log('No config files loaded, check if this is intentional'); + process.env['MK_WARNED_ABOUT_CONFIG'] = '1'; + } + + const config = configFiles.map(path => fs.readFileSync(path, 'utf-8')) .map(contents => yaml.load(contents) as Source) .reduce( (acc: Source, cur: Source) => Object.assign(acc, cur), @@ -231,13 +241,17 @@ export function loadConfig(): Config { applyEnvOverrides(config); - const url = tryCreateUrl(config.url); + const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? ''); const version = meta.version; const host = url.host; const hostname = url.hostname; const scheme = url.protocol.replace(/:$/, ''); const wsScheme = scheme.replace('http', 'ws'); + const dbDb = config.db.db ?? process.env.DATABASE_DB ?? ''; + const dbUser = config.db.user ?? process.env.DATABASE_USER ?? ''; + const dbPass = config.db.pass ?? process.env.DATABASE_PASSWORD ?? ''; + const externalMediaProxy = config.mediaProxy ? config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy : null; @@ -248,7 +262,7 @@ export function loadConfig(): Config { version, publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, url: url.origin, - port: config.port ?? parseInt(process.env.PORT ?? '', 10), + port: config.port ?? parseInt(process.env.PORT ?? '3000', 10), socket: config.socket, chmodSocket: config.chmodSocket, disableHsts: config.disableHsts, @@ -260,7 +274,7 @@ export function loadConfig(): Config { apiUrl: `${scheme}://${host}/api`, authUrl: `${scheme}://${host}/auth`, driveUrl: `${scheme}://${host}/files`, - db: config.db, + db: { ...config.db, db: dbDb, user: dbUser, pass: dbPass }, dbReplications: config.dbReplications, dbSlaves: config.dbSlaves, meilisearch: config.meilisearch, @@ -291,6 +305,7 @@ export function loadConfig(): Config { proxyRemoteFiles: config.proxyRemoteFiles, customMOTD: config.customMOTD, signToActivityPubGet: config.signToActivityPubGet ?? true, + attachLdSignatureForRelays: config.attachLdSignatureForRelays ?? true, checkActivityPubGetSignature: config.checkActivityPubGetSignature, mediaProxy: externalMediaProxy ?? internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, @@ -436,8 +451,8 @@ function applyEnvOverrides(config: Source) { // these are all the settings that can be overridden - _apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts']]); - _apply_top(['db', ['host', 'port', 'db', 'user', 'pass']]); + _apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts', 'id', 'dbReplications']]); + _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'], @@ -447,7 +462,8 @@ function applyEnvOverrides(config: Source) { _apply_top([['sentryForFrontend', 'sentryForBackend'], 'options', ['dsn', 'profileSampleRate', 'serverName', 'includeLocalVariables', 'proxy', 'keepAlive', 'caCerts']]); _apply_top(['sentryForBackend', 'enableNodeProfiling']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); - _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]); + _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); _apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); + _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]); } diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts new file mode 100644 index 0000000000000000000000000000000000000000..7be53358856454c3bacd81b45cc9a09bd4d99020 --- /dev/null +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -0,0 +1,405 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common'; +import { Brackets, In, IsNull, Not } from 'typeorm'; +import * as Redis from 'ioredis'; +import sanitizeHtml from 'sanitize-html'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; +import type { + AbuseReportNotificationRecipientRepository, + MiAbuseReportNotificationRecipient, + MiAbuseUserReport, + 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'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { IdService } from './IdService.js'; + +@Injectable() +export class AbuseReportNotificationService implements OnApplicationShutdown { + constructor( + @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, + ) { + this.redisForSub.on('message', this.onMessage); + } + + /** + * 管ç†è€…用Redisイベントを用ã„ã¦{@link abuseReports}ã®å†…容を管ç†è€…å„ä½ã«é€šçŸ¥ã™ã‚‹. + * 通知先ユーザã¯{@link getModeratorIds}ã®å–å¾—çµæžœã«ä¾ã‚‹. + * + * @see RoleService.getModeratorIds + * @see GlobalEventService.publishAdminStream + */ + @bindThis + public async notifyAdminStream(abuseReports: MiAbuseUserReport[]) { + if (abuseReports.length <= 0) { + return; + } + + const moderatorIds = await this.roleService.getModeratorIds(true, true); + + for (const moderatorId of moderatorIds) { + for (const abuseReport of abuseReports) { + this.globalEventService.publishAdminStream( + moderatorId, + 'newAbuseUserReport', + { + id: abuseReport.id, + targetUserId: abuseReport.targetUserId, + reporterId: abuseReport.reporterId, + comment: abuseReport.comment, + }, + ); + } + } + } + + /** + * Mailを用ã„ã¦{@link abuseReports}ã®å†…容を管ç†è€…å„ä½ã«é€šçŸ¥ã™ã‚‹. + * メールアドレスã®é€ä¿¡å…ˆã¯ä»¥ä¸‹ã®é€šã‚Š. + * - モデレータ権é™æ‰€æœ‰è€…ユーザ(è¨å®šç”»é¢ã‹ã‚‰ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã®è¨å®šã‚’è¡Œã£ã¦ã„るユーザã«é™ã‚‹) + * - metaテーブルã«è¨å®šã•ã‚Œã¦ã„るメールアドレス + * + * @see EmailService.sendEmail + */ + @bindThis + public async notifyMail(abuseReports: MiAbuseUserReport[]) { + if (abuseReports.length <= 0) { + return; + } + + const recipientEMailAddresses = await this.fetchEMailRecipients().then(it => it + .filter(it => it.isActive && it.userProfile?.emailVerified) + .map(it => it.userProfile?.email) + .filter(x => x != null), + ); + + // é€ä¿¡å…ˆã®é®®åº¦ã‚’ä¿ã¤ãŸã‚ã€æ¯Žå›žå–å¾—ã™ã‚‹ + const meta = await this.metaService.fetch(true); + recipientEMailAddresses.push( + ...(meta.email ? [meta.email] : []), + ); + + if (recipientEMailAddresses.length <= 0) { + return; + } + + for (const mailAddress of recipientEMailAddresses) { + await Promise.all( + abuseReports.map(it => { + // TODO: é€ä¿¡å‡¦ç†ã¯JobQueue化ã—ãŸã„ + return this.emailService.sendEmail( + mailAddress, + 'New Abuse Report', + sanitizeHtml(it.comment), + sanitizeHtml(it.comment), + ); + }), + ); + } + } + + /** + * SystemWebhookを用ã„ã¦{@link abuseReports}ã®å†…容を管ç†è€…å„ä½ã«é€šçŸ¥ã™ã‚‹. + * ã“ã“ã§ã¯JobQueueã¸ã®ã‚¨ãƒ³ã‚ューã®ã¿ã‚’è¡Œã†ãŸã‚ã€å³æ™‚実行ã•ã‚Œãªã„. + * + * @see SystemWebhookService.enqueueSystemWebhook + */ + @bindThis + public async notifySystemWebhook( + abuseReports: MiAbuseUserReport[], + type: 'abuseReport' | 'abuseReportResolved', + ) { + if (abuseReports.length <= 0) { + return; + } + + const recipientWebhookIds = await this.fetchWebhookRecipients() + .then(it => it + .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook') + .map(it => it.systemWebhookId) + .filter(x => x != null)); + for (const webhookId of recipientWebhookIds) { + await Promise.all( + abuseReports.map(it => { + return this.systemWebhookService.enqueueSystemWebhook( + webhookId, + type, + it, + ); + }), + ); + } + } + + /** + * é€šå ±ã®é€šçŸ¥å…ˆä¸€è¦§ã‚’å–å¾—ã™ã‚‹. + * + * @param {Object} [params] クエリã®å–å¾—æ¡ä»¶ + * @param {Object} [params.method] å–å¾—ã™ã‚‹é€šçŸ¥å…ˆã®é€šçŸ¥æ–¹æ³• + * @param {Object} [opts] 動作時ã®è©³ç´°ãªã‚ªãƒ—ション + * @param {boolean} [opts.removeUnauthorized] 副作用ã¨ã—ã¦ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿æ¨©é™ã‚’æŒãŸãªã„é€ä¿¡å…ˆãƒ¦ãƒ¼ã‚¶ã‚’DBã‹ã‚‰å‰Šé™¤ã™ã‚‹ã‹ã©ã†ã‹(default: true) + * @param {boolean} [opts.joinUser] 通知先ã®ãƒ¦ãƒ¼ã‚¶æƒ…å ±ã‚’JOINã™ã‚‹ã‹ã©ã†ã‹(default: false) + * @param {boolean} [opts.joinSystemWebhook] 通知先ã®SystemWebhookæƒ…å ±ã‚’JOINã™ã‚‹ã‹ã©ã†ã‹(default: false) + * @see removeUnauthorizedRecipientUsers + */ + @bindThis + public async fetchRecipients( + params?: { + ids?: MiAbuseReportNotificationRecipient['id'][], + method?: RecipientMethod[], + }, + opts?: { + removeUnauthorized?: boolean, + joinUser?: boolean, + joinSystemWebhook?: boolean, + }, + ): Promise<MiAbuseReportNotificationRecipient[]> { + const query = this.abuseReportNotificationRecipientRepository.createQueryBuilder('recipient'); + + if (opts?.joinUser) { + query.innerJoinAndSelect('user', 'user', 'recipient.userId = user.id'); + query.innerJoinAndSelect('recipient.userProfile', 'userProfile'); + } + + if (opts?.joinSystemWebhook) { + query.innerJoinAndSelect('recipient.systemWebhook', 'systemWebhook'); + } + + if (params?.ids) { + query.andWhere({ id: In(params.ids) }); + } + + if (params?.method) { + query.andWhere(new Brackets(qb => { + if (params.method?.includes('email')) { + qb.orWhere({ method: 'email', userId: Not(IsNull()) }); + } + if (params.method?.includes('webhook')) { + qb.orWhere({ method: 'webhook', userId: IsNull() }); + } + })); + } + + const recipients = await query.getMany(); + if (recipients.length <= 0) { + return []; + } + + // アサイン有効期é™åˆ‡ã‚Œã¯ã‚¤ãƒ™ãƒ³ãƒˆã§æ‹¾ãˆãªã„ã®ã§ã€ã“ã®ã‚¿ã‚¤ãƒŸãƒ³ã‚°ã§ãƒã‚§ãƒƒã‚¯åŠã³å‰Šé™¤ï¼ˆã‚ªãƒ—ション) + return (opts?.removeUnauthorized ?? true) + ? await this.removeUnauthorizedRecipientUsers(recipients) + : recipients; + } + + /** + * EMailã®é€šçŸ¥å…ˆä¸€è¦§ã‚’å–å¾—ã™ã‚‹. + * リレーション先ã®{@link MiUser}ãŠã‚ˆã³{@link MiUserProfile}ã‚‚åŒæ™‚ã«å–å¾—ã™ã‚‹. + * + * @param {Object} [opts] + * @param {boolean} [opts.removeUnauthorized] 副作用ã¨ã—ã¦ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿æ¨©é™ã‚’æŒãŸãªã„é€ä¿¡å…ˆãƒ¦ãƒ¼ã‚¶ã‚’DBã‹ã‚‰å‰Šé™¤ã™ã‚‹ã‹ã©ã†ã‹(default: true) + * @see removeUnauthorizedRecipientUsers + */ + @bindThis + public async fetchEMailRecipients(opts?: { + removeUnauthorized?: boolean + }): Promise<MiAbuseReportNotificationRecipient[]> { + return this.fetchRecipients({ method: ['email'] }, { joinUser: true, ...opts }); + } + + /** + * Webhookã®é€šçŸ¥å…ˆä¸€è¦§ã‚’å–å¾—ã™ã‚‹. + * リレーション先ã®{@link MiSystemWebhook}ã‚‚åŒæ™‚ã«å–å¾—ã™ã‚‹. + */ + @bindThis + public fetchWebhookRecipients(): Promise<MiAbuseReportNotificationRecipient[]> { + return this.fetchRecipients({ method: ['webhook'] }, { joinSystemWebhook: true }); + } + + /** + * 通知先を作æˆã™ã‚‹. + */ + @bindThis + public async createRecipient( + params: { + isActive: MiAbuseReportNotificationRecipient['isActive']; + name: MiAbuseReportNotificationRecipient['name']; + method: MiAbuseReportNotificationRecipient['method']; + userId: MiAbuseReportNotificationRecipient['userId']; + systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId']; + }, + updater: MiUser, + ): Promise<MiAbuseReportNotificationRecipient> { + const id = this.idService.gen(); + await this.abuseReportNotificationRecipientRepository.insert({ + ...params, + id, + }); + + const created = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: id }); + + this.moderationLogService + .log(updater, 'createAbuseReportNotificationRecipient', { + recipientId: id, + recipient: created, + }) + .then(); + + return created; + } + + /** + * 通知先を更新ã™ã‚‹. + */ + @bindThis + public async updateRecipient( + params: { + id: MiAbuseReportNotificationRecipient['id']; + isActive: MiAbuseReportNotificationRecipient['isActive']; + name: MiAbuseReportNotificationRecipient['name']; + method: MiAbuseReportNotificationRecipient['method']; + userId: MiAbuseReportNotificationRecipient['userId']; + systemWebhookId: MiAbuseReportNotificationRecipient['systemWebhookId']; + }, + updater: MiUser, + ): Promise<MiAbuseReportNotificationRecipient> { + const beforeEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id }); + + await this.abuseReportNotificationRecipientRepository.update(params.id, { + isActive: params.isActive, + updatedAt: new Date(), + name: params.name, + method: params.method, + userId: params.userId, + systemWebhookId: params.systemWebhookId, + }); + + const afterEntity = await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: params.id }); + + this.moderationLogService + .log(updater, 'updateAbuseReportNotificationRecipient', { + recipientId: params.id, + before: beforeEntity, + after: afterEntity, + }) + .then(); + + return afterEntity; + } + + /** + * 通知先を削除ã™ã‚‹. + */ + @bindThis + public async deleteRecipient( + id: MiAbuseReportNotificationRecipient['id'], + updater: MiUser, + ) { + const entity = await this.abuseReportNotificationRecipientRepository.findBy({ id }); + + await this.abuseReportNotificationRecipientRepository.delete(id); + + this.moderationLogService + .log(updater, 'deleteAbuseReportNotificationRecipient', { + recipientId: id, + recipient: entity, + }) + .then(); + } + + /** + * モデレータ権é™ã‚’æŒãŸãªã„(*1)通知先ユーザを削除ã™ã‚‹. + * + * *1: 以下ã®ä¸¡æ–¹ã‚’満ãŸã™ã‚‚ã®ã®äº‹ã‚’言ㆠ+ * - 通知先ã«ãƒ¦ãƒ¼ã‚¶IDãŒè¨å®šã•ã‚Œã¦ã„ã‚‹ + * - 付与ãƒãƒ¼ãƒ«ã«ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿æ¨©é™ãŒãªã„ or アサインã®æœ‰åŠ¹æœŸé™ãŒåˆ‡ã‚Œã¦ã„ã‚‹ + * + * @param recipients 通知先一覧ã®é…列 + * @returns {@lisk recipients}ã‹ã‚‰ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿æ¨©é™ã‚’æŒãŸãªã„通知先を削除ã—ãŸé…列 + */ + @bindThis + private async removeUnauthorizedRecipientUsers(recipients: MiAbuseReportNotificationRecipient[]): Promise<MiAbuseReportNotificationRecipient[]> { + const userRecipients = recipients.filter(it => it.userId !== null); + const recipientUserIds = new Set(userRecipients.map(it => it.userId).filter(x => x != null)); + if (recipientUserIds.size <= 0) { + // ユーザãŒé€šçŸ¥å…ˆã¨ã—ã¦è¨å®šã•ã‚Œã¦ã„ãªã„å ´åˆã€ã“ã®é–¢æ•°ã§ã®å‡¦ç†ã‚’è¡Œã†ã¹ãレコードãŒç„¡ã„ + return recipients; + } + + // モデレータ権é™ã®æœ‰ç„¡ã§é€šçŸ¥å…ˆè¨å®šã‚’振り分ã‘ã‚‹ + const authorizedUserIds = await this.roleService.getModeratorIds(true, true); + const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>(); + const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>(); + for (const recipient of userRecipients) { + // eslint-disable-next-line + if (authorizedUserIds.includes(recipient.userId!)) { + authorizedUserRecipients.push(recipient); + } else { + unauthorizedUserRecipients.push(recipient); + } + } + + // モデレータ権é™ã‚’æŒãŸãªã„通知先をDBã‹ã‚‰å‰Šé™¤ã™ã‚‹ + if (unauthorizedUserRecipients.length > 0) { + await this.abuseReportNotificationRecipientRepository.delete(unauthorizedUserRecipients.map(it => it.id)); + } + const nonUserRecipients = recipients.filter(it => it.userId === null); + return [...nonUserRecipients, ...authorizedUserRecipients].sort((a, b) => a.id.localeCompare(b.id)); + } + + @bindThis + private async onMessage(_: string, data: string): Promise<void> { + const obj = JSON.parse(data); + if (obj.channel !== 'internal') { + return; + } + + const { type } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'roleUpdated': + case 'roleDeleted': + case 'userRoleUnassigned': { + // å ´åˆã«ã‚ˆã£ã¦ã¯ã‚ャッシュ更新よりも先ã«ã“ã“ãŒå‘¼ã°ã‚Œã¦ã—ã¾ã†å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§nextTickã§é…延実行 + process.nextTick(async () => { + const recipients = await this.abuseReportNotificationRecipientRepository.findBy({ + userId: Not(IsNull()), + }); + await this.removeUnauthorizedRecipientUsers(recipients); + }); + break; + } + default: { + break; + } + } + } + + @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/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts new file mode 100644 index 0000000000000000000000000000000000000000..007c3f1bf99e6c8b1421a4f38971f98e6e80f656 --- /dev/null +++ b/packages/backend/src/core/AbuseReportService.ts @@ -0,0 +1,138 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { IdService } from './IdService.js'; + +@Injectable() +export class AbuseReportService { + constructor( + @Inject(DI.abuseUserReportsRepository) + private abuseUserReportsRepository: AbuseUserReportsRepository, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private idService: IdService, + private abuseReportNotificationService: AbuseReportNotificationService, + private queueService: QueueService, + private instanceActorService: InstanceActorService, + private apRendererService: ApRendererService, + private moderationLogService: ModerationLogService, + ) { + } + + /** + * ユーザã‹ã‚‰ã®é€šå ±ã‚’DBã«è¨˜éŒ²ã—ã€ãã®å†…容を下記ã®æ‰‹æ®µã§ç®¡ç†è€…å„ä½ã«é€šçŸ¥ã™ã‚‹. + * - 管ç†è€…用Redisイベント + * - EMail(モデレータ権é™æ‰€æœ‰è€…ユーザ+metaテーブルã«è¨å®šã•ã‚Œã¦ã„るメールアドレス) + * - SystemWebhook + * + * @param params é€šå ±å†…å®¹. ã‚‚ã—複数件ã®é€šå ±ã«å¯¾å¿œã—ãŸæ™‚ã®ãŸã‚ã«ã€ã‚らã‹ã˜ã‚複数件を処ç†ã§ãã‚‹å‰æã§è€ƒãˆã‚‹ + * @see AbuseReportNotificationService.notify + */ + @bindThis + public async report(params: { + targetUserId: MiAbuseUserReport['targetUserId'], + targetUserHost: MiAbuseUserReport['targetUserHost'], + reporterId: MiAbuseUserReport['reporterId'], + reporterHost: MiAbuseUserReport['reporterHost'], + comment: string, + }[]) { + const entities = params.map(param => { + return { + id: this.idService.gen(), + targetUserId: param.targetUserId, + targetUserHost: param.targetUserHost, + reporterId: param.reporterId, + reporterHost: param.reporterHost, + comment: param.comment, + }; + }); + + const reports = Array.of<MiAbuseUserReport>(); + for (const entity of entities) { + const report = await this.abuseUserReportsRepository.insertOne(entity); + reports.push(report); + } + + return Promise.all([ + this.abuseReportNotificationService.notifyAdminStream(reports), + this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'), + this.abuseReportNotificationService.notifyMail(reports), + ]); + } + + /** + * é€šå ±ã‚’è§£æ±ºã—ã€ãã®å†…容を下記ã®æ‰‹æ®µã§ç®¡ç†è€…å„ä½ã«é€šçŸ¥ã™ã‚‹. + * - SystemWebhook + * + * @param params é€šå ±å†…å®¹. ã‚‚ã—複数件ã®é€šå ±ã«å¯¾å¿œã—ãŸæ™‚ã®ãŸã‚ã«ã€ã‚らã‹ã˜ã‚複数件を処ç†ã§ãã‚‹å‰æã§è€ƒãˆã‚‹ + * @param operator é€šå ±ã‚’å‡¦ç†ã—ãŸãƒ¦ãƒ¼ã‚¶ + * @see AbuseReportNotificationService.notify + */ + @bindThis + public async resolve( + params: { + reportId: string; + forward: boolean; + }[], + operator: MiUser, + ) { + const paramsMap = new Map(params.map(it => [it.reportId, it])); + const reports = await this.abuseUserReportsRepository.findBy({ + id: In(params.map(it => it.reportId)), + }); + + const targetUserMap = new Map(); + for (const report of reports) { + const shouldForward = paramsMap.get(report.id)!.forward; + + if (shouldForward && report.targetUserHost != null) { + targetUserMap.set(report.id, await this.usersRepository.findOneByOrFail({ id: report.targetUserId })); + } else { + targetUserMap.set(report.id, null); + } + } + + for (const report of reports) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const ps = paramsMap.get(report.id)!; + + await this.abuseUserReportsRepository.update(report.id, { + resolved: true, + assigneeId: operator.id, + forwarded: ps.forward && report.targetUserHost !== null, + }); + + const targetUser = targetUserMap.get(report.id)!; + if (targetUser != null) { + const actor = await this.instanceActorService.getInstanceActor(); + + // eslint-disable-next-line + const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment); + const contextAssignedFlag = this.apRendererService.addContext(flag); + this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false); + } + + this.moderationLogService + .log(operator, 'resolveAbuseReport', { + reportId: report.id, + report: report, + forwarded: ps.forward && report.targetUserHost !== null, + }); + } + + return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) + .then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved')); + } +} diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts index 9b60df2cae99b92e5965ad75bc58311d585ba5ce..40a9db01c057d70c0fbdacc5e38567c0ffa58df2 100644 --- a/packages/backend/src/core/AnnouncementService.ts +++ b/packages/backend/src/core/AnnouncementService.ts @@ -67,7 +67,7 @@ export class AnnouncementService { @bindThis public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> { - const announcement = await this.announcementsRepository.insert({ + const announcement = await this.announcementsRepository.insertOne({ id: this.idService.gen(), updatedAt: null, title: values.title, @@ -79,7 +79,7 @@ export class AnnouncementService { silence: values.silence, needConfirmationToRead: values.needConfirmationToRead, userId: values.userId, - }).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0])); + }); const packed = await this.announcementEntityService.pack(announcement); diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts index 21e31d79a4292dc3a99fa90e71d00ba4c020ba10..4efd6122b18b580283aef0da9150e9221f32e2e1 100644 --- a/packages/backend/src/core/AvatarDecorationService.ts +++ b/packages/backend/src/core/AvatarDecorationService.ts @@ -29,7 +29,7 @@ export class AvatarDecorationService implements OnApplicationShutdown { private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { - this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); + this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s this.redisForSub.on('message', this.onMessage); } @@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown { @bindThis public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> { - const created = await this.avatarDecorationsRepository.insert({ + const created = await this.avatarDecorationsRepository.insertOne({ id: this.idService.gen(), ...options, - }).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0])); + }); this.globalEventService.publishInternalEvent('avatarDecorationCreated', created); diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index d008e7ec52a3c6cd4b3fcdee28ef95d793dbb024..6725ebe75b14503583df80ad055a1455c7c91eb0 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -56,10 +56,10 @@ export class CacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new MemoryKVCache<MiUser>(Infinity); - this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity); - this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity); - this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity); + this.userByIdCache = new MemoryKVCache<MiUser>(1000 * 60 * 5); // 5m + this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m + this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m + this.uriPersonCache = new MemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', { lifetime: 1000 * 60 * 30, // 30m @@ -135,14 +135,14 @@ export class CacheService implements OnApplicationShutdown { if (user == null) { this.userByIdCache.delete(body.id); this.localUserByIdCache.delete(body.id); - for (const [k, v] of this.uriPersonCache.cache.entries()) { + for (const [k, v] of this.uriPersonCache.entries) { if (v.value?.id === body.id) { this.uriPersonCache.delete(k); } } } else { this.userByIdCache.set(user.id, user); - for (const [k, v] of this.uriPersonCache.cache.entries()) { + for (const [k, v] of this.uriPersonCache.entries) { if (v.value?.id === user.id) { this.uriPersonCache.set(k, user); } diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts index bb8be26ce6c5bda7955b6bde155096b5382d679f..929a9db0645a3a6ae72d852c61c91d9363dc4dce 100644 --- a/packages/backend/src/core/ClipService.ts +++ b/packages/backend/src/core/ClipService.ts @@ -41,17 +41,17 @@ export class ClipService { const currentCount = await this.clipsRepository.countBy({ userId: me.id, }); - if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) { + if (currentCount >= (await this.roleService.getUserPolicies(me.id)).clipLimit) { throw new ClipService.TooManyClipsError(); } - const clip = await this.clipsRepository.insert({ + const clip = await this.clipsRepository.insertOne({ id: this.idService.gen(), userId: me.id, name: name, isPublic: isPublic, description: description, - }).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0])); + }); return clip; } @@ -102,7 +102,7 @@ export class ClipService { const currentCount = await this.clipNotesRepository.countBy({ clipId: clip.id, }); - if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { + if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) { throw new ClipService.TooManyClipNotesError(); } diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 9baec9a59fc9d70aa941bdc5c542a8bb24cd3146..7e51d3afa468093455f38edf4db0de285491d2ba 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -5,6 +5,14 @@ import { Module } from '@nestjs/common'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; +import { AbuseReportService } from '@/core/AbuseReportService.js'; +import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; +import { + AbuseReportNotificationRecipientEntityService, +} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { UserSearchService } from '@/core/UserSearchService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AnnouncementService } from './AnnouncementService.js'; @@ -53,10 +61,11 @@ import { UserFollowingService } from './UserFollowingService.js'; import { UserKeypairService } from './UserKeypairService.js'; import { UserListService } from './UserListService.js'; import { UserMutingService } from './UserMutingService.js'; +import { UserRenoteMutingService } from './UserRenoteMutingService.js'; import { UserSuspendService } from './UserSuspendService.js'; import { UserAuthService } from './UserAuthService.js'; import { VideoProcessingService } from './VideoProcessingService.js'; -import { WebhookService } from './WebhookService.js'; +import { UserWebhookService } from './UserWebhookService.js'; import { ProxyAccountService } from './ProxyAccountService.js'; import { UtilityService } from './UtilityService.js'; import { FileInfoService } from './FileInfoService.js'; @@ -144,6 +153,8 @@ import type { Provider } from '@nestjs/common'; //#region æ–‡å—列ベースã§ã®injection用(循環å‚照対応ã®ãŸã‚) const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService }; +const $AbuseReportService: Provider = { provide: 'AbuseReportService', useExisting: AbuseReportService }; +const $AbuseReportNotificationService: Provider = { provide: 'AbuseReportNotificationService', useExisting: AbuseReportNotificationService }; const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService }; const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService }; const $AnnouncementService: Provider = { provide: 'AnnouncementService', useExisting: AnnouncementService }; @@ -193,10 +204,13 @@ const $UserFollowingService: Provider = { provide: 'UserFollowingService', useEx const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService }; const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService }; const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService }; +const $UserRenoteMutingService: Provider = { provide: 'UserRenoteMutingService', useExisting: UserRenoteMutingService }; +const $UserSearchService: Provider = { provide: 'UserSearchService', useExisting: UserSearchService }; const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService }; const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: UserAuthService }; const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService }; -const $WebhookService: Provider = { provide: 'WebhookService', useExisting: WebhookService }; +const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService }; +const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; @@ -225,6 +239,7 @@ const $ChartManagementService: Provider = { provide: 'ChartManagementService', u const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService }; const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService }; +const $AbuseReportNotificationRecipientEntityService: Provider = { provide: 'AbuseReportNotificationRecipientEntityService', useExisting: AbuseReportNotificationRecipientEntityService }; const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService }; const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService }; const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService }; @@ -258,6 +273,7 @@ const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', u const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService }; const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService }; +const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -285,6 +301,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting ], providers: [ LoggerService, + AbuseReportService, + AbuseReportNotificationService, AccountMoveService, AccountUpdateService, AnnouncementService, @@ -334,10 +352,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserKeypairService, UserListService, UserMutingService, + UserRenoteMutingService, + UserSearchService, UserSuspendService, UserAuthService, VideoProcessingService, - WebhookService, + UserWebhookService, + SystemWebhookService, UtilityService, FileInfoService, SearchService, @@ -366,6 +387,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AbuseUserReportEntityService, AnnouncementEntityService, + AbuseReportNotificationRecipientEntityService, AntennaEntityService, AppEntityService, AuthSessionEntityService, @@ -399,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting RoleEntityService, ReversiGameEntityService, MetaEntityService, + SystemWebhookEntityService, ApAudienceService, ApDbResolverService, @@ -422,6 +445,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#region æ–‡å—列ベースã§ã®injection用(循環å‚照対応ã®ãŸã‚) $LoggerService, + $AbuseReportService, + $AbuseReportNotificationService, $AccountMoveService, $AccountUpdateService, $AnnouncementService, @@ -471,10 +496,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserKeypairService, $UserListService, $UserMutingService, + $UserRenoteMutingService, + $UserSearchService, $UserSuspendService, $UserAuthService, $VideoProcessingService, - $WebhookService, + $UserWebhookService, + $SystemWebhookService, $UtilityService, $FileInfoService, $SearchService, @@ -503,6 +531,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AbuseUserReportEntityService, $AnnouncementEntityService, + $AbuseReportNotificationRecipientEntityService, $AntennaEntityService, $AppEntityService, $AuthSessionEntityService, @@ -536,6 +565,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $RoleEntityService, $ReversiGameEntityService, $MetaEntityService, + $SystemWebhookEntityService, $ApAudienceService, $ApDbResolverService, @@ -560,6 +590,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting exports: [ QueueModule, LoggerService, + AbuseReportService, + AbuseReportNotificationService, AccountMoveService, AccountUpdateService, AnnouncementService, @@ -609,10 +641,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting UserKeypairService, UserListService, UserMutingService, + UserRenoteMutingService, + UserSearchService, UserSuspendService, UserAuthService, VideoProcessingService, - WebhookService, + UserWebhookService, + SystemWebhookService, UtilityService, FileInfoService, SearchService, @@ -640,6 +675,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AbuseUserReportEntityService, AnnouncementEntityService, + AbuseReportNotificationRecipientEntityService, AntennaEntityService, AppEntityService, AuthSessionEntityService, @@ -673,6 +709,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting RoleEntityService, ReversiGameEntityService, MetaEntityService, + SystemWebhookEntityService, ApAudienceService, ApDbResolverService, @@ -696,6 +733,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#region æ–‡å—列ベースã§ã®injection用(循環å‚照対応ã®ãŸã‚) $LoggerService, + $AbuseReportService, + $AbuseReportNotificationService, $AccountMoveService, $AccountUpdateService, $AnnouncementService, @@ -745,10 +784,13 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $UserKeypairService, $UserListService, $UserMutingService, + $UserRenoteMutingService, + $UserSearchService, $UserSuspendService, $UserAuthService, $VideoProcessingService, - $WebhookService, + $UserWebhookService, + $SystemWebhookService, $UtilityService, $FileInfoService, $SearchService, @@ -776,6 +818,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AbuseUserReportEntityService, $AnnouncementEntityService, + $AbuseReportNotificationRecipientEntityService, $AntennaEntityService, $AppEntityService, $AuthSessionEntityService, @@ -809,6 +852,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $RoleEntityService, $ReversiGameEntityService, $MetaEntityService, + $SystemWebhookEntityService, $ApAudienceService, $ApDbResolverService, diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index bfbc2b172da65207d7e297c8e12f622c84ba1f41..cd906a72af174c645d91a6bd3d2135aa85234b64 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -26,7 +26,7 @@ const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; @Injectable() export class CustomEmojiService implements OnApplicationShutdown { - private cache: MemoryKVCache<MiEmoji | null>; + private emojisCache: MemoryKVCache<MiEmoji | null>; public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>; constructor( @@ -49,7 +49,7 @@ export class CustomEmojiService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private driveService: DriveService, ) { - this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); + this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', { lifetime: 1000 * 60 * 30, // 30m @@ -77,7 +77,7 @@ export class CustomEmojiService implements OnApplicationShutdown { localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; }, moderator?: MiUser): Promise<MiEmoji> { - const emoji = await this.emojisRepository.insert({ + const emoji = await this.emojisRepository.insertOne({ id: this.idService.gen(), updatedAt: new Date(), name: data.name, @@ -91,7 +91,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, - }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + }); if (data.host == null) { this.localEmojisCache.refresh(); @@ -142,6 +142,13 @@ export class CustomEmojiService implements OnApplicationShutdown { this.localEmojisCache.refresh(); + if (data.driveFile != null) { + const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() }); + if (file && file.id !== data.driveFile.id) { + await this.driveService.deleteFile(file, false, moderator ? moderator : undefined); + } + } + const packed = await this.emojiEntityService.packDetailed(emoji.id); if (emoji.name === data.name) { @@ -350,14 +357,14 @@ export class CustomEmojiService implements OnApplicationShutdown { if (name == null) return null; if (host == null) return null; - const newHost = host === this.config.host ? null : host; + const newHost = host === this.config.host ? null : host; const queryOrNull = async () => (await this.emojisRepository.findOneBy({ name, host: newHost ?? IsNull(), })) ?? null; - const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull); + const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull); if (emoji == null) return null; return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl ã—ã¦ã‚‹ã®ã¯å¾Œæ–¹äº’æ›æ€§ã®ãŸã‚(publicUrlã¯stringãªã®ã§??ã¯ã ã‚) @@ -384,7 +391,7 @@ export class CustomEmojiService implements OnApplicationShutdown { */ @bindThis public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> { - const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null); + const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null); const emojisQuery: any[] = []; const hosts = new Set(notCachedEmojis.map(e => e.host)); for (const host of hosts) { @@ -399,7 +406,7 @@ export class CustomEmojiService implements OnApplicationShutdown { select: ['name', 'host', 'originalUrl', 'publicUrl'], }) : []; for (const emoji of _emojis) { - this.cache.set(`${emoji.name} ${emoji.host}`, emoji); + this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji); } } @@ -424,7 +431,7 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public dispose(): void { - this.cache.dispose(); + this.emojisCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 79b614edbacac4edb725eb10c0be72767fb27086..7f1b8f3efb0feb917919d0f4b3346fb25a9d2e19 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -4,12 +4,15 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import { Not, IsNull } from 'typeorm'; +import type { FollowingsRepository, MiUser, UsersRepository } from '@/models/_.js'; import { QueueService } from '@/core/QueueService.js'; -import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; 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'; @Injectable() export class DeleteAccountService { @@ -17,9 +20,14 @@ export class DeleteAccountService { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - private userSuspendService: UserSuspendService, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private apRendererService: ApRendererService, private queueService: QueueService, private globalEventService: GlobalEventService, + private moderationLogService: ModerationLogService, ) { } @@ -27,16 +35,52 @@ export class DeleteAccountService { public async deleteAccount(user: { id: string; host: string | null; - }): Promise<void> { + }, 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 (moderator != null) { + this.moderationLogService.log(moderator, 'deleteAccount', { + userId: user.id, + userUsername: _user.username, + userHost: user.host, + }); + } + // 物ç†å‰Šé™¤ã™ã‚‹å‰ã«Delete activityã‚’é€ä¿¡ã™ã‚‹ - await this.userSuspendService.doPostSuspend(user).catch(e => {}); + if (this.userEntityService.isLocalUser(user)) { + // 知り得る全SharedInboxã«Deleteé…ä¿¡ + const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); - this.queueService.createDeleteAccountJob(user, { - soft: false, - }); + const queue: string[] = []; + + const followings = await this.followingsRepository.find({ + where: [ + { followerSharedInbox: Not(IsNull()) }, + { followeeSharedInbox: Not(IsNull()) }, + ], + select: ['followerSharedInbox', 'followeeSharedInbox'], + }); + + const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); + + for (const inbox of inboxes) { + if (inbox != null && !queue.includes(inbox)) queue.push(inbox); + } + + for (const inbox of queue) { + this.queueService.deliver(user, content, inbox, true); + } + + this.queueService.createDeleteAccountJob(user, { + soft: false, + }); + } else { + // リモートユーザーã®å‰Šé™¤ã¯ã€å®Œå…¨ã«DBã‹ã‚‰ç‰©ç†å‰Šé™¤ã—ã¦ã—ã¾ã†ã¨å†åº¦é€£åˆã—ã¦ãã¦ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒå¾©æ´»ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ãŸã‚ã€soft指定ã™ã‚‹ + this.queueService.createDeleteAccountJob(user, { + soft: true, + }); + } await this.usersRepository.update(user.id, { isDeleted: true, diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index af5451bfc8813bf8933eb2a2f543a588f70fe6e0..46fa4243a7d518aa613d1c04f718f39916023c71 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { UtilityService } from '@/core/UtilityService.js'; type AddFileArgs = { /** User who wish to add file */ @@ -127,6 +128,7 @@ export class DriveService { private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, + private utilityService: UtilityService, ) { const logger = new Logger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); @@ -220,7 +222,7 @@ export class DriveService { file.size = size; file.storedInternal = false; - return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); + return await this.driveFilesRepository.insertOne(file); } else { // use internal storage const accessKey = randomUUID(); const thumbnailAccessKey = 'thumbnail-' + randomUUID(); @@ -254,7 +256,7 @@ export class DriveService { file.md5 = hash; file.size = size; - return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); + return await this.driveFilesRepository.insertOne(file); } } @@ -567,6 +569,7 @@ export class DriveService { sensitive ?? false : false; + if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; @@ -594,7 +597,7 @@ export class DriveService { file.type = info.type.mime; file.storedInternal = false; - file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0])); + file = await this.driveFilesRepository.insertOne(file); } catch (err) { // duplicate key error (when already registered) if (isDuplicateKeyValueError(err)) { diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 08f8f80a6ed9151783e743ada5a4a43a965a4869..435dbbae28a95f62e645aac738605e2eaea45d1c 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -16,6 +16,7 @@ import type { 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 { @@ -32,6 +33,7 @@ export class EmailService { private loggerService: LoggerService, private utilityService: UtilityService, private httpRequestService: HttpRequestService, + private queueService: QueueService, ) { this.logger = this.loggerService.getLogger('email'); } diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts index 5725c795ed22de934fca9fcf2ec00cab1c0486a3..bd86a80cbde5fdae9232c9e8c9872219a9500d74 100644 --- a/packages/backend/src/core/FanoutTimelineEndpointService.ts +++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts @@ -56,9 +56,6 @@ export class FanoutTimelineEndpointService { @bindThis private async getMiNotes(ps: TimelineOptions): Promise<MiNote[]> { - let noteIds: string[]; - let shouldFallbackToDb = false; - // 呼ã³å‡ºã—å…ƒã¨ä»¥ä¸‹ã®å‡¦ç†ã‚’シンプルã«ã™ã‚‹ãŸã‚ã«dbFallbackã‚’ç½®ãæ›ãˆã‚‹ if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]); @@ -68,12 +65,11 @@ export class FanoutTimelineEndpointService { const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId); // TODO: ã„ã„æ„Ÿã˜ã«getMulti内ã§ã‚½ãƒ¼ãƒˆæ¸ˆã ã‹ã‚‰uniqã™ã‚‹ã¨ãã«redisResultãŒå…¨ã¦ã‚½ãƒ¼ãƒˆæ¸ˆãªã®ã‚’利用ã—ã¦å†ã‚½ãƒ¼ãƒˆã‚’é¿ã‘ãŸã„ - const redisResultIds = Array.from(new Set(redisResult.flat(1))); - - redisResultIds.sort(idCompare); - noteIds = redisResultIds.slice(0, ps.limit); + const redisResultIds = Array.from(new Set(redisResult.flat(1))).sort(idCompare); - shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0); + let noteIds = redisResultIds.slice(0, ps.limit); + const oldestNoteId = ascending ? redisResultIds[0] : redisResultIds[redisResultIds.length - 1]; + const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId; if (!shouldFallbackToDb) { let filter = ps.noteFilter ?? (_note => true); diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index e73e1d3a9d0c8167b806d086561c324108bfb633..7aeeb781786c8b60d7d00b091eebcf0902ee73cd 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -56,11 +56,11 @@ export class FederatedInstanceService implements OnApplicationShutdown { const index = await this.instancesRepository.findOneBy({ host }); if (index == null) { - const i = await this.instancesRepository.insert({ + const i = await this.instancesRepository.insertOne({ id: this.idService.gen(), host, firstRetrievedAt: new Date(), - }).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0])); + }); this.federatedInstanceCache.set(host, i); return i; diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index 9a9eae3cae81f4c19cf050b51539b175424394ba..cc66e9fe3a47eabd65e5212997efa5b1370df56c 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -12,7 +12,7 @@ import FFmpeg from 'fluent-ffmpeg'; import isSvg from 'is-svg'; import probeImageSize from 'probe-image-size'; import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; -import { encode } from 'blurhash'; +import * as blurhash from 'blurhash'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; @@ -283,7 +283,7 @@ export class FileInfoService { } /** - * Calculate average color of image + * Calculate blurhash string of image */ @bindThis private getBlurhash(path: string, type: string): Promise<string> { @@ -298,7 +298,7 @@ export class FileInfoService { let hash; try { - hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5); + hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5); } catch (e) { return reject(e); } diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 22871adb16bc4bc9529f5543b093a65ab54e43c1..753011cded8983ada947cb70c5d469a157dab18d 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -18,6 +18,7 @@ import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import type { MiSignin } from '@/models/Signin.js'; import type { MiPage } from '@/models/Page.js'; import type { MiWebhook } from '@/models/Webhook.js'; +import type { MiSystemWebhook } from '@/models/SystemWebhook.js'; import type { MiMeta } from '@/models/Meta.js'; import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -135,6 +136,7 @@ export interface NoteEventTypes { }; replied: { id: MiNote['id']; + userId: MiUser['id']; }; } type NoteStreamEventTypes = { @@ -212,6 +214,10 @@ type SerializedAll<T> = { [K in keyof T]: Serialized<T[K]>; }; +type UndefinedAsNullAll<T> = { + [K in keyof T]: T[K] extends undefined ? null : T[K]; +} + export interface InternalEventTypes { userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; }; @@ -231,6 +237,9 @@ export interface InternalEventTypes { webhookCreated: MiWebhook; webhookDeleted: MiWebhook; webhookUpdated: MiWebhook; + systemWebhookCreated: MiSystemWebhook; + systemWebhookDeleted: MiSystemWebhook; + systemWebhookUpdated: MiSystemWebhook; antennaCreated: MiAntenna; antennaDeleted: MiAntenna; antennaUpdated: MiAntenna; @@ -247,43 +256,45 @@ export interface InternalEventTypes { userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; }; } +type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>; + // name/messages(spec) pairs dictionary export type GlobalEvents = { internal: { name: 'internal'; - payload: EventUnionFromDictionary<SerializedAll<InternalEventTypes>>; + payload: EventTypesToEventPayload<InternalEventTypes>; }; broadcast: { name: 'broadcast'; - payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>; + payload: EventTypesToEventPayload<BroadcastTypes>; }; main: { name: `mainStream:${MiUser['id']}`; - payload: EventUnionFromDictionary<SerializedAll<MainEventTypes>>; + payload: EventTypesToEventPayload<MainEventTypes>; }; drive: { name: `driveStream:${MiUser['id']}`; - payload: EventUnionFromDictionary<SerializedAll<DriveEventTypes>>; + payload: EventTypesToEventPayload<DriveEventTypes>; }; note: { name: `noteStream:${MiNote['id']}`; - payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>; + payload: EventTypesToEventPayload<NoteStreamEventTypes>; }; userList: { name: `userListStream:${MiUserList['id']}`; - payload: EventUnionFromDictionary<SerializedAll<UserListEventTypes>>; + payload: EventTypesToEventPayload<UserListEventTypes>; }; roleTimeline: { name: `roleTimelineStream:${MiRole['id']}`; - payload: EventUnionFromDictionary<SerializedAll<RoleTimelineEventTypes>>; + payload: EventTypesToEventPayload<RoleTimelineEventTypes>; }; antenna: { name: `antennaStream:${MiAntenna['id']}`; - payload: EventUnionFromDictionary<SerializedAll<AntennaEventTypes>>; + payload: EventTypesToEventPayload<AntennaEventTypes>; }; admin: { name: `adminStream:${MiUser['id']}`; - payload: EventUnionFromDictionary<SerializedAll<AdminEventTypes>>; + payload: EventTypesToEventPayload<AdminEventTypes>; }; notes: { name: 'notesStream'; @@ -291,11 +302,11 @@ export type GlobalEvents = { }; reversi: { name: `reversiStream:${MiUser['id']}`; - payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>; + payload: EventTypesToEventPayload<ReversiEventTypes>; }; reversiGame: { name: `reversiGameStream:${MiReversiGame['id']}`; - payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>; + payload: EventTypesToEventPayload<ReversiGameEventTypes>; }; }; diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index 96d9b099927b5ad58f28aab81ed0be996b11b9c9..f102461a5065fd7f669d2e9dda3e4fdf99eddeb9 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -15,7 +15,7 @@ export class LoggerService { } @bindThis - public getLogger(domain: string, color?: KEYWORD | undefined, store?: boolean) { - return new Logger(domain, color, store); + public getLogger(domain: string, color?: KEYWORD | undefined) { + return new Logger(domain, color); } } diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 625df1feaa82fcbb9ddfd3e3541dae3cd32a82e7..1b6951ca19109b517aebd4daf72f007b51b6f5bb 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -6,17 +6,19 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { Window, XMLSerializer } from 'happy-dom'; +import { Window, DocumentFragment, XMLSerializer } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; -import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; +import type { DefaultTreeAdapterMap } from 'parse5'; import type * as mfm from '@transfem-org/sfm-js'; -const treeAdapter = TreeAdapter.defaultTreeAdapter; +const treeAdapter = parse5.defaultTreeAdapter; +type Node = DefaultTreeAdapterMap['node']; +type ChildNode = DefaultTreeAdapterMap['childNode']; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; @@ -46,7 +48,7 @@ export class MfmService { return text.trim(); - function getText(node: TreeAdapter.Node): string { + function getText(node: Node): string { if (treeAdapter.isTextNode(node)) return node.value; if (!treeAdapter.isElementNode(node)) return ''; if (node.nodeName === 'br') return '\n'; @@ -58,7 +60,7 @@ export class MfmService { return ''; } - function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { + function appendChildren(childNodes: ChildNode[]): void { if (childNodes) { for (const n of childNodes) { analyze(n); @@ -66,14 +68,16 @@ export class MfmService { } } - function analyze(node: TreeAdapter.Node) { + function analyze(node: Node) { if (treeAdapter.isTextNode(node)) { text += node.value; return; } // Skip comment or document type node - if (!treeAdapter.isElementNode(node)) return; + if (!treeAdapter.isElementNode(node)) { + return; + } switch (node.nodeName) { case 'br': { @@ -81,8 +85,7 @@ export class MfmService { break; } - case 'a': - { + case 'a': { const txt = getText(node); const rel = node.attrs.find(x => x.name === 'rel'); const href = node.attrs.find(x => x.name === 'href'); @@ -90,7 +93,7 @@ export class MfmService { // ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚° if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { text += txt; - // メンション + // メンション } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { const part = txt.split('@'); @@ -102,7 +105,7 @@ export class MfmService { } else if (part.length === 3) { text += txt; } - // ãã®ä»– + // ãã®ä»– } else { const generateLink = () => { if (!href && !txt) { @@ -130,8 +133,7 @@ export class MfmService { break; } - case 'h1': - { + case 'h1': { text += '**ã€'; appendChildren(node.childNodes); text += '】**\n'; @@ -139,8 +141,7 @@ export class MfmService { } case 'h2': - case 'h3': - { + case 'h3': { text += '**'; appendChildren(node.childNodes); text += '**\n'; @@ -148,16 +149,14 @@ export class MfmService { } case 'b': - case 'strong': - { + case 'strong': { text += '**'; appendChildren(node.childNodes); text += '**'; break; } - case 'small': - { + case 'small': { text += '<small>'; appendChildren(node.childNodes); text += '</small>'; @@ -165,8 +164,7 @@ export class MfmService { } case 's': - case 'del': - { + case 'del': { text += '~~'; appendChildren(node.childNodes); text += '~~'; @@ -174,8 +172,7 @@ export class MfmService { } case 'i': - case 'em': - { + case 'em': { text += '<i>'; appendChildren(node.childNodes); text += '</i>'; @@ -214,8 +211,7 @@ export class MfmService { case 'p': case 'h4': case 'h5': - case 'h6': - { + case 'h6': { text += '\n\n'; appendChildren(node.childNodes); break; @@ -228,8 +224,7 @@ export class MfmService { case 'article': case 'li': case 'dt': - case 'dd': - { + case 'dd': { text += '\n'; appendChildren(node.childNodes); break; @@ -483,6 +478,8 @@ export class MfmService { const doc = window.document; + const body = doc.createElement('p'); + async function appendChildren(children: mfm.MfmNode[], targetElement: any): Promise<void> { if (children) { for (const child of await Promise.all(children.map(async (x) => await (handlers as any)[x.type](x)))) targetElement.appendChild(child); @@ -661,7 +658,7 @@ export class MfmService { }, }; - await appendChildren(nodes, doc.body); + await appendChildren(nodes, body); if (quoteUri !== null) { const a = doc.createElement('a'); @@ -675,9 +672,15 @@ export class MfmService { quote.innerHTML += 'RE: '; quote.appendChild(a); - doc.body.appendChild(quote); + body.appendChild(quote); + } + + let result = new XMLSerializer().serializeToString(body); + + if (inline) { + result = result.replace(/^<p>/, '').replace(/<\/p>$/, ''); } - return inline ? doc.body.innerHTML : `<p>${doc.body.innerHTML}</p>`; + return result; } } diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index 6c155c9a627c145c784442ec46d490d484c445bf..2c02af217d916d6884c3f54732f5ca1f980246c8 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -9,7 +9,8 @@ import type { ModerationLogsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; -import { ModerationLogPayloads, moderationLogTypes } from '@/types.js'; +import type { ModerationLogPayloads } from '@/types.js'; +import { moderationLogTypes } from '@/types.js'; @Injectable() export class ModerationLogService { diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 41efa76f3f6ede0c0c11667f6a6dc0bd8726843b..c252336f995c7dde574c1b64e1e85092c8b321bb 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -38,7 +38,7 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; -import { WebhookService } from '@/core/WebhookService.js'; +import { UserWebhookService } from '@/core/UserWebhookService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { AntennaService } from '@/core/AntennaService.js'; import { QueueService } from '@/core/QueueService.js'; @@ -61,7 +61,6 @@ import { CacheService } from '@/core/CacheService.js'; import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -207,7 +206,7 @@ export class NoteCreateService implements OnApplicationShutdown { private federatedInstanceService: FederatedInstanceService, private hashtagService: HashtagService, private antennaService: AntennaService, - private webhookService: WebhookService, + private webhookService: UserWebhookService, private featuredService: FeaturedService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, @@ -282,7 +281,7 @@ export class NoteCreateService implements OnApplicationShutdown { data.visibility = 'home'; } - if (this.isRenote(data)) { + if (data.renote) { switch (data.renote.visibility) { case 'public': // public noteã¯ç„¡æ¡ä»¶ã«renoteå¯èƒ½ @@ -368,6 +367,9 @@ export class NoteCreateService implements OnApplicationShutdown { mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); } + // if the host is media-silenced, custom emojis are not allowed + if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; + tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { @@ -417,175 +419,8 @@ export class NoteCreateService implements OnApplicationShutdown { host: MiUser['host']; isBot: MiUser['isBot']; noindex: MiUser['noindex']; - }, data: Option, silent = false): Promise<MiNote> { - // ãƒãƒ£ãƒ³ãƒãƒ«å¤–ã«ãƒªãƒ—ライã—ãŸã‚‰å¯¾è±¡ã®ã‚¹ã‚³ãƒ¼ãƒ—ã«åˆã‚ã›ã‚‹ - // (クライアントサイドã§ã‚„ã£ã¦ã‚‚良ã„処ç†ã ã¨æ€ã†ã‘ã©ã¨ã‚Šã‚ãˆãšã‚µãƒ¼ãƒãƒ¼ã‚µã‚¤ãƒ‰ã§) - if (data.reply && data.channel && data.reply.channelId !== data.channel.id) { - if (data.reply.channelId) { - data.channel = await this.channelsRepository.findOneBy({ id: data.reply.channelId }); - } else { - data.channel = null; - } - } - - // ãƒãƒ£ãƒ³ãƒãƒ«å†…ã«ãƒªãƒ—ライã—ãŸã‚‰å¯¾è±¡ã®ã‚¹ã‚³ãƒ¼ãƒ—ã«åˆã‚ã›ã‚‹ - // (クライアントサイドã§ã‚„ã£ã¦ã‚‚良ã„処ç†ã ã¨æ€ã†ã‘ã©ã¨ã‚Šã‚ãˆãšã‚µãƒ¼ãƒãƒ¼ã‚µã‚¤ãƒ‰ã§) - if (data.reply && (data.channel == null) && data.reply.channelId) { - data.channel = await this.channelsRepository.findOneBy({ id: data.reply.channelId }); - } - - if (data.createdAt == null) data.createdAt = new Date(); - if (data.visibility == null) data.visibility = 'public'; - if (data.localOnly == null) data.localOnly = false; - if (data.channel != null) data.visibility = 'public'; - 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; - if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { - data.visibility = 'home'; - } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { - data.visibility = 'home'; - } - } - - const hasProhibitedWords = await this.checkProhibitedWordsContain({ - cw: data.cw, - text: data.text, - pollChoices: data.poll?.choices, - }, 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); - - if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { - data.visibility = 'home'; - } - - if (data.renote) { - switch (data.renote.visibility) { - case 'public': - // public noteã¯ç„¡æ¡ä»¶ã«renoteå¯èƒ½ - break; - case 'home': - // home noteã¯home以下ã«renoteå¯èƒ½ - if (data.visibility === 'public') { - data.visibility = 'home'; - } - break; - case 'followers': - // 他人ã®followers noteã¯reject - if (data.renote.userId !== user.id) { - throw new Error('Renote target is not public or home'); - } - - // Renote対象ãŒfollowersãªã‚‰followersã«ã™ã‚‹ - data.visibility = 'followers'; - break; - case 'specified': - // specified / direct noteã¯reject - throw new Error('Renote target is not public or home'); - } - } - - // Check blocking - if (this.isRenote(data) && !this.isQuote(data)) { - if (data.renote.userHost === null) { - if (data.renote.userId !== user.id) { - const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); - if (blocked) { - throw new Error('blocked'); - } - } - } - } - - // 返信対象ãŒpublicã§ã¯ãªã„ãªã‚‰homeã«ã™ã‚‹ - if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') { - data.visibility = 'home'; - } - - // ãƒãƒ¼ã‚«ãƒ«ã®ã¿ã‚’Renoteã—ãŸã‚‰ãƒãƒ¼ã‚«ãƒ«ã®ã¿ã«ã™ã‚‹ - if (data.renote && data.renote.localOnly && data.channel == null) { - data.localOnly = true; - } - - // ãƒãƒ¼ã‚«ãƒ«ã®ã¿ã«ãƒªãƒ—ライã—ãŸã‚‰ãƒãƒ¼ã‚«ãƒ«ã®ã¿ã«ã™ã‚‹ - if (data.reply && data.reply.localOnly && data.channel == null) { - data.localOnly = true; - } - - if (data.text) { - if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { - data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); - } - data.text = data.text.trim(); - if (data.text === '') { - data.text = null; - } - } else { - data.text = null; - } - - let tags = data.apHashtags; - let emojis = data.apEmojis; - let mentionedUsers = data.apMentions; - - // Parse MFM if needed - if (!tags || !emojis || !mentionedUsers) { - const tokens = (data.text ? mfm.parse(data.text)! : []); - const cwTokens = data.cw ? mfm.parse(data.cw)! : []; - const choiceTokens = data.poll && data.poll.choices - ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) - : []; - - const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); - - tags = data.apHashtags ?? extractHashtags(combinedTokens); - - emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens); - - mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); - } - - tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); - - if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { - mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId })); - } - - if (data.visibility === 'specified') { - if (data.visibleUsers == null) throw new Error('invalid param'); - - for (const u of data.visibleUsers) { - if (!mentionedUsers.some(x => x.id === u.id)) { - mentionedUsers.push(u); - } - } - - if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) { - data.visibleUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId })); - } - } - - if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) { - throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions'); - } - - const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); - - setImmediate('post created', { signal: this.#shutdownController.signal }).then( - () => this.postNoteImported(note, user, data, silent, tags!, mentionedUsers!), - () => { /* aborted, ignore this */ }, - ); - - return note; + }, data: Option): Promise<MiNote> { + return this.create(user, data, true); } @bindThis @@ -705,7 +540,7 @@ export class NoteCreateService implements OnApplicationShutdown { const meta = await this.metaService.fetch(); this.notesChart.update(note, true); - if (meta.enableChartsForRemoteUser || (user.host == null)) { + if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) { this.perUserNotesChart.update(user, note, true); } @@ -817,7 +652,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.webhookService.getActiveWebhooks().then(webhooks => { webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'note', { + this.queueService.userWebhookDeliver(webhook, 'note', { note: noteObj, }); } @@ -831,6 +666,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.reply) { this.globalEventService.publishNoteStream(data.reply.id, 'replied', { id: note.id, + userId: user.id, }); // 通知 if (data.reply.userHost === null) { @@ -855,7 +691,7 @@ export class NoteCreateService implements OnApplicationShutdown { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'reply', { + this.queueService.userWebhookDeliver(webhook, 'reply', { note: noteObj, }); } @@ -895,7 +731,7 @@ export class NoteCreateService implements OnApplicationShutdown { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'renote', { + this.queueService.userWebhookDeliver(webhook, 'renote', { note: noteObj, }); } @@ -964,105 +800,6 @@ export class NoteCreateService implements OnApplicationShutdown { if (!user.noindex) this.index(note); } - @bindThis - private async postNoteImported(note: MiNote, user: { - id: MiUser['id']; - username: MiUser['username']; - host: MiUser['host']; - 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 (meta.enableChartsForRemoteUser || (user.host == null)) { - this.perUserNotesChart.update(user, note, true); - } - - // Register host - 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); - } else if (!note.renote) { - this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); - } - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { - this.instanceChart.updateNote(i.host, note, true); - } - }); - } - - if (data.renote && data.text) { - // Increment notes count (user) - this.incNotesCountOfUser(user); - } else if (!data.renote) { - // Increment notes count (user) - this.incNotesCountOfUser(user); - } - - this.pushToTl(note, user); - - this.antennaService.addNoteToAntennas(note, user); - - if (data.reply) { - this.saveReply(data.reply, note); - } - - if (data.reply == null) { - // TODO: ã‚ャッシュ - this.followingsRepository.findBy({ - followeeId: user.id, - notify: 'normal', - }).then(followings => { - for (const following of followings) { - // TODO: ワードミュート考慮 - this.notificationService.createNotification(following.followerId, 'note', { - noteId: note.id, - }, user.id); - } - }); - } - - if (data.renote && data.text == null && data.renote.userId !== user.id && !user.isBot) { - this.incRenoteCount(data.renote); - } - - if (data.poll && data.poll.expiresAt) { - const delay = data.poll.expiresAt.getTime() - Date.now(); - this.queueService.endedPollNotificationQueue.add(note.id, { - noteId: note.id, - }, { - delay, - removeOnComplete: true, - }); - } - - // Pack the note - const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); - - if (data.channel) { - this.channelsRepository.increment({ id: data.channel.id }, 'notesCount', 1); - this.channelsRepository.update(data.channel.id, { - lastNotedAt: new Date(), - }); - - this.notesRepository.countBy({ - userId: user.id, - channelId: data.channel.id, - }).then(count => { - // ã“ã®å‡¦ç†ãŒè¡Œã‚れるã®ã¯ãƒŽãƒ¼ãƒˆä½œæˆå¾Œãªã®ã§ã€ãƒŽãƒ¼ãƒˆãŒä¸€ã¤ã—ã‹ãªã‹ã£ãŸã‚‰æœ€åˆã®æŠ•ç¨¿ã ã¨åˆ¤æ–ã§ãã‚‹ - // TODO: ã¨ã¯ã„ãˆãƒŽãƒ¼ãƒˆã‚’削除ã—ã¦ä½•å›žã‚‚投稿ã™ã‚Œã°ãã®åˆ†ã ã‘インクリメントã•ã‚Œã‚‹é›‘ã•ã‚‚ã‚ã‚‹ã®ã§ã©ã†ã«ã‹ã—ãŸã„ - if (count === 1) { - this.channelsRepository.increment({ id: data.channel!.id }, 'usersCount', 1); - } - }); - } - - // Register to search database - if (!user.noindex) this.index(note); - } - @bindThis private isRenote(note: Option): note is Option & { renote: MiNote } { return note.renote != null; @@ -1134,7 +871,7 @@ export class NoteCreateService implements OnApplicationShutdown { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'mention', { + this.queueService.userWebhookDeliver(webhook, 'mention', { note: detailPackedNote, }); } @@ -1185,7 +922,7 @@ export class NoteCreateService implements OnApplicationShutdown { const mentions = extractMentions(tokens); let mentionedUsers = (await Promise.all(mentions.map(m => this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), - ))).filter(isNotNull); + ))).filter(x => x != null); // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => @@ -1280,10 +1017,13 @@ export class NoteCreateService implements OnApplicationShutdown { } } - if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身ã®HTL - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 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); + if (note.fileIds.length > 0) { + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + } } } diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index af65750a01fb9c3035c9fdbfa8b565646cb5891b..7ce6d7c605a73da210d37f9e5999812ddb81e219 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -99,7 +99,7 @@ export class NoteDeleteService { this.deliverToConcerned(user, note, content); } - // also deliever delete activity to cascaded notes + // also deliver delete activity to cascaded notes const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes for (const cascadingNote of federatedLocalCascadingNotes) { if (!cascadingNote.user) continue; diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 0cb58d04a2a952805969e6d918eee85b7fd3c2c9..5ff0f26e2bac1fdaa965159130f25a4121a2febc 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -31,7 +31,7 @@ import InstanceChart from '@/core/chart/charts/instance.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; -import { WebhookService } from '@/core/WebhookService.js'; +import { UserWebhookService } from '@/core/UserWebhookService.js'; import { QueueService } from '@/core/QueueService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -51,7 +51,6 @@ import { CacheService } from '@/core/CacheService.js'; import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited'; @@ -203,7 +202,7 @@ export class NoteEditService implements OnApplicationShutdown { private notificationService: NotificationService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, - private webhookService: WebhookService, + private webhookService: UserWebhookService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, @@ -388,6 +387,9 @@ export class NoteEditService implements OnApplicationShutdown { mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); } + // if the host is media-silenced, custom emojis are not allowed + if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; + tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { @@ -631,7 +633,7 @@ export class NoteEditService implements OnApplicationShutdown { this.webhookService.getActiveWebhooks().then(webhooks => { webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'note', { + this.queueService.userWebhookDeliver(webhook, 'note', { note: noteObj, }); } @@ -666,7 +668,7 @@ export class NoteEditService implements OnApplicationShutdown { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('edited')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'edited', { + this.queueService.userWebhookDeliver(webhook, 'edited', { note: noteObj, }); } @@ -801,7 +803,7 @@ export class NoteEditService implements OnApplicationShutdown { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('edited')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'edited', { + this.queueService.userWebhookDeliver(webhook, 'edited', { note: detailPackedNote, }); } @@ -838,7 +840,7 @@ export class NoteEditService implements OnApplicationShutdown { const mentions = extractMentions(tokens); let mentionedUsers = (await Promise.all(mentions.map(m => this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), - ))).filter(isNotNull) as MiUser[]; + ))).filter(x => x !== null) as MiUser[]; // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => @@ -933,10 +935,13 @@ export class NoteEditService implements OnApplicationShutdown { } } - if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身ã®HTL - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); - if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 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); + if (note.fileIds.length > 0) { + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + } } } diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 216734e9e5e414cbcb6204c153e9e8f486426cc5..b10b8e5899557426ebbaa1b1b7a6ed12f8e564a0 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -7,10 +7,17 @@ import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { QUEUE, baseQueueOptions } from '@/queue/const.js'; +import { baseQueueOptions, QUEUE } from '@/queue/const.js'; import { allSettled } from '@/misc/promise-tracker.js'; +import { + DeliverJobData, + EndedPollNotificationJobData, + InboxJobData, + RelationshipJobData, + UserWebhookDeliverJobData, + SystemWebhookDeliverJobData, +} from '../queue/types.js'; import type { Provider } from '@nestjs/common'; -import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; export type SystemQueue = Bull.Queue<Record<string, unknown>>; export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>; @@ -19,7 +26,8 @@ export type InboxQueue = Bull.Queue<InboxJobData>; export type DbQueue = Bull.Queue; export type RelationshipQueue = Bull.Queue<RelationshipJobData>; export type ObjectStorageQueue = Bull.Queue; -export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>; +export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>; +export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>; const $system: Provider = { provide: 'queue:system', @@ -63,9 +71,15 @@ const $objectStorage: Provider = { inject: [DI.config], }; -const $webhookDeliver: Provider = { - provide: 'queue:webhookDeliver', - useFactory: (config: Config) => new Bull.Queue(QUEUE.WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.WEBHOOK_DELIVER)), +const $userWebhookDeliver: Provider = { + provide: 'queue:userWebhookDeliver', + useFactory: (config: Config) => new Bull.Queue(QUEUE.USER_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.USER_WEBHOOK_DELIVER)), + inject: [DI.config], +}; + +const $systemWebhookDeliver: Provider = { + provide: 'queue:systemWebhookDeliver', + useFactory: (config: Config) => new Bull.Queue(QUEUE.SYSTEM_WEBHOOK_DELIVER, baseQueueOptions(config, QUEUE.SYSTEM_WEBHOOK_DELIVER)), inject: [DI.config], }; @@ -80,7 +94,8 @@ const $webhookDeliver: Provider = { $db, $relationship, $objectStorage, - $webhookDeliver, + $userWebhookDeliver, + $systemWebhookDeliver, ], exports: [ $system, @@ -90,7 +105,8 @@ const $webhookDeliver: Provider = { $db, $relationship, $objectStorage, - $webhookDeliver, + $userWebhookDeliver, + $systemWebhookDeliver, ], }) export class QueueModule implements OnApplicationShutdown { @@ -102,7 +118,8 @@ export class QueueModule implements OnApplicationShutdown { @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, + @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, + @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, ) {} public async dispose(): Promise<void> { @@ -117,7 +134,8 @@ export class QueueModule implements OnApplicationShutdown { this.dbQueue.close(), this.relationshipQueue.close(), this.objectStorageQueue.close(), - this.webhookDeliverQueue.close(), + this.userWebhookDeliverQueue.close(), + this.systemWebhookDeliverQueue.close(), ]); } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 103813acf293b962968c3ec24ab0aebf2c4a0772..be5f10771a9f4e89c6a4e13c64041b5097a5a1dd 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -8,16 +8,34 @@ import { Inject, Injectable } from '@nestjs/common'; import type { IActivity } from '@/core/activitypub/type.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; +import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; -import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; +import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; +import type { + DbJobData, + DeliverJobData, + RelationshipJobData, + SystemWebhookDeliverJobData, + ThinUser, + UserWebhookDeliverJobData, +} from '../queue/types.js'; +import type { + DbQueue, + DeliverQueue, + EndedPollNotificationQueue, + InboxQueue, + ObjectStorageQueue, + RelationshipQueue, + SystemQueue, + UserWebhookDeliverQueue, + SystemWebhookDeliverQueue, +} from './QueueModule.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; import { MiNote } from '@/models/Note.js'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; @Injectable() export class QueueService { @@ -32,7 +50,8 @@ export class QueueService { @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:relationship') public relationshipQueue: RelationshipQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, + @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, + @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, ) { this.systemQueue.add('tickCharts', { }, { @@ -490,9 +509,13 @@ export class QueueService { }); } + /** + * @see UserWebhookDeliverJobData + * @see WebhookDeliverProcessorService + */ @bindThis - public webhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) { - const data = { + public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) { + const data: UserWebhookDeliverJobData = { type, content, webhookId: webhook.id, @@ -503,7 +526,33 @@ export class QueueService { eventId: randomUUID(), }; - return this.webhookDeliverQueue.add(webhook.id, data, { + return this.userWebhookDeliverQueue.add(webhook.id, data, { + attempts: 4, + backoff: { + type: 'custom', + }, + removeOnComplete: true, + removeOnFail: true, + }); + } + + /** + * @see SystemWebhookDeliverJobData + * @see WebhookDeliverProcessorService + */ + @bindThis + public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) { + const data: SystemWebhookDeliverJobData = { + type, + content, + webhookId: webhook.id, + to: webhook.url, + secret: webhook.secret, + createdAt: Date.now(), + eventId: randomUUID(), + }; + + return this.systemWebhookDeliverQueue.add(webhook.id, data, { attempts: 4, backoff: { type: 'custom', diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index c0b59e635d2dbf2b9d3f1306260b8193c0331852..17ff1687868ff17e6fc018d68ed5550146660679 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js'; 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'; const FALLBACK = '\u2764'; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; @@ -107,6 +108,8 @@ 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); @@ -120,11 +123,16 @@ export class ReactionService { throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.'); } + // Check if note is Renote + if (isRenote(note) && !isQuote(note)) { + throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.'); + } + let reaction = _reaction ?? FALLBACK; if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) { reaction = '\u2764'; - } else if (_reaction) { + } else if (_reaction != null) { const custom = reaction.match(isCustomEmojiRegexp); if (custom) { const reacterHost = this.utilityService.toPunyNullable(user.host); @@ -145,6 +153,11 @@ export class ReactionService { if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) { reaction = FALLBACK; } + + // for media silenced host, custom emoji reactions are not allowed + if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) { + reaction = FALLBACK; + } } else { // リアクションã¨ã—ã¦ä½¿ã†æ¨©é™ãŒãªã„ reaction = FALLBACK; @@ -217,8 +230,6 @@ export class ReactionService { } } - const meta = await this.metaService.fetch(); - if (meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserReactionsChart.update(user, note); } diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index e9dc9b57afef6415d7eda5ccdac9a974c7d40fb0..db32114346ef264567e4905a1a682a709985d676 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -35,7 +35,7 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); + this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m } @bindThis @@ -53,11 +53,11 @@ export class RelayService { @bindThis public async addRelay(inbox: string): Promise<MiRelay> { - const relay = await this.relaysRepository.insert({ + const relay = await this.relaysRepository.insertOne({ id: this.idService.gen(), inbox, status: 'requesting', - }).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0])); + }); const relayActor = await this.getRelayActor(); const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 53a7234823cf597fa04638f409a811012795dcfa..8c0a8f6cc7825a2c3f953739ddec71c15e1664ad 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { ModuleRef } from '@nestjs/core'; +import { reversiUpdateKeys } from 'misskey-js'; import * as Reversi from 'misskey-reversi'; import { IsNull, LessThan, MoreThan } from 'typeorm'; import type { @@ -281,7 +282,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @bindThis private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> { - const game = await this.reversiGamesRepository.insert({ + const game = await this.reversiGamesRepository.insertOne({ id: this.idService.gen(), user1Id: parentId, user2Id: childId, @@ -294,10 +295,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { bw: 'random', isLlotheo: false, noIrregularRules: options.noIrregularRules, - }).then(x => this.reversiGamesRepository.findOneOrFail({ - where: { id: x.identifiers[0].id }, - relations: ['user1', 'user2'], - })); + }, { relations: ['user1', 'user2'] }); this.cacheGame(game); const packed = await this.reversiGameEntityService.packDetail(game); @@ -402,7 +400,33 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) { + public isValidReversiUpdateKey(key: unknown): key is typeof reversiUpdateKeys[number] { + if (typeof key !== 'string') return false; + return (reversiUpdateKeys as string[]).includes(key); + } + + @bindThis + public isValidReversiUpdateValue<K extends typeof reversiUpdateKeys[number]>(key: K, value: unknown): value is MiReversiGame[K] { + switch (key) { + case 'map': + return Array.isArray(value) && value.every(row => typeof row === 'string'); + case 'bw': + return typeof value === 'string' && ['random', '1', '2'].includes(value); + case 'isLlotheo': + return typeof value === 'boolean'; + case 'canPutEverywhere': + return typeof value === 'boolean'; + case 'loopedBoard': + return typeof value === 'boolean'; + case 'timeLimitForEachTurn': + return typeof value === 'number' && value >= 0; + default: + return false; + } + } + + @bindThis + public async updateSettings<K extends typeof reversiUpdateKeys[number]>(gameId: MiReversiGame['id'], user: MiUser, key: K, value: MiReversiGame[K]) { const game = await this.get(gameId); if (game == null) throw new Error('game not found'); if (game.isStarted) return; @@ -410,10 +434,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if ((game.user1Id === user.id) && game.user1Ready) return; if ((game.user2Id === user.id) && game.user2Ready) return; - if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return; - - // TODO: ã‚ˆã‚ŠåŽ³æ ¼ãªãƒãƒªãƒ‡ãƒ¼ã‚·ãƒ§ãƒ³ - const updatedGame = { ...game, [key]: value, diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index f5a753afc717b6868277fdc43860d89ed2ced4d2..7984dc56279ed58314f0c2560618475673c82f85 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -48,6 +48,7 @@ export type RolePolicies = { canHideAds: boolean; driveCapacityMb: number; alwaysMarkNsfw: boolean; + canUpdateBioMedia: boolean; pinLimit: number; antennaLimit: number; wordMuteLimit: number; @@ -78,6 +79,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canHideAds: false, driveCapacityMb: 100, alwaysMarkNsfw: false, + canUpdateBioMedia: true, pinLimit: 5, antennaLimit: 5, wordMuteLimit: 200, @@ -129,10 +131,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { private moderationLogService: ModerationLogService, private fanoutTimelineService: FanoutTimelineService, ) { - //this.onMessage = this.onMessage.bind(this); - - this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1); - this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1); + this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h + this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m this.redisForSub.on('message', this.onMessage); } @@ -381,6 +381,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), + canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)), pinLimit: calc('pinLimit', vs => Math.max(...vs)), antennaLimit: calc('antennaLimit', vs => Math.max(...vs)), wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)), @@ -416,14 +417,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> { + public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> { const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); - const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); - const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ - roleId: In(moderatorRoles.map(r => r.id)), - }) : []; + const moderatorRoles = includeAdmins + ? roles.filter(r => r.isModerator || r.isAdministrator) + : roles.filter(r => r.isModerator); + // TODO: isRootãªã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚‚å«ã‚ã‚‹ - return assigns.map(a => a.userId); + const assigns = moderatorRoles.length > 0 + ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) }) + : []; + + const now = Date.now(); + const result = [ + // Setを経由ã—ã¦é‡è¤‡ã‚’除去(ユーザIDã¯é‡è¤‡ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§ï¼‰ + ...new Set( + assigns + .filter(it => + (excludeExpire) + ? (it.expiresAt == null || it.expiresAt.getTime() > now) + : true, + ) + .map(a => a.userId), + ), + ]; + + return result.sort((x, y) => x.localeCompare(y)); } @bindThis @@ -477,12 +496,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } } - const created = await this.roleAssignmentsRepository.insert({ + const created = await this.roleAssignmentsRepository.insertOne({ id: this.idService.gen(now), expiresAt: expiresAt, roleId: roleId, userId: userId, - }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0])); + }); this.rolesRepository.update(roleId, { lastUsedAt: new Date(), @@ -490,14 +509,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { this.globalEventService.publishInternalEvent('userRoleAssigned', created); - if (role.isPublic) { + const user = await this.usersRepository.findOneByOrFail({ id: userId }); + + if (role.isPublic && user.host === null) { this.notificationService.createNotification(userId, 'roleAssigned', { roleId: roleId, }); } if (moderator) { - const user = await this.usersRepository.findOneByOrFail({ id: userId }); this.moderationLogService.log(moderator, 'assignRole', { roleId: roleId, roleName: role.name, @@ -564,7 +584,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @bindThis public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> { const date = new Date(); - const created = await this.rolesRepository.insert({ + const created = await this.rolesRepository.insertOne({ id: this.idService.gen(date.getTime()), updatedAt: date, lastUsedAt: date, @@ -582,7 +602,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canEditMembersByModerator: values.canEditMembersByModerator, displayOrder: values.displayOrder, policies: values.policies, - }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); + }); this.globalEventService.publishInternalEvent('roleCreated', created); diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index e3d69e5e94fe70006d20c46c3bf10b74bacf1faa..80907a89214a8afed443cd68009abbf0ebf9e876 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -22,6 +22,7 @@ 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() export class SignupService { @@ -36,6 +37,7 @@ export class SignupService { private usedUsernamesRepository: UsedUsernamesRepository, private utilityService: UtilityService, + private userService: UserService, private userEntityService: UserEntityService, private idService: IdService, private metaService: MetaService, @@ -155,7 +157,8 @@ export class SignupService { })); }); - this.usersChart.update(account, true); + this.usersChart.update(account, true).then(); + this.userService.notifySystemWebhook(account, 'userCreated').then(); return { account, secret }; } diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc6851f788da65298a47095a79c2a4e5239fe811 --- /dev/null +++ b/packages/backend/src/core/SystemWebhookService.ts @@ -0,0 +1,233 @@ +/* + * 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 type { MiUser, SystemWebhooksRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { GlobalEvents, GlobalEventService } from '@/core/GlobalEventService.js'; +import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueService } from '@/core/QueueService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import Logger from '@/logger.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +export class SystemWebhookService implements OnApplicationShutdown { + private logger: Logger; + private activeSystemWebhooksFetched = false; + private activeSystemWebhooks: MiSystemWebhook[] = []; + + constructor( + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + @Inject(DI.systemWebhooksRepository) + private systemWebhooksRepository: SystemWebhooksRepository, + private idService: IdService, + private queueService: QueueService, + private moderationLogService: ModerationLogService, + private loggerService: LoggerService, + private globalEventService: GlobalEventService, + ) { + this.redisForSub.on('message', this.onMessage); + this.logger = this.loggerService.getLogger('webhook'); + } + + @bindThis + public async fetchActiveSystemWebhooks() { + if (!this.activeSystemWebhooksFetched) { + this.activeSystemWebhooks = await this.systemWebhooksRepository.findBy({ + isActive: true, + }); + this.activeSystemWebhooksFetched = true; + } + + return this.activeSystemWebhooks; + } + + /** + * SystemWebhook ã®ä¸€è¦§ã‚’å–å¾—ã™ã‚‹. + */ + @bindThis + public async fetchSystemWebhooks(params?: { + ids?: MiSystemWebhook['id'][]; + isActive?: MiSystemWebhook['isActive']; + on?: MiSystemWebhook['on']; + }): Promise<MiSystemWebhook[]> { + const query = this.systemWebhooksRepository.createQueryBuilder('systemWebhook'); + if (params) { + if (params.ids && params.ids.length > 0) { + query.andWhere('systemWebhook.id IN (:...ids)', { ids: params.ids }); + } + if (params.isActive !== undefined) { + query.andWhere('systemWebhook.isActive = :isActive', { isActive: params.isActive }); + } + if (params.on && params.on.length > 0) { + query.andWhere(':on <@ systemWebhook.on', { on: params.on }); + } + } + + return query.getMany(); + } + + /** + * SystemWebhook を作æˆã™ã‚‹. + */ + @bindThis + public async createSystemWebhook( + params: { + isActive: MiSystemWebhook['isActive']; + name: MiSystemWebhook['name']; + on: MiSystemWebhook['on']; + url: MiSystemWebhook['url']; + secret: MiSystemWebhook['secret']; + }, + updater: MiUser, + ): Promise<MiSystemWebhook> { + const id = this.idService.gen(); + await this.systemWebhooksRepository.insert({ + ...params, + id, + }); + + const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id }); + this.globalEventService.publishInternalEvent('systemWebhookCreated', webhook); + this.moderationLogService + .log(updater, 'createSystemWebhook', { + systemWebhookId: webhook.id, + webhook: webhook, + }) + .then(); + + return webhook; + } + + /** + * SystemWebhook ã‚’æ›´æ–°ã™ã‚‹. + */ + @bindThis + public async updateSystemWebhook( + params: { + id: MiSystemWebhook['id']; + isActive: MiSystemWebhook['isActive']; + name: MiSystemWebhook['name']; + on: MiSystemWebhook['on']; + url: MiSystemWebhook['url']; + secret: MiSystemWebhook['secret']; + }, + updater: MiUser, + ): Promise<MiSystemWebhook> { + const beforeEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: params.id }); + await this.systemWebhooksRepository.update(beforeEntity.id, { + updatedAt: new Date(), + isActive: params.isActive, + name: params.name, + on: params.on, + url: params.url, + secret: params.secret, + }); + + const afterEntity = await this.systemWebhooksRepository.findOneByOrFail({ id: beforeEntity.id }); + this.globalEventService.publishInternalEvent('systemWebhookUpdated', afterEntity); + this.moderationLogService + .log(updater, 'updateSystemWebhook', { + systemWebhookId: beforeEntity.id, + before: beforeEntity, + after: afterEntity, + }) + .then(); + + return afterEntity; + } + + /** + * SystemWebhook を削除ã™ã‚‹. + */ + @bindThis + public async deleteSystemWebhook(id: MiSystemWebhook['id'], updater: MiUser) { + const webhook = await this.systemWebhooksRepository.findOneByOrFail({ id }); + await this.systemWebhooksRepository.delete(id); + + this.globalEventService.publishInternalEvent('systemWebhookDeleted', webhook); + this.moderationLogService + .log(updater, 'deleteSystemWebhook', { + systemWebhookId: webhook.id, + webhook, + }) + .then(); + } + + /** + * SystemWebhook ã‚’Webhooké…é€ã‚ューã«è¿½åŠ ã™ã‚‹ + * @see QueueService.systemWebhookDeliver + */ + @bindThis + public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, 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}`); + return; + } + + if (!webhookEntity.on.includes(type)) { + this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`); + return; + } + + return this.queueService.systemWebhookDeliver(webhookEntity, type, content); + } + + @bindThis + private async onMessage(_: string, data: string): Promise<void> { + const obj = JSON.parse(data); + if (obj.channel !== 'internal') { + return; + } + + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'systemWebhookCreated': { + if (body.isActive) { + this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body)); + } + break; + } + case 'systemWebhookUpdated': { + if (body.isActive) { + const i = this.activeSystemWebhooks.findIndex(a => a.id === body.id); + if (i > -1) { + this.activeSystemWebhooks[i] = MiSystemWebhook.deserialize(body); + } else { + this.activeSystemWebhooks.push(MiSystemWebhook.deserialize(body)); + } + } else { + this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id); + } + break; + } + case 'systemWebhookDeleted': { + this.activeSystemWebhooks = this.activeSystemWebhooks.filter(a => a.id !== body.id); + break; + } + default: + break; + } + } + + @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/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts index 96f389b54c558779da91a77b494857f1753b4364..2f1310b8efe778fe908b03aa883bd119ff705405 100644 --- a/packages/backend/src/core/UserBlockingService.ts +++ b/packages/backend/src/core/UserBlockingService.ts @@ -16,7 +16,7 @@ import Logger from '@/logger.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import { WebhookService } from '@/core/WebhookService.js'; +import { UserWebhookService } from '@/core/UserWebhookService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; @@ -46,7 +46,7 @@ export class UserBlockingService implements OnModuleInit { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private webhookService: WebhookService, + private webhookService: UserWebhookService, private apRendererService: ApRendererService, private loggerService: LoggerService, ) { @@ -121,7 +121,7 @@ export class UserBlockingService implements OnModuleInit { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'unfollow', { + this.queueService.userWebhookDeliver(webhook, 'unfollow', { user: packed, }); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index deeecdeb1f1290085076246567bc9bf4fea4ccb3..6aab8fde70c2c39387e8a4b7e7ab9cb49ceb381f 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -16,7 +16,7 @@ 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 { WebhookService } from '@/core/WebhookService.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'; @@ -82,7 +82,7 @@ export class UserFollowingService implements OnModuleInit { private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, - private webhookService: WebhookService, + private webhookService: UserWebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, private fanoutTimelineService: FanoutTimelineService, @@ -279,8 +279,10 @@ export class UserFollowingService implements OnModuleInit { }); // é€šçŸ¥ã‚’ä½œæˆ - this.notificationService.createNotification(follower.id, 'followRequestAccepted', { - }, followee.id); + if (follower.host === null) { + this.notificationService.createNotification(follower.id, 'followRequestAccepted', { + }, followee.id); + } } if (alreadyFollowed) return; @@ -331,7 +333,7 @@ export class UserFollowingService implements OnModuleInit { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'follow', { + this.queueService.userWebhookDeliver(webhook, 'follow', { user: packed, }); } @@ -345,7 +347,7 @@ export class UserFollowingService implements OnModuleInit { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'followed', { + this.queueService.userWebhookDeliver(webhook, 'followed', { user: packed, }); } @@ -398,7 +400,7 @@ export class UserFollowingService implements OnModuleInit { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'unfollow', { + this.queueService.userWebhookDeliver(webhook, 'unfollow', { user: packed, }); } @@ -517,7 +519,7 @@ export class UserFollowingService implements OnModuleInit { followerId: follower.id, }); - const followRequest = await this.followRequestsRepository.insert({ + const followRequest = await this.followRequestsRepository.insertOne({ id: this.idService.gen(), followerId: follower.id, followeeId: followee.id, @@ -531,7 +533,7 @@ export class UserFollowingService implements OnModuleInit { followeeHost: followee.host, followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined, followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined, - }).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0])); + }); // Publish receiveRequest event if (this.userEntityService.isLocalUser(followee)) { @@ -740,7 +742,7 @@ export class UserFollowingService implements OnModuleInit { const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow')); for (const webhook of webhooks) { - this.queueService.webhookDeliver(webhook, 'unfollow', { + this.queueService.userWebhookDeliver(webhook, 'unfollow', { user: packedFollowee, }); } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 51ac99179a6a8828309d5b76c2b03dfccfc0c464..92d61cd103f7347156191dd92284666cb26b3f9a 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -25,7 +25,7 @@ export class UserKeypairService implements OnApplicationShutdown { ) { this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h - memoryCacheLifetime: Infinity, + memoryCacheLifetime: 1000 * 60 * 60, // 1h fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }), toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), diff --git a/packages/backend/src/core/UserListService.ts b/packages/backend/src/core/UserListService.ts index bbdcfed73836a43c2847529bdb81c438091f09d9..6333356fe9e8505b4d0526c63b08ea0b40c3acb9 100644 --- a/packages/backend/src/core/UserListService.ts +++ b/packages/backend/src/core/UserListService.ts @@ -95,7 +95,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit { const currentCount = await this.userListMembershipsRepository.countBy({ userListId: list.id, }); - if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { + if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) { throw new UserListService.TooManyUsersError(); } diff --git a/packages/backend/src/core/UserRenoteMutingService.ts b/packages/backend/src/core/UserRenoteMutingService.ts new file mode 100644 index 0000000000000000000000000000000000000000..bdc5e23f4bd6a1db05b506ab3fe2bfe35913adf7 --- /dev/null +++ b/packages/backend/src/core/UserRenoteMutingService.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import type { RenoteMutingsRepository } from '@/models/_.js'; +import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; + +import { IdService } from '@/core/IdService.js'; +import type { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { CacheService } from '@/core/CacheService.js'; + +@Injectable() +export class UserRenoteMutingService { + constructor( + @Inject(DI.renoteMutingsRepository) + private renoteMutingsRepository: RenoteMutingsRepository, + + private idService: IdService, + private cacheService: CacheService, + ) { + } + + @bindThis + public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> { + await this.renoteMutingsRepository.insert({ + id: this.idService.gen(), + muterId: user.id, + muteeId: target.id, + }); + + await this.cacheService.renoteMutingsCache.refresh(user.id); + } + + @bindThis + public async unmute(mutings: MiRenoteMuting[]): Promise<void> { + if (mutings.length === 0) return; + + await this.renoteMutingsRepository.delete({ + id: In(mutings.map(m => m.id)), + }); + + const muterIds = [...new Set(mutings.map(m => m.muterId))]; + for (const muterId of muterIds) { + await this.cacheService.renoteMutingsCache.refresh(muterId); + } + } +} diff --git a/packages/backend/src/core/UserSearchService.ts b/packages/backend/src/core/UserSearchService.ts new file mode 100644 index 0000000000000000000000000000000000000000..0d03cf6ee00f210fb6815d6c428a0ca8c2bf5ab5 --- /dev/null +++ b/packages/backend/src/core/UserSearchService.ts @@ -0,0 +1,205 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets, SelectQueryBuilder } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { type FollowingsRepository, MiUser, type UsersRepository } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import type { Config } from '@/config.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { Packed } from '@/misc/json-schema.js'; + +function defaultActiveThreshold() { + return new Date(Date.now() - 1000 * 60 * 60 * 24 * 30); +} + +@Injectable() +export class UserSearchService { + constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + private userEntityService: UserEntityService, + ) { + } + + /** + * ユーザåã¨ãƒ›ã‚¹ãƒˆåã«ã‚ˆã‚‹ãƒ¦ãƒ¼ã‚¶æ¤œç´¢ã‚’è¡Œã†. + * + * - 検索çµæžœã«ã¯å„ªå…ˆé †ä½ãŒã¤ã‘られã¦ãŠã‚Šã€ä»¥ä¸‹ã®é †åºã§æ¤œç´¢ãŒè¡Œã‚れる. + * 1. フォãƒãƒ¼ã—ã¦ã„るユーザã®ã†ã¡ã€ä¸€å®šæœŸé–“以内(※)ã«æ›´æ–°ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ + * 2. フォãƒãƒ¼ã—ã¦ã„るユーザã®ã†ã¡ã€ä¸€å®šæœŸé–“以内ã«æ›´æ–°ã•ã‚Œã¦ã„ãªã„ユーザ + * 3. フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザã®ã†ã¡ã€ä¸€å®šæœŸé–“以内ã«æ›´æ–°ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ + * 4. フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザã®ã†ã¡ã€ä¸€å®šæœŸé–“以内ã«æ›´æ–°ã•ã‚Œã¦ã„ãªã„ユーザ + * - ãƒã‚°ã‚¤ãƒ³ã—ã¦ã„ãªã„å ´åˆã¯ã€ä»¥ä¸‹ã®é †åºã§æ¤œç´¢ãŒè¡Œã‚れる. + * 1. 一定期間以内ã«æ›´æ–°ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ + * 2. 一定期間以内ã«æ›´æ–°ã•ã‚Œã¦ã„ãªã„ユーザ + * - ãã‚Œãžã‚Œã®æ¤œç´¢çµæžœã¯ãƒ¦ãƒ¼ã‚¶åã®æ˜‡é †ã§ã‚½ãƒ¼ãƒˆã•ã‚Œã‚‹. + * - 動作的ã«ã¯å…ˆã«ç™»å ´ã—ãŸæ¤œç´¢çµæžœã®ç™»å ´ä½ç½®ãŒå„ªå…ˆã•ã‚Œã‚‹(æ¡ä»¶çš„ã«ãƒ¦ãƒ¼ã‚¶IDãŒé‡è¤‡ã™ã‚‹ã“ã¨ã¯ãªã„ãŒ). + * (1ã§æ—¢ã«ãƒ’ットã—ã¦ã„ãŸå ´åˆã€2, 3, 4ã§ãƒ’ットã—ã¦ã‚‚無視ã•ã‚Œã‚‹ï¼‰ + * - ユーザåã¨ãƒ›ã‚¹ãƒˆåã®æ¤œç´¢æ¡ä»¶ã¯ãã‚Œãžã‚Œå‰æ–¹ä¸€è‡´ã§æ¤œç´¢ã•ã‚Œã‚‹. + * - ユーザåã®æ¤œç´¢ã¯å¤§æ–‡å—å°æ–‡å—を区別ã—ãªã„. + * - ホストåã®æ¤œç´¢ã¯å¤§æ–‡å—å°æ–‡å—を区別ã—ãªã„. + * - 検索çµæžœã¯æœ€å¤§ã§ {@link opts.limit} 件ã¾ã§ã¨ãªã‚‹. + * + * ※一定期間ã¨ã¯ {@link params.activeThreshold} ã§æŒ‡å®šã•ã‚ŒãŸæ—¥æ™‚ã‹ã‚‰ç¾åœ¨ã¾ã§ã®æœŸé–“を指ã™. + * + * @param params 検索æ¡ä»¶. + * @param opts 関数ã®å‹•ä½œã‚’制御ã™ã‚‹ã‚ªãƒ—ション. + * @param me 検索を実行ã™ã‚‹ãƒ¦ãƒ¼ã‚¶ã®æƒ…å ±. 未ãƒã‚°ã‚¤ãƒ³ã®å ´åˆã¯æŒ‡å®šã—ãªã„. + * @see {@link UserSearchService#buildSearchUserQueries} + * @see {@link UserSearchService#buildSearchUserNoLoginQueries} + */ + @bindThis + public async search( + params: { + username?: string | null, + host?: string | null, + activeThreshold?: Date, + }, + opts?: { + limit?: number, + detail?: boolean, + }, + me?: MiUser | null, + ): Promise<Packed<'User'>[]> { + const queries = me ? this.buildSearchUserQueries(me, params) : this.buildSearchUserNoLoginQueries(params); + + let resultSet = new Set<MiUser['id']>(); + const limit = opts?.limit ?? 10; + for (const query of queries) { + const ids = await query + .select('user.id') + .limit(limit - resultSet.size) + .orderBy('user.usernameLower', 'ASC') + .getRawMany<{ user_id: MiUser['id'] }>() + .then(res => res.map(x => x.user_id)); + + resultSet = new Set([...resultSet, ...ids]); + if (resultSet.size >= limit) { + break; + } + } + + return this.userEntityService.packMany<'UserLite' | 'UserDetailed'>( + [...resultSet].slice(0, limit), + me, + { schema: opts?.detail ? 'UserDetailed' : 'UserLite' }, + ); + } + + /** + * ãƒã‚°ã‚¤ãƒ³æ¸ˆã¿ãƒ¦ãƒ¼ã‚¶ã«ã‚ˆã‚‹æ¤œç´¢å®Ÿè¡Œæ™‚ã®ã‚¯ã‚¨ãƒªä¸€è¦§ã‚’構築ã™ã‚‹. + * @param me + * @param params + * @private + */ + @bindThis + private buildSearchUserQueries( + me: MiUser, + params: { + username?: string | null, + host?: string | null, + activeThreshold?: Date, + }, + ) { + // デフォルト30日以内ã«æ›´æ–°ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’アクティブユーザーã¨ã™ã‚‹ + const activeThreshold = params.activeThreshold ?? defaultActiveThreshold(); + + const followingUserQuery = this.followingsRepository.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + const activeFollowingUsersQuery = this.generateUserQueryBuilder(params) + .andWhere(`user.id IN (${followingUserQuery.getQuery()})`) + .andWhere('user.updatedAt > :activeThreshold', { activeThreshold }); + activeFollowingUsersQuery.setParameters(followingUserQuery.getParameters()); + + const inactiveFollowingUsersQuery = this.generateUserQueryBuilder(params) + .andWhere(`user.id IN (${followingUserQuery.getQuery()})`) + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); + })); + inactiveFollowingUsersQuery.setParameters(followingUserQuery.getParameters()); + + // 自分自身ãŒãƒ’ットã™ã‚‹ã¨ã—ãŸã‚‰ã“ã“ + const activeUserQuery = this.generateUserQueryBuilder(params) + .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`) + .andWhere('user.updatedAt > :activeThreshold', { activeThreshold }); + activeUserQuery.setParameters(followingUserQuery.getParameters()); + + const inactiveUserQuery = this.generateUserQueryBuilder(params) + .andWhere(`user.id NOT IN (${followingUserQuery.getQuery()})`) + .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); + inactiveUserQuery.setParameters(followingUserQuery.getParameters()); + + return [activeFollowingUsersQuery, inactiveFollowingUsersQuery, activeUserQuery, inactiveUserQuery]; + } + + /** + * ãƒã‚°ã‚¤ãƒ³ã—ã¦ã„ãªã„ユーザã«ã‚ˆã‚‹æ¤œç´¢å®Ÿè¡Œæ™‚ã®ã‚¯ã‚¨ãƒªä¸€è¦§ã‚’構築ã™ã‚‹. + * @param params + * @private + */ + @bindThis + private buildSearchUserNoLoginQueries(params: { + username?: string | null, + host?: string | null, + activeThreshold?: Date, + }) { + // デフォルト30日以内ã«æ›´æ–°ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’アクティブユーザーã¨ã™ã‚‹ + const activeThreshold = params.activeThreshold ?? defaultActiveThreshold(); + + const activeUserQuery = this.generateUserQueryBuilder(params) + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold }); + })); + + const inactiveUserQuery = this.generateUserQueryBuilder(params) + .andWhere('user.updatedAt <= :activeThreshold', { activeThreshold }); + + return [activeUserQuery, inactiveUserQuery]; + } + + /** + * ユーザ検索クエリã§å…±é€šã™ã‚‹æŠ½å‡ºæ¡ä»¶ã‚’ã‚らã‹ã˜ã‚è¨å®šã—ãŸã‚¯ã‚¨ãƒªãƒ“ルダを生æˆã™ã‚‹. + * @param params + * @private + */ + @bindThis + private generateUserQueryBuilder(params: { + username?: string | null, + host?: string | null, + }): SelectQueryBuilder<MiUser> { + const userQuery = this.usersRepository.createQueryBuilder('user'); + + if (params.username) { + userQuery.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(params.username.toLowerCase()) + '%' }); + } + + if (params.host) { + if (params.host === this.config.hostname || params.host === '.') { + userQuery.andWhere('user.host IS NULL'); + } else { + userQuery.andWhere('user.host LIKE :host', { + host: sqlLikeEscape(params.host.toLowerCase()) + '%', + }); + } + } + + userQuery.andWhere('user.isSuspended = FALSE'); + + return userQuery; + } +} diff --git a/packages/backend/src/core/UserService.ts b/packages/backend/src/core/UserService.ts index 72fa4d928d915583f9539e2bbcc1dda64b0fc384..9b1961c631cd82bd0c25ebe82bb9efa084ca335b 100644 --- a/packages/backend/src/core/UserService.ts +++ b/packages/backend/src/core/UserService.ts @@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; @Injectable() export class UserService { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + private systemWebhookService: SystemWebhookService, + private userEntityService: UserEntityService, ) { } @@ -50,4 +53,23 @@ export class UserService { }); } } + + /** + * SystemWebhookを用ã„ã¦ãƒ¦ãƒ¼ã‚¶ã«é–¢ã™ã‚‹æ“作内容を管ç†è€…å„ä½ã«é€šçŸ¥ã™ã‚‹. + * ã“ã“ã§ã¯JobQueueã¸ã®ã‚¨ãƒ³ã‚ューã®ã¿ã‚’è¡Œã†ãŸã‚ã€å³æ™‚実行ã•ã‚Œãªã„. + * + * @see SystemWebhookService.enqueueSystemWebhook + */ + @bindThis + public async notifySystemWebhook(user: MiUser, type: 'userCreated') { + const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' }); + const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] }); + for (const webhookId of recipientWebhookIds) { + await this.systemWebhookService.enqueueSystemWebhook( + webhookId, + type, + packedUser, + ); + } + } } diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index d594a223f4e2d27d0fbbc2ac65eca1e12f8048ad..7920e58e367856d702d040cc3663babbbd64c630 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Not, IsNull } from 'typeorm'; -import type { FollowingsRepository } from '@/models/_.js'; +import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -13,24 +13,75 @@ import { DI } from '@/di-symbols.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RelationshipJobData } from '@/queue/types.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; @Injectable() export class UserSuspendService { constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + @Inject(DI.followRequestsRepository) + private followRequestsRepository: FollowRequestsRepository, + private userEntityService: UserEntityService, private queueService: QueueService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, + private moderationLogService: ModerationLogService, ) { } @bindThis - public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> { + public async suspend(user: MiUser, moderator: MiUser): Promise<void> { + await this.usersRepository.update(user.id, { + isSuspended: true, + }); + + this.moderationLogService.log(moderator, 'suspend', { + userId: user.id, + userUsername: user.username, + userHost: user.host, + }); + + (async () => { + await this.postSuspend(user).catch(e => {}); + await this.unFollowAll(user).catch(e => {}); + })(); + } + + @bindThis + public async unsuspend(user: MiUser, moderator: MiUser): Promise<void> { + await this.usersRepository.update(user.id, { + isSuspended: false, + }); + + this.moderationLogService.log(moderator, 'unsuspend', { + userId: user.id, + userUsername: user.username, + userHost: user.host, + }); + + (async () => { + await this.postUnsuspend(user).catch(e => {}); + })(); + } + + @bindThis + private async postSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); + this.followRequestsRepository.delete({ + followeeId: user.id, + }); + this.followRequestsRepository.delete({ + followerId: user.id, + }); + if (this.userEntityService.isLocalUser(user)) { // 知り得る全SharedInboxã«Deleteé…ä¿¡ const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); @@ -58,7 +109,7 @@ export class UserSuspendService { } @bindThis - public async doPostUnsuspend(user: MiUser): Promise<void> { + private async postUnsuspend(user: MiUser): Promise<void> { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); if (this.userEntityService.isLocalUser(user)) { @@ -86,4 +137,26 @@ export class UserSuspendService { } } } + + @bindThis + private async unFollowAll(follower: MiUser) { + const followings = await this.followingsRepository.find({ + where: { + followerId: follower.id, + followeeId: Not(IsNull()), + }, + }); + + const jobs: RelationshipJobData[] = []; + for (const following of followings) { + if (following.followeeId && following.followerId) { + jobs.push({ + from: { id: following.followerId }, + to: { id: following.followeeId }, + silent: true, + }); + } + } + this.queueService.createUnfollowJob(jobs); + } } diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts new file mode 100644 index 0000000000000000000000000000000000000000..e96bfeea9581ab900067379e8e1e227724f835bd --- /dev/null +++ b/packages/backend/src/core/UserWebhookService.ts @@ -0,0 +1,99 @@ +/* + * 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 type { WebhooksRepository } from '@/models/_.js'; +import type { MiWebhook } from '@/models/Webhook.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; + +@Injectable() +export class UserWebhookService implements OnApplicationShutdown { + private activeWebhooksFetched = false; + private activeWebhooks: MiWebhook[] = []; + + constructor( + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + @Inject(DI.webhooksRepository) + private webhooksRepository: WebhooksRepository, + ) { + this.redisForSub.on('message', this.onMessage); + } + + @bindThis + public async getActiveWebhooks() { + if (!this.activeWebhooksFetched) { + this.activeWebhooks = await this.webhooksRepository.findBy({ + active: true, + }); + this.activeWebhooksFetched = true; + } + + return this.activeWebhooks; + } + + @bindThis + private async onMessage(_: string, data: string): Promise<void> { + const obj = JSON.parse(data); + if (obj.channel !== 'internal') { + return; + } + + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'webhookCreated': { + if (body.active) { + this.activeWebhooks.push({ // TODO: ã“ã®ã‚ãŸã‚Šã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºå‡¦ç†ã¯å„modelファイル内ã«é–¢æ•°ã¨ã—ã¦exportã—ãŸã„ + ...body, + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + user: null, // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ + }); + } + break; + } + case 'webhookUpdated': { + if (body.active) { + const i = this.activeWebhooks.findIndex(a => a.id === body.id); + if (i > -1) { + this.activeWebhooks[i] = { // TODO: ã“ã®ã‚ãŸã‚Šã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºå‡¦ç†ã¯å„modelファイル内ã«é–¢æ•°ã¨ã—ã¦exportã—ãŸã„ + ...body, + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + user: null, // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ + }; + } else { + this.activeWebhooks.push({ // TODO: ã“ã®ã‚ãŸã‚Šã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºå‡¦ç†ã¯å„modelファイル内ã«é–¢æ•°ã¨ã—ã¦exportã—ãŸã„ + ...body, + latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, + user: null, // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ + }); + } + } else { + this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id); + } + break; + } + case 'webhookDeleted': { + this.activeWebhooks = this.activeWebhooks.filter(a => a.id !== body.id); + break; + } + default: + break; + } + } + + @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/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 21c4af3ca55c8fd7acc1090bc7f9508eeed6ccb2..22871e44f8fe142cf7002ad0a3893c4cf5a38cf5 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -42,6 +42,12 @@ export class UtilityService { return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } + @bindThis + public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean { + if (!silencedHosts || host == null) return false; + return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); + } + @bindThis public concatNoteContentsForKeyWordCheck(content: { cw?: string | null; diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index 374536a7410113a0073d3ed95271d2a7f33dd412..1517dd00744509e35306c6546f29342674448259 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -5,7 +5,7 @@ import { URL } from 'node:url'; import { Injectable } from '@nestjs/common'; -import { query as urlQuery } from '@/misc/prelude/url.js'; +import { XMLParser } from 'fast-xml-parser'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; @@ -22,6 +22,10 @@ export type IWebFinger = { const urlRegex = /^https?:\/\//; const mRegex = /^([^@]+)@(.*)/; +// we have the colons here, because URL.protocol does as well, so it's +// more uniform in the places we use both +const defaultProtocol = process.env.MISSKEY_WEBFINGER_USE_HTTP?.toLowerCase() === 'true' ? 'http:' : 'https:'; + @Injectable() export class WebfingerService { constructor( @@ -31,25 +35,76 @@ export class WebfingerService { @bindThis public async webfinger(query: string): Promise<IWebFinger> { - const url = this.genUrl(query); + const hostMetaUrl = this.queryToHostMetaUrl(query); + const template = await this.fetchWebFingerTemplateFromHostMeta(hostMetaUrl) ?? this.queryToWebFingerTemplate(query); + const url = this.genUrl(query, template); return await this.httpRequestService.getJson<IWebFinger>(url, 'application/jrd+json, application/json'); } @bindThis - private genUrl(query: string): string { + private genUrl(query: string, template: string): string { + if (template.indexOf('{uri}') < 0) throw new Error(`Invalid webFingerUrl: ${template}`); + + if (query.match(urlRegex)) { + return template.replace('{uri}', encodeURIComponent(query)); + } + + const m = query.match(mRegex); + if (m) { + return template.replace('{uri}', encodeURIComponent(`acct:${query}`)); + } + + throw new Error(`Invalid query (${query})`); + } + + @bindThis + private queryToWebFingerTemplate(query: string): string { + if (query.match(urlRegex)) { + const u = new URL(query); + return `${u.protocol}//${u.hostname}/.well-known/webfinger?resource={uri}`; + } + + const m = query.match(mRegex); + if (m) { + const hostname = m[2]; + return `${defaultProtocol}//${hostname}/.well-known/webfinger?resource={uri}`; + } + + throw new Error(`Invalid query (${query})`); + } + + @bindThis + private queryToHostMetaUrl(query: string): string { if (query.match(urlRegex)) { const u = new URL(query); - return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query }); + return `${u.protocol}//${u.hostname}/.well-known/host-meta`; } const m = query.match(mRegex); if (m) { const hostname = m[2]; - const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true'; - return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`; + return `${defaultProtocol}//${hostname}/.well-known/host-meta`; } throw new Error(`Invalid query (${query})`); } + + @bindThis + private async fetchWebFingerTemplateFromHostMeta(url: string): Promise<string | null> { + try { + const res = await this.httpRequestService.getHtml(url, 'application/xrd+xml'); + const options = { + ignoreAttributes: false, + isArray: (_name: string, jpath: string) => jpath === 'XRD.Link', + }; + const parser = new XMLParser(options); + const hostMeta = parser.parse(res); + const template = (hostMeta['XRD']['Link'] as Array<any>).filter(p => p['@_rel'] === 'lrdd')[0]['@_template']; + return template.indexOf('{uri}') < 0 ? null : template; + } catch (err) { + console.error(`error while request host-meta for ${url}: ${err}`); + return null; + } + } } diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts deleted file mode 100644 index 6be34977b064d24180e1efeec66df4b7ed316e8c..0000000000000000000000000000000000000000 --- a/packages/backend/src/core/WebhookService.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 type { WebhooksRepository } from '@/models/_.js'; -import type { MiWebhook } from '@/models/Webhook.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; -import type { GlobalEvents } from '@/core/GlobalEventService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; - -@Injectable() -export class WebhookService implements OnApplicationShutdown { - private webhooksFetched = false; - private webhooks: MiWebhook[] = []; - - constructor( - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - - @Inject(DI.webhooksRepository) - private webhooksRepository: WebhooksRepository, - ) { - //this.onMessage = this.onMessage.bind(this); - this.redisForSub.on('message', this.onMessage); - } - - @bindThis - public async getActiveWebhooks() { - if (!this.webhooksFetched) { - this.webhooks = await this.webhooksRepository.findBy({ - active: true, - }); - this.webhooksFetched = true; - } - - return this.webhooks; - } - - @bindThis - private async 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 'webhookCreated': - if (body.active) { - this.webhooks.push({ // TODO: ã“ã®ã‚ãŸã‚Šã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºå‡¦ç†ã¯å„modelファイル内ã«é–¢æ•°ã¨ã—ã¦exportã—ãŸã„ - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ - }); - } - break; - case 'webhookUpdated': - if (body.active) { - const i = this.webhooks.findIndex(a => a.id === body.id); - if (i > -1) { - this.webhooks[i] = { // TODO: ã“ã®ã‚ãŸã‚Šã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºå‡¦ç†ã¯å„modelファイル内ã«é–¢æ•°ã¨ã—ã¦exportã—ãŸã„ - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ - }; - } else { - this.webhooks.push({ // TODO: ã“ã®ã‚ãŸã‚Šã®ãƒ‡ã‚·ãƒªã‚¢ãƒ©ã‚¤ã‚ºå‡¦ç†ã¯å„modelファイル内ã«é–¢æ•°ã¨ã—ã¦exportã—ãŸã„ - ...body, - latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null, - user: null, // joinãªã‚«ãƒ©ãƒ ã¯é€šå¸¸å–ã£ã¦ã“ãªã„ã®ã§ - }); - } - } else { - this.webhooks = this.webhooks.filter(a => a.id !== body.id); - } - break; - case 'webhookDeleted': - this.webhooks = this.webhooks.filter(a => a.id !== body.id); - break; - default: - break; - } - } - } - - @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/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 0fccc7b95086147a0776584b1f029e4cbfe7520b..5a5a76f7d65f34cbc365fb789e9bca50c861d80d 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; import { concat, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { getApIds } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { ApObject } from './type.js'; @@ -41,7 +40,7 @@ export class ApAudienceService { const limit = promiseLimit<MiUser | null>(2); const mentionedUsers = (await Promise.all( others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), - )).filter(isNotNull); + )).filter(x => x != null); if (toGroups.public.length > 0) { return { diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 44680a2ed5d5d4e009d2032a2ed3ce1d204d0e22..062af397321e55e5eafd389250165627f087991d 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); - this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); + this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h + this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h } @bindThis diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index cf668165666e3287290726d551c47f3791e2c65c..6a28cbad15083d3dd42ac11a52207390024a08be 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -27,8 +27,8 @@ import { QueueService } from '@/core/QueueService.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { MiRemoteUser } from '@/models/User.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { AbuseReportService } from '@/core/AbuseReportService.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'; @@ -57,9 +57,6 @@ export class ApInboxService { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - @Inject(DI.abuseUserReportsRepository) - private abuseUserReportsRepository: AbuseUserReportsRepository, - @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, @@ -68,6 +65,7 @@ export class ApInboxService { private utilityService: UtilityService, private idService: IdService, private metaService: MetaService, + private abuseReportService: AbuseReportService, private userFollowingService: UserFollowingService, private apAudienceService: ApAudienceService, private reactionService: ReactionService, @@ -539,20 +537,19 @@ export class ApInboxService { const userIds = uris .filter(uri => uri.startsWith(this.config.url + '/users/')) .map(uri => uri.split('/').at(-1)) - .filter(isNotNull); + .filter(x => x != null); const users = await this.usersRepository.findBy({ id: In(userIds), }); if (users.length < 1) return 'skip'; - await this.abuseUserReportsRepository.insert({ - id: this.idService.gen(), + await this.abuseReportService.report([{ targetUserId: users[0].id, targetUserHost: users[0].host, reporterId: actor.id, reporterHost: actor.host, comment: `${activity.content}\n${JSON.stringify(uris, null, 2)}`, - }); + }]); return 'ok'; } diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index 6d53ce5147ddbfd35317465d08ebf318f12a1992..318710fa93602d2f847161c094db3ba8019650e0 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -25,7 +25,7 @@ export class ApMfmService { } @bindThis - public getNoteHtml(note: MiNote, apAppend?: string) { + public getNoteHtml(note: Pick<MiNote, 'text' | 'mentionedRemoteUsers'>, apAppend?: string) { let noMisskeyContent = false; const srcMfm = (note.text ?? '') + (apAppend ?? ''); diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 90784fdc1d662478d06222a1d752903a10f81190..55d1054de91190ab0a484a93160090f52ebda659 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -26,7 +26,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, InstancesRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; import { MetaService } from '../MetaService.js'; import { JsonLdService } from './JsonLdService.js'; @@ -338,7 +337,7 @@ export class ApRendererService { const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => { if (ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter(isNotNull); + return ids.map(id => items.find(item => item.id === id)).filter(x => x != null); }; let inReplyTo; @@ -793,6 +792,13 @@ export class ApRendererService { @bindThis public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> { + // Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. + // When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. + // This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. + if (!this.config.attachLdSignatureForRelays) { + return activity; + } + const keypair = await this.userKeypairService.getUserKeypair(user.id); const jsonLd = this.jsonLdService.use(); @@ -855,7 +861,7 @@ export class ApRendererService { if (names.length === 0) return []; const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); - const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); + const emojis = names.map(name => allEmojis.get(name)).filter(x => x != null); return emojis; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index fd8d65445a6196cd4727eb94ef88f7590581032e..63871b38f9f57bc68f75bf63ce284337f738a510 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -6,6 +6,7 @@ import * as crypto from 'node:crypto'; import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; +import { Window } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiUser } from '@/models/User.js'; @@ -182,7 +183,8 @@ export class ApRequestService { * @param url URL to fetch */ @bindThis - public async signedGet(url: string, user: { id: MiUser['id'] }): Promise<unknown> { + public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> { + const _followAlternate = followAlternate ?? true; const keypair = await this.userKeypairService.getUserKeypair(user.id); const req = ApRequestCreator.createSignedGet({ @@ -200,9 +202,52 @@ export class ApRequestService { headers: req.request.headers, }, { throwErrorWhenResponseNotOk: true, - validators: [validateContentTypeSetAsActivityPub], }); + //#region リクエスト先ãŒhtmlã‹ã¤activity+jsonã¸ã®alternate linkã‚¿ã‚°ãŒã‚ã‚‹ã¨ã + const contentType = res.headers.get('content-type'); + + if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) { + const html = await res.text(); + const window = new Window({ + settings: { + disableJavaScriptEvaluation: true, + disableJavaScriptFileLoading: true, + disableCSSFileLoading: true, + disableComputedStyleRendering: true, + handleDisabledFileLoadingAsSuccess: true, + navigation: { + disableMainFrameNavigation: true, + disableChildFrameNavigation: true, + disableChildPageNavigation: true, + disableFallbackToSetURL: true, + }, + timer: { + maxTimeout: 0, + maxIntervalTime: 0, + maxIntervalIterations: 0, + }, + }, + }); + const document = window.document; + try { + document.documentElement.innerHTML = html; + + const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]'); + if (alternate) { + const href = alternate.getAttribute('href'); + if (href) { + return await this.signedGet(href, user, false); + } + } + } catch (e) { + // something went wrong parsing the HTML, ignore the whole thing + } + } + //#endregion + + validateContentTypeSetAsActivityPub(res); + const finalUrl = res.url; // redirects may have been involved const activity = await res.json() as IObject; diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 3d98c5b76443e71c1c844a508222ce684ae60f8f..b281ac9728fe6e96daff02cb026cc9bc5f021e00 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -82,7 +82,7 @@ export class ApImageService { url: image.url, user: actor, uri: image.url, - sensitive: image.sensitive, + sensitive: !!(image.sensitive), isLink: !shouldBeCached, comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), }); diff --git a/packages/backend/src/core/activitypub/models/ApMentionService.ts b/packages/backend/src/core/activitypub/models/ApMentionService.ts index 0ced7e88aff25b0a27c11c4b7d5220a554cba788..2cd151fa048ff5e3433a30382c29c3c2869507bc 100644 --- a/packages/backend/src/core/activitypub/models/ApMentionService.ts +++ b/packages/backend/src/core/activitypub/models/ApMentionService.ts @@ -8,7 +8,6 @@ import promiseLimit from 'promise-limit'; import type { MiUser } from '@/models/_.js'; import { toArray, unique } from '@/misc/prelude/array.js'; import { bindThis } from '@/decorators.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { isMention } from '../type.js'; import { Resolver } from '../ApResolverService.js'; import { ApPersonService } from './ApPersonService.js'; @@ -28,7 +27,7 @@ export class ApMentionService { const limit = promiseLimit<MiUser | null>(2); const mentionedUsers = (await Promise.all( hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), - )).filter(isNotNull); + )).filter(x => x != null); return mentionedUsers; } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 4827baad847108af83651fdfdc0618161261644d..7b7a7921fbca40a1692b86a9c46db189ed552f6d 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -25,7 +25,6 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; @@ -84,9 +83,10 @@ export class ApNoteService { @bindThis public validateNote(object: IObject, uri: string): Error | null { const expectHost = this.utilityService.extractDbHost(uri); + const apType = getApType(object); - if (!validPost.includes(getApType(object))) { - return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${getApType(object)}`); + if (apType == null || !validPost.includes(apType)) { + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: invalid object type ${apType ?? 'undefined'}`); } if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { @@ -258,7 +258,7 @@ export class ApNoteService { } }; - const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(isNotNull)); + const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); @@ -358,7 +358,7 @@ export class ApNoteService { value, object, }); - throw new Error('invalid note'); + throw err; } const note = object as IPost; @@ -471,19 +471,19 @@ export class ApNoteService { | { status: 'ok'; res: MiNote } | { status: 'permerror' | 'temperror' } > => { - if (!/^https?:/.test(uri)) return { status: 'permerror' }; + if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; try { const res = await this.resolveNote(uri, { resolver }); if (res == null) return { status: 'permerror' }; return { status: 'ok', res }; } catch (e) { return { - status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror', + status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror', }; } }; - const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter((x): x is string => typeof x === 'string')); + const uris = unique([note._misskey_quote, note.quoteUrl, note.quoteUri].filter(x => x != null)); const results = await Promise.all(uris.map(tryResolveNote)); quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); @@ -496,10 +496,10 @@ export class ApNoteService { // vote if (reply && reply.hasPoll) { - const replyPoll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); + const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); const tryCreateVote = async (name: string, index: number): Promise<null> => { - if (replyPoll.expiresAt && Date.now() > new Date(replyPoll.expiresAt).getTime()) { + if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); } else if (index >= 0) { this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); @@ -512,7 +512,7 @@ export class ApNoteService { }; if (note.name) { - return await tryCreateVote(note.name, replyPoll.choices.findIndex(x => x === note.name)); + return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); } } @@ -637,7 +637,7 @@ export class ApNoteService { this.logger.info(`register emoji host=${host}, name=${name}`); - return await this.emojisRepository.insert({ + return await this.emojisRepository.insertOne({ id: this.idService.gen(), host, name, @@ -646,7 +646,7 @@ export class ApNoteService { publicUrl: tag.icon.url, updatedAt: new Date(), aliases: [], - }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); + }); })); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 224b8e8c3f64ec8c98c6972225fce5c375330664..10b1fc0bf4b9e70bb11072f45e4195f6533b4cc1 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -34,11 +34,11 @@ import { StatusError } from '@/misc/status-error.js'; 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'; -import { isNotNull } from '@/misc/is-not-null.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -48,7 +48,7 @@ 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, IObject } from '../type.js'; +import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; const nameLength = 128; const summaryLength = 2048; @@ -101,6 +101,8 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + + private roleService: RoleService, ) { } @@ -246,6 +248,11 @@ export class ApPersonService implements OnModuleInit { return this.apImageService.resolveImage(user, img).catch(() => null); })); + if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null)) + && !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) { + return {}; + } + /* we don't want to return nulls on errors! if the database fields are already null, nothing changes; if the database has old @@ -301,6 +308,21 @@ export class ApPersonService implements OnModuleInit { const isBot = getApType(object) === 'Service' || getApType(object) === 'Application'; + const [followingVisibility, followersVisibility] = await Promise.all( + [ + this.isPublicCollection(person.following, resolver), + this.isPublicCollection(person.followers, resolver), + ].map((p): Promise<'public' | 'private'> => p + .then(isPublic => isPublic ? 'public' : 'private') + .catch(err => { + if (!(err instanceof StatusError) || err.isRetryable) { + 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}/); const url = getOneApHrefNullable(person.url); @@ -375,6 +397,8 @@ export class ApPersonService implements OnModuleInit { description: _description, url, fields, + followingVisibility, + followersVisibility, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, userHost: host, @@ -483,6 +507,23 @@ export class ApPersonService implements OnModuleInit { const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); + const [followingVisibility, followersVisibility] = await Promise.all( + [ + this.isPublicCollection(person.following, resolver), + this.isPublicCollection(person.followers, resolver), + ].map((p): Promise<'public' | 'private' | undefined> => p + .then(isPublic => isPublic ? 'public' : 'private') + .catch(err => { + if (!(err instanceof StatusError) || err.isRetryable) { + this.logger.error('error occurred while fetching following/followers collection', { stack: err }); + // Do not update the visibiility on transient errors. + return undefined; + } + return 'private'; + }) + ) + ); + const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const url = getOneApHrefNullable(person.url); @@ -554,6 +595,8 @@ export class ApPersonService implements OnModuleInit { url, fields, description: _description, + followingVisibility, + followersVisibility, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, listenbrainz: person.listenbrainz ?? null, @@ -667,7 +710,7 @@ export class ApPersonService implements OnModuleInit { // ã¨ã‚Šã‚ãˆãšidを別ã®æ™‚é–“ã§ç”Ÿæˆã—ã¦é †ç•ªã‚’ç¶æŒ let td = 0; - for (const note of featuredNotes.filter(isNotNull)) { + for (const note of featuredNotes.filter(x => x != null)) { td -= 1000; transactionalEntityManager.insert(MiUserNotePining, { id: this.idService.gen(Date.now() + td), @@ -726,4 +769,16 @@ export class ApPersonService implements OnModuleInit { return 'ok'; } + + @bindThis + private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise<boolean> { + if (collection) { + const resolved = await resolver.resolveCollection(collection); + if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) { + return true; + } + } + + return false; + } } diff --git a/packages/backend/src/core/activitypub/models/ApQuestionService.ts b/packages/backend/src/core/activitypub/models/ApQuestionService.ts index d1936cfe1dff4e6ba863a6df74cb832aa780e567..73004d10b0c5eedb59bb865cd6e4d72f012c53e3 100644 --- a/packages/backend/src/core/activitypub/models/ApQuestionService.ts +++ b/packages/backend/src/core/activitypub/models/ApQuestionService.ts @@ -10,7 +10,6 @@ import type { Config } from '@/config.js'; import type { IPoll } from '@/models/Poll.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { isQuestion } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApResolverService } from '../ApResolverService.js'; @@ -52,7 +51,7 @@ export class ApQuestionService { const choices = question[multiple ? 'anyOf' : 'oneOf'] ?.map((x) => x.name) - .filter(isNotNull) + .filter(x => x != null) ?? []; const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); @@ -75,10 +74,10 @@ export class ApQuestionService { //#region ã“ã®ã‚µãƒ¼ãƒãƒ¼ã«æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ã‚‹ã‹ const note = await this.notesRepository.findOneBy({ uri }); - if (note == null) throw new Error('Question is not registed'); + if (note == null) throw new Error('Question is not registered'); const poll = await this.pollsRepository.findOneBy({ noteId: note.id }); - if (poll == null) throw new Error('Question is not registed'); + if (poll == null) throw new Error('Question is not registered'); //#endregion // resolve new Question object diff --git a/packages/backend/src/core/activitypub/models/tag.ts b/packages/backend/src/core/activitypub/models/tag.ts index e7ceec3262a38fd9baf305dbc7a3740b2f979490..f75cc45f7e4f714709261d3daa761c3ed605c7e6 100644 --- a/packages/backend/src/core/activitypub/models/tag.ts +++ b/packages/backend/src/core/activitypub/models/tag.ts @@ -4,7 +4,6 @@ */ import { toArray } from '@/misc/prelude/array.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { isHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js'; @@ -16,7 +15,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined): return hashtags.map(tag => { const m = tag.name.match(/^#(.+)/); return m ? m[1] : null; - }).filter(isNotNull); + }).filter(x => x != null); } export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 8edd8a1aba08e3fd14bc5fc38a333f99a881aaf9..2f58825de1a36819a7de1c3e6e6b4acf40651e08 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -60,11 +60,14 @@ export function getApId(value: string | IObject): string { /** * Get ActivityStreams Object type + * + * タイプ判定ãŒã§ããªã‹ã£ãŸå ´åˆã«ã€ã‚ãˆã¦ã‚¨ãƒ©ãƒ¼ã§ã¯ãªãnullã‚’è¿”ã™ã‚ˆã†ã«ã—ã¦ã„る。 + * 詳細: https://github.com/misskey-dev/misskey/issues/14239 */ -export function getApType(value: IObject): string { +export function getApType(value: IObject): string | null { if (typeof value.type === 'string') return value.type; if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0]; - throw new Error('cannot detect type'); + return null; } export function getOneApHrefNullable(value: ApObject | undefined): string | undefined { @@ -97,19 +100,23 @@ export interface IActivity extends IObject { export interface ICollection extends IObject { type: 'Collection'; totalItems: number; - items: ApObject; + first?: IObject | string; + items?: ApObject; } export interface IOrderedCollection extends IObject { type: 'OrderedCollection'; totalItems: number; - orderedItems: ApObject; + first?: IObject | string; + orderedItems?: ApObject; } export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; -export const isPost = (object: IObject): object is IPost => - validPost.includes(getApType(object)); +export const isPost = (object: IObject): object is IPost => { + const type = getApType(object); + return type != null && validPost.includes(type); +}; export interface IPost extends IObject { type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; @@ -159,8 +166,10 @@ export const isTombstone = (object: IObject): object is ITombstone => export const validActor = ['Person', 'Service', 'Group', 'Organization', 'Application']; -export const isActor = (object: IObject): object is IActor => - validActor.includes(getApType(object)); +export const isActor = (object: IObject): object is IActor => { + const type = getApType(object); + return type != null && validActor.includes(type); +}; export interface IActor extends IObject { type: 'Person' | 'Service' | 'Organization' | 'Group' | 'Application'; @@ -246,12 +255,16 @@ export interface IKey extends IObject { publicKeyPem: string | Buffer; } +export const validDocumentTypes = ['Audio', 'Document', 'Image', 'Page', 'Video']; + export interface IApDocument extends IObject { type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video'; } -export const isDocument = (object: IObject): object is IApDocument => - ['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object)); +export const isDocument = (object: IObject): object is IApDocument => { + const type = getApType(object); + return type != null && validDocumentTypes.includes(type); +}; export interface IApImage extends IApDocument { type: 'Image'; @@ -329,7 +342,10 @@ export const isAccept = (object: IObject): object is IAccept => getApType(object export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject'; export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add'; export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove'; -export const isLike = (object: IObject): object is ILike => getApType(object) === 'Like' || getApType(object) === 'EmojiReaction' || getApType(object) === 'EmojiReact'; +export const isLike = (object: IObject): object is ILike => { + const type = getApType(object); + return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type); +}; export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/packages/backend/src/core/chart/ChartLoggerService.ts b/packages/backend/src/core/chart/ChartLoggerService.ts index afc728d5647854677dae54ab1b356d8f0dafaea1..20815ea96869471846fb5fb967c0414ef1a23c6b 100644 --- a/packages/backend/src/core/chart/ChartLoggerService.ts +++ b/packages/backend/src/core/chart/ChartLoggerService.ts @@ -14,6 +14,6 @@ export class ChartLoggerService { constructor( private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('chart', 'white', process.env.NODE_ENV !== 'test'); + this.logger = this.loggerService.getLogger('chart', 'white'); } } diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index 28e441d72c303cc8ef3297cb8b73bb4815edeea4..c2329a2f7357be5b095a1d99d119db41f694016e 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -47,7 +47,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') .select('instance.host') - .where("instance.suspensionState != 'none'"); + .where('instance.suspensionState != \'none\''); const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f') .select('f.followerHost') @@ -89,7 +89,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di .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("instance.suspensionState = 'none'") + .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() .then(x => parseInt(x.count, 10)), @@ -97,7 +97,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di .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("instance.suspensionState = 'none'") + .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() .then(x => parseInt(x.count, 10)), diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index f10e30ef1048657eaa4e19321ce70179d746e0a8..af5485a46ee9c1ce05fc5f855afbf42b8505b4c1 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -14,7 +14,8 @@ import { EntitySchema, LessThan, Between } from 'typeorm'; import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import type { Repository, DataSource } from 'typeorm'; +import { MiRepository, miRepository } from '@/models/_.js'; +import type { DataSource, Repository } from 'typeorm'; const COLUMN_PREFIX = '___' as const; const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const; @@ -145,10 +146,10 @@ export default abstract class Chart<T extends Schema> { group: string | null; }[] = []; // ↓ã«ã—ãŸã„ã‘ã©findOneã¨ã‹ã§åž‹ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ - //private repositoryForHour: Repository<RawRecord<T>>; - //private repositoryForDay: Repository<RawRecord<T>>; - private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>; - private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>; + //private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>; + //private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>; + private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; + private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; /** * 1æ—¥ã«ä¸€å›žç¨‹åº¦å®Ÿè¡Œã•ã‚Œã‚Œã°è‰¯ã„よã†ãªè¨ˆç®—処ç†ã‚’入れる(主ã«CASCADE削除ãªã©ã‚¢ãƒ—リケーションå´ã§æ„ŸçŸ¥ã§ããªã„変動ã«ã‚ˆã‚‹ã‚ºãƒ¬ã®ä¿®æ£ç”¨) @@ -211,6 +212,10 @@ export default abstract class Chart<T extends Schema> { } { const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({ name: + span === 'hour' ? `ChartX${name}` : + span === 'day' ? `ChartDayX${name}` : + new Error('not happen') as never, + tableName: span === 'hour' ? `__chart__${camelToSnake(name)}` : span === 'day' ? `__chart_day__${camelToSnake(name)}` : new Error('not happen') as never, @@ -271,8 +276,8 @@ export default abstract class Chart<T extends Schema> { this.logger = logger; const { hour, day } = Chart.schemaToEntity(name, schema, grouped); - this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour); - this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day); + this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); + this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); } @bindThis @@ -387,11 +392,11 @@ export default abstract class Chart<T extends Schema> { } // æ–°è¦ãƒã‚°æŒ¿å…¥ - log = await repository.insert({ + log = await repository.insertOne({ date: date, ...(group ? { group: group } : {}), ...columns, - }).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord<T>; + }) as RawRecord<T>; this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`); diff --git a/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e23c194c58f00a0ebc4006d2baa88672cc13b4c --- /dev/null +++ b/packages/backend/src/core/entities/AbuseReportNotificationRecipientEntityService.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { Packed } from '@/misc/json-schema.js'; +import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; + +@Injectable() +export class AbuseReportNotificationRecipientEntityService { + constructor( + @Inject(DI.abuseReportNotificationRecipientRepository) + private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, + private userEntityService: UserEntityService, + private systemWebhookEntityService: SystemWebhookEntityService, + ) { + } + + @bindThis + public async pack( + src: MiAbuseReportNotificationRecipient['id'] | MiAbuseReportNotificationRecipient, + opts?: { + users: Map<string, Packed<'UserLite'>>, + webhooks: Map<string, Packed<'SystemWebhook'>>, + }, + ): Promise<Packed<'AbuseReportNotificationRecipient'>> { + const recipient = typeof src === 'object' + ? src + : await this.abuseReportNotificationRecipientRepository.findOneByOrFail({ id: src }); + const user = recipient.userId + ? (opts?.users.get(recipient.userId) ?? await this.userEntityService.pack<'UserLite'>(recipient.userId)) + : undefined; + const webhook = recipient.systemWebhookId + ? (opts?.webhooks.get(recipient.systemWebhookId) ?? await this.systemWebhookEntityService.pack(recipient.systemWebhookId)) + : undefined; + + return { + id: recipient.id, + isActive: recipient.isActive, + updatedAt: recipient.updatedAt.toISOString(), + name: recipient.name, + method: recipient.method, + userId: recipient.userId ?? undefined, + user: user, + systemWebhookId: recipient.systemWebhookId ?? undefined, + systemWebhook: webhook, + }; + } + + @bindThis + public async packMany( + src: MiAbuseReportNotificationRecipient['id'][] | MiAbuseReportNotificationRecipient[], + ): Promise<Packed<'AbuseReportNotificationRecipient'>[]> { + const objs = src.filter((it): it is MiAbuseReportNotificationRecipient => typeof it === 'object'); + const ids = src.filter((it): it is MiAbuseReportNotificationRecipient['id'] => typeof it === 'string'); + if (ids.length > 0) { + objs.push( + ...await this.abuseReportNotificationRecipientRepository.findBy({ id: In(ids) }), + ); + } + + const userIds = objs.map(it => it.userId).filter(x => x != null); + const users: Map<string, Packed<'UserLite'>> = (userIds.length > 0) + ? await this.userEntityService.packMany(userIds) + .then(it => new Map(it.map(it => [it.id, it]))) + : new Map(); + + const systemWebhookIds = objs.map(it => it.systemWebhookId).filter(x => x != null); + const systemWebhooks: Map<string, Packed<'SystemWebhook'>> = (systemWebhookIds.length > 0) + ? await this.systemWebhookEntityService.packMany(systemWebhookIds) + .then(it => new Map(it.map(it => [it.id, it]))) + : new Map(); + + return Promise + .all( + objs.map(it => this.pack(it, { users: users, webhooks: systemWebhooks })), + ) + .then(it => it.sort((a, b) => a.id.localeCompare(b.id))); + } +} + diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts index b0e1d1ab3659dbba9201b511c6c0072d296d70e8..a13c244c19a165fe9489b928f339d887507687c0 100644 --- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts +++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts @@ -10,7 +10,6 @@ import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import type { Packed } from '@/misc/json-schema.js'; import { UserEntityService } from './UserEntityService.js'; @@ -63,7 +62,7 @@ export class AbuseUserReportEntityService { ) { const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId); const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId); - const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull); + const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(x => x != null); const _userMap = await this.userEntityService.packMany( [..._reporters, ..._targetUsers, ..._assignees], null, diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts index 3855a2843688db300961a899795cd1069d9c8dc5..d91564590692574a12c2e31d812263a5fbf76ff3 100644 --- a/packages/backend/src/core/entities/ClipEntityService.ts +++ b/packages/backend/src/core/entities/ClipEntityService.ts @@ -53,7 +53,7 @@ export class ClipEntityService { isPublic: clip.isPublic, favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined, - notesCount: meId ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined, + notesCount: (meId === clip.userId) ? await this.clipNotesRepository.countBy({ clipId: clip.id }) : undefined, }); } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 02ff2e775468ccf109bbdedbe858516918e64aaf..c485555f90f92dfecd9c373207ea9e47d8286bea 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -16,7 +16,6 @@ import { appendQuery, query } from '@/misc/prelude/url.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; import { UtilityService } from '../UtilityService.js'; import { VideoProcessingService } from '../VideoProcessingService.js'; @@ -261,11 +260,11 @@ export class DriveFileEntityService { files: MiDriveFile[], options?: PackOptions, ): Promise<Packed<'DriveFile'>[]> { - const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull); + const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null); const _userMap = await this.userEntityService.packMany(_user) .then(users => new Map(users.map(user => [user.id, user]))); const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {}))); - return items.filter(isNotNull); + return items.filter(x => x != null); } @bindThis @@ -290,6 +289,6 @@ export class DriveFileEntityService { ): Promise<Packed<'DriveFile'>[]> { if (fileIds.length === 0) return []; const filesMap = await this.packManyByIdsMap(fileIds, options); - return fileIds.map(id => filesMap.get(id)).filter(isNotNull); + return fileIds.map(id => filesMap.get(id)).filter(x => x != null); } } diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts index d110f7afc6326fff4ae83d2712487f52a76e58dc..4aa7104c1e321fa52cf87af5c7aa940b4674f5c0 100644 --- a/packages/backend/src/core/entities/FlashEntityService.ts +++ b/packages/backend/src/core/entities/FlashEntityService.ts @@ -49,6 +49,7 @@ export class FlashEntityService { title: flash.title, summary: flash.summary, script: flash.script, + visibility: flash.visibility, likedCount: flash.likedCount, isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined, }); diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 002a93397de7bf26258e059285a90628a37e0839..7695e6dfa73e1f4cd5286fc5ddda7d9d401220ed 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -50,6 +50,7 @@ export class InstanceEntityService { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), + isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, @@ -63,8 +64,9 @@ export class InstanceEntityService { @bindThis public packMany( instances: MiInstance[], + me?: { id: MiUser['id']; } | null | undefined, ) { - return Promise.all(instances.map(x => this.pack(x))); + return Promise.all(instances.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/core/entities/InviteCodeEntityService.ts b/packages/backend/src/core/entities/InviteCodeEntityService.ts index 26f57e12990348454a0722911481d22d4e7f507a..5d3e823a2a9d5a376f9f27714019d0ea1e74b6ea 100644 --- a/packages/backend/src/core/entities/InviteCodeEntityService.ts +++ b/packages/backend/src/core/entities/InviteCodeEntityService.ts @@ -12,7 +12,6 @@ import type { MiUser } from '@/models/User.js'; import type { MiRegistrationTicket } from '@/models/RegistrationTicket.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { UserEntityService } from './UserEntityService.js'; @Injectable() @@ -59,8 +58,8 @@ export class InviteCodeEntityService { tickets: MiRegistrationTicket[], me: { id: MiUser['id'] }, ) { - const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull); - const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull); + const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(x => x != null); + const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(x => x != null); const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me) .then(users => new Map(users.map(u => [u.id, u]))); return Promise.all( diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 34d46e50e5fe3110616309b8617091ec2b1188dc..3128b762f4f56feeb579a1c8eedddf316efa46bf 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -49,6 +49,22 @@ export class MetaEntityService { })) .getMany(); + // クライアントã®æ‰‹é–“を減らã™ãŸã‚ã‚らã‹ã˜ã‚JSONã«å¤‰æ›ã—ã¦ãŠã + let defaultLightTheme = null; + let defaultDarkTheme = null; + if (instance.defaultLightTheme) { + try { + defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme)); + } catch (e) { + } + } + if (instance.defaultDarkTheme) { + try { + defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme)); + } catch (e) { + } + } + const packed: Packed<'MetaLite'> = { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, @@ -92,9 +108,8 @@ export class MetaEntityService { backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, maxNoteTextLength: this.config.maxNoteLength, - // クライアントã®æ‰‹é–“を減らã™ãŸã‚ã‚らã‹ã˜ã‚JSONã«å¤‰æ›ã—ã¦ãŠã - defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null, - defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null, + defaultLightTheme, + defaultDarkTheme, defaultLike: instance.defaultLike, ads: ads.map(ad => ({ id: ad.id, @@ -116,6 +131,7 @@ export class MetaEntityService { mediaProxy: this.config.mediaProxy, enableUrlPreview: instance.urlPreviewEnabled, + noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', }; return packed; @@ -156,4 +172,3 @@ export class MetaEntityService { return packDetailed; } } - diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index ca755ea2863d62a07172d360fb76d5925bd6e4e0..493723ac453261bb0bb66e5254bcf113d0fdbda3 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -14,7 +14,6 @@ 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 { bindThis } from '@/decorators.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -293,7 +292,7 @@ export class NoteEntityService implements OnModuleInit { packedFiles.set(k, v); } } - return fileIds.map(id => packedFiles.get(id)).filter(isNotNull); + return fileIds.map(id => packedFiles.get(id)).filter(x => x != null); } @bindThis @@ -465,12 +464,12 @@ export class NoteEntityService implements OnModuleInit { await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)); // TODO: 本当㯠renote ã¨ã‹ reply ãŒãªã„ã®ã« renoteId ã¨ã‹ replyId ãŒã‚ã£ãŸã‚‰ã“ã“ã§è§£æ±ºã—ã¦ãŠã - const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull); + const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null); const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); const users = [ ...notes.map(({ user, userId }) => user ?? userId), - ...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull), - ...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull), + ...notes.map(({ replyUserId }) => replyUserId).filter(x => x != null), + ...notes.map(({ renoteUserId }) => renoteUserId).filter(x => x != null), ]; const packedUsers = await this.userEntityService.packMany(users, me) .then(users => new Map(users.map(u => [u.id, u]))); diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 18b9d148c4e7d582096e538fdadd583db180575a..e2de450756c5325213637ac931eacb5a48943a7c 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -13,7 +13,6 @@ import type { MiGroupedNotification, MiNotification } from '@/models/Notificatio import type { MiNote } from '@/models/Note.js'; import type { Packed } from '@/misc/json-schema.js'; import { bindThis } from '@/decorators.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { FilterUnionByProperty, groupedNotificationTypes } from '@/types.js'; import { CacheService } from '@/core/CacheService.js'; import { RoleEntityService } from './RoleEntityService.js'; @@ -103,7 +102,7 @@ export class NotificationEntityService implements OnModuleInit { user, reaction: reaction.reaction, }; - }))).filter(r => isNotNull(r.user)); + }))).filter(r => r.user != null); // if all users have been deleted, don't show this notification if (reactions.length === 0) { return null; @@ -124,7 +123,7 @@ export class NotificationEntityService implements OnModuleInit { } return this.userEntityService.pack(userId, { id: meId }); - }))).filter(isNotNull); + }))).filter(x => x != null); // if all users have been deleted, don't show this notification if (users.length === 0) { return null; @@ -181,7 +180,7 @@ export class NotificationEntityService implements OnModuleInit { validNotifications = await this.#filterValidNotifier(validNotifications, meId); - const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(isNotNull); + const noteIds = validNotifications.map(x => 'noteId' in x ? x.noteId : null).filter(x => x != null); const notes = noteIds.length > 0 ? await this.notesRepository.find({ where: { id: In(noteIds) }, relations: ['user', 'reply', 'reply.user', 'renote', 'renote.user'], @@ -223,7 +222,7 @@ export class NotificationEntityService implements OnModuleInit { ); }); - return (await Promise.all(packPromises)).filter(isNotNull); + return (await Promise.all(packPromises)).filter(x => x != null); } @bindThis @@ -305,7 +304,7 @@ export class NotificationEntityService implements OnModuleInit { this.cacheService.userProfileCache.fetch(meId).then(p => new Set(p.mutedInstances)), ]); - const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(isNotNull); + const notifierIds = notifications.map(notification => 'notifierId' in notification ? notification.notifierId : null).filter(x => x != null); const notifiers = notifierIds.length > 0 ? await this.usersRepository.find({ where: { id: In(notifierIds) }, }) : []; @@ -313,7 +312,7 @@ export class NotificationEntityService implements OnModuleInit { const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => { const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers); return isValid ? notification : null; - }))) as [T | null] ).filter(isNotNull); + }))) as [T | null] ).filter(x => x != null); return filteredNotifications; } diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts index 142d9e81dbc8df95de823acb4e79f4e132d00f07..46bf51bb6d28a6dce272161e2c649de44e91167d 100644 --- a/packages/backend/src/core/entities/PageEntityService.ts +++ b/packages/backend/src/core/entities/PageEntityService.ts @@ -14,7 +14,6 @@ import type { MiPage } from '@/models/Page.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import { UserEntityService } from './UserEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -106,7 +105,7 @@ export class PageEntityService { script: page.script, eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, - attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)), + attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(x => x != null)), likedCount: page.likedCount, isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, }); diff --git a/packages/backend/src/core/entities/SystemWebhookEntityService.ts b/packages/backend/src/core/entities/SystemWebhookEntityService.ts new file mode 100644 index 0000000000000000000000000000000000000000..e18734091c62218e29281f4871de7e4a5a15632a --- /dev/null +++ b/packages/backend/src/core/entities/SystemWebhookEntityService.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { MiSystemWebhook, SystemWebhooksRepository } from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { Packed } from '@/misc/json-schema.js'; + +@Injectable() +export class SystemWebhookEntityService { + constructor( + @Inject(DI.systemWebhooksRepository) + private systemWebhooksRepository: SystemWebhooksRepository, + ) { + } + + @bindThis + public async pack( + src: MiSystemWebhook['id'] | MiSystemWebhook, + opts?: { + webhooks: Map<string, MiSystemWebhook> + }, + ): Promise<Packed<'SystemWebhook'>> { + const webhook = typeof src === 'object' + ? src + : opts?.webhooks.get(src) ?? await this.systemWebhooksRepository.findOneByOrFail({ id: src }); + + return { + id: webhook.id, + isActive: webhook.isActive, + updatedAt: webhook.updatedAt.toISOString(), + latestSentAt: webhook.latestSentAt?.toISOString() ?? null, + latestStatus: webhook.latestStatus, + name: webhook.name, + on: webhook.on, + url: webhook.url, + secret: webhook.secret, + }; + } + + @bindThis + public async packMany(src: MiSystemWebhook['id'][] | MiSystemWebhook[]): Promise<Packed<'SystemWebhook'>[]> { + if (src.length === 0) { + return []; + } + + const webhooks = Array.of<MiSystemWebhook>(); + webhooks.push( + ...src.filter((it): it is MiSystemWebhook => typeof it === 'object'), + ); + + const ids = src.filter((it): it is MiSystemWebhook['id'] => typeof it === 'string'); + if (ids.length > 0) { + webhooks.push( + ...await this.systemWebhooksRepository.findBy({ id: In(ids) }), + ); + } + + return Promise + .all( + webhooks.map(x => + this.pack(x, { + webhooks: new Map(webhooks.map(x => [x.id, x])), + }), + ), + ) + .then(it => it.sort((a, b) => a.id.localeCompare(b.id))); + } +} + diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 9e234318b58370ff84852fe23384abd1681c4ff7..2b1c4d5c63b71841cb07565075fdce72f2ef9b6e 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -49,7 +49,6 @@ import { IdService } from '@/core/IdService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { isNotNull } from '@/misc/is-not-null.js'; import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; @@ -491,12 +490,12 @@ export class UserEntityService implements OnModuleInit { const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null; const followingCount = profile == null ? null : - (profile.followingVisibility === 'public') || isMe ? user.followingCount : + (profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount : (profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : null; const followersCount = profile == null ? null : - (profile.followersVisibility === 'public') || isMe ? user.followersCount : + (profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount : (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; @@ -548,11 +547,15 @@ export class UserEntityService implements OnModuleInit { emojis: this.customEmojiService.populateEmojis(user.emojis, checkHost), onlineStatus: this.getOnlineStatus(user), // パフォーマンス上ã®ç†ç”±ã§ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ - badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({ - name: r.name, - iconUrl: r.iconUrl, - displayOrder: r.displayOrder, - }))) : undefined, + badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs + .filter((r) => r.isPublic || iAmModerator) + .sort((a, b) => b.displayOrder - a.displayOrder) + .map((r) => ({ + name: r.name, + iconUrl: r.iconUrl, + displayOrder: r.displayOrder, + })) + ) : undefined, ...(isDetailed ? { url: profile!.url, @@ -560,7 +563,7 @@ export class UserEntityService implements OnModuleInit { movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, alsoKnownAs: user.alsoKnownAs ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) - .then(xs => xs.length === 0 ? null : xs.filter(isNotNull)) + .then(xs => xs.length === 0 ? null : xs.filter(x => x != null)) : null, updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 2c70344c941ab62094865d630200c13618461b42..0be2149a0a18a33b43e7a1a6834dcd837160f499 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -108,5 +108,6 @@ async function net() { // FS STAT async function fs() { - return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 })); + const io = await si.disksIO().catch(() => null); + return io ?? { rIO_sec: 0, wIO_sec: 0 }; } diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 564a8db70a813d88f29d756c2dfd47ba358ffe8e..d4a21ab625a7f7dff67762f76d21e5b90db15fe5 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -49,6 +49,7 @@ export const DI = { swSubscriptionsRepository: Symbol('swSubscriptionsRepository'), hashtagsRepository: Symbol('hashtagsRepository'), abuseUserReportsRepository: Symbol('abuseUserReportsRepository'), + abuseReportNotificationRecipientRepository: Symbol('abuseReportNotificationRecipientRepository'), registrationTicketsRepository: Symbol('registrationTicketsRepository'), authSessionsRepository: Symbol('authSessionsRepository'), accessTokensRepository: Symbol('accessTokensRepository'), @@ -70,6 +71,7 @@ export const DI = { channelFavoritesRepository: Symbol('channelFavoritesRepository'), registryItemsRepository: Symbol('registryItemsRepository'), webhooksRepository: Symbol('webhooksRepository'), + systemWebhooksRepository: Symbol('systemWebhooksRepository'), adsRepository: Symbol('adsRepository'), passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'), retentionAggregationsRepository: Symbol('retentionAggregationsRepository'), diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index d4705af60114759ded008a6c221ad84c6d78618c..ff5363a425d72c6a4d162043f48027b6e0abcdf9 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -22,31 +22,27 @@ type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; export default class Logger { private context: Context; private parentLogger: Logger | null = null; - private store: boolean; - constructor(context: string, color?: KEYWORD, store = true) { + constructor(context: string, color?: KEYWORD) { this.context = { name: context, color: color, }; - this.store = store; } @bindThis - public createSubLogger(context: string, color?: KEYWORD, store = true): Logger { - const logger = new Logger(context, color, store); + public createSubLogger(context: string, color?: KEYWORD): Logger { + const logger = new Logger(context, color); logger.parentLogger = this; return logger; } @bindThis - private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = [], store = true): void { + private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subContexts: Context[] = []): void { if (envOption.quiet) return; - if (!this.store) store = false; - if (level === 'debug') store = false; if (this.parentLogger) { - this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts), store); + this.parentLogger.log(level, message, data, important, [this.context].concat(subContexts)); return; } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index bba64a06eff2dbb2dd28faa5bad1bb7041e08022..f9692ce5d5f3362a9c3ee2897aefb9eca4e5d95d 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -7,23 +7,23 @@ import * as Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; export class RedisKVCache<T> { - private redisClient: Redis.Redis; - private name: string; - private lifetime: number; - private memoryCache: MemoryKVCache<T>; - private fetcher: (key: string) => Promise<T>; - private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T | undefined; - - constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: { - lifetime: RedisKVCache<T>['lifetime']; - memoryCacheLifetime: number; - fetcher: RedisKVCache<T>['fetcher']; - toRedisConverter: RedisKVCache<T>['toRedisConverter']; - fromRedisConverter: RedisKVCache<T>['fromRedisConverter']; - }) { - this.redisClient = redisClient; - this.name = name; + private readonly lifetime: number; + private readonly memoryCache: MemoryKVCache<T>; + private readonly fetcher: (key: string) => Promise<T>; + private readonly toRedisConverter: (value: T) => string; + private readonly fromRedisConverter: (value: string) => T | undefined; + + constructor( + private redisClient: Redis.Redis, + private name: string, + opts: { + lifetime: RedisKVCache<T>['lifetime']; + memoryCacheLifetime: number; + fetcher: RedisKVCache<T>['fetcher']; + toRedisConverter: RedisKVCache<T>['toRedisConverter']; + fromRedisConverter: RedisKVCache<T>['fromRedisConverter']; + }, + ) { this.lifetime = opts.lifetime; this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime); this.fetcher = opts.fetcher; @@ -55,7 +55,13 @@ export class RedisKVCache<T> { const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`); if (cached == null) return undefined; - return this.fromRedisConverter(cached); + + const value = this.fromRedisConverter(cached); + if (value !== undefined) { + this.memoryCache.set(key, value); + } + + return value; } @bindThis @@ -66,6 +72,10 @@ export class RedisKVCache<T> { /** * ã‚ャッシュãŒã‚ã‚Œã°ãれを返ã—ã€ç„¡ã‘ã‚Œã°fetcherを呼ã³å‡ºã—ã¦çµæžœã‚’ã‚ャッシュ&è¿”ã—ã¾ã™ + * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons: + * * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster. + * * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value. + * * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses. */ @bindThis public async fetch(key: string): Promise<T> { @@ -77,14 +87,14 @@ export class RedisKVCache<T> { // Cache MISS const value = await this.fetcher(key); - this.set(key, value); + await this.set(key, value); return value; } @bindThis public async refresh(key: string) { const value = await this.fetcher(key); - this.set(key, value); + await this.set(key, value); // TODO: イベント発行ã—ã¦ä»–プãƒã‚»ã‚¹ã®ãƒ¡ãƒ¢ãƒªã‚ャッシュも更新ã§ãるよã†ã«ã™ã‚‹ } @@ -101,23 +111,23 @@ export class RedisKVCache<T> { } export class RedisSingleCache<T> { - private redisClient: Redis.Redis; - private name: string; - private lifetime: number; - private memoryCache: MemorySingleCache<T>; - private fetcher: () => Promise<T>; - private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T | undefined; - - constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: { - lifetime: RedisSingleCache<T>['lifetime']; - memoryCacheLifetime: number; - fetcher: RedisSingleCache<T>['fetcher']; - toRedisConverter: RedisSingleCache<T>['toRedisConverter']; - fromRedisConverter: RedisSingleCache<T>['fromRedisConverter']; - }) { - this.redisClient = redisClient; - this.name = name; + private readonly lifetime: number; + private readonly memoryCache: MemorySingleCache<T>; + private readonly fetcher: () => Promise<T>; + private readonly toRedisConverter: (value: T) => string; + private readonly fromRedisConverter: (value: string) => T | undefined; + + constructor( + private redisClient: Redis.Redis, + private name: string, + opts: { + lifetime: number; + memoryCacheLifetime: number; + fetcher: RedisSingleCache<T>['fetcher']; + toRedisConverter: RedisSingleCache<T>['toRedisConverter']; + fromRedisConverter: RedisSingleCache<T>['fromRedisConverter']; + }, + ) { this.lifetime = opts.lifetime; this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime); this.fetcher = opts.fetcher; @@ -149,7 +159,13 @@ export class RedisSingleCache<T> { const cached = await this.redisClient.get(`singlecache:${this.name}`); if (cached == null) return undefined; - return this.fromRedisConverter(cached); + + const value = this.fromRedisConverter(cached); + if (value !== undefined) { + this.memoryCache.set(value); + } + + return value; } @bindThis @@ -160,6 +176,10 @@ export class RedisSingleCache<T> { /** * ã‚ャッシュãŒã‚ã‚Œã°ãれを返ã—ã€ç„¡ã‘ã‚Œã°fetcherを呼ã³å‡ºã—ã¦çµæžœã‚’ã‚ャッシュ&è¿”ã—ã¾ã™ + * This awaits the call to Redis to ensure that the write succeeded, which is important for a few reasons: + * * Other code uses this to synchronize changes between worker processes. A failed write can internally de-sync the cluster. + * * Without an `await`, consecutive calls could race. An unlucky race could result in the older write overwriting the newer value. + * * Not awaiting here makes the entire cache non-consistent. The prevents many possible uses. */ @bindThis public async fetch(): Promise<T> { @@ -171,14 +191,14 @@ export class RedisSingleCache<T> { // Cache MISS const value = await this.fetcher(); - this.set(value); + await this.set(value); return value; } @bindThis public async refresh() { const value = await this.fetcher(); - this.set(value); + await this.set(value); // TODO: イベント発行ã—ã¦ä»–プãƒã‚»ã‚¹ã®ãƒ¡ãƒ¢ãƒªã‚ャッシュも更新ã§ãるよã†ã«ã™ã‚‹ } @@ -187,22 +207,12 @@ export class RedisSingleCache<T> { // TODO: メモリ節約ã®ãŸã‚ã‚ã¾ã‚Šå‚ç…§ã•ã‚Œãªã„ã‚ーを定期的ã«å‰Šé™¤ã§ãるよã†ã«ã™ã‚‹ï¼Ÿ export class MemoryKVCache<T> { - /** - * データをæŒã¤ãƒžãƒƒãƒ— - * @deprecated ã“れを直接æ“作ã™ã‚‹ã¹ãã§ã¯ãªã„ - */ - public cache: Map<string, { date: number; value: T; }>; - private lifetime: number; - private gcIntervalHandle: NodeJS.Timeout; + private readonly cache = new Map<string, { date: number; value: T; }>(); + private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m - constructor(lifetime: MemoryKVCache<never>['lifetime']) { - this.cache = new Map(); - this.lifetime = lifetime; - - this.gcIntervalHandle = setInterval(() => { - this.gc(); - }, 1000 * 60 * 3); - } + constructor( + private readonly lifetime: number, + ) {} @bindThis /** @@ -287,10 +297,14 @@ export class MemoryKVCache<T> { @bindThis public gc(): void { const now = Date.now(); + for (const [key, { date }] of this.cache.entries()) { - if ((now - date) > this.lifetime) { - this.cache.delete(key); - } + // The map is ordered from oldest to youngest. + // We can stop once we find an entry that's still active, because all following entries must *also* be active. + const age = now - date; + if (age < this.lifetime) break; + + this.cache.delete(key); } } @@ -298,16 +312,19 @@ export class MemoryKVCache<T> { public dispose(): void { clearInterval(this.gcIntervalHandle); } + + public get entries() { + return this.cache.entries(); + } } export class MemorySingleCache<T> { private cachedAt: number | null = null; private value: T | undefined; - private lifetime: number; - constructor(lifetime: MemorySingleCache<never>['lifetime']) { - this.lifetime = lifetime; - } + constructor( + private lifetime: number, + ) {} @bindThis public set(value: T): void { diff --git a/packages/backend/src/misc/is-not-null.ts b/packages/backend/src/misc/is-not-null.ts deleted file mode 100644 index 8d9dc8bb396e04b004d482abeda128c431397878..0000000000000000000000000000000000000000 --- a/packages/backend/src/misc/is-not-null.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T { - return input != null; -} diff --git a/packages/backend/src/misc/is-user-related.ts b/packages/backend/src/misc/is-user-related.ts index 93c9b2b814bfabcf1afaa039a6a87bb52db09fab..862d6e6a3839be434535c49dec2668c6154a43ce 100644 --- a/packages/backend/src/misc/is-user-related.ts +++ b/packages/backend/src/misc/is-user-related.ts @@ -4,6 +4,10 @@ */ export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean { + if (!note) { + return false; + } + if (userIds.has(note.userId) && !ignoreAuthor) { return true; } diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 41e5bfe9e4ada6d1a153e3e27dc240d3592a304a..a721b8663c3c32328047f3d7150b8eab8b0d2ae0 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -4,12 +4,12 @@ */ import { - packedUserLiteSchema, - packedUserDetailedNotMeOnlySchema, packedMeDetailedOnlySchema, - packedUserDetailedNotMeSchema, packedMeDetailedSchema, + packedUserDetailedNotMeOnlySchema, + packedUserDetailedNotMeSchema, packedUserDetailedSchema, + packedUserLiteSchema, packedUserSchema, } from '@/models/json-schema/user.js'; import { packedNoteSchema } from '@/models/json-schema/note.js'; @@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js'; import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; -import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js'; +import { packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js'; import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; import { packedChannelSchema } from '@/models/json-schema/channel.js'; import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; @@ -38,25 +38,27 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { - packedRoleLiteSchema, - packedRoleSchema, - packedRolePoliciesSchema, + packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, packedRoleCondFormulaLogicsSchema, - packedRoleCondFormulaValueNot, - packedRoleCondFormulaValueIsLocalOrRemoteSchema, packedRoleCondFormulaValueAssignedRoleSchema, packedRoleCondFormulaValueCreatedSchema, - packedRoleCondFormulaFollowersOrFollowingOrNotesSchema, + packedRoleCondFormulaValueIsLocalOrRemoteSchema, + packedRoleCondFormulaValueNot, packedRoleCondFormulaValueSchema, packedRoleCondFormulaValueUserSettingBooleanSchema, + packedRoleLiteSchema, + packedRolePoliciesSchema, + packedRoleSchema, } from '@/models/json-schema/role.js'; import { packedAdSchema } from '@/models/json-schema/ad.js'; -import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; +import { packedReversiGameDetailedSchema, packedReversiGameLiteSchema } from '@/models/json-schema/reversi-game.js'; import { - packedMetaLiteSchema, packedMetaDetailedOnlySchema, packedMetaDetailedSchema, + packedMetaLiteSchema, } from '@/models/json-schema/meta.js'; +import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js'; +import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -111,6 +113,8 @@ export const refs = { MetaLite: packedMetaLiteSchema, MetaDetailedOnly: packedMetaDetailedOnlySchema, MetaDetailed: packedMetaDetailedSchema, + SystemWebhook: packedSystemWebhookSchema, + AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema, }; export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>; diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd7fe12058a03508283077de42ce9266d8f6c40b --- /dev/null +++ b/packages/backend/src/misc/json-value.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; +export type JsonObject = {[K in string]?: JsonValue}; +export type JsonArray = JsonValue[]; + +export function isJsonObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/backend/src/misc/prelude/array.ts b/packages/backend/src/misc/prelude/array.ts index bd6c8ee8e3d7d31bfa3eb9aa438182d6c4068980..f741a0c9133911688727cd72499b7476e3783a56 100644 --- a/packages/backend/src/misc/prelude/array.ts +++ b/packages/backend/src/misc/prelude/array.ts @@ -65,44 +65,6 @@ export function maximum(xs: number[]): number { return Math.max(...xs); } -/** - * Splits an array based on the equivalence relation. - * The concatenation of the result is equal to the argument. - */ -export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { - const groups = [] as T[][]; - for (const x of xs) { - const lastGroup = groups.at(-1); - if (lastGroup !== undefined && f(lastGroup[0], x)) { - lastGroup.push(x); - } else { - groups.push([x]); - } - } - return groups; -} - -/** - * Splits an array based on the equivalence relation induced by the function. - * The concatenation of the result is equal to the argument. - */ -export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] { - return groupBy((a, b) => f(a) === f(b), xs); -} - -export function groupByX<T>(collections: T[], keySelector: (x: T) => string) { - return collections.reduce((obj: Record<string, T[]>, item: T) => { - const key = keySelector(item); - if (!Object.prototype.hasOwnProperty.call(obj, key)) { - obj[key] = []; - } - - obj[key].push(item); - - return obj; - }, {}); -} - /** * Compare two arrays by lexicographical order */ @@ -142,7 +104,3 @@ export function toArray<T>(x: T | T[] | undefined): T[] { export function toSingle<T>(x: T | T[] | undefined): T | undefined { return Array.isArray(x) ? x[0] : x; } - -export function toSingleLast<T>(x: T | T[] | undefined): T | undefined { - return Array.isArray(x) ? x.at(-1) : x; -} diff --git a/packages/backend/src/misc/prelude/maybe.ts b/packages/backend/src/misc/prelude/maybe.ts deleted file mode 100644 index 1c58ccb9c778ddb6d518a2d9c7e9b826f0276c9d..0000000000000000000000000000000000000000 --- a/packages/backend/src/misc/prelude/maybe.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export interface IMaybe<T> { - isJust(): this is IJust<T>; -} - -export interface IJust<T> extends IMaybe<T> { - get(): T; -} - -export function just<T>(value: T): IJust<T> { - return { - isJust: () => true, - get: () => value, - }; -} - -export function nothing<T>(): IMaybe<T> { - return { - isJust: () => false, - }; -} diff --git a/packages/backend/src/misc/prelude/string.ts b/packages/backend/src/misc/prelude/string.ts deleted file mode 100644 index 67ea5299619c19c7b7ae7da51ece0530eb6a9f72..0000000000000000000000000000000000000000 --- a/packages/backend/src/misc/prelude/string.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function concat(xs: string[]): string { - return xs.join(''); -} - -export function capitalize(s: string): string { - return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1)); -} - -export function toUpperCase(s: string): string { - return s.toUpperCase(); -} - -export function toLowerCase(s: string): string { - return s.toLowerCase(); -} diff --git a/packages/backend/src/models/AbuseReportNotificationRecipient.ts b/packages/backend/src/models/AbuseReportNotificationRecipient.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbff880afca4c06765e72bf6f1e23210df029100 --- /dev/null +++ b/packages/backend/src/models/AbuseReportNotificationRecipient.ts @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; +import { MiSystemWebhook } from '@/models/SystemWebhook.js'; +import { MiUserProfile } from '@/models/UserProfile.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +/** + * é€šå ±å—信時ã«é€šçŸ¥ã‚’é€ä¿¡ã™ã‚‹æ–¹æ³•. + */ +export type RecipientMethod = 'email' | 'webhook'; + +@Entity('abuse_report_notification_recipient') +export class MiAbuseReportNotificationRecipient { + @PrimaryColumn(id()) + public id: string; + + /** + * 有効ã‹ã©ã†ã‹. + */ + @Index() + @Column('boolean', { + default: true, + }) + public isActive: boolean; + + /** + * 更新日時. + */ + @Column('timestamp with time zone', { + default: () => 'CURRENT_TIMESTAMP', + }) + public updatedAt: Date; + + /** + * 通知è¨å®šå. + */ + @Column('varchar', { + length: 255, + }) + public name: string; + + /** + * 通知方法. + */ + @Index() + @Column('varchar', { + length: 64, + }) + public method: RecipientMethod; + + /** + * 通知先ã®ãƒ¦ãƒ¼ã‚¶ID. + */ + @Index() + @Column({ + ...id(), + nullable: true, + }) + public userId: MiUser['id'] | null; + + /** + * 通知先ã®ãƒ¦ãƒ¼ã‚¶. + */ + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' }) + public user: MiUser | null; + + /** + * 通知先ã®ãƒ¦ãƒ¼ã‚¶ãƒ—ãƒãƒ•ã‚£ãƒ¼ãƒ«. + */ + @ManyToOne(type => MiUserProfile, {}) + @JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' }) + public userProfile: MiUserProfile | null; + + /** + * 通知先ã®ã‚·ã‚¹ãƒ†ãƒ WebhookId. + */ + @Index() + @Column({ + ...id(), + nullable: true, + }) + public systemWebhookId: string | null; + + /** + * 通知先ã®ã‚·ã‚¹ãƒ†ãƒ Webhook. + */ + @ManyToOne(type => MiSystemWebhook, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public systemWebhook: MiSystemWebhook | null; +} diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts index efb639f07592b9983fba26c5e894d2b268c895b6..dd810681c5334bd5800d42aeb679fd6d058e43c9 100644 --- a/packages/backend/src/models/DriveFile.ts +++ b/packages/backend/src/models/DriveFile.ts @@ -83,7 +83,7 @@ export class MiDriveFile { public storedInternal: boolean; @Column('varchar', { - length: 512, + length: 1024, comment: 'The URL of the DriveFile.', }) public url: string; @@ -125,13 +125,13 @@ export class MiDriveFile { @Index() @Column('varchar', { - length: 512, nullable: true, + length: 1024, nullable: true, comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.', }) public uri: string | null; @Column('varchar', { - length: 512, nullable: true, + length: 1024, nullable: true, }) public src: string | null; diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index fb021f0f6bd61fb5a742fbdc39a7d9e5b71b72bb..07c4e28b3a6d30d1346993a371de41aadc9aee24 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -86,6 +86,11 @@ export class MiMeta { }) public silencedHosts: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public mediaSilencedHosts: string[]; + @Column('varchar', { length: 1024, nullable: true, diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 053edd6094e2b8b47b50a0a569feb96006bdba78..1eaeb86df686bd8b9cba4a902db6c9c436ada4f2 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -3,399 +3,484 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { Provider } from '@nestjs/common'; import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit, MiBubbleGameRecord, MiReversiGame } from './_.js'; +import { + MiAbuseReportNotificationRecipient, + MiAbuseUserReport, + MiAccessToken, + MiAd, + MiAnnouncement, + MiAnnouncementRead, + MiAntenna, + MiApp, + MiAuthSession, + MiAvatarDecoration, + MiBlocking, + MiBubbleGameRecord, + MiChannel, + MiChannelFavorite, + MiChannelFollowing, + MiClip, + MiClipFavorite, + MiClipNote, + MiDriveFile, + MiDriveFolder, + MiEmoji, + MiFlash, + MiFlashLike, + MiFollowing, + MiFollowRequest, + MiGalleryLike, + MiGalleryPost, + MiHashtag, + MiInstance, + MiMeta, + MiModerationLog, + MiMuting, + MiNote, + MiNoteFavorite, + MiNoteReaction, + MiNoteThreadMuting, + MiNoteUnread, + MiPage, + MiPageLike, + MiPasswordResetRequest, + MiPoll, + MiPollVote, + MiPromoNote, + MiPromoRead, + MiRegistrationTicket, + MiRegistryItem, + MiRelay, + MiRenoteMuting, + MiRepository, + miRepository, + MiRetentionAggregation, + MiReversiGame, + MiRole, + MiRoleAssignment, + MiSignin, + MiSwSubscription, + MiSystemWebhook, + MiUsedUsername, + MiUser, + MiUserIp, + MiUserKeypair, + MiUserList, + MiUserListFavorite, + MiUserListMembership, + MiUserMemo, + MiUserNotePining, + MiUserPending, + MiUserProfile, + MiUserPublickey, + MiUserSecurityKey, + MiWebhook, + NoteEdit +} from './_.js'; import type { DataSource } from 'typeorm'; -import type { Provider } from '@nestjs/common'; const $usersRepository: Provider = { provide: DI.usersRepository, - useFactory: (db: DataSource) => db.getRepository(MiUser), + useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>), inject: [DI.db], }; const $notesRepository: Provider = { provide: DI.notesRepository, - useFactory: (db: DataSource) => db.getRepository(MiNote), + useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository<MiNote>), inject: [DI.db], }; const $announcementsRepository: Provider = { provide: DI.announcementsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAnnouncement), + useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository<MiAnnouncement>), inject: [DI.db], }; const $announcementReadsRepository: Provider = { provide: DI.announcementReadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead), + useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository<MiAnnouncementRead>), inject: [DI.db], }; const $appsRepository: Provider = { provide: DI.appsRepository, - useFactory: (db: DataSource) => db.getRepository(MiApp), + useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository<MiApp>), inject: [DI.db], }; const $avatarDecorationsRepository: Provider = { provide: DI.avatarDecorationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration), + useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository<MiAvatarDecoration>), inject: [DI.db], }; const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite), + useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>), inject: [DI.db], }; const $noteThreadMutingsRepository: Provider = { provide: DI.noteThreadMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting), + useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository<MiNoteThreadMuting>), inject: [DI.db], }; const $noteReactionsRepository: Provider = { provide: DI.noteReactionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteReaction), + useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository<MiNoteReaction>), inject: [DI.db], }; const $noteUnreadsRepository: Provider = { provide: DI.noteUnreadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiNoteUnread), + useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository<MiNoteUnread>), inject: [DI.db], }; const $pollsRepository: Provider = { provide: DI.pollsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPoll), + useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>), inject: [DI.db], }; const $pollVotesRepository: Provider = { provide: DI.pollVotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPollVote), + useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository<MiPollVote>), inject: [DI.db], }; const $userProfilesRepository: Provider = { provide: DI.userProfilesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserProfile), + useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository<MiUserProfile>), inject: [DI.db], }; const $userKeypairsRepository: Provider = { provide: DI.userKeypairsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserKeypair), + useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository<MiUserKeypair>), inject: [DI.db], }; const $userPendingsRepository: Provider = { provide: DI.userPendingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserPending), + useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository<MiUserPending>), inject: [DI.db], }; const $userSecurityKeysRepository: Provider = { provide: DI.userSecurityKeysRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey), + useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository<MiUserSecurityKey>), inject: [DI.db], }; const $userPublickeysRepository: Provider = { provide: DI.userPublickeysRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserPublickey), + useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository<MiUserPublickey>), inject: [DI.db], }; const $userListsRepository: Provider = { provide: DI.userListsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserList), + useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository<MiUserList>), inject: [DI.db], }; const $userListFavoritesRepository: Provider = { provide: DI.userListFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite), + useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository<MiUserListFavorite>), inject: [DI.db], }; const $userListMembershipsRepository: Provider = { provide: DI.userListMembershipsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserListMembership), + useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository<MiUserListMembership>), inject: [DI.db], }; const $userNotePiningsRepository: Provider = { provide: DI.userNotePiningsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserNotePining), + useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository<MiUserNotePining>), inject: [DI.db], }; const $userIpsRepository: Provider = { provide: DI.userIpsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserIp), + useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository<MiUserIp>), inject: [DI.db], }; const $usedUsernamesRepository: Provider = { provide: DI.usedUsernamesRepository, - useFactory: (db: DataSource) => db.getRepository(MiUsedUsername), + useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository<MiUsedUsername>), inject: [DI.db], }; const $followingsRepository: Provider = { provide: DI.followingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFollowing), + useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository<MiFollowing>), inject: [DI.db], }; const $followRequestsRepository: Provider = { provide: DI.followRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFollowRequest), + useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository<MiFollowRequest>), inject: [DI.db], }; const $instancesRepository: Provider = { provide: DI.instancesRepository, - useFactory: (db: DataSource) => db.getRepository(MiInstance), + useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository<MiInstance>), inject: [DI.db], }; const $emojisRepository: Provider = { provide: DI.emojisRepository, - useFactory: (db: DataSource) => db.getRepository(MiEmoji), + useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository<MiEmoji>), inject: [DI.db], }; const $driveFilesRepository: Provider = { provide: DI.driveFilesRepository, - useFactory: (db: DataSource) => db.getRepository(MiDriveFile), + useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository<MiDriveFile>), inject: [DI.db], }; const $driveFoldersRepository: Provider = { provide: DI.driveFoldersRepository, - useFactory: (db: DataSource) => db.getRepository(MiDriveFolder), + useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository<MiDriveFolder>), inject: [DI.db], }; const $metasRepository: Provider = { provide: DI.metasRepository, - useFactory: (db: DataSource) => db.getRepository(MiMeta), + useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository<MiMeta>), inject: [DI.db], }; const $mutingsRepository: Provider = { provide: DI.mutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiMuting), + useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository<MiMuting>), inject: [DI.db], }; const $renoteMutingsRepository: Provider = { provide: DI.renoteMutingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting), + useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository<MiRenoteMuting>), inject: [DI.db], }; const $blockingsRepository: Provider = { provide: DI.blockingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiBlocking), + useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository<MiBlocking>), inject: [DI.db], }; const $swSubscriptionsRepository: Provider = { provide: DI.swSubscriptionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSwSubscription), + useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository<MiSwSubscription>), inject: [DI.db], }; const $hashtagsRepository: Provider = { provide: DI.hashtagsRepository, - useFactory: (db: DataSource) => db.getRepository(MiHashtag), + useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository<MiHashtag>), inject: [DI.db], }; const $abuseUserReportsRepository: Provider = { provide: DI.abuseUserReportsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport), + useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository<MiAbuseUserReport>), + inject: [DI.db], +}; + +const $abuseReportNotificationRecipientRepository: Provider = { + provide: DI.abuseReportNotificationRecipientRepository, + useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient), inject: [DI.db], }; const $registrationTicketsRepository: Provider = { provide: DI.registrationTicketsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket), + useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>), inject: [DI.db], }; const $authSessionsRepository: Provider = { provide: DI.authSessionsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAuthSession), + useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository<MiAuthSession>), inject: [DI.db], }; const $accessTokensRepository: Provider = { provide: DI.accessTokensRepository, - useFactory: (db: DataSource) => db.getRepository(MiAccessToken), + useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository<MiAccessToken>), inject: [DI.db], }; const $signinsRepository: Provider = { provide: DI.signinsRepository, - useFactory: (db: DataSource) => db.getRepository(MiSignin), + useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository<MiSignin>), inject: [DI.db], }; const $pagesRepository: Provider = { provide: DI.pagesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPage), + useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository<MiPage>), inject: [DI.db], }; const $pageLikesRepository: Provider = { provide: DI.pageLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPageLike), + useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository<MiPageLike>), inject: [DI.db], }; const $galleryPostsRepository: Provider = { provide: DI.galleryPostsRepository, - useFactory: (db: DataSource) => db.getRepository(MiGalleryPost), + useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository<MiGalleryPost>), inject: [DI.db], }; const $galleryLikesRepository: Provider = { provide: DI.galleryLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiGalleryLike), + useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository<MiGalleryLike>), inject: [DI.db], }; const $moderationLogsRepository: Provider = { provide: DI.moderationLogsRepository, - useFactory: (db: DataSource) => db.getRepository(MiModerationLog), + useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository<MiModerationLog>), inject: [DI.db], }; const $clipsRepository: Provider = { provide: DI.clipsRepository, - useFactory: (db: DataSource) => db.getRepository(MiClip), + useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository<MiClip>), inject: [DI.db], }; const $clipNotesRepository: Provider = { provide: DI.clipNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiClipNote), + useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository<MiClipNote>), inject: [DI.db], }; const $clipFavoritesRepository: Provider = { provide: DI.clipFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiClipFavorite), + useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository<MiClipFavorite>), inject: [DI.db], }; const $antennasRepository: Provider = { provide: DI.antennasRepository, - useFactory: (db: DataSource) => db.getRepository(MiAntenna), + useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository<MiAntenna>), inject: [DI.db], }; const $promoNotesRepository: Provider = { provide: DI.promoNotesRepository, - useFactory: (db: DataSource) => db.getRepository(MiPromoNote), + useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository<MiPromoNote>), inject: [DI.db], }; const $promoReadsRepository: Provider = { provide: DI.promoReadsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPromoRead), + useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository<MiPromoRead>), inject: [DI.db], }; const $relaysRepository: Provider = { provide: DI.relaysRepository, - useFactory: (db: DataSource) => db.getRepository(MiRelay), + useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository<MiRelay>), inject: [DI.db], }; const $channelsRepository: Provider = { provide: DI.channelsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannel), + useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository<MiChannel>), inject: [DI.db], }; const $channelFollowingsRepository: Provider = { provide: DI.channelFollowingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing), + useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository<MiChannelFollowing>), inject: [DI.db], }; const $channelFavoritesRepository: Provider = { provide: DI.channelFavoritesRepository, - useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite), + useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository<MiChannelFavorite>), inject: [DI.db], }; const $registryItemsRepository: Provider = { provide: DI.registryItemsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRegistryItem), + useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository<MiRegistryItem>), inject: [DI.db], }; const $webhooksRepository: Provider = { provide: DI.webhooksRepository, - useFactory: (db: DataSource) => db.getRepository(MiWebhook), + useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository<MiWebhook>), + inject: [DI.db], +}; + +const $systemWebhooksRepository: Provider = { + provide: DI.systemWebhooksRepository, + useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook), inject: [DI.db], }; const $adsRepository: Provider = { provide: DI.adsRepository, - useFactory: (db: DataSource) => db.getRepository(MiAd), + useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>), inject: [DI.db], }; const $passwordResetRequestsRepository: Provider = { provide: DI.passwordResetRequestsRepository, - useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest), + useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository<MiPasswordResetRequest>), inject: [DI.db], }; const $retentionAggregationsRepository: Provider = { provide: DI.retentionAggregationsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation), + useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository<MiRetentionAggregation>), inject: [DI.db], }; const $flashsRepository: Provider = { provide: DI.flashsRepository, - useFactory: (db: DataSource) => db.getRepository(MiFlash), + useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository<MiFlash>), inject: [DI.db], }; const $flashLikesRepository: Provider = { provide: DI.flashLikesRepository, - useFactory: (db: DataSource) => db.getRepository(MiFlashLike), + useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository<MiFlashLike>), inject: [DI.db], }; const $rolesRepository: Provider = { provide: DI.rolesRepository, - useFactory: (db: DataSource) => db.getRepository(MiRole), + useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository<MiRole>), inject: [DI.db], }; const $roleAssignmentsRepository: Provider = { provide: DI.roleAssignmentsRepository, - useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment), + useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository<MiRoleAssignment>), inject: [DI.db], }; const $userMemosRepository: Provider = { provide: DI.userMemosRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserMemo), + useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository<MiUserMemo>), inject: [DI.db], }; @@ -407,19 +492,18 @@ const $noteEditRepository: Provider = { const $bubbleGameRecordsRepository: Provider = { provide: DI.bubbleGameRecordsRepository, - useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord), + useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>), inject: [DI.db], }; const $reversiGamesRepository: Provider = { provide: DI.reversiGamesRepository, - useFactory: (db: DataSource) => db.getRepository(MiReversiGame), + useFactory: (db: DataSource) => db.getRepository(MiReversiGame).extend(miRepository as MiRepository<MiReversiGame>), inject: [DI.db], }; @Module({ - imports: [ - ], + imports: [], providers: [ $usersRepository, $notesRepository, @@ -457,6 +541,7 @@ const $reversiGamesRepository: Provider = { $swSubscriptionsRepository, $hashtagsRepository, $abuseUserReportsRepository, + $abuseReportNotificationRecipientRepository, $registrationTicketsRepository, $authSessionsRepository, $accessTokensRepository, @@ -478,6 +563,7 @@ const $reversiGamesRepository: Provider = { $channelFavoritesRepository, $registryItemsRepository, $webhooksRepository, + $systemWebhooksRepository, $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, @@ -527,6 +613,7 @@ const $reversiGamesRepository: Provider = { $swSubscriptionsRepository, $hashtagsRepository, $abuseUserReportsRepository, + $abuseReportNotificationRecipientRepository, $registrationTicketsRepository, $authSessionsRepository, $accessTokensRepository, @@ -548,6 +635,7 @@ const $reversiGamesRepository: Provider = { $channelFavoritesRepository, $registryItemsRepository, $webhooksRepository, + $systemWebhooksRepository, $adsRepository, $passwordResetRequestsRepository, $retentionAggregationsRepository, @@ -561,4 +649,5 @@ const $reversiGamesRepository: Provider = { $reversiGamesRepository, ], }) -export class RepositoryModule {} +export class RepositoryModule { +} diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6c27eae510d103e6f739baa02a9c1ef720feaed --- /dev/null +++ b/packages/backend/src/models/SystemWebhook.ts @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; +import { Serialized } from '@/types.js'; +import { id } from './util/id.js'; + +export const systemWebhookEventTypes = [ + // ユーザã‹ã‚‰ã®é€šå ±ã‚’å—ã‘ãŸã¨ã + 'abuseReport', + // é€šå ±ã‚’å‡¦ç†ã—ãŸã¨ã + 'abuseReportResolved', + // ユーザãŒä½œæˆã•ã‚ŒãŸæ™‚ + 'userCreated', +] as const; +export type SystemWebhookEventType = typeof systemWebhookEventTypes[number]; + +@Entity('system_webhook') +export class MiSystemWebhook { + @PrimaryColumn(id()) + public id: string; + + /** + * 有効ã‹ã©ã†ã‹. + */ + @Index('IDX_system_webhook_isActive', { synchronize: false }) + @Column('boolean', { + default: true, + }) + public isActive: boolean; + + /** + * 更新日時. + */ + @Column('timestamp with time zone', { + default: () => 'CURRENT_TIMESTAMP', + }) + public updatedAt: Date; + + /** + * 最後ã«é€ä¿¡ã•ã‚ŒãŸæ—¥æ™‚. + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestSentAt: Date | null; + + /** + * 最後ã«é€ä¿¡ã•ã‚ŒãŸã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã‚³ãƒ¼ãƒ‰ + */ + @Column('integer', { + nullable: true, + }) + public latestStatus: number | null; + + /** + * 通知è¨å®šå. + */ + @Column('varchar', { + length: 255, + }) + public name: string; + + /** + * イベント種別. + */ + @Index('IDX_system_webhook_on', { synchronize: false }) + @Column('varchar', { + length: 128, + array: true, + default: '{}', + }) + public on: SystemWebhookEventType[]; + + /** + * Webhooké€ä¿¡å…ˆã®URL. + */ + @Column('varchar', { + length: 1024, + }) + public url: string; + + /** + * Webhook検証用ã®å€¤. + */ + @Column('varchar', { + length: 1024, + }) + public secret: string; + + static deserialize(obj: Serialized<MiSystemWebhook>): MiSystemWebhook { + return { + ...obj, + updatedAt: new Date(obj.updatedAt), + latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null, + }; + } +} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 744a1dd4e7c6a006765da937bdde4e49ab9cd804..f7646dce2ae0b928117e1d1c773863f2545573d4 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -3,7 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm'; +import { DriverUtils } from 'typeorm/driver/DriverUtils.js'; +import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js'; +import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js'; +import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; +import { ObjectUtils } from 'typeorm/util/ObjectUtils.js'; +import { OrmUtils } from 'typeorm/util/OrmUtils.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; +import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; import { MiAd } from '@/models/Ad.js'; import { MiAnnouncement } from '@/models/Announcement.js'; @@ -61,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiWebhook } from '@/models/Webhook.js'; +import { MiSystemWebhook } from '@/models/SystemWebhook.js'; import { MiChannel } from '@/models/Channel.js'; import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; import { MiRole } from '@/models/Role.js'; @@ -71,11 +80,54 @@ import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { NoteEdit } from '@/models/NoteEdit.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; +import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; -import type { Repository } from 'typeorm'; +export interface MiRepository<T extends ObjectLiteral> { + createTableColumnNames(this: Repository<T> & MiRepository<T>): string[]; + insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>; + selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void; +} + +export const miRepository = { + createTableColumnNames() { + return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName); + }, + async insertOne(entity, findOptions?) { + const queryBuilder = this.createQueryBuilder().insert().values(entity); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mainAlias = queryBuilder.expressionMap.mainAlias!; + const name = mainAlias.name; + mainAlias.name = 't'; + const columnNames = this.createTableColumnNames(); + queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2)); + const builder = this.createQueryBuilder().addCommonTableExpression(queryBuilder, 'cte', { columnNames }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + builder.expressionMap.mainAlias!.tablePath = 'cte'; + this.selectAliasColumnNames(queryBuilder, builder); + if (findOptions) { + builder.setFindOptions(findOptions); + } + const raw = await builder.execute(); + mainAlias.name = name; + const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw); + const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw); + const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias); + return result[0]; + }, + selectAliasColumnNames(queryBuilder, builder) { + let selectOrAddSelect = (selection: string, selectionAliasName?: string) => { + selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName); + return builder.select(selection, selectionAliasName); + }; + for (const columnName of this.createTableColumnNames()) { + selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`); + } + }, +} satisfies MiRepository<ObjectLiteral>; export { MiAbuseUserReport, + MiAbuseReportNotificationRecipient, MiAccessToken, MiAd, MiAnnouncement, @@ -133,6 +185,7 @@ export { MiUserPublickey, MiUserSecurityKey, MiWebhook, + MiSystemWebhook, MiChannel, MiRetentionAggregation, MiRole, @@ -145,71 +198,73 @@ export { MiReversiGame, }; -export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>; -export type AccessTokensRepository = Repository<MiAccessToken>; -export type AdsRepository = Repository<MiAd>; -export type AnnouncementsRepository = Repository<MiAnnouncement>; -export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>; -export type AntennasRepository = Repository<MiAntenna>; -export type AppsRepository = Repository<MiApp>; -export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>; -export type AuthSessionsRepository = Repository<MiAuthSession>; -export type BlockingsRepository = Repository<MiBlocking>; -export type ChannelFollowingsRepository = Repository<MiChannelFollowing>; -export type ChannelFavoritesRepository = Repository<MiChannelFavorite>; -export type ClipsRepository = Repository<MiClip>; -export type ClipNotesRepository = Repository<MiClipNote>; -export type ClipFavoritesRepository = Repository<MiClipFavorite>; -export type DriveFilesRepository = Repository<MiDriveFile>; -export type DriveFoldersRepository = Repository<MiDriveFolder>; -export type EmojisRepository = Repository<MiEmoji>; -export type FollowingsRepository = Repository<MiFollowing>; -export type FollowRequestsRepository = Repository<MiFollowRequest>; -export type GalleryLikesRepository = Repository<MiGalleryLike>; -export type GalleryPostsRepository = Repository<MiGalleryPost>; -export type HashtagsRepository = Repository<MiHashtag>; -export type InstancesRepository = Repository<MiInstance>; -export type MetasRepository = Repository<MiMeta>; -export type ModerationLogsRepository = Repository<MiModerationLog>; -export type MutingsRepository = Repository<MiMuting>; -export type RenoteMutingsRepository = Repository<MiRenoteMuting>; -export type NotesRepository = Repository<MiNote>; -export type NoteFavoritesRepository = Repository<MiNoteFavorite>; -export type NoteReactionsRepository = Repository<MiNoteReaction>; -export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>; -export type NoteUnreadsRepository = Repository<MiNoteUnread>; -export type PagesRepository = Repository<MiPage>; -export type PageLikesRepository = Repository<MiPageLike>; -export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest>; -export type PollsRepository = Repository<MiPoll>; -export type PollVotesRepository = Repository<MiPollVote>; -export type PromoNotesRepository = Repository<MiPromoNote>; -export type PromoReadsRepository = Repository<MiPromoRead>; -export type RegistrationTicketsRepository = Repository<MiRegistrationTicket>; -export type RegistryItemsRepository = Repository<MiRegistryItem>; -export type RelaysRepository = Repository<MiRelay>; -export type SigninsRepository = Repository<MiSignin>; -export type SwSubscriptionsRepository = Repository<MiSwSubscription>; -export type UsedUsernamesRepository = Repository<MiUsedUsername>; -export type UsersRepository = Repository<MiUser>; -export type UserIpsRepository = Repository<MiUserIp>; -export type UserKeypairsRepository = Repository<MiUserKeypair>; -export type UserListsRepository = Repository<MiUserList>; -export type UserListFavoritesRepository = Repository<MiUserListFavorite>; -export type UserListMembershipsRepository = Repository<MiUserListMembership>; -export type UserNotePiningsRepository = Repository<MiUserNotePining>; -export type UserPendingsRepository = Repository<MiUserPending>; -export type UserProfilesRepository = Repository<MiUserProfile>; -export type UserPublickeysRepository = Repository<MiUserPublickey>; -export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>; -export type WebhooksRepository = Repository<MiWebhook>; -export type ChannelsRepository = Repository<MiChannel>; -export type RetentionAggregationsRepository = Repository<MiRetentionAggregation>; -export type RolesRepository = Repository<MiRole>; -export type RoleAssignmentsRepository = Repository<MiRoleAssignment>; -export type FlashsRepository = Repository<MiFlash>; -export type FlashLikesRepository = Repository<MiFlashLike>; -export type UserMemoRepository = Repository<MiUserMemo>; -export type NoteEditRepository = Repository<NoteEdit>; -export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>; -export type ReversiGamesRepository = Repository<MiReversiGame>; +export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>; +export type AbuseReportNotificationRecipientRepository = Repository<MiAbuseReportNotificationRecipient> & MiRepository<MiAbuseReportNotificationRecipient>; +export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>; +export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>; +export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>; +export type AnnouncementReadsRepository = Repository<MiAnnouncementRead> & MiRepository<MiAnnouncementRead>; +export type AntennasRepository = Repository<MiAntenna> & MiRepository<MiAntenna>; +export type AppsRepository = Repository<MiApp> & MiRepository<MiApp>; +export type AvatarDecorationsRepository = Repository<MiAvatarDecoration> & MiRepository<MiAvatarDecoration>; +export type AuthSessionsRepository = Repository<MiAuthSession> & MiRepository<MiAuthSession>; +export type BlockingsRepository = Repository<MiBlocking> & MiRepository<MiBlocking>; +export type ChannelFollowingsRepository = Repository<MiChannelFollowing> & MiRepository<MiChannelFollowing>; +export type ChannelFavoritesRepository = Repository<MiChannelFavorite> & MiRepository<MiChannelFavorite>; +export type ClipsRepository = Repository<MiClip> & MiRepository<MiClip>; +export type ClipNotesRepository = Repository<MiClipNote> & MiRepository<MiClipNote>; +export type ClipFavoritesRepository = Repository<MiClipFavorite> & MiRepository<MiClipFavorite>; +export type DriveFilesRepository = Repository<MiDriveFile> & MiRepository<MiDriveFile>; +export type DriveFoldersRepository = Repository<MiDriveFolder> & MiRepository<MiDriveFolder>; +export type EmojisRepository = Repository<MiEmoji> & MiRepository<MiEmoji>; +export type FollowingsRepository = Repository<MiFollowing> & MiRepository<MiFollowing>; +export type FollowRequestsRepository = Repository<MiFollowRequest> & MiRepository<MiFollowRequest>; +export type GalleryLikesRepository = Repository<MiGalleryLike> & MiRepository<MiGalleryLike>; +export type GalleryPostsRepository = Repository<MiGalleryPost> & MiRepository<MiGalleryPost>; +export type HashtagsRepository = Repository<MiHashtag> & MiRepository<MiHashtag>; +export type InstancesRepository = Repository<MiInstance> & MiRepository<MiInstance>; +export type MetasRepository = Repository<MiMeta> & MiRepository<MiMeta>; +export type ModerationLogsRepository = Repository<MiModerationLog> & MiRepository<MiModerationLog>; +export type MutingsRepository = Repository<MiMuting> & MiRepository<MiMuting>; +export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<MiRenoteMuting>; +export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>; +export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>; +export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>; +export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>; +export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>; +export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>; +export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>; +export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>; +export type PollsRepository = Repository<MiPoll> & MiRepository<MiPoll>; +export type PollVotesRepository = Repository<MiPollVote> & MiRepository<MiPollVote>; +export type PromoNotesRepository = Repository<MiPromoNote> & MiRepository<MiPromoNote>; +export type PromoReadsRepository = Repository<MiPromoRead> & MiRepository<MiPromoRead>; +export type RegistrationTicketsRepository = Repository<MiRegistrationTicket> & MiRepository<MiRegistrationTicket>; +export type RegistryItemsRepository = Repository<MiRegistryItem> & MiRepository<MiRegistryItem>; +export type RelaysRepository = Repository<MiRelay> & MiRepository<MiRelay>; +export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>; +export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>; +export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>; +export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>; +export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>; +export type UserKeypairsRepository = Repository<MiUserKeypair> & MiRepository<MiUserKeypair>; +export type UserListsRepository = Repository<MiUserList> & MiRepository<MiUserList>; +export type UserListFavoritesRepository = Repository<MiUserListFavorite> & MiRepository<MiUserListFavorite>; +export type UserListMembershipsRepository = Repository<MiUserListMembership> & MiRepository<MiUserListMembership>; +export type UserNotePiningsRepository = Repository<MiUserNotePining> & MiRepository<MiUserNotePining>; +export type UserPendingsRepository = Repository<MiUserPending> & MiRepository<MiUserPending>; +export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<MiUserProfile>; +export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>; +export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>; +export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>; +export type SystemWebhooksRepository = Repository<MiSystemWebhook> & MiRepository<MiWebhook>; +export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>; +export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>; +export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>; +export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiRepository<MiRoleAssignment>; +export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>; +export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>; +export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>; +export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>; +export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>; +export type NoteEditRepository = Repository<NoteEdit> & MiRepository<NoteEdit>; diff --git a/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts new file mode 100644 index 0000000000000000000000000000000000000000..6215f0f5a20284a6952c552c80dd14685d3d5d8b --- /dev/null +++ b/packages/backend/src/models/json-schema/abuse-report-notification-recipient.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedAbuseReportNotificationRecipientSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + isActive: { + type: 'boolean', + optional: false, nullable: false, + }, + updatedAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + method: { + type: 'string', + optional: false, nullable: false, + enum: ['email', 'webhook'], + }, + userId: { + type: 'string', + optional: true, nullable: false, + }, + user: { + type: 'object', + optional: true, nullable: false, + ref: 'UserLite', + }, + systemWebhookId: { + type: 'string', + optional: true, nullable: false, + }, + systemWebhook: { + type: 'object', + optional: true, nullable: false, + ref: 'SystemWebhook', + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index ca88cc0e397520521c240ead8d54826de57e5198..5ee1561c50c4fe0bf6bd1ec6f04a23d333644ee8 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -20,7 +20,7 @@ export const packedDriveFileSchema = { name: { type: 'string', optional: false, nullable: false, - example: 'lenna.jpg', + example: '192.jpg', }, type: { type: 'string', diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index a602126dc8f8d80f48e52f84799b5065042579e3..062dba9bad6449427749e733fdaf31f043a36e1f 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -88,6 +88,10 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, + isMediaSilenced: { + type: 'boolean', + optional: false, nullable: false, + }, iconUrl: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 952df649adad8feae9883f6151cc823df31fd264..42b2172409e9a71d7885a50d781dfbf15ce656f9 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -44,6 +44,11 @@ export const packedFlashSchema = { type: 'string', optional: false, nullable: false, }, + visibility: { + type: 'string', + optional: false, nullable: false, + enum: ['private', 'public'], + }, likedCount: { type: 'number', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 7edd877f80f385c764f56697bf8d1d91301169d3..1d620f16fdcfd37f9fe3fd413a7c553df7280eeb 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -263,6 +263,12 @@ export const packedMetaLiteSchema = { optional: false, nullable: false, ref: 'RolePolicies', }, + noteSearchableScope: { + type: 'string', + enum: ['local', 'global'], + optional: false, nullable: false, + default: 'local', + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 2641161c8ba2c880f6a916d926e38bfe894a860f..432c096e484c8232944df70bb9dc4cc890047456 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -204,6 +204,7 @@ export const packedNoteSchema = { reactionAcceptance: { type: 'string', optional: false, nullable: true, + enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], }, reactionEmojis: { type: 'object', diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 08580d22dece2f3538be5f2520e8ee68d3addd60..504b9b122fb0dbdb8d995a8ff630b3923b6640f6 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -232,6 +232,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canUpdateBioMedia: { + type: 'boolean', + optional: false, nullable: false, + }, pinLimit: { type: 'integer', optional: false, nullable: false, diff --git a/packages/backend/src/models/json-schema/system-webhook.ts b/packages/backend/src/models/json-schema/system-webhook.ts new file mode 100644 index 0000000000000000000000000000000000000000..d83065a7432958ab75407738d5ddca5a6d5a99ae --- /dev/null +++ b/packages/backend/src/models/json-schema/system-webhook.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; + +export const packedSystemWebhookSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + isActive: { + type: 'boolean', + optional: false, nullable: false, + }, + updatedAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: false, + }, + latestSentAt: { + type: 'string', + format: 'date-time', + optional: false, nullable: true, + }, + latestStatus: { + type: 'number', + optional: false, nullable: true, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + on: { + type: 'array', + items: { + type: 'string', + optional: false, nullable: false, + enum: systemWebhookEventTypes, + }, + }, + url: { + type: 'string', + optional: false, nullable: false, + }, + secret: { + type: 'string', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 4a1b42383f19ef5ce29b2c4a16d507126483c167..047b7f8ae9ff43b8a96b1db83dd97f30767958a7 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -5,13 +5,12 @@ // https://github.com/typeorm/typeorm/issues/2400 import pg from 'pg'; -pg.types.setTypeParser(20, Number); - import { DataSource, Logger } from 'typeorm'; import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; +import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; import { MiAd } from '@/models/Ad.js'; import { MiAnnouncement } from '@/models/Announcement.js'; @@ -69,6 +68,7 @@ import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiWebhook } from '@/models/Webhook.js'; +import { MiSystemWebhook } from '@/models/SystemWebhook.js'; import { MiChannel } from '@/models/Channel.js'; import { MiRetentionAggregation } from '@/models/RetentionAggregation.js'; import { MiRole } from '@/models/Role.js'; @@ -84,9 +84,11 @@ import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +pg.types.setTypeParser(20, Number); + export const dbLogger = new MisskeyLogger('db'); -const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); +const sqlLogger = dbLogger.createSubLogger('sql', 'gray'); class MyCustomLogger implements Logger { @bindThis @@ -168,6 +170,7 @@ export const entities = [ MiHashtag, MiSwSubscription, MiAbuseUserReport, + MiAbuseReportNotificationRecipient, MiRegistrationTicket, MiSignin, MiModerationLog, @@ -186,6 +189,7 @@ export const entities = [ MiPasswordResetRequest, MiUserPending, MiWebhook, + MiSystemWebhook, MiUserIp, MiRetentionAggregation, MiRole, @@ -236,12 +240,8 @@ export function createPostgresDataSource(config: Config) { cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbã‚’closeã—ã¦ã‚‚何故ã‹redisã®ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ãŒå†…部的ã«æ®‹ã‚Šç¶šã‘るよã†ã§ã€ãƒ†ã‚¹ãƒˆã®éš›ã«æ”¯éšœãŒå‡ºã‚‹ãŸã‚無効ã«ã™ã‚‹(ã‚ャッシュもå«ã‚ã¦ãƒ†ã‚¹ãƒˆã—ãŸã„ãŸã‚本当ã¯æœ‰åŠ¹ã«ã—ãŸã„ãŒ...) type: 'ioredis', options: { - host: config.redis.host, - port: config.redis.port, - family: config.redis.family ?? 0, - password: config.redis.pass, + ...config.redis, keyPrefix: `${config.redis.prefix}:query:`, - db: config.redis.db ?? 0, }, } : false, logging: log, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index d7316e19e3a450cea1948aa0ac8ad2537413119c..7daca687a1a903a9af3697c8979d3ccad4e6a935 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -11,7 +11,8 @@ import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; -import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; +import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; +import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; @@ -75,7 +76,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor DeleteFileProcessorService, CleanRemoteFilesProcessorService, RelationshipProcessorService, - WebhookDeliverProcessorService, + UserWebhookDeliverProcessorService, + SystemWebhookDeliverProcessorService, EndedPollNotificationProcessorService, DeliverProcessorService, InboxProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 76b6d7fb05a3acc8958b777ccd8dacc5acb9edb5..7a6169bf9cfaffe27efd1d2ec0d0b3a09ea7d469 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -5,11 +5,13 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Bull from 'bullmq'; +import * as Sentry from '@sentry/node'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; +import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; +import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; @@ -77,7 +79,8 @@ export class QueueProcessorService implements OnApplicationShutdown { private dbQueueWorker: Bull.Worker; private deliverQueueWorker: Bull.Worker; private inboxQueueWorker: Bull.Worker; - private webhookDeliverQueueWorker: Bull.Worker; + private userWebhookDeliverQueueWorker: Bull.Worker; + private systemWebhookDeliverQueueWorker: Bull.Worker; private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; @@ -87,7 +90,8 @@ export class QueueProcessorService implements OnApplicationShutdown { private config: Config, private queueLoggerService: QueueLoggerService, - private webhookDeliverProcessorService: WebhookDeliverProcessorService, + private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, + private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, @@ -139,207 +143,375 @@ export class QueueProcessorService implements OnApplicationShutdown { } //#region system - this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { - switch (job.name) { - case 'tickCharts': return this.tickChartsProcessorService.process(); - case 'resyncCharts': return this.resyncChartsProcessorService.process(); - case 'cleanCharts': return this.cleanChartsProcessorService.process(); - case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); - case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); - case 'clean': return this.cleanProcessorService.process(); - default: throw new Error(`unrecognized job type ${job.name} for system`); - } - }, { - ...baseQueueOptions(this.config, QUEUE.SYSTEM), - autorun: false, - }); - - const systemLogger = this.logger.createSubLogger('system'); - - this.systemQueueWorker - .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); + { + const processer = (job: Bull.Job) => { + switch (job.name) { + case 'tickCharts': return this.tickChartsProcessorService.process(); + case 'resyncCharts': return this.resyncChartsProcessorService.process(); + case 'cleanCharts': return this.cleanChartsProcessorService.process(); + case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); + case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); + case 'clean': return this.cleanProcessorService.process(); + default: throw new Error(`unrecognized job type ${job.name} for system`); + } + }; + + this.systemQueueWorker = new Bull.Worker(QUEUE.SYSTEM, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: System: ' + job.name }, () => processer(job)); + } else { + return processer(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.SYSTEM), + autorun: false, + }); + + const logger = this.logger.createSubLogger('system'); + + this.systemQueueWorker + .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) }); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } //#endregion //#region db - this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { - switch (job.name) { - case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); - case 'exportAccountData': return this.exportAccountDataProcessorService.process(job); - case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); - case 'exportNotes': return this.exportNotesProcessorService.process(job); - case 'exportClips': return this.exportClipsProcessorService.process(job); - case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); - case 'exportFollowing': return this.exportFollowingProcessorService.process(job); - case 'exportMuting': return this.exportMutingProcessorService.process(job); - case 'exportBlocking': return this.exportBlockingProcessorService.process(job); - case 'exportUserLists': return this.exportUserListsProcessorService.process(job); - case 'exportAntennas': return this.exportAntennasProcessorService.process(job); - case 'importFollowing': return this.importFollowingProcessorService.process(job); - case 'importNotes': return this.importNotesProcessorService.process(job); - case 'importTweetsToDb': return this.importNotesProcessorService.processTwitterDb(job); - case 'importIGToDb': return this.importNotesProcessorService.processIGDb(job); - case 'importFBToDb': return this.importNotesProcessorService.processFBDb(job); - case 'importMastoToDb': return this.importNotesProcessorService.processMastoToDb(job); - case 'importPleroToDb': return this.importNotesProcessorService.processPleroToDb(job); - case 'importKeyNotesToDb': return this.importNotesProcessorService.processKeyNotesToDb(job); - case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); - case 'importMuting': return this.importMutingProcessorService.process(job); - case 'importBlocking': return this.importBlockingProcessorService.process(job); - case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); - case 'importUserLists': return this.importUserListsProcessorService.process(job); - case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); - case 'importAntennas': return this.importAntennasProcessorService.process(job); - case 'deleteAccount': return this.deleteAccountProcessorService.process(job); - default: throw new Error(`unrecognized job type ${job.name} for db`); - } - }, { - ...baseQueueOptions(this.config, QUEUE.DB), - autorun: false, - }); - - const dbLogger = this.logger.createSubLogger('db'); - - this.dbQueueWorker - .on('active', (job) => dbLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); + { + const processer = (job: Bull.Job) => { + switch (job.name) { + case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); + case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job); + case 'exportNotes': return this.exportNotesProcessorService.process(job); + case 'exportClips': return this.exportClipsProcessorService.process(job); + case 'exportFavorites': return this.exportFavoritesProcessorService.process(job); + case 'exportFollowing': return this.exportFollowingProcessorService.process(job); + case 'exportMuting': return this.exportMutingProcessorService.process(job); + case 'exportBlocking': return this.exportBlockingProcessorService.process(job); + case 'exportUserLists': return this.exportUserListsProcessorService.process(job); + case 'exportAntennas': return this.exportAntennasProcessorService.process(job); + case 'exportAccountData': return this.exportAccountDataProcessorService.process(job); + case 'importFollowing': return this.importFollowingProcessorService.process(job); + case 'importFollowingToDb': return this.importFollowingProcessorService.processDb(job); + case 'importMuting': return this.importMutingProcessorService.process(job); + case 'importBlocking': return this.importBlockingProcessorService.process(job); + case 'importBlockingToDb': return this.importBlockingProcessorService.processDb(job); + case 'importUserLists': return this.importUserListsProcessorService.process(job); + case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job); + case 'importAntennas': return this.importAntennasProcessorService.process(job); + case 'importNotes': return this.importNotesProcessorService.process(job); + case 'importTweetsToDb': return this.importNotesProcessorService.processTwitterDb(job); + case 'importIGToDb': return this.importNotesProcessorService.processIGDb(job); + case 'importFBToDb': return this.importNotesProcessorService.processFBDb(job); + case 'importMastoToDb': return this.importNotesProcessorService.processMastoToDb(job); + case 'importPleroToDb': return this.importNotesProcessorService.processPleroToDb(job); + case 'importKeyNotesToDb': return this.importNotesProcessorService.processKeyNotesToDb(job); + case 'deleteAccount': return this.deleteAccountProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for db`); + } + }; + + this.dbQueueWorker = new Bull.Worker(QUEUE.DB, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: DB: ' + job.name }, () => processer(job)); + } else { + return processer(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.DB), + autorun: false, + }); + + const logger = this.logger.createSubLogger('db'); + + this.dbQueueWorker + .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) }); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } //#endregion //#region deliver - this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => this.deliverProcessorService.process(job), { - ...baseQueueOptions(this.config, QUEUE.DELIVER), - autorun: false, - concurrency: this.config.deliverJobConcurrency ?? 128, - limiter: { - max: this.config.deliverJobPerSec ?? 128, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const deliverLogger = this.logger.createSubLogger('deliver'); - - this.deliverQueueWorker - .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) - .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); + { + this.deliverQueueWorker = new Bull.Worker(QUEUE.DELIVER, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: Deliver' }, () => this.deliverProcessorService.process(job)); + } else { + return this.deliverProcessorService.process(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.DELIVER), + autorun: false, + concurrency: this.config.deliverJobConcurrency ?? 128, + limiter: { + max: this.config.deliverJobPerSec ?? 128, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const logger = this.logger.createSubLogger('deliver'); + + this.deliverQueueWorker + .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 : '-'}`); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: Deliver: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } //#endregion //#region inbox - this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), { - ...baseQueueOptions(this.config, QUEUE.INBOX), - autorun: false, - concurrency: this.config.inboxJobConcurrency ?? 16, - limiter: { - max: this.config.inboxJobPerSec ?? 32, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const inboxLogger = this.logger.createSubLogger('inbox'); - - this.inboxQueueWorker - .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) - .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); + { + this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: Inbox' }, () => this.inboxProcessorService.process(job)); + } else { + return this.inboxProcessorService.process(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.INBOX), + autorun: false, + concurrency: this.config.inboxJobConcurrency ?? 16, + limiter: { + max: this.config.inboxJobPerSec ?? 32, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const logger = this.logger.createSubLogger('inbox'); + + this.inboxQueueWorker + .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) }); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: Inbox: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } //#endregion - //#region webhook deliver - this.webhookDeliverQueueWorker = new Bull.Worker(QUEUE.WEBHOOK_DELIVER, (job) => this.webhookDeliverProcessorService.process(job), { - ...baseQueueOptions(this.config, QUEUE.WEBHOOK_DELIVER), - autorun: false, - concurrency: 64, - limiter: { - max: 64, - duration: 1000, - }, - settings: { - backoffStrategy: httpRelatedBackoff, - }, - }); - - const webhookLogger = this.logger.createSubLogger('webhook'); - - this.webhookDeliverQueueWorker - .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) - .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); + //#region user-webhook deliver + { + this.userWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.USER_WEBHOOK_DELIVER, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: UserWebhookDeliver' }, () => this.userWebhookDeliverProcessorService.process(job)); + } else { + return this.userWebhookDeliverProcessorService.process(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.USER_WEBHOOK_DELIVER), + autorun: false, + concurrency: 64, + limiter: { + max: 64, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const logger = this.logger.createSubLogger('user-webhook'); + + this.userWebhookDeliverQueueWorker + .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 : '-'}`); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } + //#endregion + + //#region system-webhook deliver + { + this.systemWebhookDeliverQueueWorker = new Bull.Worker(QUEUE.SYSTEM_WEBHOOK_DELIVER, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: SystemWebhookDeliver' }, () => this.systemWebhookDeliverProcessorService.process(job)); + } else { + return this.systemWebhookDeliverProcessorService.process(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.SYSTEM_WEBHOOK_DELIVER), + autorun: false, + concurrency: 16, + limiter: { + max: 16, + duration: 1000, + }, + settings: { + backoffStrategy: httpRelatedBackoff, + }, + }); + + const logger = this.logger.createSubLogger('system-webhook'); + + this.systemWebhookDeliverQueueWorker + .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 : '-'}`); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } //#endregion //#region relationship - this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { - switch (job.name) { - case 'follow': return this.relationshipProcessorService.processFollow(job); - case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); - case 'block': return this.relationshipProcessorService.processBlock(job); - case 'unblock': return this.relationshipProcessorService.processUnblock(job); - default: throw new Error(`unrecognized job type ${job.name} for relationship`); - } - }, { - ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP), - autorun: false, - concurrency: this.config.relationshipJobConcurrency ?? 16, - limiter: { - max: this.config.relationshipJobPerSec ?? 64, - duration: 1000, - }, - }); - - const relationshipLogger = this.logger.createSubLogger('relationship'); - - this.relationshipQueueWorker - .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); + { + const processer = (job: Bull.Job) => { + switch (job.name) { + case 'follow': return this.relationshipProcessorService.processFollow(job); + case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); + case 'block': return this.relationshipProcessorService.processBlock(job); + case 'unblock': return this.relationshipProcessorService.processUnblock(job); + default: throw new Error(`unrecognized job type ${job.name} for relationship`); + } + }; + + this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: Relationship: ' + job.name }, () => processer(job)); + } else { + return processer(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.RELATIONSHIP), + autorun: false, + concurrency: this.config.relationshipJobConcurrency ?? 16, + limiter: { + max: this.config.relationshipJobPerSec ?? 64, + duration: 1000, + }, + }); + + const logger = this.logger.createSubLogger('relationship'); + + this.relationshipQueueWorker + .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) }); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } //#endregion //#region object storage - this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { - switch (job.name) { - case 'deleteFile': return this.deleteFileProcessorService.process(job); - case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); - default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); - } - }, { - ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE), - autorun: false, - concurrency: 16, - }); - - const objectStorageLogger = this.logger.createSubLogger('objectStorage'); - - this.objectStorageQueueWorker - .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) - .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) - .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) })) - .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); + { + const processer = (job: Bull.Job) => { + switch (job.name) { + case 'deleteFile': return this.deleteFileProcessorService.process(job); + case 'cleanRemoteFiles': return this.cleanRemoteFilesProcessorService.process(job); + default: throw new Error(`unrecognized job type ${job.name} for objectStorage`); + } + }; + + this.objectStorageQueueWorker = new Bull.Worker(QUEUE.OBJECT_STORAGE, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: ObjectStorage: ' + job.name }, () => processer(job)); + } else { + return processer(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.OBJECT_STORAGE), + autorun: false, + concurrency: 16, + }); + + const logger = this.logger.createSubLogger('objectStorage'); + + this.objectStorageQueueWorker + .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) }); + if (config.sentryForBackend) { + Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, { + level: 'error', + extra: { job, err }, + }); + } + }) + .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); + } //#endregion //#region ended poll notification - this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => this.endedPollNotificationProcessorService.process(job), { - ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), - autorun: false, - }); + { + this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: EndedPollNotification' }, () => this.endedPollNotificationProcessorService.process(job)); + } else { + return this.endedPollNotificationProcessorService.process(job); + } + }, { + ...baseQueueOptions(this.config, QUEUE.ENDED_POLL_NOTIFICATION), + autorun: false, + }); + } //#endregion } @@ -350,7 +522,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker.run(), this.deliverQueueWorker.run(), this.inboxQueueWorker.run(), - this.webhookDeliverQueueWorker.run(), + this.userWebhookDeliverQueueWorker.run(), + this.systemWebhookDeliverQueueWorker.run(), this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), @@ -364,7 +537,8 @@ export class QueueProcessorService implements OnApplicationShutdown { this.dbQueueWorker.close(), this.deliverQueueWorker.close(), this.inboxQueueWorker.close(), - this.webhookDeliverQueueWorker.close(), + this.userWebhookDeliverQueueWorker.close(), + this.systemWebhookDeliverQueueWorker.close(), this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 132e9166121c60dd15d49165283a72be57504c36..67f689b618496a60baf6b650ee0ccf7c68784cdc 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -14,7 +14,8 @@ export const QUEUE = { DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', - WEBHOOK_DELIVER: 'webhookDeliver', + USER_WEBHOOK_DELIVER: 'userWebhookDeliver', + SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver', }; export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index d665945861e2172f4c0174ffe440b146fcde87cb..4076e9da9038f8ca7f5d5c66b882745bc6127dfd 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -45,7 +45,7 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); + this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h } @bindThis diff --git a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts index 29c1f27bb1179c67fc89ef6762e7edc8eb70c2d9..34180e5f2b09a034be3dfda2b1241c7263395a3f 100644 --- a/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts +++ b/packages/backend/src/queue/processors/EndedPollNotificationProcessorService.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { PollVotesRepository, NotesRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; +import { CacheService } from '@/core/CacheService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; @@ -24,6 +25,7 @@ export class EndedPollNotificationProcessorService { @Inject(DI.pollVotesRepository) private pollVotesRepository: PollVotesRepository, + private cacheService: CacheService, private notificationService: NotificationService, private queueLoggerService: QueueLoggerService, ) { @@ -47,9 +49,12 @@ export class EndedPollNotificationProcessorService { const userIds = [...new Set([note.userId, ...votes.map(v => v.userId)])]; for (const userId of userIds) { - this.notificationService.createNotification(userId, 'pollEnded', { - noteId: note.id, - }); + const profile = await this.cacheService.userProfileCache.fetch(userId); + if (profile.userHost === null) { + this.notificationService.createNotification(userId, 'pollEnded', { + noteId: note.id, + }); + } } } } diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index e5b7c5ac5213b3ce0d51c5209a114f461a50407f..9c033b73e2b75f6abae681ae0eaae4c337d09c82 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -76,7 +76,7 @@ export class ImportAntennasProcessorService { this.logger.warn('Validation Failed'); continue; } - const result = await this.antennasRepository.insert({ + const result = await this.antennasRepository.insertOne({ id: this.idService.gen(now.getTime()), lastUsedAt: now, userId: job.data.user.id, @@ -91,7 +91,7 @@ export class ImportAntennasProcessorService { excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, - }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); + }); this.logger.succ('Antenna created: ' + result.id); this.globalEventService.publishInternalEvent('antennaCreated', result); } diff --git a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts index a5992c28c84674c22d961134c2244a5738651cb7..db9255b35d5c182b873df21ae1f5a2e170dd6793 100644 --- a/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportUserListsProcessorService.ts @@ -79,11 +79,11 @@ export class ImportUserListsProcessorService { }); if (list == null) { - list = await this.userListsRepository.insert({ + list = await this.userListsRepository.insertOne({ id: this.idService.gen(), userId: user.id, name: listName, - }).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + }); } let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({ diff --git a/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6bef526845620dd22a5553357efdb13112eda36 --- /dev/null +++ b/packages/backend/src/queue/processors/SystemWebhookDeliverProcessorService.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Bull from 'bullmq'; +import { DI } from '@/di-symbols.js'; +import type { SystemWebhooksRepository } from '@/models/_.js'; +import type { Config } from '@/config.js'; +import type Logger from '@/logger.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { StatusError } from '@/misc/status-error.js'; +import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import { SystemWebhookDeliverJobData } from '../types.js'; + +@Injectable() +export class SystemWebhookDeliverProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.systemWebhooksRepository) + private systemWebhooksRepository: SystemWebhooksRepository, + + private httpRequestService: HttpRequestService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('webhook'); + } + + @bindThis + public async process(job: Bull.Job<SystemWebhookDeliverJobData>): Promise<string> { + try { + this.logger.debug(`delivering ${job.data.webhookId}`); + + const res = await this.httpRequestService.send(job.data.to, { + method: 'POST', + headers: { + 'User-Agent': 'Misskey-Hooks', + 'X-Misskey-Host': this.config.host, + 'X-Misskey-Hook-Id': job.data.webhookId, + 'X-Misskey-Hook-Secret': job.data.secret, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + server: this.config.url, + hookId: job.data.webhookId, + eventId: job.data.eventId, + createdAt: job.data.createdAt, + type: job.data.type, + body: job.data.content, + }), + }); + + this.systemWebhooksRepository.update({ id: job.data.webhookId }, { + latestSentAt: new Date(), + latestStatus: res.status, + }); + + return 'Success'; + } catch (res) { + this.logger.error(res as Error); + + this.systemWebhooksRepository.update({ id: job.data.webhookId }, { + latestSentAt: new Date(), + latestStatus: res instanceof StatusError ? res.statusCode : 1, + }); + + if (res instanceof StatusError) { + // 4xx + if (!res.isRetryable) { + throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`); + } + + // 5xx etc. + throw new Error(`${res.statusCode} ${res.statusMessage}`); + } else { + // DNS error, socket error, timeout ... + throw res; + } + } + } +} diff --git a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts similarity index 92% rename from packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts rename to packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts index 8c260c0137ce33e02ef50215239f75b3c29db6e2..9ec630ef7047287c35d220f4dce9c57f76f28593 100644 --- a/packages/backend/src/queue/processors/WebhookDeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/UserWebhookDeliverProcessorService.ts @@ -13,10 +13,10 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { StatusError } from '@/misc/status-error.js'; import { bindThis } from '@/decorators.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; -import type { WebhookDeliverJobData } from '../types.js'; +import { UserWebhookDeliverJobData } from '../types.js'; @Injectable() -export class WebhookDeliverProcessorService { +export class UserWebhookDeliverProcessorService { private logger: Logger; constructor( @@ -33,7 +33,7 @@ export class WebhookDeliverProcessorService { } @bindThis - public async process(job: Bull.Job<WebhookDeliverJobData>): Promise<string> { + public async process(job: Bull.Job<UserWebhookDeliverJobData>): Promise<string> { try { this.logger.debug(`delivering ${job.data.webhookId}`); diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 91718898b20d53fbec7cce148b605814b4e04816..c0d246ebbc754c2a88319937ca3be1aaf942cde2 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -131,7 +131,17 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; -export type WebhookDeliverJobData = { +export type SystemWebhookDeliverJobData = { + type: string; + content: unknown; + webhookId: MiWebhook['id']; + to: string; + secret: string; + createdAt: number; + eventId: string; +}; + +export type UserWebhookDeliverJobData = { type: string; content: unknown; webhookId: MiWebhook['id']; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index e0b187f3cfe6966b124802adf573a8bd1dcf317c..65a82181740999d72da472184857035a2a757aaf 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -53,7 +53,7 @@ export class FileServerService { private internalStorageService: InternalStorageService, private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('server', 'gray', false); + this.logger = this.loggerService.getLogger('server', 'gray'); //this.createServer = this.createServer.bind(this); } diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 716bb0944b635b13017c80876733a13e1c219282..bc8d3c0411d00418958ebb96c776daa286db6706 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -135,7 +135,7 @@ export class NodeinfoServerService { return document; }; - const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); + const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); // 10m fastify.get(nodeinfo2_1path, async (request, reply) => { const base = await cache.fetch(() => nodeinfo2(21)); diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 9eddf434f715918614a43c9cbe299be7756c06a7..30c133d9ece5dc59f252c7909e2c535e5b7a5393 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -70,7 +70,7 @@ export class ServerService implements OnApplicationShutdown { private loggerService: LoggerService, private oauth2ProviderService: OAuth2ProviderService, ) { - this.logger = this.loggerService.getLogger('server', 'gray', false); + this.logger = this.loggerService.getLogger('server', 'gray'); } @bindThis diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 271ef80554a80cb2ff0a0d540ac9ef4a7ce00eab..47f64f6609395ecbc0801e815122b3c6b1362b19 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -73,6 +73,16 @@ export class ApiCallService implements OnApplicationShutdown { reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`); } statusCode = statusCode ?? 403; + } else if (err.code === 'RATE_LIMIT_EXCEEDED') { + const info: unknown = err.info; + const unixEpochInSeconds = Date.now(); + if (typeof(info) === 'object' && info && 'resetMs' in info && typeof(info.resetMs) === 'number') { + const cooldownInSeconds = Math.ceil((info.resetMs - unixEpochInSeconds) / 1000); + // ã‚‚ã—ã‹ã™ã‚‹ã¨ãƒžã‚¤ãƒŠã‚¹ã«ãªã‚‹å¯èƒ½æ€§ãŒãªãã¯ãªã„ã®ã§ãƒžã‚¤ãƒŠã‚¹ã ã£ãŸã‚‰0ã«ã—ã¦ãŠã + reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10)); + } else { + this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`); + } } else if (!statusCode) { statusCode = 500; } @@ -93,7 +103,7 @@ export class ApiCallService implements OnApplicationShutdown { } } - #onExecError(ep: IEndpoint, data: any, err: Error): void { + #onExecError(ep: IEndpoint, data: any, err: Error, userId?: MiUser['id']): void { if (err instanceof ApiError || err instanceof AuthenticationError) { throw err; } else { @@ -108,10 +118,13 @@ export class ApiCallService implements OnApplicationShutdown { id: errId, }, }); - console.error(err, errId); if (this.config.sentryForBackend) { Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, { + level: 'error', + user: { + id: userId, + }, extra: { ep: ep.name, ps: data, @@ -305,12 +318,17 @@ export class ApiCallService implements OnApplicationShutdown { if (factor > 0) { // Rate limit await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => { - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }); + if ('info' in err) { + // errã¯Limiter.LimiterInfoã§ã‚ã‚‹ã“ã¨ãŒæœŸå¾…ã•ã‚Œã‚‹ + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }, err.info); + } else { + throw new TypeError('information must be a rate-limiter information.'); + } }); } } @@ -410,9 +428,13 @@ export class ApiCallService implements OnApplicationShutdown { // API invoking if (this.config.sentryForBackend) { - return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err))); + return await Sentry.startSpan({ + name: 'API: ' + ep.name, + }, () => ep.exec(data, user, token, file, request.ip, request.headers) + .catch((err: Error) => this.#onExecError(ep, data, err, user?.id))); } else { - return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)); + return await ep.exec(data, user, token, file, request.ip, request.headers) + .catch((err: Error) => this.#onExecError(ep, data, err, user?.id)); } } diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index ddef8db9879ac82b5ebba604ac4b00cbe8428c5f..690ff2e022822659f0bb691ee3a75655e55143a0 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -37,7 +37,7 @@ export class AuthenticateService implements OnApplicationShutdown { private cacheService: CacheService, ) { - this.appCache = new MemoryKVCache<MiApp>(Infinity); + this.appCache = new MemoryKVCache<MiApp>(1000 * 60 * 60 * 24 * 7); // 1w } @bindThis diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index f44635fba043f848c6178022f9ece9d5c9c5b2a4..4a08410cebe57f1b556d2fb3df7cdeccf3f56664 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -6,8 +6,13 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; -import * as ep___admin_meta from './endpoints/admin/meta.js'; +import * as ep___admin_abuseReport_notificationRecipient_list from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js'; +import * as ep___admin_abuseReport_notificationRecipient_show from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js'; +import * as ep___admin_abuseReport_notificationRecipient_create from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js'; +import * as ep___admin_abuseReport_notificationRecipient_update from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js'; +import * as ep___admin_abuseReport_notificationRecipient_delete from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; +import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js'; @@ -87,6 +92,11 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; +import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js'; +import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js'; +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___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'; @@ -394,6 +404,11 @@ import type { Provider } from '@nestjs/common'; const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_meta.default }; const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default }; +const $admin_abuseReport_notificationRecipient_list: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/list', useClass: ep___admin_abuseReport_notificationRecipient_list.default }; +const $admin_abuseReport_notificationRecipient_show: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/show', useClass: ep___admin_abuseReport_notificationRecipient_show.default }; +const $admin_abuseReport_notificationRecipient_create: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/create', useClass: ep___admin_abuseReport_notificationRecipient_create.default }; +const $admin_abuseReport_notificationRecipient_update: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/update', useClass: ep___admin_abuseReport_notificationRecipient_update.default }; +const $admin_abuseReport_notificationRecipient_delete: Provider = { provide: 'ep:admin/abuse-report/notification-recipient/delete', useClass: ep___admin_abuseReport_notificationRecipient_delete.default }; const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default }; const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default }; const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default }; @@ -473,6 +488,11 @@ const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useCla const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default }; const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default }; +const $admin_systemWebhook_create: Provider = { provide: 'ep:admin/system-webhook/create', useClass: ep___admin_systemWebhook_create.default }; +const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhook/delete', useClass: ep___admin_systemWebhook_delete.default }; +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 $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 }; @@ -784,6 +804,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ ApiLoggerService, $admin_meta, $admin_abuseUserReports, + $admin_abuseReport_notificationRecipient_list, + $admin_abuseReport_notificationRecipient_show, + $admin_abuseReport_notificationRecipient_create, + $admin_abuseReport_notificationRecipient_update, + $admin_abuseReport_notificationRecipient_delete, $admin_accounts_create, $admin_accounts_delete, $admin_accounts_findByEmail, @@ -863,6 +888,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_roles_unassign, $admin_roles_updateDefaultPolicies, $admin_roles_users, + $admin_systemWebhook_create, + $admin_systemWebhook_delete, + $admin_systemWebhook_list, + $admin_systemWebhook_show, + $admin_systemWebhook_update, $announcements, $announcements_show, $antennas_create, @@ -1168,6 +1198,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ exports: [ $admin_meta, $admin_abuseUserReports, + $admin_abuseReport_notificationRecipient_list, + $admin_abuseReport_notificationRecipient_show, + $admin_abuseReport_notificationRecipient_create, + $admin_abuseReport_notificationRecipient_update, + $admin_abuseReport_notificationRecipient_delete, $admin_accounts_create, $admin_accounts_delete, $admin_accounts_findByEmail, @@ -1247,6 +1282,11 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_roles_unassign, $admin_roles_updateDefaultPolicies, $admin_roles_users, + $admin_systemWebhook_create, + $admin_systemWebhook_delete, + $admin_systemWebhook_list, + $admin_systemWebhook_show, + $admin_systemWebhook_update, $announcements, $announcements_show, $antennas_create, diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index 0439cdfe5e033b91ae56bc50f49ad9703f3f2118..e94160a6571464c76975ef7ea74474ef11291993 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -32,11 +32,18 @@ export class RateLimiterService { @bindThis public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) { - return new Promise<void>((ok, reject) => { - if (this.disabled) ok(); + { + if (this.disabled) { + return Promise.resolve(); + } + + // those lines with the "wrong" brace style / indentation are + // done that way so that the *other* lines stay identical to + // Misskey, simplifying merges // Short-term limit - const min = (): void => { + // eslint-disable-next-line brace-style + const minP = () => { return new Promise<void>((ok, reject) => { const minIntervalLimiter = new Limiter({ id: `${actor}:${limitation.key}:min`, duration: limitation.minInterval! * factor, @@ -46,25 +53,27 @@ export class RateLimiterService { minIntervalLimiter.get((err, info) => { if (err) { - return reject('ERR'); + return reject({ code: 'ERR', info }); } this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); if (info.remaining === 0) { - reject('BRIEF_REQUEST_INTERVAL'); + return reject({ code: 'BRIEF_REQUEST_INTERVAL', info }); } else { if (hasLongTermLimit) { - max(); + return maxP().then(ok, reject); } else { - ok(); + return ok(); } } }); - }; + // eslint-disable-next-line brace-style + }); }; // Long term limit - const max = (): void => { + // eslint-disable-next-line brace-style + const maxP = () => { return new Promise<void>((ok, reject) => { const limiter = new Limiter({ id: `${actor}:${limitation.key}`, duration: limitation.duration! * factor, @@ -74,18 +83,19 @@ export class RateLimiterService { limiter.get((err, info) => { if (err) { - return reject('ERR'); + return reject({ code: 'ERR', info }); } this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); if (info.remaining === 0) { - reject('RATE_LIMIT_EXCEEDED'); + return reject({ code: 'RATE_LIMIT_EXCEEDED', info }); } else { - ok(); + return ok(); } }); - }; + // eslint-disable-next-line brace-style + }); }; const hasShortTermLimit = typeof limitation.minInterval === 'number'; @@ -94,12 +104,12 @@ export class RateLimiterService { typeof limitation.max === 'number'; if (hasShortTermLimit) { - min(); + return minP(); } else if (hasLongTermLimit) { - max(); + return maxP(); } else { - ok(); + return Promise.resolve(); } - }); + } } } diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts index 714e56e8c3fad6d2138cfcbe575d7b3f568fe823..70306c31135b32c88b460cc6e3622122f7554fe1 100644 --- a/packages/backend/src/server/api/SigninService.ts +++ b/packages/backend/src/server/api/SigninService.ts @@ -29,13 +29,13 @@ export class SigninService { public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) { setImmediate(async () => { // Append signin history - const record = await this.signinsRepository.insert({ + const record = await this.signinsRepository.insertOne({ id: this.idService.gen(), userId: user.id, ip: request.ip, headers: request.headers as any, success: true, - }).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0])); + }); // Publish signin event this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record)); diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 9c221314ac0885bc5013737e3ad648b501238b79..f89c3954f80e8a4d6c8320a143a9052115707285 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -196,14 +196,14 @@ export class SignupApiService { //const salt = await bcrypt.genSalt(8); const hash = await argon2.hash(password); - const pendingUser = await this.userPendingsRepository.insert({ + const pendingUser = await this.userPendingsRepository.insertOne({ id: this.idService.gen(), code, email: emailAddress!, username: username, password: hash, reason: reason, - }).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0])); + }); const link = `${this.config.url}/signup-complete/${code}`; diff --git a/packages/backend/src/server/api/StreamingApiServerService.ts b/packages/backend/src/server/api/StreamingApiServerService.ts index b8f448477b06ba0d44fb0cfe425aef282594b406..9b8464f7050dce71b6e50376ee0bf96747386612 100644 --- a/packages/backend/src/server/api/StreamingApiServerService.ts +++ b/packages/backend/src/server/api/StreamingApiServerService.ts @@ -19,7 +19,15 @@ import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; import { AuthenticateService, AuthenticationError } from './AuthenticateService.js'; import MainStreamConnection from './stream/Connection.js'; import { ChannelsService } from './stream/ChannelsService.js'; +import { RateLimiterService } from './RateLimiterService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { getIpHash } from '@/misc/get-ip-hash.js'; +import proxyAddr from 'proxy-addr'; +import ms from 'ms'; import type * as http from 'node:http'; +import type { IEndpointMeta } from './endpoints.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type Logger from '@/logger.js'; @Injectable() export class StreamingApiServerService { @@ -41,9 +49,35 @@ export class StreamingApiServerService { private notificationService: NotificationService, private usersService: UserService, private channelFollowingService: ChannelFollowingService, + private rateLimiterService: RateLimiterService, + private roleService: RoleService, + private loggerService: LoggerService, ) { } + @bindThis + private async rateLimitThis( + user: MiLocalUser | null | undefined, + requestIp: string | undefined, + limit: IEndpointMeta['limit'] & { key: NonNullable<string> }, + ) : Promise<boolean> { + let limitActor: string; + if (user) { + limitActor = user.id; + } else { + limitActor = getIpHash(requestIp || 'wtf'); + } + + const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1; + + if (factor <= 0) return false; + + // Rate limit + return await this.rateLimiterService.limit(limit, limitActor, factor) + .then(() => { return false; }) + .catch(err => { return true; }); + } + @bindThis public attach(server: http.Server): void { this.#wss = new WebSocket.WebSocketServer({ @@ -57,6 +91,21 @@ export class StreamingApiServerService { return; } + // ServerServices sets `trustProxy: true`, which inside + // fastify/request.js ends up calling `proxyAddr` in this way, + // so we do the same + const requestIp = proxyAddr(request, () => { return true; } ); + + if (await this.rateLimitThis(null, requestIp, { + key: 'wsconnect', + duration: ms('5min'), + max: 32, + })) { + socket.write('HTTP/1.1 429 Rate Limit Exceeded\r\n\r\n'); + socket.destroy(); + return; + } + const q = new URL(request.url, `http://${request.headers.host}`).searchParams; let user: MiLocalUser | null = null; @@ -94,13 +143,27 @@ export class StreamingApiServerService { return; } + const rateLimiter = () => { + // rather high limit, because when catching up at the top of a + // timeline, the frontend may render many many notes, each of + // which causes a message via `useNoteCapture` to ask for + // realtime updates of that note + return this.rateLimitThis(user, requestIp, { + key: 'wsmessage', + duration: ms('2sec'), + max: 4096, + }); + }; + const stream = new MainStreamConnection( this.channelsService, this.noteReadService, this.notificationService, this.cacheService, this.channelFollowingService, - user, app, + this.loggerService, + user, app, requestIp, + rateLimiter, ); await stream.init(); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 89f60933ef601cbc94af8df4d4cabf9df742b5ac..e2fcd1a9d03c0cf12e6922ae7575a1c09ba6c741 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -6,8 +6,18 @@ import { permissions } from 'misskey-js'; import type { KeyOf, Schema } from '@/misc/json-schema.js'; -import * as ep___admin_meta from './endpoints/admin/meta.js'; +import * as ep___admin_abuseReport_notificationRecipient_list + from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js'; +import * as ep___admin_abuseReport_notificationRecipient_show + from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js'; +import * as ep___admin_abuseReport_notificationRecipient_create + from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js'; +import * as ep___admin_abuseReport_notificationRecipient_update + from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js'; +import * as ep___admin_abuseReport_notificationRecipient_delete + from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; +import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js'; import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js'; import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js'; @@ -44,7 +54,8 @@ import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-c import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js'; import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; -import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; +import * as ep___admin_federation_refreshRemoteInstanceMetadata + from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; @@ -87,6 +98,11 @@ import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; +import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js'; +import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js'; +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___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'; @@ -392,6 +408,11 @@ import * as ep___reversi_verify from './endpoints/reversi/verify.js'; const eps = [ ['admin/meta', ep___admin_meta], ['admin/abuse-user-reports', ep___admin_abuseUserReports], + ['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list], + ['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show], + ['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create], + ['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update], + ['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete], ['admin/accounts/create', ep___admin_accounts_create], ['admin/accounts/delete', ep___admin_accounts_delete], ['admin/accounts/find-by-email', ep___admin_accounts_findByEmail], @@ -471,6 +492,11 @@ const eps = [ ['admin/roles/unassign', ep___admin_roles_unassign], ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies], ['admin/roles/users', ep___admin_roles_users], + ['admin/system-webhook/create', ep___admin_systemWebhook_create], + ['admin/system-webhook/delete', ep___admin_systemWebhook_delete], + ['admin/system-webhook/list', ep___admin_systemWebhook_list], + ['admin/system-webhook/show', ep___admin_systemWebhook_show], + ['admin/system-webhook/update', ep___admin_systemWebhook_update], ['announcements', ep___announcements], ['announcements/show', ep___announcements_show], ['antennas/create', ep___antennas_create], @@ -899,8 +925,12 @@ export interface IEndpoint { const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => { return { name: name, - get meta() { return ep.meta ?? {}; }, - get params() { return ep.paramDef; }, + get meta() { + return ep.meta ?? {}; + }, + get params() { + return ep.paramDef; + }, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..bdfbcba518970324153c41d497160acd1bc0aaa9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/create.ts @@ -0,0 +1,122 @@ +/* + * 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 { ApiError } from '@/server/api/error.js'; +import { + AbuseReportNotificationRecipientEntityService, +} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; +import { DI } from '@/di-symbols.js'; +import type { UserProfilesRepository } from '@/models/_.js'; + +export const meta = { + tags: ['admin', 'abuse-report', 'notification-recipient'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:abuse-report:notification-recipient', + + res: { + type: 'object', + ref: 'AbuseReportNotificationRecipient', + }, + + errors: { + correlationCheckEmail: { + message: 'If "method" is email, "userId" must be set.', + code: 'CORRELATION_CHECK_EMAIL', + id: '348bb8ae-575a-6fe9-4327-5811999def8f', + httpStatusCode: 400, + }, + correlationCheckWebhook: { + message: 'If "method" is webhook, "systemWebhookId" must be set.', + code: 'CORRELATION_CHECK_WEBHOOK', + id: 'b0c15051-de2d-29ef-260c-9585cddd701a', + httpStatusCode: 400, + }, + emailAddressNotSet: { + message: 'Email address is not set.', + code: 'EMAIL_ADDRESS_NOT_SET', + id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f', + httpStatusCode: 400, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + isActive: { + type: 'boolean', + }, + name: { + type: 'string', + minLength: 1, + maxLength: 255, + }, + method: { + type: 'string', + enum: ['email', 'webhook'], + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + systemWebhookId: { + type: 'string', + format: 'misskey:id', + }, + }, + required: [ + 'isActive', + 'name', + 'method', + ], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private abuseReportNotificationService: AbuseReportNotificationService, + private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.method === 'email') { + const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); + if (!ps.userId || !userProfile) { + throw new ApiError(meta.errors.correlationCheckEmail); + } + + if (!userProfile.email || !userProfile.emailVerified) { + throw new ApiError(meta.errors.emailAddressNotSet); + } + } + + if (ps.method === 'webhook' && !ps.systemWebhookId) { + throw new ApiError(meta.errors.correlationCheckWebhook); + } + + const userId = ps.method === 'email' ? ps.userId : null; + const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null; + const result = await this.abuseReportNotificationService.createRecipient( + { + isActive: ps.isActive, + name: ps.name, + method: ps.method, + userId: userId ?? null, + systemWebhookId: systemWebhookId ?? null, + }, + me, + ); + + return this.abuseReportNotificationRecipientEntityService.pack(result); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6dc44e09c9afcb0e32073c8c237cfc584dda0aa --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/delete.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; + +export const meta = { + tags: ['admin', 'abuse-report', 'notification-recipient'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:abuse-report:notification-recipient', +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + }, + required: [ + 'id', + ], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private abuseReportNotificationService: AbuseReportNotificationService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.abuseReportNotificationService.deleteRecipient( + ps.id, + me, + ); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..dad9161a8a74153941506ce0bd43c0c9e8379af9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/list.ts @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { + AbuseReportNotificationRecipientEntityService, +} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; + +export const meta = { + tags: ['admin', 'abuse-report', 'notification-recipient'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'read:admin:abuse-report:notification-recipient', + + res: { + type: 'array', + items: { + type: 'object', + ref: 'AbuseReportNotificationRecipient', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + method: { + type: 'array', + items: { + type: 'string', + enum: ['email', 'webhook'], + }, + }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private abuseReportNotificationService: AbuseReportNotificationService, + private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, + ) { + super(meta, paramDef, async (ps) => { + const recipients = await this.abuseReportNotificationService.fetchRecipients({ method: ps.method }); + return this.abuseReportNotificationRecipientEntityService.packMany(recipients); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts new file mode 100644 index 0000000000000000000000000000000000000000..557798f946e0dcc79cab4f710cca35f7d59a2b06 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/show.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { + AbuseReportNotificationRecipientEntityService, +} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['admin', 'abuse-report', 'notification-recipient'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'read:admin:abuse-report:notification-recipient', + + res: { + type: 'object', + ref: 'AbuseReportNotificationRecipient', + }, + + errors: { + noSuchRecipient: { + message: 'No such recipient.', + code: 'NO_SUCH_RECIPIENT', + id: '013de6a8-f757-04cb-4d73-cc2a7e3368e4', + kind: 'server', + httpStatusCode: 404, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + }, + required: ['id'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private abuseReportNotificationService: AbuseReportNotificationService, + private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, + ) { + super(meta, paramDef, async (ps) => { + const recipients = await this.abuseReportNotificationService.fetchRecipients({ ids: [ps.id] }); + if (recipients.length === 0) { + throw new ApiError(meta.errors.noSuchRecipient); + } + + return this.abuseReportNotificationRecipientEntityService.pack(recipients[0]); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd4b4852171ced48772be40c2cb0085ed7ea5657 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/abuse-report/notification-recipient/update.ts @@ -0,0 +1,128 @@ +/* + * 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 { ApiError } from '@/server/api/error.js'; +import { + AbuseReportNotificationRecipientEntityService, +} from '@/core/entities/AbuseReportNotificationRecipientEntityService.js'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; +import { DI } from '@/di-symbols.js'; +import type { UserProfilesRepository } from '@/models/_.js'; + +export const meta = { + tags: ['admin', 'abuse-report', 'notification-recipient'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:abuse-report:notification-recipient', + + res: { + type: 'object', + ref: 'AbuseReportNotificationRecipient', + }, + + errors: { + correlationCheckEmail: { + message: 'If "method" is email, "userId" must be set.', + code: 'CORRELATION_CHECK_EMAIL', + id: '348bb8ae-575a-6fe9-4327-5811999def8f', + httpStatusCode: 400, + }, + correlationCheckWebhook: { + message: 'If "method" is webhook, "systemWebhookId" must be set.', + code: 'CORRELATION_CHECK_WEBHOOK', + id: 'b0c15051-de2d-29ef-260c-9585cddd701a', + httpStatusCode: 400, + }, + emailAddressNotSet: { + message: 'Email address is not set.', + code: 'EMAIL_ADDRESS_NOT_SET', + id: '7cc1d85e-2f58-fc31-b644-3de8d0d3421f', + httpStatusCode: 400, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + isActive: { + type: 'boolean', + }, + name: { + type: 'string', + minLength: 1, + maxLength: 255, + }, + method: { + type: 'string', + enum: ['email', 'webhook'], + }, + userId: { + type: 'string', + format: 'misskey:id', + }, + systemWebhookId: { + type: 'string', + format: 'misskey:id', + }, + }, + required: [ + 'id', + 'isActive', + 'name', + 'method', + ], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + private abuseReportNotificationService: AbuseReportNotificationService, + private abuseReportNotificationRecipientEntityService: AbuseReportNotificationRecipientEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.method === 'email') { + const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); + if (!ps.userId || !userProfile) { + throw new ApiError(meta.errors.correlationCheckEmail); + } + + if (!userProfile.email || !userProfile.emailVerified) { + throw new ApiError(meta.errors.emailAddressNotSet); + } + } + + if (ps.method === 'webhook' && !ps.systemWebhookId) { + throw new ApiError(meta.errors.correlationCheckWebhook); + } + + const userId = ps.method === 'email' ? ps.userId : null; + const systemWebhookId = ps.method === 'webhook' ? ps.systemWebhookId : null; + const result = await this.abuseReportNotificationService.updateRecipient( + { + id: ps.id, + isActive: ps.isActive, + name: ps.name, + method: ps.method, + userId: userId ?? null, + systemWebhookId: systemWebhookId ?? null, + }, + me, + ); + + return this.abuseReportNotificationRecipientEntityService.pack(result); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index 4074e416b8a34072843f0e302c2f0af7438b05fa..01dea703a380dd3ebfc0e0acccfde54454f1a163 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; import { QueueService } from '@/core/QueueService.js'; -import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DeleteAccountService } from '@/core/DeleteAccountService.js'; export const meta = { tags: ['admin'], @@ -33,9 +33,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.usersRepository) private usersRepository: UsersRepository, - private userEntityService: UserEntityService, - private queueService: QueueService, - private userSuspendService: UserSuspendService, + private deleteAccoountService: DeleteAccountService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -48,22 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('cannot delete a root account'); } - if (this.userEntityService.isLocalUser(user)) { - // 物ç†å‰Šé™¤ã™ã‚‹å‰ã«Delete activityã‚’é€ä¿¡ã™ã‚‹ - await this.userSuspendService.doPostSuspend(user).catch(err => {}); - - this.queueService.createDeleteAccountJob(user, { - soft: false, - }); - } else { - this.queueService.createDeleteAccountJob(user, { - soft: true, // リモートユーザーã®å‰Šé™¤ã¯ã€å®Œå…¨ã«DBã‹ã‚‰ç‰©ç†å‰Šé™¤ã—ã¦ã—ã¾ã†ã¨å†åº¦é€£åˆã—ã¦ãã¦ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒå¾©æ´»ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ãŸã‚ã€soft指定ã™ã‚‹ - }); - } - - await this.usersRepository.update(user.id, { - isDeleted: true, - }); + await this.deleteAccoountService.deleteAccount(user); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 1e7a9fb3ecf33d80cf4580316cef18daa732cc75..955154f4fb08ad94725dab2b9655be9139dd7e71 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - const ad = await this.adsRepository.insert({ + const ad = await this.adsRepository.insertOne({ id: this.idService.gen(), expiresAt: new Date(ps.expiresAt), startsAt: new Date(ps.startsAt), @@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ratio: ps.ratio, place: ps.place, memo: ps.memo, - }).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id })); + }); this.moderationLogService.log(me, 'createAd', { adId: ad.id, diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 62358457ff769626ab1b0b944a1763c9508b6227..4e3d731acac9ca84a45ed0ae47b98bb14776795b 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -40,7 +40,7 @@ export const paramDef = { startsAt: { type: 'integer' }, dayOfWeek: { type: 'integer' }, }, - required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'], + required: ['id'], } as const; @Injectable() @@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ratio: ps.ratio, memo: ps.memo, imageUrl: ps.imageUrl, - expiresAt: new Date(ps.expiresAt), - startsAt: new Date(ps.startsAt), + expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined, + startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined, dayOfWeek: ps.dayOfWeek, }); diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 87eaad31a365ac676bfb2693d9ca9a1235645500..7596bf44e370f67f8290705e2ddaa78fbdcca75f 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -69,6 +69,7 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id', nullable: true }, + status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' }, }, required: [], } as const; @@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - query.andWhere('announcement.isActive = true'); + + if (ps.status === 'archived') { + query.andWhere('announcement.isActive = false'); + } else if (ps.status === 'active') { + query.andWhere('announcement.isActive = true'); + } + if (ps.userId) { query.andWhere('announcement.userId = :userId', { userId: ps.userId }); } else { diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts index d8341b3ad78cfaab6dc0272fe4439a8fb329a416..747c9f48d06e5bff071a53ef3279a4754fc5f9e4 100644 --- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts @@ -39,7 +39,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }); for (const file of files) { - this.driveService.deleteFile(file); + this.driveService.deleteFile(file, false, me); } }); } diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts index 459d8880fad372928a40967994bd5a72fee687b0..a7136d8c8cc829f0bff07c066fa333058f8ff1c3 100644 --- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts +++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts @@ -61,7 +61,7 @@ export const meta = { name: { type: 'string', optional: false, nullable: false, - example: 'lenna.jpg', + example: '192.jpg', }, type: { type: 'string', diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts index 0f551e1ba2c4f12b77af4fda2e003e28af7588f7..5ecae3161a3988c796b1449c8ea9281ef8b372fa 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -66,11 +66,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const ticketsPromises = []; for (let i = 0; i < ps.count; i++) { - ticketsPromises.push(this.registrationTicketsRepository.insert({ + ticketsPromises.push(this.registrationTicketsRepository.insertOne({ id: this.idService.gen(), expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null, code: generateInviteCode(), - }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]))); + })); } const tickets = await Promise.all(ticketsPromises); diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index ca4d63b834cdac68f0219936ec462c2e0c66e551..063bb6751b290f3e955ffd6ba60f581f4916375e 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -132,6 +132,16 @@ export const meta = { nullable: false, }, }, + mediaSilencedHosts: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'string', + optional: false, + nullable: false, + }, + }, pinnedUsers: { type: 'array', optional: false, nullable: false, @@ -586,6 +596,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, silencedHosts: instance.silencedHosts, + mediaSilencedHosts: instance.mediaSilencedHosts, sensitiveWords: instance.sensitiveWords, prohibitedWords: instance.prohibitedWords, preservedUsernames: instance.preservedUsernames, diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index 9694b3fa40a3efccc6e8979051df1ae9caa017e0..d7f9e4eaa30147d46414447376a804b1f46b4b63 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], @@ -53,7 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, + @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, + @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, ) { super(meta, paramDef, async (ps, me) => { const deliverJobCounts = await this.deliverQueue.getJobCounts(); diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index 8b0456068b8939da1ecb9ad663869824d9fb1d98..9b79100fcf50cc87b088cdca833921aab86c56d4 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -5,12 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, AbuseUserReportsRepository } from '@/models/_.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; -import { QueueService } from '@/core/QueueService.js'; -import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import type { AbuseUserReportsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { ApiError } from '@/server/api/error.js'; +import { AbuseReportService } from '@/core/AbuseReportService.js'; export const meta = { tags: ['admin'], @@ -18,6 +16,16 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'write:admin:resolve-abuse-user-report', + + errors: { + noSuchAbuseReport: { + message: 'No such abuse report.', + code: 'NO_SUCH_ABUSE_REPORT', + id: 'ac3794dd-2ce4-d878-e546-73c60c06b398', + kind: 'server', + httpStatusCode: 404, + }, + }, } as const; export const paramDef = { @@ -29,47 +37,20 @@ export const paramDef = { required: ['reportId'], } as const; -// TODO: ãƒã‚¸ãƒƒã‚¯ã‚’サービスã«åˆ‡ã‚Šå‡ºã™ - @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.abuseUserReportsRepository) private abuseUserReportsRepository: AbuseUserReportsRepository, - - private queueService: QueueService, - private instanceActorService: InstanceActorService, - private apRendererService: ApRendererService, - private moderationLogService: ModerationLogService, + private abuseReportService: AbuseReportService, ) { super(meta, paramDef, async (ps, me) => { const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId }); - - if (report == null) { - throw new Error('report not found'); + if (!report) { + throw new ApiError(meta.errors.noSuchAbuseReport); } - if (ps.forward && report.targetUserHost != null) { - const actor = await this.instanceActorService.getInstanceActor(); - const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId }); - - this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false); - } - - await this.abuseUserReportsRepository.update(report.id, { - resolved: true, - assigneeId: me.id, - forwarded: ps.forward && report.targetUserHost != null, - }); - - this.moderationLogService.log(me, 'resolveAbuseReport', { - reportId: report.id, - report: report, - forwarded: ps.forward && report.targetUserHost != null, - }); + await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts index d7209965dba1d91d356be095292b74e254887146..5cf49670be5ace2de76646a306a83d6e5fb5f841 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-policies.ts @@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { MetaService } from '@/core/MetaService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin', 'role'], @@ -33,12 +34,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( private metaService: MetaService, private globalEventService: GlobalEventService, + private moderationLogService: ModerationLogService, ) { - super(meta, paramDef, async (ps) => { + super(meta, paramDef, async (ps, me) => { + const before = await this.metaService.fetch(true); + await this.metaService.update({ policies: ps.policies, }); - this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies); + + const after = await this.metaService.fetch(true); + + this.globalEventService.publishInternalEvent('policiesUpdated', after.policies); + this.moderationLogService.log(me, 'updateServerSettings', { + before: before.policies, + after: after.policies, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 5242e0be2f96154d18010caa2f388824f773bfc9..465ad7aaafbf9a2464c7051f542cb50bc0af5ab3 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -6,7 +6,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { RolesRepository } from '@/models/_.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -50,19 +49,6 @@ export const paramDef = { }, required: [ 'roleId', - 'name', - 'description', - 'color', - 'iconUrl', - 'target', - 'condFormula', - 'isPublic', - 'isModerator', - 'isAdministrator', - 'asBadge', - 'canEditMembersByModerator', - 'displayOrder', - 'policies', ], } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts index 8a946405cc0c299f495a2b6af09ed194ab797a1e..bea1bdc4edb8bce4023c084ed9926b3f8ba07673 100644 --- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts @@ -3,18 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsNull, Not } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; -import type { MiUser } from '@/models/User.js'; -import type { RelationshipJobData } from '@/queue/types.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; +import type { UsersRepository } from '@/models/_.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { QueueService } from '@/core/QueueService.js'; export const meta = { tags: ['admin'], @@ -38,13 +32,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private userSuspendService: UserSuspendService, private roleService: RoleService, - private moderationLogService: ModerationLogService, - private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -57,42 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('cannot suspend moderator account'); } - await this.usersRepository.update(user.id, { - isSuspended: true, - }); - - this.moderationLogService.log(me, 'suspend', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - }); - - (async () => { - await this.userSuspendService.doPostSuspend(user).catch(e => {}); - await this.unFollowAll(user).catch(e => {}); - })(); - }); - } - - @bindThis - private async unFollowAll(follower: MiUser) { - const followings = await this.followingsRepository.find({ - where: { - followerId: follower.id, - followeeId: Not(IsNull()), - }, + await this.userSuspendService.suspend(user, me); }); - - const jobs: RelationshipJobData[] = []; - for (const following of followings) { - if (following.followeeId && following.followerId) { - jobs.push({ - from: { id: following.followerId }, - to: { id: following.followeeId }, - silent: true, - }); - } - } - this.queueService.createUnfollowJob(jobs); } } diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..28071e7a33824f671612ee011769a3c21aba6f20 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/create.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; +import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; + +export const meta = { + tags: ['admin', 'system-webhook'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:system-webhook', + + res: { + type: 'object', + ref: 'SystemWebhook', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + isActive: { + type: 'boolean', + }, + name: { + type: 'string', + minLength: 1, + maxLength: 255, + }, + on: { + type: 'array', + items: { + type: 'string', + enum: systemWebhookEventTypes, + }, + }, + url: { + type: 'string', + minLength: 1, + maxLength: 1024, + }, + secret: { + type: 'string', + minLength: 1, + maxLength: 1024, + }, + }, + required: [ + 'isActive', + 'name', + 'on', + 'url', + 'secret', + ], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private systemWebhookService: SystemWebhookService, + private systemWebhookEntityService: SystemWebhookEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const result = await this.systemWebhookService.createSystemWebhook( + { + isActive: ps.isActive, + name: ps.name, + on: ps.on, + url: ps.url, + secret: ps.secret, + }, + me, + ); + + return this.systemWebhookEntityService.pack(result); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts new file mode 100644 index 0000000000000000000000000000000000000000..9cdfc7e70fec21e2939cd9b3ba64786154dfe238 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/delete.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; + +export const meta = { + tags: ['admin', 'system-webhook'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:system-webhook', +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + }, + required: [ + 'id', + ], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private systemWebhookService: SystemWebhookService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.systemWebhookService.deleteSystemWebhook( + ps.id, + me, + ); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a440a774ed35daccf863e653a05d115403e8746 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/list.ts @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; +import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; + +export const meta = { + tags: ['admin', 'system-webhook'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:system-webhook', + + res: { + type: 'array', + items: { + type: 'object', + ref: 'SystemWebhook', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + isActive: { + type: 'boolean', + }, + on: { + type: 'array', + items: { + type: 'string', + enum: systemWebhookEventTypes, + }, + }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private systemWebhookService: SystemWebhookService, + private systemWebhookEntityService: SystemWebhookEntityService, + ) { + super(meta, paramDef, async (ps) => { + const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ + isActive: ps.isActive, + on: ps.on, + }); + return this.systemWebhookEntityService.packMany(webhooks); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts new file mode 100644 index 0000000000000000000000000000000000000000..75862c96a7d123e791a3e000aef6aa23de5aef78 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/show.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; +import { ApiError } from '@/server/api/error.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; + +export const meta = { + tags: ['admin', 'system-webhook'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:system-webhook', + + res: { + type: 'object', + ref: 'SystemWebhook', + }, + + errors: { + noSuchSystemWebhook: { + message: 'No such SystemWebhook.', + code: 'NO_SUCH_SYSTEM_WEBHOOK', + id: '38dd1ffe-04b4-6ff5-d8ba-4e6a6ae22c9d', + kind: 'server', + httpStatusCode: 404, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + }, + required: ['id'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private systemWebhookService: SystemWebhookService, + private systemWebhookEntityService: SystemWebhookEntityService, + ) { + super(meta, paramDef, async (ps) => { + const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [ps.id] }); + if (webhooks.length === 0) { + throw new ApiError(meta.errors.noSuchSystemWebhook); + } + + return this.systemWebhookEntityService.pack(webhooks[0]); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d68bb8f87d8598c4adf0b0608c80419c04ef600 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/update.ts @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { SystemWebhookEntityService } from '@/core/entities/SystemWebhookEntityService.js'; +import { systemWebhookEventTypes } from '@/models/SystemWebhook.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; + +export const meta = { + tags: ['admin', 'system-webhook'], + + requireCredential: true, + requireModerator: true, + secure: true, + kind: 'write:admin:system-webhook', + + res: { + type: 'object', + ref: 'SystemWebhook', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + id: { + type: 'string', + format: 'misskey:id', + }, + isActive: { + type: 'boolean', + }, + name: { + type: 'string', + minLength: 1, + maxLength: 255, + }, + on: { + type: 'array', + items: { + type: 'string', + enum: systemWebhookEventTypes, + }, + }, + url: { + type: 'string', + minLength: 1, + maxLength: 1024, + }, + secret: { + type: 'string', + minLength: 1, + maxLength: 1024, + }, + }, + required: [ + 'id', + 'isActive', + 'name', + 'on', + 'url', + 'secret', + ], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private systemWebhookService: SystemWebhookService, + private systemWebhookEntityService: SystemWebhookEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const result = await this.systemWebhookService.updateSystemWebhook( + { + id: ps.id, + isActive: ps.isActive, + name: ps.name, + on: ps.on, + url: ps.url, + secret: ps.secret, + }, + me, + ); + + return this.systemWebhookEntityService.pack(result); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts index 2c2b1bf6f55acf5303ee836135955f2666912bce..b52c638cdb6ff723fc38843c4c3c4fee0fb6e01d 100644 --- a/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/unsuspend-user.ts @@ -6,7 +6,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js'; import { DI } from '@/di-symbols.js'; @@ -33,7 +32,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private usersRepository: UsersRepository, private userSuspendService: UserSuspendService, - private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); @@ -42,17 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new Error('user not found'); } - await this.usersRepository.update(user.id, { - isSuspended: false, - }); - - this.moderationLogService.log(me, 'unsuspend', { - userId: user.id, - userUsername: user.username, - userHost: user.host, - }); - - this.userSuspendService.doPostUnsuspend(user); + await this.userSuspendService.unsuspend(user, me); }); } } 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 015a1e1f7c11b1a9778d506e7196c5c01965ee83..6bda1ae6adb08e3f344df40e1bc2041b09080d36 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -158,6 +158,13 @@ export const paramDef = { type: 'string', }, }, + mediaSilencedHosts: { + type: 'array', + nullable: true, + items: { + type: 'string', + }, + }, summalyProxy: { type: 'string', nullable: true, description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', @@ -211,6 +218,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return h !== '' && h !== lv && !set.blockedHosts?.includes(h); }); } + if (Array.isArray(ps.mediaSilencedHosts)) { + let lastValue = ''; + set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => { + const lv = lastValue; + lastValue = h; + return h !== '' && h !== lv && !set.blockedHosts?.includes(h); + }); + } if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 6b7bacb0549925a2288a4da13ecd9cdfcc4f61ce..577b9e1b1f8c347d342671c708b1b9869686f429 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id, }); - if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } @@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const now = new Date(); - const antenna = await this.antennasRepository.insert({ + const antenna = await this.antennasRepository.insertOne({ id: this.idService.gen(now.getTime()), lastUsedAt: now, userId: me.id, @@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); + }); this.globalEventService.publishInternalEvent('antennaCreated', antenna); diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts index 492705d6f9215000469d548aac20dbdabca32b79..ba847fc4f0ee343feb9879bfa3f2336c83853c22 100644 --- a/packages/backend/src/server/api/endpoints/app/create.ts +++ b/packages/backend/src/server/api/endpoints/app/create.ts @@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1'))); // Create account - const app = await this.appsRepository.insert({ + const app = await this.appsRepository.insertOne({ id: this.idService.gen(), userId: me ? me.id : null, name: ps.name, @@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- permission, callbackUrl: ps.callbackUrl, secret: secret, - }).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0])); + }); return await this.appEntityService.pack(app, null, { detail: true, diff --git a/packages/backend/src/server/api/endpoints/auth/session/generate.ts b/packages/backend/src/server/api/endpoints/auth/session/generate.ts index 26dd8931385e4651744159df72f0ed01cca4ba96..f8ddfdb75cbd4bf22904102f10bec38e93a8806c 100644 --- a/packages/backend/src/server/api/endpoints/auth/session/generate.ts +++ b/packages/backend/src/server/api/endpoints/auth/session/generate.ts @@ -78,11 +78,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const token = randomUUID(); // Create session token document - const doc = await this.authSessionsRepository.insert({ + const doc = await this.authSessionsRepository.insertOne({ id: this.idService.gen(), appId: app.id, token: token, - }).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0])); + }); return { token: doc.token, diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts index 506621574969dc3d0e3952b481562de641490d88..fd6c164449f6216ae9794332755353ec64b2f13e 100644 --- a/packages/backend/src/server/api/endpoints/blocking/create.ts +++ b/packages/backend/src/server/api/endpoints/blocking/create.ts @@ -6,9 +6,10 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; +import type { UsersRepository, BlockingsRepository, MutingsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { UserMutingService } from '@/core/UserMutingService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -69,9 +70,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + private userEntityService: UserEntityService, private getterService: GetterService, private userBlockingService: UserBlockingService, + private userMutingService: UserMutingService, ) { super(meta, paramDef, async (ps, me) => { const blocker = await this.usersRepository.findOneByOrFail({ id: me.id }); @@ -99,7 +104,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.alreadyBlocking); } - await this.userBlockingService.block(blocker, blockee); + await Promise.all([ + this.userBlockingService.block(blocker, blockee), + this.mutingsRepository.exists({ + where: { + muterId: blocker.id, + muteeId: blockee.id, + }, + }).then(exists => { + if (!exists) { + this.userMutingService.mute(blocker, blockee, null); + } + }), + ]); return await this.userEntityService.pack(blockee.id, blocker, { schema: 'UserDetailedNotMe', diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts index cebb30733848381df97a87eea837bf58856dd9a4..a4cd1b1cde3adc51c0a6b3ae6f8fda012c83b7c0 100644 --- a/packages/backend/src/server/api/endpoints/blocking/delete.ts +++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts @@ -6,9 +6,10 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UsersRepository, BlockingsRepository } from '@/models/_.js'; +import type { UsersRepository, BlockingsRepository, MutingsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; +import { UserMutingService } from '@/core/UserMutingService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -69,9 +70,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + private userEntityService: UserEntityService, private getterService: GetterService, private userBlockingService: UserBlockingService, + private userMutingService: UserMutingService, ) { super(meta, paramDef, async (ps, me) => { const blocker = await this.usersRepository.findOneByOrFail({ id: me.id }); @@ -100,7 +105,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } // Delete blocking - await this.userBlockingService.unblock(blocker, blockee); + await Promise.all([ + this.userBlockingService.unblock(blocker, blockee), + this.mutingsRepository.findOneBy({ + muterId: blocker.id, + muteeId: blockee.id, + }).then(exists => { + if (exists) { + this.userMutingService.unmute([exists]); + } + }), + ]); return await this.userEntityService.pack(blockee.id, blocker, { schema: 'UserDetailedNotMe', diff --git a/packages/backend/src/server/api/endpoints/channels/create.ts b/packages/backend/src/server/api/endpoints/channels/create.ts index 2866db54240c2ab9dd52f33e528c5539360c4ec9..e3a6d2d670e69bfa0302d7a844e3fd601542f688 100644 --- a/packages/backend/src/server/api/endpoints/channels/create.ts +++ b/packages/backend/src/server/api/endpoints/channels/create.ts @@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - const channel = await this.channelsRepository.insert({ + const channel = await this.channelsRepository.insertOne({ id: this.idService.gen(), userId: me.id, name: ps.name, @@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- isSensitive: ps.isSensitive ?? false, ...(ps.color !== undefined ? { color: ps.color } : {}), allowRenoteToExternal: ps.allowRenoteToExternal ?? true, - } as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0])); + } as MiChannel); return await this.channelEntityService.pack(channel, me); }); diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 3b44ba81b3e66f63f0fa6a3a3f2c631ce3a0fb2c..603a3ccf3dc5860afd0f6a9548063a8508233948 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -3,7 +3,7 @@ * 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 { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipService } from '@/core/ClipService.js'; @@ -41,7 +41,7 @@ export const paramDef = { isPublic: { type: 'boolean' }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, }, - required: ['clipId', 'name'], + required: ['clipId'], } as const; @Injectable() diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index 10c521332d1adb3935ee0d69d2fcb08b14867297..d615036ce80d73ddf1f2fdbbe88b18058f7cb6d9 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -9,6 +9,8 @@ import type { DriveFilesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['drive'], @@ -37,6 +39,7 @@ export const paramDef = { folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] }, + searchQuery: { type: 'string', default: '' } }, required: [], } as const; @@ -60,6 +63,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- query.andWhere('file.folderId IS NULL'); } + if (ps.searchQuery.length > 0) { + const args = { searchQuery: `%${sqlLikeEscape(ps.searchQuery)}%` }; + query.andWhere(new Brackets((qb) => { + qb + .where('file.name ILIKE :searchQuery', args) + .orWhere('file.comment ILIKE :searchQuery', args); + })); + } + if (ps.type) { if (ps.type.endsWith('/*')) { query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); diff --git a/packages/backend/src/server/api/endpoints/drive/folders.ts b/packages/backend/src/server/api/endpoints/drive/folders.ts index 8c4848f8e15e4159c80713007fe673da0a1aad9d..9bcd824882fc33839fcd0130288b209eaf18fbc3 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders.ts @@ -9,6 +9,7 @@ import type { DriveFoldersRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['drive'], @@ -35,6 +36,7 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, + searchQuery: { type: 'string', default: '' } }, required: [], } as const; @@ -58,6 +60,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- query.andWhere('folder.parentId IS NULL'); } + if (ps.searchQuery.length > 0) { + query.andWhere('folder.name ILIKE :searchQuery', { searchQuery: `%${sqlLikeEscape(ps.searchQuery)}%` }); + } const folders = await query.limit(ps.limit).getMany(); return await Promise.all(folders.map(folder => this.driveFolderEntityService.pack(folder))); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/create.ts b/packages/backend/src/server/api/endpoints/drive/folders/create.ts index c94070d9ff099f96a223224723b2e30da26bfef4..08d9d9cdc3404b43fe43a047edb63f4ba4015bd1 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/create.ts @@ -75,12 +75,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } // Create folder - const folder = await this.driveFoldersRepository.insert({ + const folder = await this.driveFoldersRepository.insertOne({ id: this.idService.gen(), name: ps.name, parentId: parent !== null ? parent.id : null, userId: me.id, - }).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0])); + }); const folderObj = await this.driveFolderEntityService.pack(folder); diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 52b8b335b50118adc7bf9caebecfea6815052640..62b04e1df36dc73ffa467e1c926acea00126947a 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -95,15 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // Check if the circular reference will occur const checkCircle = async (folderId: string): Promise<boolean> => { - // Fetch folder - const folder2 = await this.driveFoldersRepository.findOneBy({ + const folder2 = await this.driveFoldersRepository.findOneByOrFail({ id: folderId, }); - if (folder2!.id === folder!.id) { + if (folder2.id === folder.id) { return true; - } else if (folder2!.parentId) { - return await checkCircle(folder2!.parentId); + } else if (folder2.parentId) { + return await checkCircle(folder2.parentId); } else { return false; } diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index c3f2247b69591c71aa264d40ac26d27a01c52ea3..c1ce3f2238c5d9c8fe32798c79381a35ab88f6ae 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -203,7 +203,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const instances = await query.limit(ps.limit).offset(ps.offset).getMany(); - return await this.instanceEntityService.packMany(instances); + return await this.instanceEntityService.packMany(instances, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index bac54970ab853c69993670c9e5932870c7304b94..69900bff9acae94cb7cca44800b923dfa4b1eba5 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -107,9 +107,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); return await awaitAll({ - topSubInstances: this.instanceEntityService.packMany(topSubInstances), + topSubInstances: this.instanceEntityService.packMany(topSubInstances, me), otherFollowersCount: Math.max(0, allSubCount - gotSubCount), - topPubInstances: this.instanceEntityService.packMany(topPubInstances), + topPubInstances: this.instanceEntityService.packMany(topPubInstances, me), otherFollowingCount: Math.max(0, allPubCount - gotPubCount), }); }); diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 361496e17e12162fbbd9000d165ef84323036c89..64f13a577e3579710e063b8caf1a4102778f240e 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { - const flash = await this.flashsRepository.insert({ + const flash = await this.flashsRepository.insertOne({ id: this.idService.gen(), userId: me.id, updatedAt: new Date(), @@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- script: ps.script, permissions: ps.permissions, visibility: ps.visibility, - }).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0])); + }); return await this.flashEntityService.pack(flash); }); diff --git a/packages/backend/src/server/api/endpoints/flash/delete.ts b/packages/backend/src/server/api/endpoints/flash/delete.ts index d3d47e5deb93b2fa5e0e19e36fddc740d598e3c2..6912450abf7ee65fb4b303f31a6cf04e604b3ae8 100644 --- a/packages/backend/src/server/api/endpoints/flash/delete.ts +++ b/packages/backend/src/server/api/endpoints/flash/delete.ts @@ -4,9 +4,11 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { FlashsRepository } from '@/models/_.js'; +import type { FlashsRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.flashsRepository) private flashsRepository: FlashsRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private moderationLogService: ModerationLogService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const flash = await this.flashsRepository.findOneBy({ id: ps.flashId }); + if (flash == null) { throw new ApiError(meta.errors.noSuchFlash); } - if (flash.userId !== me.id) { + + if (!await this.roleService.isModerator(me) && flash.userId !== me.id) { throw new ApiError(meta.errors.accessDenied); } await this.flashsRepository.delete(flash.id); + + if (flash.userId !== me.id) { + const user = await this.usersRepository.findOneByOrFail({ id: flash.userId }); + this.moderationLogService.log(me, 'deleteFlash', { + flashId: flash.id, + flashUserId: flash.userId, + flashUserUsername: user.username, + flash, + }); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts index b07cdf1ed979cc503cdfede79664a84836ec39b5..504a9c789e8d4268e17c18b68bac36a08c34bf73 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/create.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/create.ts @@ -12,7 +12,6 @@ import type { MiDriveFile } from '@/models/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; -import { isNotNull } from '@/misc/is-not-null.js'; export const meta = { tags: ['gallery'], @@ -70,13 +69,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- id: fileId, userId: me.id, }), - ))).filter(isNotNull); + ))).filter(x => x != null); if (files.length === 0) { throw new Error(); } - const post = await this.galleryPostsRepository.insert(new MiGalleryPost({ + const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({ id: this.idService.gen(), updatedAt: new Date(), title: ps.title, @@ -84,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- userId: me.id, isSensitive: ps.isSensitive, fileIds: files.map(file => file.id), - })).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0])); + })); return await this.galleryPostEntityService.pack(post, me); }); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts index 527e3fb52ddc03e40e37f1db3a808af6ec9f5dde..b6b94db1613e8093f8e0065ee4b41fc547ead85e 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/delete.ts @@ -5,8 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { GalleryPostsRepository } from '@/models/_.js'; +import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -22,6 +24,12 @@ export const meta = { code: 'NO_SUCH_POST', id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5', }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496', + }, }, } as const; @@ -38,18 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.galleryPostsRepository) private galleryPostsRepository: GalleryPostsRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private moderationLogService: ModerationLogService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { - const post = await this.galleryPostsRepository.findOneBy({ - id: ps.postId, - userId: me.id, - }); + const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId }); if (post == null) { throw new ApiError(meta.errors.noSuchPost); } + if (!await this.roleService.isModerator(me) && post.userId !== me.id) { + throw new ApiError(meta.errors.accessDenied); + } + await this.galleryPostsRepository.delete(post.id); + + if (post.userId !== me.id) { + const user = await this.usersRepository.findOneByOrFail({ id: post.userId }); + this.moderationLogService.log(me, 'deleteGalleryPost', { + postId: post.id, + postUserId: post.userId, + postUserUsername: user.username, + post, + }); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 8bd83ff5ba0af3a4f154aaa99a312b89207b7426..5243ee9603142376795469697a266cb7993ee390 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -10,7 +10,6 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js import type { MiDriveFile } from '@/models/DriveFile.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { DI } from '@/di-symbols.js'; -import { isNotNull } from '@/misc/is-not-null.js'; export const meta = { tags: ['gallery'], @@ -48,7 +47,7 @@ export const paramDef = { } }, isSensitive: { type: 'boolean', default: false }, }, - required: ['postId', 'title', 'fileIds'], + required: ['postId'], } as const; @Injectable() @@ -63,15 +62,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private galleryPostEntityService: GalleryPostEntityService, ) { super(meta, paramDef, async (ps, me) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - this.driveFilesRepository.findOneBy({ - id: fileId, - userId: me.id, - }), - ))).filter(isNotNull); - - if (files.length === 0) { - throw new Error(); + let files: Array<MiDriveFile> | undefined; + + if (ps.fileIds) { + files = (await Promise.all(ps.fileIds.map(fileId => + this.driveFilesRepository.findOneBy({ + id: fileId, + userId: me.id, + }), + ))).filter(x => x != null); + + if (files.length === 0) { + throw new Error(); + } } await this.galleryPostsRepository.update({ @@ -82,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- title: ps.title, description: ps.description, isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id), + fileIds: files ? files.map(file => file.id) : undefined, }); const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index a4e61a0e8f2fab268414ad221c475b5789ceb922..084d4af658178e57e0ad24c85e25d834f3e3fa3a 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -14,12 +14,19 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model import { WebAuthnService } from '@/core/WebAuthnService.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { incorrectPassword: { message: 'Incorrect password.', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index cc6e9ee42d242ffb144c0e4478731c38ecc10d43..6ab50a57c93fbcd0f8633fd0759f89fb3c0d9039 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -12,12 +12,19 @@ import { DI } from '@/di-symbols.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { userNotFound: { message: 'User not found.', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 7283159f874e55f4c4870a25c4f7736831cba42c..888d0fc6efe2ca341710718f03a362dfae11d705 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -14,12 +14,19 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { incorrectPassword: { message: 'Incorrect password.', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 098fd5930328218462c7c8649afd2b3eb72ebb55..614fd0c498b86073360b1f53ed613c646e314289 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -13,10 +13,17 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, errors: { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 8da331505b82428ea8daa9568826e48bc0080d9a..27738253734fb4b67678a290401a3c2219dd3af8 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -13,12 +13,19 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { incorrectPassword: { message: 'Incorrect password.', diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index 6aedde717c93f65000c2b4ee53690290bd63e479..f131c7e9d12f7c0b4c46b501bea72376a6c06259 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -10,10 +10,17 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index af4d601ad6a86d9e298234a41f47a2c4d8d2d762..565eaaafc01e3d870d498f6e601e09bb5f6fa0b4 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -11,10 +11,17 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, } as const; 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 b4661a93e2b1859e57dc76b168a21af5a0c867ee..bc46163e3d27f6198fa91e4959eb58ce53f0a01a 100644 --- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -78,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (file.size === 0) throw new ApiError(meta.errors.emptyFile); const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); - if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } this.queueService.createImportAntennasJob(me, antennas); diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index e1cdfdc18548b8abdfe701dfa3a0f76182bc8406..814ffb5488bd98041a0b552e3c8207668bef4227 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -11,10 +11,17 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index aa2f85845f0f80293b58c31bfa19c7076fcf459b..6cc22e79948da1c2daf0dd8ecac664a901e6d99b 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; +import { RolePolicies, RoleService } from '@/core/RoleService.js'; import { CacheService } from '@/core/CacheService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; @@ -272,8 +272,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const profileUpdates = {} as Partial<MiUserProfile>; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - - if (ps.name !== undefined) updates.name = ps.name; + let policies: RolePolicies | null = null; + + if (ps.name !== undefined) { + if (ps.name === null) { + updates.name = null; + } else { + const trimmedName = ps.name.trim(); + updates.name = trimmedName === '' ? null : trimmedName; + } + } if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; @@ -306,14 +314,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } if (ps.mutedWords !== undefined) { - checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + policies ??= await this.roleService.getUserPolicies(user.id); + checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit); validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } if (ps.hardMutedWords !== undefined) { - checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + policies ??= await this.roleService.getUserPolicies(user.id); + checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit); validateMuteWordRegex(ps.hardMutedWords); profileUpdates.hardMutedWords = ps.hardMutedWords; } @@ -333,12 +343,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { - if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); + policies ??= await this.roleService.getUserPolicies(user.id); + if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; } if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.avatarId) { + policies ??= await this.roleService.getUserPolicies(user.id); + if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); + const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -354,6 +368,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } if (ps.bannerId) { + policies ??= await this.roleService.getUserPolicies(user.id); + if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); + const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -384,14 +401,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } if (ps.avatarDecorations) { + policies ??= await this.roleService.getUserPolicies(user.id); const decorations = await this.avatarDecorationService.getAll(true); - const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); + const myRoles = await this.roleService.getUserRoles(user.id); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); - if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, 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 535a3ea308500c355975d1579214468af735386e..9eb7f5b3a0333f1eab499d8e64fa38d5c5aba40b 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -85,18 +85,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const currentWebhooksCount = await this.webhooksRepository.countBy({ userId: me.id, }); - if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) { + if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) { throw new ApiError(meta.errors.tooManyWebhooks); } - const webhook = await this.webhooksRepository.insert({ + const webhook = await this.webhooksRepository.insertOne({ id: this.idService.gen(), userId: me.id, name: ps.name, url: ps.url, secret: ps.secret, on: ps.on, - }).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0])); + }); this.globalEventService.publishInternalEvent('webhookCreated', webhook); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 6e380d76f89e55cc671184e34e5bf50daf048666..07a25bd82aad612a29b100e14f2eb2ff61ec202d 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -34,13 +34,13 @@ export const paramDef = { webhookId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, url: { type: 'string', minLength: 1, maxLength: 1024 }, - secret: { type: 'string', maxLength: 1024, default: '' }, + secret: { type: 'string', nullable: true, maxLength: 1024 }, on: { type: 'array', items: { type: 'string', enum: webhookEventTypes, } }, active: { type: 'boolean' }, }, - required: ['webhookId', 'name', 'url', 'on', 'active'], + required: ['webhookId'], } as const; // TODO: ãƒã‚¸ãƒƒã‚¯ã‚’サービスã«åˆ‡ã‚Šå‡ºã™ @@ -66,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- await this.webhooksRepository.update(webhook.id, { name: ps.name, url: ps.url, - secret: ps.secret, + secret: ps.secret === null ? '' : ps.secret, on: ps.on, active: ps.active, }); diff --git a/packages/backend/src/server/api/endpoints/invite/create.ts b/packages/backend/src/server/api/endpoints/invite/create.ts index 0ff125ad9cbe392cbb257114bc9256b0fa4db275..a70b587da7e5be61eeaea7e6a031d6ddaa0f0526 100644 --- a/packages/backend/src/server/api/endpoints/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/invite/create.ts @@ -66,13 +66,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - const ticket = await this.registrationTicketsRepository.insert({ + const ticket = await this.registrationTicketsRepository.insertOne({ id: this.idService.gen(), createdBy: me, createdById: me.id, expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null, code: generateInviteCode(), - }).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])); + }); return await this.inviteCodeEntityService.pack(ticket, me); }); 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 ce5ddadb9e2d71183907e1e8e566f6507ef0d06d..fdc9a779564ba67ac5fd47e41d23aee91b1ffd6f 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -141,9 +141,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- timelineConfig = [ `homeTimeline:${me.id}`, 'localTimeline', + `localTimelineWithReplyTo:${me.id}`, ]; } + const [ + followings, + ] = await Promise.all([ + this.cacheService.userFollowingsCache.fetch(me.id), + ]); + const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ untilId, sinceId, @@ -155,6 +162,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, excludeBots: !ps.withBots, + noteFilter: note => { + if (note.reply && note.reply.visibility === 'followers') { + if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; + } + + return true; + }, dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ untilId, sinceId, diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index a91c506afd2721e9e501969b070e40ac91c694a2..f33f49075bc0a11b58f1b9164e558a25c878fc86 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -144,12 +144,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } // Create vote - const vote = await this.pollVotesRepository.insert({ + const vote = await this.pollVotesRepository.insertOne({ id: this.idService.gen(createdAt.getTime()), noteId: note.id, userId: me.id, choice: ps.choice, - }).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0])); + }); // Increment votes count const index = ps.choice + 1; // In SQL, array index is 1 based diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index b9899608bf16238b977f464f8526d9921dda97f0..0f0dcca605abd61f9bf4adac60b561c9a8817b63 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -36,6 +36,12 @@ export const meta = { code: 'YOU_HAVE_BEEN_BLOCKED', id: '20ef5475-9f38-4e4c-bd33-de6d979498ec', }, + + cannotReactToRenote: { + message: 'You cannot react to Renote.', + code: 'CANNOT_REACT_TO_RENOTE', + id: 'eaccdc08-ddef-43fe-908f-d108faad57f5', + }, }, } as const; @@ -62,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- await this.reactionService.create(me, note, ps.reaction).catch(err => { if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); 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/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 1e5869663fbbd36c22ea2d85a623d5b479cfeda7..1a14703e6eb2647d54e568b20465b87830012454 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- excludePureRenotes: !ps.withRenotes, noteFilter: note => { if (note.reply && note.reply.visibility === 'followers') { - if (!Object.hasOwn(followings, note.reply.userId)) return false; + if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; } if (!ps.withBots && note.user?.isBot) return false; diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index 3a02d359f80bac522dd0eae953d8406f6c331460..fa03b0b4575f74412d6143474a121ffe90d6ff1a 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } }); - const page = await this.pagesRepository.insert(new MiPage({ + const page = await this.pagesRepository.insertOne(new MiPage({ id: this.idService.gen(), updatedAt: new Date(), title: ps.title, @@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- alignCenter: ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned, font: ps.font, - })).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0])); + })); return await this.pageEntityService.pack(page); }); diff --git a/packages/backend/src/server/api/endpoints/pages/delete.ts b/packages/backend/src/server/api/endpoints/pages/delete.ts index aa2ba75a4162e3fbabbb6e2dd651d2aa5b7f7ee9..f2bc946788fd80a7ed83043b32a007f455665ef5 100644 --- a/packages/backend/src/server/api/endpoints/pages/delete.ts +++ b/packages/backend/src/server/api/endpoints/pages/delete.ts @@ -4,9 +4,11 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository } from '@/models/_.js'; +import type { PagesRepository, UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private moderationLogService: ModerationLogService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const page = await this.pagesRepository.findOneBy({ id: ps.pageId }); + if (page == null) { throw new ApiError(meta.errors.noSuchPage); } - if (page.userId !== me.id) { + + if (!await this.roleService.isModerator(me) && page.userId !== me.id) { throw new ApiError(meta.errors.accessDenied); } await this.pagesRepository.delete(page.id); + + if (page.userId !== me.id) { + const user = await this.usersRepository.findOneByOrFail({ id: page.userId }); + this.moderationLogService.log(me, 'deletePage', { + pageId: page.id, + pageUserId: page.userId, + pageUserUsername: user.username, + page, + }); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index b8e5e70a25a4fa5b1146dffbfca04d400c792e71..f11bbbcb1a475c02c721bde041dc6f78070fe04f 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -70,7 +70,7 @@ export const paramDef = { alignCenter: { type: 'boolean' }, hideTitleWhenPinned: { type: 'boolean' }, }, - required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], + required: ['pageId'], } as const; @Injectable() @@ -91,9 +91,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.accessDenied); } - let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await this.driveFilesRepository.findOneBy({ + const eyeCatchingImage = await this.driveFilesRepository.findOneBy({ id: ps.eyeCatchingImageId, userId: me.id, }); @@ -116,23 +115,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- await this.pagesRepository.update(page.id, { updatedAt: new Date(), title: ps.title, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - name: ps.name === undefined ? page.name : ps.name, + name: ps.name, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, script: ps.script, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - font: ps.font === undefined ? page.font : ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId === null - ? null - : ps.eyeCatchingImageId === undefined - ? page.eyeCatchingImageId - : eyeCatchingImage!.id, + alignCenter: ps.alignCenter, + hideTitleWhenPinned: ps.hideTitleWhenPinned, + font: ps.font, + eyeCatchingImageId: ps.eyeCatchingImageId, }); }); } diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts index 784766bcb5510fab6c672e3e861f33dc30af573a..15832ef7f8d7b3b120ea8b57f8288efad44fa838 100644 --- a/packages/backend/src/server/api/endpoints/pinned-users.ts +++ b/packages/backend/src/server/api/endpoints/pinned-users.ts @@ -12,7 +12,6 @@ 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'; -import { isNotNull } from '@/misc/is-not-null.js'; export const meta = { tags: ['users'], @@ -53,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- host: acct.host ?? IsNull(), }))); - return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' }); + return await this.userEntityService.packMany(users.filter(x => x != null), me, { schema: 'UserDetailed' }); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 39bf0cc428b5a1ff6fd52090e9bbf4ed50f3dbbf..84a1f010d437324fd5dc1bb3727d6fc1a59a3531 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -6,12 +6,11 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { IdService } from '@/core/IdService.js'; -import type { RenoteMutingsRepository } from '@/models/_.js'; -import type { MiRenoteMuting } from '@/models/RenoteMuting.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js"; +import type { RenoteMutingsRepository } from '@/models/_.js'; export const meta = { tags: ['account'], @@ -62,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private renoteMutingsRepository: RenoteMutingsRepository, private getterService: GetterService, - private idService: IdService, + private userRenoteMutingService: UserRenoteMutingService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -79,21 +78,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }); // Check if already muting - const exist = await this.renoteMutingsRepository.findOneBy({ - muterId: muter.id, - muteeId: mutee.id, + const exist = await this.renoteMutingsRepository.exists({ + where: { + muterId: muter.id, + muteeId: mutee.id, + }, }); - if (exist != null) { + if (exist === true) { throw new ApiError(meta.errors.alreadyMuting); } // Create mute - await this.renoteMutingsRepository.insert({ - id: this.idService.gen(), - muterId: muter.id, - muteeId: mutee.id, - } as MiRenoteMuting); + await this.userRenoteMutingService.mute(muter, mutee); }); } } diff --git a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts index 6e037cc07ef0aa2a50e2c123e16bb3bb10092477..1a584b8404801228dad350759922a6a30656ebad 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/delete.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/delete.ts @@ -5,10 +5,11 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RenoteMutingsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; +import { UserRenoteMutingService } from "@/core/UserRenoteMutingService.js"; +import type { RenoteMutingsRepository } from '@/models/_.js'; export const meta = { tags: ['account'], @@ -53,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private renoteMutingsRepository: RenoteMutingsRepository, private getterService: GetterService, + private userRenoteMutingService: UserRenoteMutingService, ) { super(meta, paramDef, async (ps, me) => { const muter = me; @@ -79,9 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } // Delete mute - await this.renoteMutingsRepository.delete({ - id: exist.id, - }); + await this.userRenoteMutingService.unmute([exist]); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 7ce7734f53ff857bf070c454b6127f4b3b4c438c..a8b4319a61c10cde2b6cecac5c360b3efbed9195 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy(ps.userId != null @@ -93,22 +95,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followersVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.followersVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { + if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) { + if (profile.followersVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.followersVisibility === 'followers') { + if (me == null) { throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { + throw new ApiError(meta.errors.forbidden); + } } } } diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 6b3389f0b24213e97d16d968d19fa072c57a42b9..feda5bb353b332bf99cd125892f69a203d7a84a1 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -12,6 +12,7 @@ import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -90,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy(ps.userId != null @@ -102,22 +104,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followingVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.followingVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { + if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) { + if (profile.followingVisibility === 'private') { + if (me == null || (me.id !== user.id)) { + throw new ApiError(meta.errors.forbidden); + } + } else if (profile.followingVisibility === 'followers') { + if (me == null) { throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { + throw new ApiError(meta.errors.forbidden); + } } } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index e2db71c5c7f4b918aa51388204534364afafe219..7e44d501ab31f42b97fe15cb8a1f7f735171156c 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -100,15 +100,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const currentCount = await this.userListsRepository.countBy({ userId: me.id, }); - if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { + if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) { throw new ApiError(meta.errors.tooManyUserLists); } - const userList = await this.userListsRepository.insert({ + const userList = await this.userListsRepository.insertOne({ id: this.idService.gen(), userId: me.id, name: ps.name, - } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + } as MiUserList); const users = (await this.userListMembershipsRepository.findBy({ userListId: ps.listId, diff --git a/packages/backend/src/server/api/endpoints/users/lists/create.ts b/packages/backend/src/server/api/endpoints/users/lists/create.ts index 952580e639fd7f95f9b4378f3448e3f11d45fb64..7daf05ba4eceb7e61b59a477d35eb65537589762 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create.ts @@ -61,15 +61,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const currentCount = await this.userListsRepository.countBy({ userId: me.id, }); - if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) { + if (currentCount >= (await this.roleService.getUserPolicies(me.id)).userListLimit) { throw new ApiError(meta.errors.tooManyUserLists); } - const userList = await this.userListsRepository.insert({ + const userList = await this.userListsRepository.insertOne({ id: this.idService.gen(), userId: me.id, name: ps.name, - } as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0])); + } as MiUserList); return await this.userListEntityService.pack(userList); }); diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts index aca883a052bb146f310ddf9be94482015920be45..7805ae328861c22151ec0c63ae9989ce182e8f97 100644 --- a/packages/backend/src/server/api/endpoints/users/reactions.ts +++ b/packages/backend/src/server/api/endpoints/users/reactions.ts @@ -12,6 +12,7 @@ import { DI } from '@/di-symbols.js'; import { CacheService } from '@/core/CacheService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { RoleService } from '@/core/RoleService.js'; +import { isUserRelated } from '@/misc/is-user-related.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -74,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { + const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>(); const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users if (!iAmModerator) { const user = await this.cacheService.findUserById(ps.userId); @@ -85,8 +87,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if ((me == null || me.id !== ps.userId) && !profile.publicReactions) { throw new ApiError(meta.errors.reactionsNotPublic); } + + // early return if me is blocked by requesting user + if (userIdsWhoBlockingMe.has(ps.userId)) { + return []; + } } + const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>(); + const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('reaction.userId = :userId', { userId: ps.userId }) @@ -94,9 +103,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.queryService.generateVisibilityQuery(query, me); - const reactions = await query + const reactions = (await query .limit(ps.limit) - .getMany(); + .getMany()).filter(reaction => { + if (reaction.note?.userId === ps.userId) return true; // we can see reactions to note of requesting user + if (me && isUserRelated(reaction.note, userIdsWhoBlockingMe)) return false; + if (me && isUserRelated(reaction.note, userIdsWhoMeMuting)) return false; + + return true; + }); return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true }); }); diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 0685858d77fb2e9609d6b3ab6800018d256f746c..5ff6de37d2caf4438db7cd16a8f3c5c1e3fb7c4d 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -3,17 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import sanitizeHtml from 'sanitize-html'; -import { Inject, Injectable } from '@nestjs/common'; -import type { AbuseUserReportsRepository, UserProfilesRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { MetaService } from '@/core/MetaService.js'; -import { EmailService } from '@/core/EmailService.js'; -import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; +import { AbuseReportService } from '@/core/AbuseReportService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -57,71 +51,32 @@ export const paramDef = { @Injectable() export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.abuseUserReportsRepository) - private abuseUserReportsRepository: AbuseUserReportsRepository, - - @Inject(DI.userProfilesRepository) - private userProfilesRepository: UserProfilesRepository, - - private idService: IdService, - private metaService: MetaService, - private emailService: EmailService, private getterService: GetterService, private roleService: RoleService, - private globalEventService: GlobalEventService, + private abuseReportService: AbuseReportService, ) { super(meta, paramDef, async (ps, me) => { // Lookup user - const user = await this.getterService.getUser(ps.userId).catch(err => { + const targetUser = await this.getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); - if (user.id === me.id) { + if (targetUser.id === me.id) { throw new ApiError(meta.errors.cannotReportYourself); } - if (await this.roleService.isAdministrator(user)) { + if (await this.roleService.isAdministrator(targetUser)) { throw new ApiError(meta.errors.cannotReportAdmin); } - const report = await this.abuseUserReportsRepository.insert({ - id: this.idService.gen(), - targetUserId: user.id, - targetUserHost: user.host, + await this.abuseReportService.report([{ + targetUserId: targetUser.id, + targetUserHost: targetUser.host, reporterId: me.id, reporterHost: null, comment: ps.comment, - }).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0])); - - // Publish event to moderators - setImmediate(async () => { - const moderators = await this.roleService.getModerators(); - - for (const moderator of moderators) { - this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', { - id: report.id, - targetUserId: report.targetUserId, - reporterId: report.reporterId, - comment: report.comment, - }); - - const profile = await this.userProfilesRepository.findOneBy({ userId: moderator.id }); - - if (profile?.email) { - this.emailService.sendEmail(profile.email, 'New abuse report', - sanitizeHtml(ps.comment), - sanitizeHtml(ps.comment)); - } - } - - const meta = await this.metaService.fetch(); - if (meta.maintainerEmail) { - this.emailService.sendEmail(meta.maintainerEmail, 'New abuse report', - sanitizeHtml(ps.comment), - sanitizeHtml(ps.comment)); - } - }); + }]); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 7b3bdab327eb84e82f3b6593aa24cf21df10c3a1..8ff952dcb544fa593e03026dc2c474dbaf781e3f 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -3,15 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Brackets } from 'typeorm'; -import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, FollowingsRepository } from '@/models/_.js'; -import type { Config } from '@/config.js'; -import type { MiUser } from '@/models/User.js'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; +import { UserSearchService } from '@/core/UserSearchService.js'; export const meta = { tags: ['users'], @@ -49,89 +43,16 @@ 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, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - - private userEntityService: UserEntityService, + private userSearchService: UserSearchService, ) { - super(meta, paramDef, async (ps, me) => { - const setUsernameAndHostQuery = (query = this.usersRepository.createQueryBuilder('user')) => { - if (ps.username) { - query.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); - } - - if (ps.host) { - if (ps.host === this.config.hostname || ps.host === '.') { - query.andWhere('user.host IS NULL'); - } else { - query.andWhere('user.host LIKE :host', { - host: sqlLikeEscape(ps.host.toLowerCase()) + '%', - }); - } - } - - return query; - }; - - const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30æ—¥ - - let users: MiUser[] = []; - - if (me) { - const followingQuery = this.followingsRepository.createQueryBuilder('following') - .select('following.followeeId') - .where('following.followerId = :followerId', { followerId: me.id }); - - const query = setUsernameAndHostQuery() - .andWhere(`user.id IN (${ followingQuery.getQuery() })`) - .andWhere('user.id != :meId', { meId: me.id }) - .andWhere('user.isSuspended = FALSE') - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })); - - query.setParameters(followingQuery.getParameters()); - - users = await query - .orderBy('user.usernameLower', 'ASC') - .limit(ps.limit) - .getMany(); - - if (users.length < ps.limit) { - const otherQuery = setUsernameAndHostQuery() - .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`) - .andWhere('user.isSuspended = FALSE') - .andWhere('user.updatedAt IS NOT NULL'); - - otherQuery.setParameters(followingQuery.getParameters()); - - const otherUsers = await otherQuery - .orderBy('user.updatedAt', 'DESC') - .limit(ps.limit - users.length) - .getMany(); - - users = users.concat(otherUsers); - } - } else { - const query = setUsernameAndHostQuery() - .andWhere('user.isSuspended = FALSE') - .andWhere('user.updatedAt IS NOT NULL'); - - users = await query - .orderBy('user.updatedAt', 'DESC') - .limit(ps.limit - users.length) - .getMany(); - } - - return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }); + super(meta, paramDef, (ps, me) => { + return this.userSearchService.search({ + username: ps.username, + host: ps.host, + }, { + limit: ps.limit, + detail: ps.detail, + }, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index df9d9f6312803f59fe3eb63b38874f90c80dfb01..0b0136066d72a4cbfedca9739ded82d675cde968 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30æ—¥ ps.query = ps.query.trim(); - const isUsername = ps.query.startsWith('@'); + const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1; let users: MiUser[] = []; - if (isUsername) { - const usernameQuery = this.usersRepository.createQueryBuilder('user') - .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE'); + const nameQuery = this.usersRepository.createQueryBuilder('user') + .where(new Brackets(qb => { + qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); + + if (isUsername) { + qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }); + } else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); + } + })) + .andWhere(new Brackets(qb => { + qb + .where('user.updatedAt IS NULL') + .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); + })) + .andWhere('user.isSuspended = FALSE'); + + if (ps.origin === 'local') { + nameQuery.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + nameQuery.andWhere('user.host IS NOT NULL'); + } + + users = await nameQuery + .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') + .limit(ps.limit) + .offset(ps.offset) + .getMany(); + + if (users.length < ps.limit) { + const profQuery = this.userProfilesRepository.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); if (ps.origin === 'local') { - usernameQuery.andWhere('user.host IS NULL'); + profQuery.andWhere('prof.userHost IS NULL'); } else if (ps.origin === 'remote') { - usernameQuery.andWhere('user.host IS NOT NULL'); + profQuery.andWhere('prof.userHost IS NOT NULL'); } - users = await usernameQuery - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .limit(ps.limit) - .offset(ps.offset) - .getMany(); - } else { - const nameQuery = this.usersRepository.createQueryBuilder('user') - .where(new Brackets(qb => { - qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); - - // Also search username if it qualifies as username - if (this.userEntityService.validateLocalUsername(ps.query)) { - qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); - } - })) + const query = this.usersRepository.createQueryBuilder('user') + .where(`user.id IN (${ profQuery.getQuery() })`) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); })) - .andWhere('user.isSuspended = FALSE'); - - if (ps.origin === 'local') { - nameQuery.andWhere('user.host IS NULL'); - } else if (ps.origin === 'remote') { - nameQuery.andWhere('user.host IS NOT NULL'); - } + .andWhere('user.isSuspended = FALSE') + .setParameters(profQuery.getParameters()); - users = await nameQuery + users = users.concat(await query .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') .limit(ps.limit) .offset(ps.offset) - .getMany(); - - if (users.length < ps.limit) { - const profQuery = this.userProfilesRepository.createQueryBuilder('prof') - .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); - - if (ps.origin === 'local') { - profQuery.andWhere('prof.userHost IS NULL'); - } else if (ps.origin === 'remote') { - profQuery.andWhere('prof.userHost IS NOT NULL'); - } - - const query = this.usersRepository.createQueryBuilder('user') - .where(`user.id IN (${ profQuery.getQuery() })`) - .andWhere(new Brackets(qb => { - qb - .where('user.updatedAt IS NULL') - .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); - })) - .andWhere('user.isSuspended = FALSE') - .setParameters(profQuery.getParameters()); - - users = users.concat(await query - .orderBy('user.updatedAt', 'DESC', 'NULLS LAST') - .limit(ps.limit) - .offset(ps.offset) - .getMany(), - ); - } + .getMany(), + ); } return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' }); diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts index 5210e4d2bc3c83f42be0bb7e24a9967ec5802140..f124aa9f39e8ba42bce811f2fa64aecbd5cb406f 100644 --- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts +++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts @@ -25,7 +25,7 @@ export class OpenApiServerService { public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.get('/api-doc', async (_request, reply) => { reply.header('Cache-Control', 'public, max-age=86400'); - return await reply.sendFile('/redoc.html', staticAssets); + return await reply.sendFile('/api-doc.html', staticAssets); }); fastify.get('/api.json', (_request, reply) => { reply.header('Cache-Control', 'public, max-age=600'); diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 436f9a44bb42da13ffe5b9462d0af78ac15c2378..fb5954fee0c2eed5fbda6b4dc4af8deab9021631 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { info: { version: config.version, title: 'Misskey API', - 'x-logo': { url: '/static-assets/api-doc.png' }, }, externalDocs: { diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 41c0feccc7e37df269bb1657f06cd71470bc9c34..f102cb42e1c783d22bf01b4f8b94275f3a5cd75d 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -14,9 +14,15 @@ import { CacheService } from '@/core/CacheService.js'; import { MiFollowing, MiUserProfile } from '@/models/_.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; +import { isJsonObject } from '@/misc/json-value.js'; +import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import type Logger from '@/logger.js'; + +const MAX_CHANNELS_PER_CONNECTION = 32; /** * Main stream connection @@ -25,10 +31,11 @@ import type Channel from './channel.js'; export default class Connection { public user?: MiUser; public token?: MiAccessToken; + private rateLimiter?: () => Promise<boolean>; private wsConnection: WebSocket.WebSocket; public subscriber: StreamEventEmitter; private channels: Channel[] = []; - private subscribingNotes: any = {}; + private subscribingNotes: Partial<Record<string, number>> = {}; private cachedNotes: Packed<'Note'>[] = []; public userProfile: MiUserProfile | null = null; public following: Record<string, Pick<MiFollowing, 'withReplies'> | undefined> = {}; @@ -38,6 +45,9 @@ export default class Connection { public userIdsWhoMeMutingRenotes: Set<string> = new Set(); public userMutedInstances: Set<string> = new Set(); private fetchIntervalId: NodeJS.Timeout | null = null; + private activeRateLimitRequests: number = 0; + private closingConnection: boolean = false; + private logger: Logger; constructor( private channelsService: ChannelsService, @@ -45,12 +55,18 @@ export default class Connection { private notificationService: NotificationService, private cacheService: CacheService, private channelFollowingService: ChannelFollowingService, + loggerService: LoggerService, user: MiUser | null | undefined, token: MiAccessToken | null | undefined, + private ip: string, + rateLimiter: () => Promise<boolean>, ) { if (user) this.user = user; if (token) this.token = token; + if (rateLimiter) this.rateLimiter = rateLimiter; + + this.logger = loggerService.getLogger('streaming', 'coral'); } @bindThis @@ -101,7 +117,30 @@ export default class Connection { */ @bindThis private async onWsConnectionMessage(data: WebSocket.RawData) { - let obj: Record<string, any>; + let obj: JsonObject; + + if (this.closingConnection) return; + + if (this.rateLimiter) { + // this 4096 should match the `max` of the `rateLimiter`, see + // StreamingApiServerService + if (this.activeRateLimitRequests <= 4096) { + this.activeRateLimitRequests++; + const shouldRateLimit = await this.rateLimiter(); + this.activeRateLimitRequests--; + + if (shouldRateLimit) return; + if (this.closingConnection) return; + } else { + let connectionInfo = `IP ${this.ip}`; + if (this.user) connectionInfo += `, user ID ${this.user.id}`; + + this.logger.warn(`Closing a connection (${connectionInfo}) due to an excessive influx of messages.`); + this.closingConnection = true; + this.wsConnection.close(1008, 'Please stop spamming the streaming API.'); + return; + } + } try { obj = JSON.parse(data.toString()); @@ -151,7 +190,8 @@ export default class Connection { } @bindThis - private readNote(body: any) { + private readNote(body: JsonValue | undefined) { + if (!isJsonObject(body)) return; const id = body.id; const note = this.cachedNotes.find(n => n.id === id); @@ -163,7 +203,7 @@ export default class Connection { } @bindThis - private onReadNotification(payload: any) { + private onReadNotification(payload: JsonValue | undefined) { this.notificationService.readAllNotification(this.user!.id); } @@ -171,16 +211,15 @@ export default class Connection { * 投稿購èªè¦æ±‚時 */ @bindThis - private onSubscribeNote(payload: any) { - if (!payload.id) return; - - if (this.subscribingNotes[payload.id] == null) { - this.subscribingNotes[payload.id] = 0; - } + private onSubscribeNote(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; + if (!payload.id || typeof payload.id !== 'string') return; - this.subscribingNotes[payload.id]++; + const current = this.subscribingNotes[payload.id] ?? 0; + const updated = current + 1; + this.subscribingNotes[payload.id] = updated; - if (this.subscribingNotes[payload.id] === 1) { + if (updated === 1) { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } } @@ -189,11 +228,15 @@ export default class Connection { * 投稿購èªè§£é™¤è¦æ±‚時 */ @bindThis - private onUnsubscribeNote(payload: any) { - if (!payload.id) return; - - this.subscribingNotes[payload.id]--; - if (this.subscribingNotes[payload.id] <= 0) { + private onUnsubscribeNote(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; + if (!payload.id || typeof payload.id !== 'string') return; + + const current = this.subscribingNotes[payload.id]; + if (current == null) return; + const updated = current - 1; + this.subscribingNotes[payload.id] = updated; + if (updated <= 0) { delete this.subscribingNotes[payload.id]; this.subscriber.off(`noteStream:${payload.id}`, this.onNoteStreamMessage); } @@ -201,6 +244,18 @@ export default class Connection { @bindThis private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) { + // we must not send to the frontend information about notes from + // users who blocked the logged-in user, even when they're replies + // to notes the logged-in user can see + if (data.type === 'replied') { + const noteUserId = data.body.body.userId; + if (noteUserId !== null) { + if (this.userIdsWhoBlockingMe.has(noteUserId)) { + return; + } + } + } + this.sendMessageToWs('noteUpdated', { id: data.body.id, type: data.type, @@ -212,17 +267,24 @@ export default class Connection { * ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šè¦æ±‚時 */ @bindThis - private onChannelConnectRequested(payload: any) { + private onChannelConnectRequested(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; const { channel, id, params, pong } = payload; - this.connectChannel(id, params, channel, pong); + if (typeof id !== 'string') return; + if (typeof channel !== 'string') return; + if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return; + if (typeof params !== 'undefined' && !isJsonObject(params)) return; + this.connectChannel(id, params, channel, pong ?? undefined); } /** * ãƒãƒ£ãƒ³ãƒãƒ«åˆ‡æ–è¦æ±‚時 */ @bindThis - private onChannelDisconnectRequested(payload: any) { + private onChannelDisconnectRequested(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; const { id } = payload; + if (typeof id !== 'string') return; this.disconnectChannel(id); } @@ -230,7 +292,7 @@ export default class Connection { * クライアントã«ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸é€ä¿¡ */ @bindThis - public sendMessageToWs(type: string, payload: any) { + public sendMessageToWs(type: string, payload: JsonObject) { this.wsConnection.send(JSON.stringify({ type: type, body: payload, @@ -241,7 +303,11 @@ export default class Connection { * ãƒãƒ£ãƒ³ãƒãƒ«ã«æŽ¥ç¶š */ @bindThis - public connectChannel(id: string, params: any, channel: string, pong = false) { + public connectChannel(id: string, params: JsonObject | undefined, channel: string, pong = false) { + if (this.channels.length >= MAX_CHANNELS_PER_CONNECTION) { + return; + } + const channelService = this.channelsService.getChannelService(channel); if (channelService.requireCredential && this.user == null) { @@ -288,7 +354,12 @@ export default class Connection { * @param data メッセージ */ @bindThis - private onChannelMessageRequested(data: any) { + private onChannelMessageRequested(data: JsonValue | undefined) { + if (!isJsonObject(data)) return; + if (typeof data.id !== 'string') return; + if (typeof data.type !== 'string') return; + if (typeof data.body === 'undefined') return; + const channel = this.channels.find(c => c.id === data.id); if (channel != null && channel.onMessage != null) { channel.onMessage(data.type, data.body); diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts index 49b0ae1d5b17cc3ed7a3e5f039e240c9703123c3..ae9c7e3e99866771d653863a1fb4e831b38003e8 100644 --- a/packages/backend/src/server/api/stream/channel.ts +++ b/packages/backend/src/server/api/stream/channel.ts @@ -8,6 +8,7 @@ import { isInstanceMuted } from '@/misc/is-instance-muted.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { Packed } from '@/misc/json-schema.js'; +import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import type Connection from './Connection.js'; /** @@ -81,10 +82,12 @@ export default abstract class Channel { this.connection = connection; } + public send(payload: { type: string, body: JsonValue }): void + public send(type: string, payload: JsonValue): void @bindThis - public send(typeOrPayload: any, payload?: any) { - const type = payload === undefined ? typeOrPayload.type : typeOrPayload; - const body = payload === undefined ? typeOrPayload.body : payload; + public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) { + const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string); + const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload; this.connection.sendMessageToWs('channel', { id: this.id, @@ -93,11 +96,11 @@ export default abstract class Channel { }); } - public abstract init(params: any): void; + public abstract init(params: JsonObject): void; public dispose?(): void; - public onMessage?(type: string, body: any): void; + public onMessage?(type: string, body: JsonValue): void; } export type MiChannelService<T extends boolean> = { diff --git a/packages/backend/src/server/api/stream/channels/admin.ts b/packages/backend/src/server/api/stream/channels/admin.ts index 92b6d2ac040f9d4eb74858925c6d9bc2a4a87c82..355d5dba21a6eea1efcf78f8e8e64e31ec5c0dbd 100644 --- a/packages/backend/src/server/api/stream/channels/admin.ts +++ b/packages/backend/src/server/api/stream/channels/admin.ts @@ -5,6 +5,7 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class AdminChannel extends Channel { @@ -14,7 +15,7 @@ class AdminChannel extends Channel { public static kind = 'read:admin:stream'; @bindThis - public async init(params: any) { + public async init(params: JsonObject) { // Subscribe admin stream this.subscriber.on(`adminStream:${this.user!.id}`, data => { this.send(data); diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 4a1d2dd109d22dc923dc9dcbcb7b3ac2e356dfb2..53dc7f18b63b120a9a14e92d04541e609d7f9de2 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class AntennaChannel extends Channel { @@ -27,8 +28,9 @@ class AntennaChannel extends Channel { } @bindThis - public async init(params: any) { - this.antennaId = params.antennaId as string; + public async init(params: JsonObject) { + if (typeof params.antennaId !== 'string') return; + this.antennaId = params.antennaId; // Subscribe stream this.subscriber.on(`antennaStream:${this.antennaId}`, this.onEvent); 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 01be2d20895737bd40c1c1ebcb329607356ce8b5..647e9cab8114787cef22d2b8ead602f1ca8a9051 100644 --- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts @@ -11,6 +11,7 @@ import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { MiMeta } from '@/models/Meta.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { MiChannelService } from '../channel.js'; class BubbleTimelineChannel extends Channel { @@ -35,13 +36,13 @@ class BubbleTimelineChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.btlAvailable) return; - this.withRenotes = params.withRenotes ?? true; - this.withFiles = params.withFiles ?? false; - this.withBots = params.withBots ?? true; + this.withRenotes = !!(params.withRenotes ?? true); + this.withFiles = !!(params.withFiles ?? false); + this.withBots = !!(params.withBots ?? true); this.instance = await this.metaService.fetch(); // Subscribe events diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 865e4fed192ec18ca35555ad2324dbe9fd23042a..226e161122fa92ae701e5e40d262f4d86440ff0f 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class ChannelChannel extends Channel { @@ -30,9 +31,10 @@ class ChannelChannel extends Channel { @bindThis public async init(params: any) { - this.channelId = params.channelId as string; - this.withFiles = params.withFiles ?? false; - this.withRenotes = params.withRenotes ?? true; + if (typeof params.channelId !== 'string') return; + this.channelId = params.channelId; + this.withFiles = !!(params.withFiles ?? false); + this.withRenotes = !!(params.withRenotes ?? true); // Subscribe stream this.subscriber.on('notesStream', this.onNote); diff --git a/packages/backend/src/server/api/stream/channels/drive.ts b/packages/backend/src/server/api/stream/channels/drive.ts index 0d9b48630511e1b583721327602c3be0b4615fcb..03768f3d23ef48c36d0e7e772a1f520e581a0972 100644 --- a/packages/backend/src/server/api/stream/channels/drive.ts +++ b/packages/backend/src/server/api/stream/channels/drive.ts @@ -5,6 +5,7 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class DriveChannel extends Channel { @@ -14,7 +15,7 @@ class DriveChannel extends Channel { public static kind = 'read:account'; @bindThis - public async init(params: any) { + public async init(params: JsonObject) { // Subscribe drive stream this.subscriber.on(`driveStream:${this.user!.id}`, data => { this.send(data); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 0a894147a247a6c2c1c0f66177089e582f413b07..6fe76747ee6647006a3adf356ffdf00348054aac 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class GlobalTimelineChannel extends Channel { @@ -33,13 +34,13 @@ class GlobalTimelineChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; - this.withRenotes = params.withRenotes ?? true; - this.withFiles = params.withFiles ?? false; - this.withBots = params.withBots ?? true; + this.withRenotes = !!(params.withRenotes ?? true); + this.withFiles = !!(params.withFiles ?? false); + this.withBots = !!(params.withBots ?? true); // Subscribe events this.subscriber.on('notesStream', this.onNote); diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 57bada5d9cfb5f5737d542c6a46fec8bf0b0f44f..8105f15cb17ae4c21c486c3e839b7218c78e700d 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -9,6 +9,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class HashtagChannel extends Channel { @@ -28,11 +29,11 @@ class HashtagChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { + if (!Array.isArray(params.q)) return; + if (!params.q.every(x => Array.isArray(x) && x.every(y => typeof y === 'string'))) return; this.q = params.q; - if (this.q == null) return; - // Subscribe stream this.subscriber.on('notesStream', this.onNote); } diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 84ff24146945b516cb8856ff40f8fc57e0e59f7c..359ab3e2233a29e8481a77e9a39aced2175d0412 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -8,6 +8,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class HomeTimelineChannel extends Channel { @@ -29,9 +30,9 @@ class HomeTimelineChannel extends Channel { } @bindThis - public async init(params: any) { - this.withRenotes = params.withRenotes ?? true; - this.withFiles = params.withFiles ?? false; + public async init(params: JsonObject) { + this.withRenotes = !!(params.withRenotes ?? true); + this.withFiles = !!(params.withFiles ?? false); this.subscriber.on('notesStream', this.onNote); } @@ -59,7 +60,7 @@ class HomeTimelineChannel extends Channel { const reply = note.reply; if (this.following[note.userId]?.withReplies) { // 自分ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ã¯å¼¾ã - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; } else { // 「ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ãŒè¡Œã£ãŸè¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€ŒæŠ•ç¨¿è€…ã®æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã„å ´åˆ if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; @@ -74,7 +75,7 @@ class HomeTimelineChannel extends Channel { if (note.renote.reply) { const reply = note.renote.reply; // 自分ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ã®ãƒªãƒŽãƒ¼ãƒˆã¯å¼¾ã - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; } } diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index b83d3ec81722d7e814dd94de3c0a06e3d6cdddad..01645fe657706b23d0322b015e90be3236c0d129 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class HybridTimelineChannel extends Channel { @@ -35,14 +36,14 @@ class HybridTimelineChannel extends Channel { } @bindThis - public async init(params: any): Promise<void> { + public async init(params: JsonObject): Promise<void> { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withRenotes = params.withRenotes ?? true; - this.withReplies = params.withReplies ?? false; - this.withBots = params.withBots ?? true; - this.withFiles = params.withFiles ?? false; + this.withRenotes = !!(params.withRenotes ?? true); + this.withReplies = !!(params.withReplies ?? false); + this.withFiles = !!(params.withFiles ?? false); + this.withBots = !!(params.withBots ?? true); // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -78,7 +79,7 @@ class HybridTimelineChannel extends Channel { const reply = note.reply; if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) { // 自分ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ã¯å¼¾ã - if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; } else { // 「ãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€Œãƒãƒ£ãƒ³ãƒãƒ«æŽ¥ç¶šä¸»ãŒè¡Œã£ãŸè¿”ä¿¡ã€ã§ã‚‚ãªã‘ã‚Œã°ã€ã€ŒæŠ•ç¨¿è€…ã®æŠ•ç¨¿è€…自身ã¸ã®è¿”ä¿¡ã€ã§ã‚‚ãªã„å ´åˆ if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; @@ -87,7 +88,15 @@ class HybridTimelineChannel extends Channel { if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return; - if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; + // 純粋ãªãƒªãƒŽãƒ¼ãƒˆï¼ˆå¼•ç”¨ãƒªãƒŽãƒ¼ãƒˆã§ãªã„リノート)ã®å ´åˆ + if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { + if (!this.withRenotes) return; + if (note.renote.reply) { + const reply = note.renote.reply; + // 自分ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ã®ãƒªãƒŽãƒ¼ãƒˆã¯å¼¾ã + if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; + } + } if (this.user && note.renoteId && !note.text) { if (note.renote && Object.keys(note.renote.reactions).length > 0) { diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 48cc76c497e89982198c53ebd6ee85eff8b3a1c6..1f9d25b44da87f7050e47a6fe5c82c49519eb37f 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class LocalTimelineChannel extends Channel { @@ -34,14 +35,14 @@ class LocalTimelineChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withRenotes = params.withRenotes ?? true; - this.withReplies = params.withReplies ?? false; - this.withBots = params.withBots ?? true; - this.withFiles = params.withFiles ?? false; + this.withRenotes = !!(params.withRenotes ?? true); + this.withReplies = !!(params.withReplies ?? false); + this.withFiles = !!(params.withFiles ?? false); + this.withBots = !!(params.withBots ?? true); // Subscribe events this.subscriber.on('notesStream', this.onNote); diff --git a/packages/backend/src/server/api/stream/channels/main.ts b/packages/backend/src/server/api/stream/channels/main.ts index a12976d69d1ab96d8348f0f9908ec47a82ac0b31..863d7f4c4e450d48bdc17e4b56ae56b9962015b0 100644 --- a/packages/backend/src/server/api/stream/channels/main.ts +++ b/packages/backend/src/server/api/stream/channels/main.ts @@ -7,6 +7,7 @@ import { Injectable } from '@nestjs/common'; import { isInstanceMuted, isUserFromMutedInstance } from '@/misc/is-instance-muted.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class MainChannel extends Channel { @@ -25,7 +26,7 @@ class MainChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { // Subscribe main stream channel this.subscriber.on(`mainStream:${this.user!.id}`, async data => { switch (data.type) { diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index 061aa76904a48984fec3b5e92b8e3c8f413c5418..91b62255b4f616969793e23337c1b0fe0abddce4 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -6,6 +6,8 @@ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { isJsonObject } from '@/misc/json-value.js'; +import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; const ev = new Xev(); @@ -22,19 +24,22 @@ class QueueStatsChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { ev.addListener('queueStats', this.onStats); } @bindThis - private onStats(stats: any) { + private onStats(stats: JsonObject) { this.send('stats', stats); } @bindThis - public onMessage(type: string, body: any) { + public onMessage(type: string, body: JsonValue) { switch (type) { case 'requestLog': + if (!isJsonObject(body)) return; + if (typeof body.id !== 'string') return; + if (typeof body.length !== 'number') return; ev.once(`queueStatsLog:${body.id}`, statsLog => { this.send('statsLog', statsLog); }); diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts index f4a3a09367e3a4f89bda8211739b5779edce4e86..7597a1cfa3324b3711681cc052d8d8a9e0689b66 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -9,7 +9,10 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ReversiService } from '@/core/ReversiService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { isJsonObject } from '@/misc/json-value.js'; +import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; +import { reversiUpdateKeys } from 'misskey-js'; class ReversiGameChannel extends Channel { public readonly chName = 'reversiGame'; @@ -28,25 +31,42 @@ class ReversiGameChannel extends Channel { } @bindThis - public async init(params: any) { - this.gameId = params.gameId as string; + public async init(params: JsonObject) { + if (typeof params.gameId !== 'string') return; + this.gameId = params.gameId; this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send); } @bindThis - public onMessage(type: string, body: any) { + public onMessage(type: string, body: JsonValue) { switch (type) { - case 'ready': this.ready(body); break; - case 'updateSettings': this.updateSettings(body.key, body.value); break; - case 'cancel': this.cancelGame(); break; - case 'putStone': this.putStone(body.pos, body.id); break; + case 'ready': + if (typeof body !== 'boolean') return; + this.ready(body); + break; + case 'updateSettings': + if (!isJsonObject(body)) return; + if (!this.reversiService.isValidReversiUpdateKey(body.key)) return; + if (!this.reversiService.isValidReversiUpdateValue(body.key, body.value)) return; + + this.updateSettings(body.key, body.value); + break; + case 'cancel': + this.cancelGame(); + break; + case 'putStone': + if (!isJsonObject(body)) return; + if (typeof body.pos !== 'number') return; + if (typeof body.id !== 'string') return; + this.putStone(body.pos, body.id); + break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; } } @bindThis - private async updateSettings(key: string, value: any) { + private async updateSettings<K extends typeof reversiUpdateKeys[number]>(key: K, value: MiReversiGame[K]) { if (this.user == null) return; this.reversiService.updateSettings(this.gameId!, this.user, key, value); diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts index 3998a0fd363e2d37db2addef82912b222c54c181..6e889397245d246880d7b2fc86933a02bd90327b 100644 --- a/packages/backend/src/server/api/stream/channels/reversi.ts +++ b/packages/backend/src/server/api/stream/channels/reversi.ts @@ -5,6 +5,7 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class ReversiChannel extends Channel { @@ -21,7 +22,7 @@ class ReversiChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { this.subscriber.on(`reversiStream:${this.user!.id}`, this.send); } diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 6a4ad224607b3578ef7dc4c3db39d6e880bbf665..fcfa26c38b39fc8a500657d32f465dc695acde6f 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -8,6 +8,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class RoleTimelineChannel extends Channel { @@ -28,8 +29,9 @@ class RoleTimelineChannel extends Channel { } @bindThis - public async init(params: any) { - this.roleId = params.roleId as string; + public async init(params: JsonObject) { + if (typeof params.roleId !== 'string') return; + this.roleId = params.roleId; this.subscriber.on(`roleTimelineStream:${this.roleId}`, this.onEvent); } diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index eb4d8c9992cba2aecba82ee712ee4a7e9769a607..ec5352d12dbadbb4d40fb868e56e910f10796aa0 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -6,6 +6,8 @@ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { isJsonObject } from '@/misc/json-value.js'; +import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; const ev = new Xev(); @@ -22,19 +24,20 @@ class ServerStatsChannel extends Channel { } @bindThis - public async init(params: any) { + public async init(params: JsonObject) { ev.addListener('serverStats', this.onStats); } @bindThis - private onStats(stats: any) { + private onStats(stats: JsonObject) { this.send('stats', stats); } @bindThis - public onMessage(type: string, body: any) { + public onMessage(type: string, body: JsonValue) { switch (type) { case 'requestLog': + if (!isJsonObject(body)) return; ev.once(`serverStatsLog:${body.id}`, statsLog => { this.send('statsLog', statsLog); }); diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 14b30a157cd002e88f8d08f32be4da4c29126f40..4f38351e94edad6776dc2d92df74b882154eac06 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -10,6 +10,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; +import type { JsonObject } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; class UserListChannel extends Channel { @@ -36,10 +37,11 @@ class UserListChannel extends Channel { } @bindThis - public async init(params: any) { - this.listId = params.listId as string; - this.withFiles = params.withFiles ?? false; - this.withRenotes = params.withRenotes ?? true; + public async init(params: JsonObject) { + if (typeof params.listId !== 'string') return; + this.listId = params.listId; + this.withFiles = !!(params.withFiles ?? false); + this.withRenotes = !!(params.withRenotes ?? true); // Check existence and owner const listExist = await this.userListsRepository.exists({ diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 702e306febf6e4baf3b431017e75c7be9f031b0d..0af90a844b0c9fe0569c1b864f4c38aee2cf2fb9 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -25,7 +25,16 @@ 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, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { + DbQueue, + DeliverQueue, + EndedPollNotificationQueue, + InboxQueue, + ObjectStorageQueue, + SystemQueue, + UserWebhookDeliverQueue, + SystemWebhookDeliverQueue, +} from '@/core/QueueModule.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; @@ -111,7 +120,8 @@ export class ClientServerService { @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, - @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, + @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, + @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, ) { //this.createServer = this.createServer.bind(this); } @@ -242,7 +252,8 @@ export class ClientServerService { this.inboxQueue, this.dbQueue, this.objectStorageQueue, - this.webhookDeliverQueue, + this.userWebhookDeliverQueue, + this.systemWebhookDeliverQueue, ].map(q => new BullMQAdapter(q)), serverAdapter, }); diff --git a/packages/backend/src/server/web/FeedService.ts b/packages/backend/src/server/web/FeedService.ts index dc7f6452c8dc544b96bd9fd250d5480348f386f1..57a32ca934321a1a81303806ae57f13c295493f6 100644 --- a/packages/backend/src/server/web/FeedService.ts +++ b/packages/backend/src/server/web/FeedService.ts @@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; +import { MfmService } from "@/core/MfmService.js"; +import { parse as mfmParse } from '@transfem-org/sfm-js'; @Injectable() export class FeedService { @@ -33,6 +35,7 @@ export class FeedService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private idService: IdService, + private mfmService: MfmService, ) { } @@ -76,13 +79,14 @@ export class FeedService { id: In(note.fileIds), }) : []; const file = files.find(file => file.type.startsWith('image/')); + const text = note.text; feed.addItem({ title: `New note by ${author.name}`, link: `${this.config.url}/notes/${note.id}`, date: this.idService.parse(note.id).date, description: note.cw ?? undefined, - content: note.text ?? undefined, + content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined, image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined, }); } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 8f8f08a3051bbe659a09e31ca76a0b74ca68b27a..ef804b5bfdbc613814ae3e0e2b325b4c583cf6c9 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -17,20 +17,33 @@ import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; import { MiMeta } from '@/models/Meta.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; +import * as Redis from 'ioredis'; +import { RedisKVCache } from '@/misc/cache.js'; @Injectable() export class UrlPreviewService { private logger: Logger; + private previewCache: RedisKVCache<SummalyResult>; constructor( @Inject(DI.config) private config: Config, + @Inject(DI.redis) + private redisClient: Redis.Redis, + private metaService: MetaService, private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('url-preview'); + this.previewCache = new RedisKVCache<SummalyResult>(this.redisClient, 'summaly', { + lifetime: 1000 * 60 * 60 * 24, // 1d + memoryCacheLifetime: 1000 * 60 * 10, // 10m + fetcher: (key: string) => { throw new Error('the UrlPreview cache should never fetch'); }, + toRedisConverter: (value) => JSON.stringify(value), + fromRedisConverter: (value) => JSON.parse(value), + }); } @bindThis @@ -75,9 +88,19 @@ export class UrlPreviewService { }; } + const key = `${url}@${lang}`; + const cached = await this.previewCache.get(key); + if (cached !== undefined) { + this.logger.info(`Returning cache preview of ${key}`); + // Cache 7days + reply.header('Cache-Control', 'max-age=604800, immutable'); + + return cached; + } + this.logger.info(meta.urlPreviewSummaryProxyUrl - ? `(Proxy) Getting preview of ${url}@${lang} ...` - : `Getting preview of ${url}@${lang} ...`); + ? `(Proxy) Getting preview of ${key} ...` + : `Getting preview of ${key} ...`); try { const summary = meta.urlPreviewSummaryProxyUrl @@ -97,6 +120,8 @@ export class UrlPreviewService { summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); + this.previewCache.set(key, summary); + // Cache 7days reply.header('Cache-Control', 'max-age=604800, immutable'); diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 0543cc2b65f2f902bb3957bc5788ece5f5e20627..38e37ce093f46978a32ad656adf388bffdd06722 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -29,7 +29,8 @@ let forceError = localStorage.getItem('forceError'); if (forceError != null) { - renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') + renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.'); + return; } //#region Detect language & fetch translations @@ -181,7 +182,12 @@ document.head.appendChild(css); } - function renderError(code, details) { + async function renderError(code, details) { + // Cannot set property 'innerHTML' of null ã‚’å›žé¿ + if (document.readyState === 'loading') { + await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve)); + } + let errorsElement = document.getElementById('errors'); if (!errorsElement) { @@ -340,6 +346,6 @@ #errorInfo { width: 50%; } - `) + }`) } })(); diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 4007632291bab14cd0226841c43769c7bba48985..36cec20c857c92be0399a1ac794ccfbf0c472a3f 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -32,6 +32,7 @@ html 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='manifest' href='/manifest.json') diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index edcf2530bc6e7cff768f15f2a7864eb1c89b5662..d83d4140960beaa11e79b608ae959c7d90eec404 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -92,6 +92,16 @@ export const moderationLogTypes = [ 'deleteAvatarDecoration', 'unsetUserAvatar', 'unsetUserBanner', + 'createSystemWebhook', + 'updateSystemWebhook', + 'deleteSystemWebhook', + 'createAbuseReportNotificationRecipient', + 'updateAbuseReportNotificationRecipient', + 'deleteAbuseReportNotificationRecipient', + 'deleteAccount', + 'deletePage', + 'deleteFlash', + 'deleteGalleryPost', ] as const; export type ModerationLogPayloads = { @@ -289,6 +299,55 @@ export type ModerationLogPayloads = { userHost: string | null; fileId: string; }; + createSystemWebhook: { + systemWebhookId: string; + webhook: any; + }; + updateSystemWebhook: { + systemWebhookId: string; + before: any; + after: any; + }; + deleteSystemWebhook: { + systemWebhookId: string; + webhook: any; + }; + createAbuseReportNotificationRecipient: { + recipientId: string; + recipient: any; + }; + updateAbuseReportNotificationRecipient: { + recipientId: string; + before: any; + after: any; + }; + deleteAbuseReportNotificationRecipient: { + recipientId: string; + recipient: any; + }; + deleteAccount: { + userId: string; + userUsername: string; + userHost: string | null; + }; + deletePage: { + pageId: string; + pageUserId: string; + pageUserUsername: string; + page: any; + }; + deleteFlash: { + flashId: string; + flashUserId: string; + flashUserUsername: string; + flash: any; + }; + deleteGalleryPost: { + postId: string; + postUserId: string; + postUserUsername: string; + post: any; + }; }; export type Serialized<T> = { diff --git a/packages/backend/test-server/.eslintrc.cjs b/packages/backend/test-server/.eslintrc.cjs deleted file mode 100644 index c261741a36a14a28b0925800a7672bbcdf676c9d..0000000000000000000000000000000000000000 --- a/packages/backend/test-server/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../../shared/.eslintrc.js', - ], - rules: { - 'import/order': ['warn', { - 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - 'pathGroups': [ - { - 'pattern': '@/**', - 'group': 'external', - 'position': 'after' - } - ], - }], - 'no-restricted-globals': [ - 'error', - { - 'name': '__dirname', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - }, - { - 'name': '__filename', - 'message': 'Not in ESModule. Use `import.meta.url` instead.' - } - ] - }, -}; diff --git a/packages/backend/test-server/eslint.config.js b/packages/backend/test-server/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..b9c16d469fc3947b18d70b63f3eb77e853a95763 --- /dev/null +++ b/packages/backend/test-server/eslint.config.js @@ -0,0 +1,43 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'import/order': ['warn', { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + pathGroups: [{ + pattern: '@/**', + group: 'external', + position: 'after', + }], + }], + 'no-restricted-globals': ['error', { + name: '__dirname', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }, { + name: '__filename', + message: 'Not in ESModule. Use `import.meta.url` instead.', + }], + }, + }, +]; diff --git a/packages/backend/test/.eslintrc.cjs b/packages/backend/test/.eslintrc.cjs deleted file mode 100644 index 41ecea0c3fbaa6448c91e8f277b29c95a883558a..0000000000000000000000000000000000000000 --- a/packages/backend/test/.eslintrc.cjs +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: ['../.eslintrc.cjs'], - env: { - node: true, - jest: true, - }, -}; diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/compose.yml similarity index 94% rename from packages/backend/test/docker-compose.yml rename to packages/backend/test/compose.yml index f2d899075808abfb2868de68929ff9cb41f0922a..6593fc33dd6c5b67a46bac99229d2f2cee8d9617 100644 --- a/packages/backend/test/docker-compose.yml +++ b/packages/backend/test/compose.yml @@ -1,5 +1,3 @@ -version: "3" - services: redistest: image: redis:7 diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 13c56b88a6938090d3a99ca459d6d4f79728a546..06548fa7da039738829e9f0e28b7454b0460c209 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -206,7 +206,7 @@ describe('2è¦ç´ èªè¨¼', () => { username, }, alice); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); + assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); const signinResponse = await api('signin', { ...signinParam(), @@ -248,7 +248,7 @@ describe('2è¦ç´ èªè¨¼', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + } as any) as any, alice); assert.strictEqual(keyDoneResponse.status, 200); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.name, keyName); @@ -257,22 +257,22 @@ describe('2è¦ç´ èªè¨¼', () => { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual(usersShowResponse.body.securityKeys, true); + assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true); const signinResponse = await api('signin', { ...signinParam(), }); assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.body.i, undefined); - assert.notEqual(signinResponse.body.challenge, undefined); - assert.notEqual(signinResponse.body.allowCredentials, undefined); - assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url')); + assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined); + assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined); + assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url')); const signinResponse2 = await api('signin', signinWithSecurityKeyParam({ keyName, credentialId, requestOptions: signinResponse.body, - })); + } as any)); assert.strictEqual(signinResponse2.status, 200); assert.notEqual(signinResponse2.body.i, undefined); @@ -307,7 +307,7 @@ describe('2è¦ç´ èªè¨¼', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + } as any) as any, alice); assert.strictEqual(keyDoneResponse.status, 200); const passwordLessResponse = await api('i/2fa/password-less', { @@ -319,7 +319,7 @@ describe('2è¦ç´ èªè¨¼', () => { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual(usersShowResponse.body.usePasswordLessLogin, true); + assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true); const signinResponse = await api('signin', { ...signinParam(), @@ -333,7 +333,7 @@ describe('2è¦ç´ èªè¨¼', () => { keyName, credentialId, requestOptions: signinResponse.body, - }), + } as any), password: '', }); assert.strictEqual(signinResponse2.status, 200); @@ -370,7 +370,7 @@ describe('2è¦ç´ èªè¨¼', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + } as any) as any, alice); assert.strictEqual(keyDoneResponse.status, 200); const renamedKey = 'other-key'; @@ -383,6 +383,7 @@ describe('2è¦ç´ èªè¨¼', () => { const iResponse = await api('i', { }, alice); assert.strictEqual(iResponse.status, 200); + assert.ok(iResponse.body.securityKeysList); const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url')); assert.strictEqual(securityKeys.length, 1); assert.strictEqual(securityKeys[0].name, renamedKey); @@ -419,13 +420,14 @@ describe('2è¦ç´ èªè¨¼', () => { keyName, credentialId, creationOptions: registerKeyResponse.body, - }) as any, alice); + } as any) as any, alice); assert.strictEqual(keyDoneResponse.status, 200); // テストã®å®Ÿè¡Œé †ã«ã‚ˆã£ã¦ã¯è¤‡æ•°æ®‹ã£ã¦ã‚‹ã®ã§å…¨éƒ¨æ¶ˆã™ const iResponse = await api('i', { }, alice); assert.strictEqual(iResponse.status, 200); + assert.ok(iResponse.body.securityKeysList); for (const key of iResponse.body.securityKeysList) { const removeKeyResponse = await api('i/2fa/remove-key', { token: otpToken(registerResponse.body.secret), @@ -439,7 +441,7 @@ describe('2è¦ç´ èªè¨¼', () => { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual(usersShowResponse.body.securityKeys, false); + assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false); const signinResponse = await api('signin', { ...signinParam(), @@ -470,7 +472,7 @@ describe('2è¦ç´ èªè¨¼', () => { username, }); assert.strictEqual(usersShowResponse.status, 200); - assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true); + assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true); const unregisterResponse = await api('i/2fa/unregister', { token: otpToken(registerResponse.body.secret), diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 101238b60150459abcdfb2eed7088ffbcbe67691..6ac14cd8dcdefe503f4d49eb75b977cd37fff2a8 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -163,8 +163,7 @@ describe('アンテナ', () => { }); test('ãŒä¸Šé™ã„ã£ã±ã„ã¾ã§ä½œæˆã§ãã‚‹ã“ã¨', async () => { - // antennaLimit + 1ã¾ã§ä½œã‚Œã‚‹ã®ãŒã‚モ - const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({ + const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit)].map(() => successfulApiCall({ endpoint: 'antennas/create', parameters: { ...defaultParam }, user: alice, diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index c61b0c2a867423d2d6c7a51869662114bd64f13a..2dd645d97a99345e9d4383651a74ffc685e4c50a 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -410,21 +410,21 @@ describe('API visibility', () => { test('[HTL] public-post ㌠自分ãŒè¦‹ã‚Œã‚‹', async () => { const res = await api('notes/timeline', { limit: 100 }, alice); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === pub.id); + const notes = res.body.filter(n => n.id === pub.id); assert.strictEqual(notes[0].text, 'x'); }); test('[HTL] public-post ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ã‹ã‚‰è¦‹ã‚Œãªã„', async () => { const res = await api('notes/timeline', { limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === pub.id); + const notes = res.body.filter(n => n.id === pub.id); assert.strictEqual(notes.length, 0); }); test('[HTL] followers-post ㌠フォãƒãƒ¯ãƒ¼ã‹ã‚‰è¦‹ã‚Œã‚‹', async () => { const res = await api('notes/timeline', { limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === fol.id); + const notes = res.body.filter(n => n.id === fol.id); assert.strictEqual(notes[0].text, 'x'); }); //#endregion @@ -433,21 +433,21 @@ describe('API visibility', () => { test('[replies] followers-reply ㌠フォãƒãƒ¯ãƒ¼ã‹ã‚‰è¦‹ã‚Œã‚‹', async () => { const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, follower); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); + const notes = res.body.filter(n => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[replies] followers-reply ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (リプライ先ã§ã¯ãªã„) ã‹ã‚‰è¦‹ã‚Œãªã„', async () => { const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, other); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); + const notes = res.body.filter(n => n.id === folR.id); assert.strictEqual(notes.length, 0); }); test('[replies] followers-reply ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (リプライ先ã§ã‚ã‚‹) ã‹ã‚‰è¦‹ã‚Œã‚‹', async () => { const res = await api('notes/replies', { noteId: tgt.id, limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); + const notes = res.body.filter(n => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); //#endregion @@ -456,14 +456,14 @@ describe('API visibility', () => { test('[mentions] followers-reply ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (リプライ先ã§ã‚ã‚‹) ã‹ã‚‰è¦‹ã‚Œã‚‹', async () => { const res = await api('notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folR.id); + const notes = res.body.filter(n => n.id === folR.id); assert.strictEqual(notes[0].text, 'x'); }); test('[mentions] followers-mention ㌠éžãƒ•ã‚©ãƒãƒ¯ãƒ¼ (メンション先ã§ã‚ã‚‹) ã‹ã‚‰è¦‹ã‚Œã‚‹', async () => { const res = await api('notes/mentions', { limit: 100 }, target); assert.strictEqual(res.status, 200); - const notes = res.body.filter((n: any) => n.id === folM.id); + const notes = res.body.filter(n => n.id === folM.id); assert.strictEqual(notes[0].text, '@target x'); }); //#endregion diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index e4f798498f56dd0810f78ee75fd66dac9941e6a3..35b0e59383fba0fd51be1e17d8dd30942740b440 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -6,7 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, post, signup } from '../utils.js'; +import { api, castAsError, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Block', () => { @@ -33,7 +33,7 @@ describe('Block', () => { const res = await api('following/create', { userId: alice.id }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); + assert.strictEqual(castAsError(res.body).error.id, 'c4ab57cc-4e41-45e9-bfd9-584f61e35ce0'); }); test('ブãƒãƒƒã‚¯ã•ã‚Œã¦ã„るユーザーã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ããªã„', async () => { @@ -42,7 +42,8 @@ describe('Block', () => { const res = await api('notes/reactions/create', { noteId: note.id, reaction: 'ðŸ‘' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.id, '20ef5475-9f38-4e4c-bd33-de6d979498ec'); }); test('ブãƒãƒƒã‚¯ã•ã‚Œã¦ã„るユーザーã«è¿”ä¿¡ã§ããªã„', async () => { @@ -51,7 +52,8 @@ describe('Block', () => { const res = await api('notes/create', { replyId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); }); test('ブãƒãƒƒã‚¯ã•ã‚Œã¦ã„るユーザーã®ãƒŽãƒ¼ãƒˆã‚’Renoteã§ããªã„', async () => { @@ -60,7 +62,7 @@ describe('Block', () => { const res = await api('notes/create', { renoteId: note.id, text: 'yo' }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); + assert.strictEqual(castAsError(res.body).error.id, 'b390d7e1-8a5e-46ed-b625-06271cafd3d3'); }); // TODO: ユーザーリストã«å…¥ã‚Œã‚‰ã‚Œãªã„テスト diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index ba6f9d6a651c05565522a6624f4de31ca7713f98..a130c3698db768b9a6f2860aba6a9d4ffe3c44b3 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -79,14 +79,14 @@ describe('クリップ', () => { }; const deleteClip = async (parameters: Misskey.entities.ClipsDeleteRequest, request: Partial<ApiRequest<'clips/delete'>> = {}): Promise<void> => { - return await successfulApiCall({ + await successfulApiCall({ endpoint: 'clips/delete', parameters, user: alice, ...request, }, { status: 204, - }) as any as void; + }); }; const show = async (parameters: Misskey.entities.ClipsShowRequest, request: Partial<ApiRequest<'clips/show'>> = {}): Promise<Misskey.entities.Clip> => { @@ -153,8 +153,7 @@ describe('クリップ', () => { }); test('ã®ä½œæˆã¯ãƒãƒªã‚·ãƒ¼ã§å®šã‚られãŸæ•°ä»¥ä¸Šã¯ã§ããªã„。', async () => { - // ãƒãƒªã‚·ãƒ¼ + 1ã¾ã§ä½œã‚Œã‚‹ã¨ã„ã†æ‰€ãŒãƒŸã‚½ - const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + const clipLimit = DEFAULT_POLICIES.clipLimit; for (let i = 0; i < clipLimit; i++) { await create(); } @@ -327,7 +326,7 @@ describe('クリップ', () => { }); test('ã®ä¸€è¦§(clips/list)ãŒå–å¾—ã§ãã‚‹(上é™ã„ã£ã±ã„)', async () => { - const clipLimit = DEFAULT_POLICIES.clipLimit + 1; + const clipLimit = DEFAULT_POLICIES.clipLimit; const clips = await createMany({}, clipLimit); const res = await list({ parameters: { limit: 1 }, // FIXME: 無視ã•ã‚Œã¦11全部返ã£ã¦ãã‚‹ @@ -455,25 +454,25 @@ describe('クリップ', () => { let aliceClip: Misskey.entities.Clip; const favorite = async (parameters: Misskey.entities.ClipsFavoriteRequest, request: Partial<ApiRequest<'clips/favorite'>> = {}): Promise<void> => { - return successfulApiCall({ + await successfulApiCall({ endpoint: 'clips/favorite', parameters, user: alice, ...request, }, { status: 204, - }) as any as void; + }); }; const unfavorite = async (parameters: Misskey.entities.ClipsUnfavoriteRequest, request: Partial<ApiRequest<'clips/unfavorite'>> = {}): Promise<void> => { - return successfulApiCall({ + await successfulApiCall({ endpoint: 'clips/unfavorite', parameters, user: alice, ...request, }, { status: 204, - }) as any as void; + }); }; const myFavorites = async (request: Partial<ApiRequest<'clips/my-favorites'>> = {}): Promise<Misskey.entities.Clip[]> => { @@ -705,7 +704,7 @@ describe('クリップ', () => { // TODO: 17000msãらã„ã‹ã‹ã‚‹... test('ã‚’ãƒãƒªã‚·ãƒ¼ã§å®šã‚られãŸä¸Šé™ã„ã£ã±ã„(200)を超ãˆã¦è¿½åŠ ã¯ã§ããªã„。', async () => { - const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit + 1; + const noteLimit = DEFAULT_POLICIES.noteEachClipsLimit; const noteList = await Promise.all([...Array(noteLimit)].map((_, i) => post(alice, { text: `test ${i}`, }) as unknown)) as Misskey.entities.Note[]; diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts index 828c5200ef0ddcd11aa79bd6406c0301fc6d6f8e..43a73163ebabc65600ad233db8ed35630d447e57 100644 --- a/packages/backend/test/e2e/drive.ts +++ b/packages/backend/test/e2e/drive.ts @@ -23,7 +23,7 @@ describe('Drive', () => { const marker = Math.random().toString(); - const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'; + const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'; const catcher = makeStreamCatcher( alice, @@ -41,14 +41,14 @@ describe('Drive', () => { const file = await catcher; assert.strictEqual(res.status, 204); - assert.strictEqual(file.name, 'Lenna.jpg'); + assert.strictEqual(file.name, '192.jpg'); assert.strictEqual(file.type, 'image/jpeg'); }); test('ãƒãƒ¼ã‚«ãƒ«ã‹ã‚‰ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã§ãã‚‹', async () => { // APIレスãƒãƒ³ã‚¹ã‚’直接使用ã™ã‚‹ã®ã§ utils.js uploadFile ãŒé€šéŽã™ã‚‹ã“ã¨ã§æˆåŠŸã¨ã™ã‚‹ - const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画åƒ' }); + const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画åƒ' }); assert.strictEqual(res.body?.name, 'テスト画åƒ.jpg'); assert.strictEqual(res.body.type, 'image/jpeg'); diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index bc89dc37f4d88fa0e7d6b0ca7e152c3fc6c0739f..5aaec7f6f9d0ec5d05a9a83da17f7c511b18466a 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -10,7 +10,7 @@ import * as assert from 'assert'; // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; import { MiUser } from '@/models/_.js'; -import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; +import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Endpoints', () => { @@ -117,12 +117,21 @@ describe('Endpoints', () => { assert.strictEqual(res.body.birthday, myBirthday); }); - test('åå‰ã‚’空白ã«ã§ãã‚‹', async () => { + test('åå‰ã‚’空白ã®ã¿ã«ã—ãŸå ´åˆnullã«ãªã‚‹', async () => { const res = await api('i/update', { name: ' ', }, alice); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.name, ' '); + assert.strictEqual(res.body.name, null); + }); + + test('åå‰ã®å‰å¾Œã«ç©ºç™½ï¼ˆãƒ›ãƒ¯ã‚¤ãƒˆã‚¹ãƒšãƒ¼ã‚¹ï¼‰ã‚’入れã¦ã‚‚トリムã•ã‚Œã‚‹', async () => { + const res = await api('i/update', { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space + name: ' ゠ㄠㆠ\u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff', + }, alice); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.name, 'ã‚ ã„ ã†'); }); test('誕生日ã®è¨å®šã‚’削除ã§ãã‚‹', async () => { @@ -155,7 +164,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body.id, alice.id); + assert.strictEqual((res.body as unknown as { id: string }).id, alice.id); }); test('ユーザーãŒå˜åœ¨ã—ãªã‹ã£ãŸã‚‰æ€’ã‚‹', async () => { @@ -266,6 +275,68 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 400); }); + test('リノートã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ããªã„', async () => { + const bobNote = await post(bob, { text: 'hi' }); + const bobRenote = await post(bob, { renoteId: bobNote.id }); + + const res = await api('notes/reactions/create', { + noteId: bobRenote.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 400); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.code, 'CANNOT_REACT_TO_RENOTE'); + }); + + test('引用ã«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ãã‚‹', async () => { + const bobNote = await post(bob, { text: 'hi' }); + const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id }); + + const res = await api('notes/reactions/create', { + noteId: bobRenote.id, + reaction: '🚀', + }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('空文å—列ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã¯\u2764ã«ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ã•ã‚Œã‚‹', async () => { + const bobNote = await post(bob, { text: 'hi' }); + + const res = await api('notes/reactions/create', { + noteId: bobNote.id, + reaction: '', + }, alice); + + assert.strictEqual(res.status, 204); + + const reaction = await api('notes/reactions', { + noteId: bobNote.id, + }); + + assert.strictEqual(reaction.body.length, 1); + assert.strictEqual(reaction.body[0].type, '\u2764'); + }); + + test('絵文å—ã§ã¯ãªã„æ–‡å—列ã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã¯\u2764ã«ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ã•ã‚Œã‚‹', async () => { + const bobNote = await post(bob, { text: 'hi' }); + + const res = await api('notes/reactions/create', { + noteId: bobNote.id, + reaction: 'Hello!', + }, alice); + + assert.strictEqual(res.status, 204); + + const reaction = await api('notes/reactions', { + noteId: bobNote.id, + }); + + assert.strictEqual(reaction.body.length, 1); + assert.strictEqual(reaction.body[0].type, '\u2764'); + }); + test('空ã®ãƒ‘ラメータã§æ€’られる', async () => { // @ts-expect-error param must not be empty const res = await api('notes/reactions/create', {}, alice); @@ -523,7 +594,7 @@ describe('Endpoints', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); - assert.strictEqual(res.body!.name, 'Lenna.jpg'); + assert.strictEqual(res.body!.name, '192.jpg'); }); test('ファイルã«åå‰ã‚’付ã‘られる', async () => { @@ -993,7 +1064,7 @@ describe('Endpoints', () => { userId: bob.id, }, alice); assert.strictEqual(res1.status, 204); - assert.strictEqual(res2.body?.memo, memo); + assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo); }); test('自分ã«é–¢ã™ã‚‹ãƒ¡ãƒ¢ã‚’æ›´æ–°ã§ãã‚‹', async () => { @@ -1008,7 +1079,7 @@ describe('Endpoints', () => { userId: alice.id, }, alice); assert.strictEqual(res1.status, 204); - assert.strictEqual(res2.body?.memo, memo); + assert.strictEqual((res2.body as unknown as { memo: string })?.memo, memo); }); test('メモを削除ã§ãã‚‹', async () => { @@ -1029,7 +1100,7 @@ describe('Endpoints', () => { }, alice); // memoã«ã¯å¸¸ã«æ–‡å—列ã‹nullãŒå…¥ã£ã¦ã„ã‚‹(5cac151) - assert.strictEqual(res.body.memo, null); + assert.strictEqual((res.body as unknown as { memo: string | null }).memo, null); }); test('メモã¯å€‹äººã”ã¨ã«ç‹¬ç«‹ã—ã¦ä¿å˜ã•ã‚Œã‚‹', async () => { @@ -1056,8 +1127,8 @@ describe('Endpoints', () => { }, carol), ]); - assert.strictEqual(resAlice.body.memo, memoAliceToBob); - assert.strictEqual(resCarol.body.memo, memoCarolToBob); + assert.strictEqual((resAlice.body as unknown as { memo: string }).memo, memoAliceToBob); + assert.strictEqual((resCarol.body as unknown as { memo: string }).memo, memoCarolToBob); }); }); }); diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts index 80a5331a6dcecf2bf38f55730cd91c3a174e0bf6..4bcecc9716cb5a7d5452c88fe2f69ebc844a4b27 100644 --- a/packages/backend/test/e2e/exports.ts +++ b/packages/backend/test/e2e/exports.ts @@ -61,14 +61,14 @@ describe('export-clips', () => { }); test('basic export', async () => { - let res = await api('clips/create', { + const res1 = await api('clips/create', { name: 'foo', description: 'bar', }, alice); - assert.strictEqual(res.status, 200); + assert.strictEqual(res1.status, 200); - res = await api('i/export-clips', {}, alice); - assert.strictEqual(res.status, 204); + const res2 = await api('i/export-clips', {}, alice); + assert.strictEqual(res2.status, 204); const exported = await pollFirstDriveFile(); assert.strictEqual(exported[0].name, 'foo'); @@ -77,7 +77,7 @@ describe('export-clips', () => { }); test('export with notes', async () => { - let res = await api('clips/create', { + const res = await api('clips/create', { name: 'foo', description: 'bar', }, alice); @@ -96,15 +96,15 @@ describe('export-clips', () => { }); for (const note of [note1, note2]) { - res = await api('clips/add-note', { + const res2 = await api('clips/add-note', { clipId: clip.id, noteId: note.id, }, alice); - assert.strictEqual(res.status, 204); + assert.strictEqual(res2.status, 204); } - res = await api('i/export-clips', {}, alice); - assert.strictEqual(res.status, 204); + const res3 = await api('i/export-clips', {}, alice); + assert.strictEqual(res3.status, 204); const exported = await pollFirstDriveFile(); assert.strictEqual(exported[0].name, 'foo'); @@ -116,19 +116,19 @@ describe('export-clips', () => { }); test('multiple clips', async () => { - let res = await api('clips/create', { + const res1 = await api('clips/create', { name: 'kawaii', description: 'kawaii', }, alice); - assert.strictEqual(res.status, 200); - const clip1 = res.body; + assert.strictEqual(res1.status, 200); + const clip1 = res1.body; - res = await api('clips/create', { + const res2 = await api('clips/create', { name: 'yuri', description: 'yuri', }, alice); - assert.strictEqual(res.status, 200); - const clip2 = res.body; + assert.strictEqual(res2.status, 200); + const clip2 = res2.body; const note1 = await post(alice, { text: 'baz1', @@ -138,20 +138,26 @@ describe('export-clips', () => { text: 'baz2', }); - res = await api('clips/add-note', { - clipId: clip1.id, - noteId: note1.id, - }, alice); - assert.strictEqual(res.status, 204); + { + const res = await api('clips/add-note', { + clipId: clip1.id, + noteId: note1.id, + }, alice); + assert.strictEqual(res.status, 204); + } - res = await api('clips/add-note', { - clipId: clip2.id, - noteId: note2.id, - }, alice); - assert.strictEqual(res.status, 204); + { + const res = await api('clips/add-note', { + clipId: clip2.id, + noteId: note2.id, + }, alice); + assert.strictEqual(res.status, 204); + } - res = await api('i/export-clips', {}, alice); - assert.strictEqual(res.status, 204); + { + const res = await api('i/export-clips', {}, alice); + assert.strictEqual(res.status, 204); + } const exported = await pollFirstDriveFile(); assert.strictEqual(exported[0].name, 'kawaii'); @@ -163,7 +169,7 @@ describe('export-clips', () => { }); test('Clipping other user\'s note', async () => { - let res = await api('clips/create', { + const res = await api('clips/create', { name: 'kawaii', description: 'kawaii', }, alice); @@ -175,14 +181,14 @@ describe('export-clips', () => { visibility: 'followers', }); - res = await api('clips/add-note', { + const res2 = await api('clips/add-note', { clipId: clip.id, noteId: note.id, }, alice); - assert.strictEqual(res.status, 204); + assert.strictEqual(res2.status, 204); - res = await api('i/export-clips', {}, alice); - assert.strictEqual(res.status, 204); + const res3 = await api('i/export-clips', {}, alice); + assert.strictEqual(res3.status, 204); const exported = await pollFirstDriveFile(); assert.strictEqual(exported[0].name, 'kawaii'); diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index 4851ed14be6ac96b446286608ec1eab192df163e..7efd688ec279186c5c276ca96a2dd16b01bc7b88 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -153,6 +153,23 @@ describe('Webリソース', () => { path: path('nonexisting'), status: 404, })); + + describe(' has entry such ', () => { + beforeEach(() => { + post(alice, { text: "**a**" }) + }); + + test('MFMã‚’å«ã¾ãªã„。', async () => { + const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text()); + const _body: unknown = content.body; + // JSONフィードã®ã¨ãã¯æ”¹ã‚ã¦æ–‡å—列化ã™ã‚‹ + const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string; + + if (body.includes("**a**")) { + throw new Error("MFM shouldn't be included"); + } + }); + }) }); describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 35050130dc1be23f8f3a4576b16359ab40b2cc43..fd798bdb25bc40ab399900cea13975960e875e8f 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -7,19 +7,20 @@ import { INestApplicationContext } from '@nestjs/common'; process.env.NODE_ENV = 'test'; +import { setTimeout } from 'node:timers/promises'; import * as assert from 'assert'; import { loadConfig } from '@/config.js'; -import { MiUser, UsersRepository } from '@/models/_.js'; +import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { jobQueue } from '@/boot/common.js'; -import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js'; +import { api, castAsError, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Account Move', () => { let jq: INestApplicationContext; let url: URL; - let root: any; + let root: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse; let carol: misskey.entities.SignupResponse; @@ -42,7 +43,7 @@ describe('Account Move', () => { dave = await signup({ username: 'dave' }); eve = await signup({ username: 'eve' }); frank = await signup({ username: 'frank' }); - Users = connection.getRepository(MiUser); + Users = connection.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>); }, 1000 * 60 * 2); afterAll(async () => { @@ -92,8 +93,8 @@ describe('Account Move', () => { }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); - assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER'); + assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Unable to add duplicated aliases to alsoKnownAs', async () => { @@ -102,8 +103,8 @@ describe('Account Move', () => { }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'INVALID_PARAM'); - assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532'); + assert.strictEqual(castAsError(res.body).error.code, 'INVALID_PARAM'); + assert.strictEqual(castAsError(res.body).error.id, '3d81ceae-475f-4600-b2a8-2bc116157532'); }); test('Unable to add itself', async () => { @@ -112,8 +113,8 @@ describe('Account Move', () => { }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'FORBIDDEN_TO_SET_YOURSELF'); - assert.strictEqual(res.body.error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); + assert.strictEqual(castAsError(res.body).error.code, 'FORBIDDEN_TO_SET_YOURSELF'); + assert.strictEqual(castAsError(res.body).error.id, '25c90186-4ab0-49c8-9bba-a1fa6c202ba4'); }); test('Unable to add a nonexisting local account to alsoKnownAs', async () => { @@ -122,16 +123,16 @@ describe('Account Move', () => { }, bob); assert.strictEqual(res1.status, 400); - assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER'); - assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(castAsError(res1.body).error.code, 'NO_SUCH_USER'); + assert.strictEqual(castAsError(res1.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); const res2 = await api('i/update', { alsoKnownAs: ['@alice', 'nonexist'], }, bob); assert.strictEqual(res2.status, 400); - assert.strictEqual(res2.body.error.code, 'NO_SUCH_USER'); - assert.strictEqual(res2.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(castAsError(res2.body).error.code, 'NO_SUCH_USER'); + assert.strictEqual(castAsError(res2.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Able to add two existing local account to alsoKnownAs', async () => { @@ -240,8 +241,8 @@ describe('Account Move', () => { }, root); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NOT_ROOT_FORBIDDEN'); - assert.strictEqual(res.body.error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); + assert.strictEqual(castAsError(res.body).error.code, 'NOT_ROOT_FORBIDDEN'); + assert.strictEqual(castAsError(res.body).error.id, '4362e8dc-731f-4ad8-a694-be2a88922a24'); }); test('Unable to move to a nonexisting local account', async () => { @@ -250,8 +251,8 @@ describe('Account Move', () => { }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NO_SUCH_USER'); - assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); + assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_USER'); + assert.strictEqual(castAsError(res.body).error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5'); }); test('Unable to move if alsoKnownAs is invalid', async () => { @@ -260,8 +261,8 @@ describe('Account Move', () => { }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); - assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS'); + assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); }); test('Relationships have been properly migrated', async () => { @@ -271,43 +272,51 @@ describe('Account Move', () => { assert.strictEqual(move.status, 200); - await sleep(1000 * 3); // wait for jobs to finish + await setTimeout(1000 * 3); // wait for jobs to finish // Unfollow delayed? const aliceFollowings = await api('users/following', { userId: alice.id, }, alice); assert.strictEqual(aliceFollowings.status, 200); + assert.ok(aliceFollowings); assert.strictEqual(aliceFollowings.body.length, 3); const carolFollowings = await api('users/following', { userId: carol.id, }, carol); assert.strictEqual(carolFollowings.status, 200); + assert.ok(carolFollowings); assert.strictEqual(carolFollowings.body.length, 2); assert.strictEqual(carolFollowings.body[0].followeeId, bob.id); assert.strictEqual(carolFollowings.body[1].followeeId, alice.id); const blockings = await api('blocking/list', {}, dave); assert.strictEqual(blockings.status, 200); + assert.ok(blockings); assert.strictEqual(blockings.body.length, 2); assert.strictEqual(blockings.body[0].blockeeId, bob.id); assert.strictEqual(blockings.body[1].blockeeId, alice.id); const mutings = await api('mute/list', {}, dave); assert.strictEqual(mutings.status, 200); + assert.ok(mutings); assert.strictEqual(mutings.body.length, 2); assert.strictEqual(mutings.body[0].muteeId, bob.id); assert.strictEqual(mutings.body[1].muteeId, alice.id); const rootLists = await api('users/lists/list', {}, root); assert.strictEqual(rootLists.status, 200); + assert.ok(rootLists); + assert.ok(rootLists.body[0].userIds); assert.strictEqual(rootLists.body[0].userIds.length, 2); assert.ok(rootLists.body[0].userIds.find((id: string) => id === bob.id)); assert.ok(rootLists.body[0].userIds.find((id: string) => id === alice.id)); const eveLists = await api('users/lists/list', {}, eve); assert.strictEqual(eveLists.status, 200); + assert.ok(eveLists); + assert.ok(eveLists.body[0].userIds); assert.strictEqual(eveLists.body[0].userIds.length, 1); assert.ok(eveLists.body[0].userIds.find((id: string) => id === bob.id)); }); @@ -330,7 +339,7 @@ describe('Account Move', () => { }); test('Unfollowed after 10 sec (24 hours in production).', async () => { - await sleep(1000 * 8); + await setTimeout(1000 * 8); const following = await api('users/following', { userId: alice.id, @@ -346,8 +355,8 @@ describe('Account Move', () => { }, bob); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'DESTINATION_ACCOUNT_FORBIDS'); - assert.strictEqual(res.body.error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); + assert.strictEqual(castAsError(res.body).error.code, 'DESTINATION_ACCOUNT_FORBIDS'); + assert.strictEqual(castAsError(res.body).error.id, 'b5c90186-4ab0-49c8-9bba-a1f766282ba4'); }); test('Follow and follower counts are properly adjusted', async () => { @@ -418,8 +427,9 @@ describe('Account Move', () => { ] as const)('Prohibit access after moving: %s', async (endpoint) => { const res = await api(endpoint, {}, alice); assert.strictEqual(res.status, 403); - assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit access after moving: /antennas/update', async () => { @@ -437,16 +447,19 @@ describe('Account Move', () => { }, alice); assert.strictEqual(res.status, 403); - assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit access after moving: /drive/files/create', async () => { + // FIXME: 一旦逃ã’ã¦ãŠã const res = await uploadFile(alice); assert.strictEqual(res.status, 403); - assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual((res.body! as any as { error: misskey.api.APIError }).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); test('Prohibit updating alsoKnownAs after moving', async () => { @@ -455,8 +468,8 @@ describe('Account Move', () => { }, alice); assert.strictEqual(res.status, 403); - assert.strictEqual(res.body.error.code, 'YOUR_ACCOUNT_MOVED'); - assert.strictEqual(res.body.error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); + assert.strictEqual(castAsError(res.body).error.code, 'YOUR_ACCOUNT_MOVED'); + assert.strictEqual(castAsError(res.body).error.id, '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31'); }); }); }); diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index 0e52c5decc37429bf09e21d548829f600b43bf5a..f37da288b776a11698161aa25125aab971c650b8 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -47,8 +47,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test('ミュートã—ã¦ã„るユーザーã‹ã‚‰ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã•ã‚Œã¦ã‚‚ã€hasUnreadMentions ㌠true ã«ãªã‚‰ãªã„', async () => { @@ -92,9 +92,9 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test('タイムラインã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ã®RenoteãŒå«ã¾ã‚Œãªã„', async () => { @@ -108,9 +108,9 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); }); @@ -124,8 +124,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒªãƒ—ライãŒå«ã¾ã‚Œãªã„', async () => { @@ -138,8 +138,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒªãƒ—ライãŒå«ã¾ã‚Œãªã„', async () => { @@ -152,8 +152,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®å¼•ç”¨ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -166,8 +166,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -180,8 +180,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼é€šçŸ¥ãŒå«ã¾ã‚Œãªã„', async () => { @@ -193,8 +193,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); await api('following/delete', { userId: alice.id }, bob); await api('following/delete', { userId: alice.id }, carol); @@ -210,8 +210,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); await api('following/delete', { userId: alice.id }, bob); await api('following/delete', { userId: alice.id }, carol); @@ -228,8 +228,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒªãƒ—ライãŒå«ã¾ã‚Œãªã„', async () => { const aliceNote = await post(alice, { text: 'hi' }); @@ -241,8 +241,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒªãƒ—ライãŒå«ã¾ã‚Œãªã„', async () => { @@ -255,8 +255,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®å¼•ç”¨ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -269,8 +269,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -283,8 +283,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); test('通知ã«ãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã‹ã‚‰ã®ãƒ•ã‚©ãƒãƒ¼é€šçŸ¥ãŒå«ã¾ã‚Œãªã„', async () => { @@ -296,8 +296,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); await api('following/delete', { userId: alice.id }, bob); await api('following/delete', { userId: alice.id }, carol); @@ -313,8 +313,8 @@ describe('Mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === bob.id), true); - assert.strictEqual(res.body.some((notification: any) => notification.userId === carol.id), false); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), false); }); }); }); diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index bda31d964074a8905cc8bfd4cbf7ebe24ee7e39a..5937eb9b4924a7961d0b545dc466a15c5079fc38 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -3,16 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import type { Repository } from "typeorm"; + 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, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; +import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Note', () => { - let Notes: any; + let Notes: Repository<MiNote>; let root: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse; @@ -41,7 +43,7 @@ describe('Note', () => { }); test('ファイルを添付ã§ãã‚‹', async () => { - const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); const res = await api('notes/create', { fileIds: [file.id], @@ -53,7 +55,7 @@ describe('Note', () => { }, 1000 * 10); test('他人ã®ãƒ•ã‚¡ã‚¤ãƒ«ã§æ€’られる', async () => { - const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); + const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); const res = await api('notes/create', { text: 'test', @@ -61,8 +63,8 @@ describe('Note', () => { }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); - assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); + assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }, 1000 * 10); test('å˜åœ¨ã—ãªã„ファイルã§æ€’られる', async () => { @@ -72,8 +74,8 @@ describe('Note', () => { }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); - assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); + assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); test('ä¸æ£ãªãƒ•ã‚¡ã‚¤ãƒ«IDã§æ€’られる', async () => { @@ -81,8 +83,8 @@ describe('Note', () => { fileIds: ['kyoppie'], }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NO_SUCH_FILE'); - assert.strictEqual(res.body.error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); + assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_FILE'); + assert.strictEqual(castAsError(res.body).error.id, 'b6992544-63e7-67f0-fa7f-32444b1b5306'); }); test('返信ã§ãã‚‹', async () => { @@ -101,6 +103,7 @@ describe('Note', () => { assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); + assert.ok(res.body.createdNote.reply); assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); }); @@ -118,6 +121,7 @@ describe('Note', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.ok(res.body.createdNote.renote); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); @@ -137,6 +141,7 @@ describe('Note', () => { assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(res.body.createdNote.text, alicePost.text); assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.ok(res.body.createdNote.renote); assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); @@ -218,7 +223,7 @@ describe('Note', () => { }, bob); assert.strictEqual(bobReply.status, 400); - assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE'); + assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE'); }); test('visibility: specifiedãªãƒŽãƒ¼ãƒˆã«å¯¾ã—ã¦visibility: specifiedã§è¿”ä¿¡ã§ãã‚‹', async () => { @@ -256,7 +261,7 @@ describe('Note', () => { }, bob); assert.strictEqual(bobReply.status, 400); - assert.strictEqual(bobReply.body.error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY'); + assert.strictEqual(castAsError(bobReply.body).error.code, 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY'); }); test('æ–‡å—æ•°ãŽã‚ŠãŽã‚Šã§æ€’られãªã„', async () => { @@ -333,6 +338,7 @@ describe('Note', () => { assert.strictEqual(res.body.createdNote.text, post.text); const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id }); + assert.ok(noteDoc); assert.deepStrictEqual(noteDoc.mentions, [bob.id]); }); @@ -345,6 +351,7 @@ describe('Note', () => { assert.strictEqual(res.status, 200); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.ok(res.body.createdNote.files); assert.strictEqual(res.body.createdNote.files.length, 1); assert.strictEqual(res.body.createdNote.files[0].id, file.body!.id); }); @@ -363,8 +370,9 @@ describe('Note', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - const myNote = res.body.find((note: { id: string; files: { id: string }[] }) => note.id === createdNote.body.createdNote.id); - assert.notEqual(myNote, null); + const myNote = res.body.find(note => note.id === createdNote.body.createdNote.id); + assert.ok(myNote); + assert.ok(myNote.files); assert.strictEqual(myNote.files.length, 1); assert.strictEqual(myNote.files[0].id, file.body!.id); }); @@ -389,7 +397,9 @@ describe('Note', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); - assert.notEqual(myNote, null); + assert.ok(myNote); + assert.ok(myNote.renote); + assert.ok(myNote.renote.files); assert.strictEqual(myNote.renote.files.length, 1); assert.strictEqual(myNote.renote.files[0].id, file.body!.id); }); @@ -415,7 +425,9 @@ describe('Note', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === reply.body.createdNote.id); - assert.notEqual(myNote, null); + assert.ok(myNote); + assert.ok(myNote.reply); + assert.ok(myNote.reply.files); assert.strictEqual(myNote.reply.files.length, 1); assert.strictEqual(myNote.reply.files[0].id, file.body!.id); }); @@ -446,7 +458,10 @@ describe('Note', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); const myNote = res.body.find((note: { id: string }) => note.id === renoted.body.createdNote.id); - assert.notEqual(myNote, null); + assert.ok(myNote); + assert.ok(myNote.renote); + assert.ok(myNote.renote.reply); + assert.ok(myNote.renote.reply.files); assert.strictEqual(myNote.renote.reply.files.length, 1); assert.strictEqual(myNote.renote.reply.files[0].id, file.body!.id); }); @@ -474,7 +489,7 @@ describe('Note', () => { priority: 0, value: true, }, - } as any, + }, }, root); assert.strictEqual(res.status, 200); @@ -498,7 +513,7 @@ describe('Note', () => { }, alice); assert.strictEqual(liftnsfw.status, 400); - assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE'); + assert.strictEqual(castAsError(liftnsfw.body).error.code, 'RESTRICTED_BY_ROLE'); const oldaddnsfw = await api('drive/files/update', { fileId: file.body!.id, @@ -710,7 +725,7 @@ describe('Note', () => { }, alice); assert.strictEqual(note1.status, 400); - assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); + assert.strictEqual(castAsError(note1.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); }); test('ç¦æ¢ãƒ¯ãƒ¼ãƒ‰ã‚’å«ã‚€æŠ•ç¨¿ã¯ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ (æ£è¦è¡¨ç¾)', async () => { @@ -727,7 +742,7 @@ describe('Note', () => { }, alice); assert.strictEqual(note2.status, 400); - assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); + assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); }); test('ç¦æ¢ãƒ¯ãƒ¼ãƒ‰ã‚’å«ã‚€æŠ•ç¨¿ã¯ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ (スペースアンド)', async () => { @@ -744,7 +759,7 @@ describe('Note', () => { }, alice); assert.strictEqual(note2.status, 400); - assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS'); + assert.strictEqual(castAsError(note2.body).error.code, 'CONTAINS_PROHIBITED_WORDS'); }); test('ç¦æ¢ãƒ¯ãƒ¼ãƒ‰ã‚’å«ã‚“ã§ã‚‹ãƒªãƒ¢ãƒ¼ãƒˆãƒŽãƒ¼ãƒˆã‚‚エラーã«ãªã‚‹', async () => { @@ -786,7 +801,7 @@ describe('Note', () => { priority: 1, value: 0, }, - } as any, + }, }, root); assert.strictEqual(res.status, 200); @@ -807,7 +822,7 @@ describe('Note', () => { }, alice); assert.strictEqual(note.status, 400); - assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS'); + assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS'); await api('admin/roles/unassign', { userId: alice.id, @@ -840,7 +855,7 @@ describe('Note', () => { priority: 1, value: 0, }, - } as any, + }, }, root); assert.strictEqual(res.status, 200); @@ -863,7 +878,7 @@ describe('Note', () => { }, alice); assert.strictEqual(note.status, 400); - assert.strictEqual(note.body.error.code, 'CONTAINS_TOO_MANY_MENTIONS'); + assert.strictEqual(castAsError(note.body).error.code, 'CONTAINS_TOO_MANY_MENTIONS'); await api('admin/roles/unassign', { userId: alice.id, @@ -896,7 +911,7 @@ describe('Note', () => { priority: 1, value: 1, }, - } as any, + }, }, root); assert.strictEqual(res.status, 200); @@ -951,6 +966,7 @@ describe('Note', () => { assert.strictEqual(deleteOneRes.status, 204); let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); + assert.ok(mainNote); assert.strictEqual(mainNote.repliesCount, 1); const deleteTwoRes = await api('notes/delete', { @@ -959,6 +975,7 @@ describe('Note', () => { assert.strictEqual(deleteTwoRes.status, 204); mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id }); + assert.ok(mainNote); assert.strictEqual(mainNote.repliesCount, 0); }); }); @@ -980,7 +997,7 @@ describe('Note', () => { }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'UNAVAILABLE'); + assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE'); }); afterAll(async () => { @@ -992,7 +1009,7 @@ describe('Note', () => { const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice); assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE'); + assert.strictEqual(castAsError(res.body).error.code, 'NO_SUCH_NOTE'); }); test('ä¸å¯è¦–ãªãƒŽãƒ¼ãƒˆã¯ç¿»è¨³ã§ããªã„', async () => { @@ -1000,7 +1017,7 @@ describe('Note', () => { const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob); assert.strictEqual(bobTranslateAttempt.status, 400); - assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE'); + assert.strictEqual(castAsError(bobTranslateAttempt.body).error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE'); }); test('text: null ãªãƒŽãƒ¼ãƒˆã‚’翻訳ã™ã‚‹ã¨ç©ºã®ãƒ¬ã‚¹ãƒãƒ³ã‚¹ãŒè¿”ã£ã¦ãã‚‹', async () => { @@ -1016,7 +1033,7 @@ describe('Note', () => { // NOTE: デフォルトã§ã¯ç™»éŒ²ã•ã‚Œã¦ã„ãªã„ã®ã§è½ã¡ã‚‹ assert.strictEqual(res.status, 400); - assert.strictEqual(res.body.error.code, 'UNAVAILABLE'); + assert.strictEqual(castAsError(res.body).error.code, 'UNAVAILABLE'); }); }); }); diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 1abbb4f04494e541b287bdc263fe41de15871793..0f636b9ae2058151f92e0d2725b82bf2aff47898 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -6,7 +6,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { api, post, signup, sleep, waitFire } from '../utils.js'; +import { setTimeout } from 'node:timers/promises'; +import { api, post, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Renote Mute', () => { @@ -35,15 +36,15 @@ describe('Renote Mute', () => { const carolNote = await post(carol, { text: 'hi' }); // redisã«è¿½åŠ ã•ã‚Œã‚‹ã®ã‚’待㤠- await sleep(100); + await setTimeout(100); const res = await api('notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolRenote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); }); test('タイムラインã«ãƒªãƒŽãƒ¼ãƒˆãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®å¼•ç”¨ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -52,15 +53,15 @@ describe('Renote Mute', () => { const carolNote = await post(carol, { text: 'hi' }); // redisã«è¿½åŠ ã•ã‚Œã‚‹ã®ã‚’待㤠- await sleep(100); + await setTimeout(100); const res = await api('notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolRenote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolRenote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); }); // #12956 @@ -69,14 +70,14 @@ describe('Renote Mute', () => { const bobRenote = await post(bob, { renoteId: carolNote.id }); // redisã«è¿½åŠ ã•ã‚Œã‚‹ã®ã‚’待㤠- await sleep(100); + await setTimeout(100); const res = await api('notes/local-timeline', {}, alice); assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobRenote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobRenote.id), true); }); test('ストリームã«ãƒªãƒŽãƒ¼ãƒˆãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®ãƒªãƒŽãƒ¼ãƒˆãŒæµã‚Œãªã„', async () => { diff --git a/packages/backend/test/e2e/reversi-game.ts b/packages/backend/test/e2e/reversi-game.ts new file mode 100644 index 0000000000000000000000000000000000000000..788255beac12118f52c5cf62a5ef4c58e624f306 --- /dev/null +++ b/packages/backend/test/e2e/reversi-game.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { ReversiMatchResponse } from 'misskey-js/entities.js'; +import { api, signup } from '../utils.js'; +import type * as misskey from 'misskey-js'; + +describe('ReversiGame', () => { + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + + beforeAll(async () => { + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 60 * 2); + + test('matches when alice invites bob and bob accepts', async () => { + const response1 = await api('reversi/match', { userId: bob.id }, alice); + assert.strictEqual(response1.status, 204); + assert.strictEqual(response1.body, null); + const response2 = await api('reversi/match', { userId: alice.id }, bob); + assert.strictEqual(response2.status, 200); + assert.notStrictEqual(response2.body, null); + const body = response2.body as ReversiMatchResponse; + assert.strictEqual(body.user1.id, alice.id); + assert.strictEqual(body.user2.id, bob.id); + }); +}); diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index b0a70074c6a37c8cddc0ef9b58e5f95de4e67abe..72f26a38e0fd1e87661b1c951d05ac33dafaeb71 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -34,6 +34,7 @@ describe('Streaming', () => { let kyoko: misskey.entities.SignupResponse; let chitose: misskey.entities.SignupResponse; let kanako: misskey.entities.SignupResponse; + let erin: misskey.entities.SignupResponse; // Remote users let akari: misskey.entities.SignupResponse; @@ -53,6 +54,7 @@ describe('Streaming', () => { kyoko = await signup({ username: 'kyoko' }); chitose = await signup({ username: 'chitose' }); kanako = await signup({ username: 'kanako' }); + erin = await signup({ username: 'erin' }); // erin: A generic fifth participant akari = await signup({ username: 'akari', host: 'example.com' }); chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); @@ -71,6 +73,12 @@ describe('Streaming', () => { // Follow: kyoko => chitose await api('following/create', { userId: chitose.id }, kyoko); + // Follow: erin <=> ayano each other. + // erin => ayano: withReplies: true + await api('following/create', { userId: ayano.id, withReplies: true }, erin); + // ayano => erin: withReplies: false + await api('following/create', { userId: erin.id, withReplies: false }, ayano); + // Mute: chitose => kanako await api('mute/create', { userId: kanako.id }, chitose); @@ -297,6 +305,28 @@ describe('Streaming', () => { assert.strictEqual(fired, true); }); + + test('withReplies: true ã®ã¨ã自分ã®followers投稿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライãŒæµã‚Œã‚‹', async () => { + const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + erin, 'homeTimeline', // erin:home + () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, true); + }); + + test('withReplies: false ã§ã‚‚自分ã®æŠ•ç¨¿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライãŒæµã‚Œã‚‹', async () => { + const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post + msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin + ); + + assert.strictEqual(fired, true); + }); }); // Home describe('Local Timeline', () => { @@ -475,6 +505,38 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); + + test('withReplies: true ã®ã¨ã自分ã®followers投稿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライãŒæµã‚Œã‚‹', async () => { + const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + erin, 'homeTimeline', // erin:home + () => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, true); + }); + + test('withReplies: false ã§ã‚‚自分ã®æŠ•ç¨¿ã«å¯¾ã™ã‚‹ãƒªãƒ—ライãŒæµã‚Œã‚‹', async () => { + const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post + msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin + ); + + assert.strictEqual(fired, true); + }); + + test('withReplies: true ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„人ã®followersノートã«å¯¾ã™ã‚‹ãƒªãƒ—ライãŒæµã‚Œãªã„', async () => { + const fired = await waitFire( + erin, 'homeTimeline', // erin:home + () => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano), // ayano reply to chitose's post + msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano + ); + + assert.strictEqual(fired, false); + }); }); describe('Global Timeline', () => { diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ce6e477812fed8ba31a0f8d751dab0dacded956 --- /dev/null +++ b/packages/backend/test/e2e/synalio/abuse-report.ts @@ -0,0 +1,360 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { entities } from 'misskey-js'; +import { beforeEach, describe, test } from '@jest/globals'; +import { + api, + captureWebhook, + randomString, + role, + signup, + startJobQueue, + UserToken, + WEBHOOK_HOST, +} from '../../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('[シナリオ] ãƒ¦ãƒ¼ã‚¶é€šå ±', () => { + let queue: INestApplicationContext; + let admin: entities.SignupResponse; + let alice: entities.SignupResponse; + let bob: entities.SignupResponse; + + async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> { + const res = await api( + 'admin/system-webhook/create', + { + isActive: true, + name: randomString(), + on: ['abuseReport'], + url: WEBHOOK_HOST, + secret: randomString(), + ...args, + }, + credential ?? admin, + ); + return res.body; + } + + async function createAbuseReportNotificationRecipient(args?: Partial<entities.AdminAbuseReportNotificationRecipientCreateRequest>, credential?: UserToken): Promise<entities.AdminAbuseReportNotificationRecipientCreateResponse> { + const res = await api( + 'admin/abuse-report/notification-recipient/create', + { + isActive: true, + name: randomString(), + method: 'webhook', + ...args, + }, + credential ?? admin, + ); + return res.body; + } + + async function createAbuseReport(args?: Partial<entities.UsersReportAbuseRequest>, credential?: UserToken): Promise<entities.EmptyResponse> { + const res = await api( + 'users/report-abuse', + { + userId: alice.id, + comment: randomString(), + ...args, + }, + credential ?? admin, + ); + return res.body; + } + + async function resolveAbuseReport(args?: Partial<entities.AdminResolveAbuseUserReportRequest>, credential?: UserToken): Promise<entities.EmptyResponse> { + const res = await api( + 'admin/resolve-abuse-user-report', + { + reportId: admin.id, + ...args, + }, + credential ?? admin, + ); + return res.body; + } + + // ------------------------------------------------------------------------------------------- + + beforeAll(async () => { + queue = await startJobQueue(); + admin = await signup({ username: 'admin' }); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + + await role(admin, { isAdministrator: true }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await queue.close(); + }); + + // ------------------------------------------------------------------------------------------- + + describe('SystemWebhook', () => { + beforeEach(async () => { + const webhooks = await api('admin/system-webhook/list', {}, admin); + for (const webhook of webhooks.body) { + await api('admin/system-webhook/delete', { id: webhook.id }, admin); + } + }); + + test('é€šå ±ã‚’å—ã‘㟠-> abuseReportãŒé€å‡ºã•ã‚Œã‚‹', async () => { + const webhook = await createSystemWebhook({ + on: ['abuseReport'], + isActive: true, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }); + + console.log(JSON.stringify(webhookBody, null, 2)); + + expect(webhookBody.hookId).toBe(webhook.id); + expect(webhookBody.type).toBe('abuseReport'); + expect(webhookBody.body.targetUserId).toBe(alice.id); + expect(webhookBody.body.reporterId).toBe(bob.id); + expect(webhookBody.body.comment).toBe(abuse.comment); + }); + + test('é€šå ±ã‚’å—ã‘㟠-> abuseReportãŒé€å‡ºã•ã‚Œã‚‹ -> 解決 -> abuseReportResolvedãŒé€å‡ºã•ã‚Œã‚‹', async () => { + const webhook = await createSystemWebhook({ + on: ['abuseReport', 'abuseReportResolved'], + isActive: true, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody1 = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }); + + console.log(JSON.stringify(webhookBody1, null, 2)); + expect(webhookBody1.hookId).toBe(webhook.id); + expect(webhookBody1.type).toBe('abuseReport'); + expect(webhookBody1.body.targetUserId).toBe(alice.id); + expect(webhookBody1.body.reporterId).toBe(bob.id); + expect(webhookBody1.body.assigneeId).toBeNull(); + expect(webhookBody1.body.resolved).toBe(false); + expect(webhookBody1.body.comment).toBe(abuse.comment); + + // 解決 + const webhookBody2 = await captureWebhook(async () => { + await resolveAbuseReport({ + reportId: webhookBody1.body.id, + forward: false, + }, admin); + }); + + console.log(JSON.stringify(webhookBody2, null, 2)); + expect(webhookBody2.hookId).toBe(webhook.id); + expect(webhookBody2.type).toBe('abuseReportResolved'); + expect(webhookBody2.body.targetUserId).toBe(alice.id); + expect(webhookBody2.body.reporterId).toBe(bob.id); + expect(webhookBody2.body.assigneeId).toBe(admin.id); + expect(webhookBody2.body.resolved).toBe(true); + expect(webhookBody2.body.comment).toBe(abuse.comment); + }); + + test('é€šå ±ã‚’å—ã‘㟠-> abuseReportãŒæœªè¨±å¯ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„', async () => { + const webhook = await createSystemWebhook({ + on: [], + isActive: true, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }).catch(e => e.message); + + expect(webhookBody).toBe('timeout'); + }); + + test('é€šå ±ã‚’å—ã‘㟠-> abuseReportãŒæœªè¨±å¯ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„ -> 解決 -> abuseReportResolvedãŒé€å‡ºã•ã‚Œã‚‹', async () => { + const webhook = await createSystemWebhook({ + on: ['abuseReportResolved'], + isActive: true, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody1 = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }).catch(e => e.message); + + expect(webhookBody1).toBe('timeout'); + + const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; + + // 解決 + const webhookBody2 = await captureWebhook(async () => { + await resolveAbuseReport({ + reportId: abuseReportId, + forward: false, + }, admin); + }); + + console.log(JSON.stringify(webhookBody2, null, 2)); + expect(webhookBody2.hookId).toBe(webhook.id); + expect(webhookBody2.type).toBe('abuseReportResolved'); + expect(webhookBody2.body.targetUserId).toBe(alice.id); + expect(webhookBody2.body.reporterId).toBe(bob.id); + expect(webhookBody2.body.assigneeId).toBe(admin.id); + expect(webhookBody2.body.resolved).toBe(true); + expect(webhookBody2.body.comment).toBe(abuse.comment); + }); + + test('é€šå ±ã‚’å—ã‘㟠-> abuseReportãŒé€å‡ºã•ã‚Œã‚‹ -> 解決 -> abuseReportResolvedãŒæœªè¨±å¯ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„', async () => { + const webhook = await createSystemWebhook({ + on: ['abuseReport'], + isActive: true, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody1 = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }); + + console.log(JSON.stringify(webhookBody1, null, 2)); + expect(webhookBody1.hookId).toBe(webhook.id); + expect(webhookBody1.type).toBe('abuseReport'); + expect(webhookBody1.body.targetUserId).toBe(alice.id); + expect(webhookBody1.body.reporterId).toBe(bob.id); + expect(webhookBody1.body.assigneeId).toBeNull(); + expect(webhookBody1.body.resolved).toBe(false); + expect(webhookBody1.body.comment).toBe(abuse.comment); + + // 解決 + const webhookBody2 = await captureWebhook(async () => { + await resolveAbuseReport({ + reportId: webhookBody1.body.id, + forward: false, + }, admin); + }).catch(e => e.message); + + expect(webhookBody2).toBe('timeout'); + }); + + test('é€šå ±ã‚’å—ã‘㟠-> abuseReportãŒæœªè¨±å¯ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„ -> 解決 -> abuseReportResolvedãŒæœªè¨±å¯ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„', async () => { + const webhook = await createSystemWebhook({ + on: [], + isActive: true, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody1 = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }).catch(e => e.message); + + expect(webhookBody1).toBe('timeout'); + + const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; + + // 解決 + const webhookBody2 = await captureWebhook(async () => { + await resolveAbuseReport({ + reportId: abuseReportId, + forward: false, + }, admin); + }).catch(e => e.message); + + expect(webhookBody2).toBe('timeout'); + }); + + test('é€šå ±ã‚’å—ã‘㟠-> WebhookãŒç„¡åŠ¹ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„', async () => { + const webhook = await createSystemWebhook({ + on: ['abuseReport', 'abuseReportResolved'], + isActive: false, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody1 = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }).catch(e => e.message); + + expect(webhookBody1).toBe('timeout'); + + const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; + + // 解決 + const webhookBody2 = await captureWebhook(async () => { + await resolveAbuseReport({ + reportId: abuseReportId, + forward: false, + }, admin); + }).catch(e => e.message); + + expect(webhookBody2).toBe('timeout'); + }); + + test('é€šå ±ã‚’å—ã‘㟠-> 通知è¨å®šãŒç„¡åŠ¹ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„', async () => { + const webhook = await createSystemWebhook({ + on: ['abuseReport', 'abuseReportResolved'], + isActive: true, + }); + await createAbuseReportNotificationRecipient({ systemWebhookId: webhook.id, isActive: false }); + + // é€šå ±(bob -> alice) + const abuse = { + userId: alice.id, + comment: randomString(), + }; + const webhookBody1 = await captureWebhook(async () => { + await createAbuseReport(abuse, bob); + }).catch(e => e.message); + + expect(webhookBody1).toBe('timeout'); + + const abuseReportId = (await api('admin/abuse-user-reports', {}, admin)).body[0].id; + + // 解決 + const webhookBody2 = await captureWebhook(async () => { + await resolveAbuseReport({ + reportId: abuseReportId, + forward: false, + }, admin); + }).catch(e => e.message); + + expect(webhookBody2).toBe('timeout'); + }); + }); +}); diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb0f68dfeab7e9626de241d9d9bb1095217f07fa --- /dev/null +++ b/packages/backend/test/e2e/synalio/user-create.ts @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setTimeout } from 'node:timers/promises'; +import { entities } from 'misskey-js'; +import { beforeEach, describe, test } from '@jest/globals'; +import { + api, + captureWebhook, + randomString, + role, + signup, + startJobQueue, + UserToken, + WEBHOOK_HOST, +} from '../../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('[シナリオ] ユーザ作æˆ', () => { + let queue: INestApplicationContext; + let admin: entities.SignupResponse; + + async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> { + const res = await api( + 'admin/system-webhook/create', + { + isActive: true, + name: randomString(), + on: ['userCreated'], + url: WEBHOOK_HOST, + secret: randomString(), + ...args, + }, + credential ?? admin, + ); + return res.body; + } + + // ------------------------------------------------------------------------------------------- + + beforeAll(async () => { + queue = await startJobQueue(); + admin = await signup({ username: 'admin' }); + + await role(admin, { isAdministrator: true }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await queue.close(); + }); + + // ------------------------------------------------------------------------------------------- + + describe('SystemWebhook', () => { + beforeEach(async () => { + const webhooks = await api('admin/system-webhook/list', {}, admin); + for (const webhook of webhooks.body) { + await api('admin/system-webhook/delete', { id: webhook.id }, admin); + } + }); + + test('ユーザãŒä½œæˆã•ã‚ŒãŸ -> userCreatedãŒé€å‡ºã•ã‚Œã‚‹', async () => { + const webhook = await createSystemWebhook({ + on: ['userCreated'], + isActive: true, + }); + + let alice: any = null; + const webhookBody = await captureWebhook(async () => { + alice = await signup({ username: 'alice' }); + }); + + // webhookã®é€å‡ºå¾Œã«ã„ã‚ã„ã‚ã‚„ã£ã¦ã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…ã¤å¿…è¦ãŒã‚ã‚‹ + await setTimeout(2000); + + console.log(alice); + console.log(JSON.stringify(webhookBody, null, 2)); + + expect(webhookBody.hookId).toBe(webhook.id); + expect(webhookBody.type).toBe('userCreated'); + + const body = webhookBody.body as entities.UserLite; + expect(alice.id).toBe(body.id); + expect(alice.name).toBe(body.name); + expect(alice.username).toBe(body.username); + expect(alice.host).toBe(body.host); + expect(alice.avatarUrl).toBe(body.avatarUrl); + expect(alice.avatarBlurhash).toBe(body.avatarBlurhash); + expect(alice.avatarDecorations).toEqual(body.avatarDecorations); + expect(alice.isBot).toBe(body.isBot); + expect(alice.isCat).toBe(body.isCat); + expect(alice.instance).toEqual(body.instance); + expect(alice.emojis).toEqual(body.emojis); + expect(alice.onlineStatus).toBe(body.onlineStatus); + expect(alice.badgeRoles).toEqual(body.badgeRoles); + }); + + test('ãƒ¦ãƒ¼ã‚¶ä½œæˆ -> userCreatedãŒæœªè¨±å¯ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„', async () => { + await createSystemWebhook({ + on: [], + isActive: true, + }); + + let alice: any = null; + const webhookBody = await captureWebhook(async () => { + alice = await signup({ username: 'alice' }); + }).catch(e => e.message); + + expect(webhookBody).toBe('timeout'); + expect(alice.id).not.toBeNull(); + }); + + test('ãƒ¦ãƒ¼ã‚¶ä½œæˆ -> WebhookãŒç„¡åŠ¹ã®å ´åˆã¯é€å‡ºã•ã‚Œãªã„', async () => { + await createSystemWebhook({ + on: ['userCreated'], + isActive: false, + }); + + let alice: any = null; + const webhookBody = await captureWebhook(async () => { + alice = await signup({ username: 'alice' }); + }).catch(e => e.message); + + expect(webhookBody).toBe('timeout'); + expect(alice.id).not.toBeNull(); + }); + }); +}); diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 53bb6eb765a6c224aaa82a0ad47c79ade5b86b50..1ac99df884fe3e6dae23be0e18364d454b4b01da 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -33,9 +33,9 @@ describe('Note thread mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolReply.id), false); + assert.strictEqual(res.body.some(note => note.id === carolReplyWithoutMention.id), false); }); test('ミュートã—ã¦ã„るスレッドã‹ã‚‰ãƒ¡ãƒ³ã‚·ãƒ§ãƒ³ã•ã‚Œã¦ã‚‚ã€hasUnreadMentions ㌠true ã«ãªã‚‰ãªã„', async () => { @@ -93,8 +93,8 @@ describe('Note thread mute', () => { assert.strictEqual(res.status, 200); assert.strictEqual(Array.isArray(res.body), true); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); - assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); + assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReply.id), false); + assert.strictEqual(res.body.some(notification => 'note' in notification && notification.note.id === carolReplyWithoutMention.id), false); // NOTE: bobã®æŠ•ç¨¿ã¯ã‚¹ãƒ¬ãƒƒãƒ‰ãƒŸãƒ¥ãƒ¼ãƒˆå‰ã«è¡Œã‚ã‚ŒãŸãŸã‚通知ã«å«ã¾ã‚Œã¦ã„ã¦ã‚‚よㄠ}); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 5487292afcd50f0b1803ebdb490bc38e291faca1..d12be2a9acdc18b9a196804518cc2a7aa4da8522 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -7,17 +7,26 @@ // pnpm jest -- e2e/timelines.ts import * as assert from 'assert'; -import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js'; +import { setTimeout } from 'node:timers/promises'; +import { Redis } from 'ioredis'; +import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js'; +import { loadConfig } from '@/config.js'; function genHost() { return randomString() + '.example.com'; } function waitForPushToTl() { - return sleep(500); + return setTimeout(500); } +let redisForTimelines: Redis; + describe('Timelines', () => { + beforeAll(() => { + redisForTimelines = new Redis(loadConfig().redisForTimelines); + }); + describe('Home TL', () => { test.concurrent('自分㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { const [alice] = await Promise.all([signup()]); @@ -28,15 +37,15 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' }); @@ -44,15 +53,15 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi' }); @@ -60,16 +69,16 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -77,8 +86,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -86,7 +95,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -94,8 +103,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã®DM返信ãŒå«ã¾ã‚Œãªã„', async () => { @@ -103,7 +112,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); @@ -111,16 +120,17 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + await api('following/create', { userId: carol.id }, bob); await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -128,8 +138,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®è¡Œã£ãŸåˆ¥ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -139,7 +149,7 @@ describe('Timelines', () => { await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, bob); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -147,9 +157,27 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); + assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®è‡ªåˆ†ã® visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: alice.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id).text, 'hi'); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); }); test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®è¡Œã£ãŸåˆ¥ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®æŠ•ç¨¿ã¸ã® visibility: specified ãªè¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -158,7 +186,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: carol.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); @@ -166,15 +194,15 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); }); test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼è‡ªèº«ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); @@ -182,15 +210,15 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }); test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã‹ã‚‰ã®è‡ªåˆ†ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -198,8 +226,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('自分ã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -212,15 +240,15 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã®æŠ•ç¨¿ã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); @@ -228,15 +256,15 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('[withRenotes: false] フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã®æŠ•ç¨¿ã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { renoteId: carolNote.id }); @@ -246,15 +274,15 @@ describe('Timelines', () => { withRenotes: false, }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('[withRenotes: false] フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã®æŠ•ç¨¿ã®å¼•ç”¨ãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -264,22 +292,22 @@ describe('Timelines', () => { withRenotes: false, }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人ã¸ã® visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -287,7 +315,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -295,8 +323,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -305,7 +333,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -313,8 +341,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -329,7 +357,7 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -344,14 +372,14 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('[withFiles: true] フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒ•ã‚¡ã‚¤ãƒ«ä»˜ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const [bobFile, carolFile] = await Promise.all([ uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), @@ -365,10 +393,10 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100, withFiles: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); }, 1000 * 10); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { @@ -376,14 +404,14 @@ describe('Timelines', () => { const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('自分㮠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -395,23 +423,23 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®è‡ªèº«ã‚’ visibleUserIds ã«æŒ‡å®šã—㟠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); await waitForPushToTl(); const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®è‡ªèº«ã‚’ visibleUserIds ã«æŒ‡å®šã—㟠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -423,21 +451,21 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーã®è‡ªèº«ã‚’ visibleUserIds ã«æŒ‡å®šã—ã¦ã„ãªã„ visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã® visibility: specified ãªãƒŽãƒ¼ãƒˆã«è¿”ä¿¡ã—ãŸã¨ãã®è‡ªèº«ã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -450,8 +478,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'ok'); }); /* TODO @@ -465,8 +493,8 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'ok'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id).text, 'ok'); }); */ @@ -481,7 +509,45 @@ describe('Timelines', () => { const res = await api('notes/timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + test.concurrent('FTT: ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã® HTL ã«ã¯ãƒ—ッシュã•ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('following/create', { + userId: alice.id, + }, bob); + + const aliceNote = await post(alice, { text: 'I\'m Alice.' }); + const bobNote = await post(bob, { text: 'I\'m Bob.' }); + const carolNote = await post(carol, { text: 'I\'m Carol.' }); + + await waitForPushToTl(); + + // NOTE: notes/timeline ã 㨠DB ã¸ã®ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ãŒåŠ¹ãã®ã§ Redis を直接見ã¦ç¢ºã‹ã‚ã‚‹ + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 1); + + const bobHTL = await redisForTimelines.lrange(`list:homeTimeline:${bob.id}`, 0, -1); + assert.strictEqual(bobHTL.includes(aliceNote.id), true); + assert.strictEqual(bobHTL.includes(bobNote.id), true); + assert.strictEqual(bobHTL.includes(carolNote.id), false); + }); + + test.concurrent('FTT: リモートユーザー㮠HTL ã«ã¯ãƒ—ッシュã•ã‚Œãªã„', async () => { + const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]); + + await api('following/create', { + userId: alice.id, + }, bob); + + await post(alice, { text: 'I\'m Alice.' }); + await post(bob, { text: 'I\'m Bob.' }); + + await waitForPushToTl(); + + // NOTE: notes/timeline ã 㨠DB ã¸ã®ãƒ•ã‚©ãƒ¼ãƒ«ãƒãƒƒã‚¯ãŒåŠ¹ãã®ã§ Redis を直接見ã¦ç¢ºã‹ã‚ã‚‹ + assert.strictEqual(await redisForTimelines.exists(`list:homeTimeline:${bob.id}`), 0); }); }); @@ -496,8 +562,8 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('他人ã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -510,8 +576,8 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); }); test.concurrent('他人ã®ãã®äººè‡ªèº«ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -524,8 +590,8 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }); test.concurrent('ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { @@ -538,7 +604,7 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('リモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -550,7 +616,7 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); // å«ã¾ã‚Œã¦ã‚‚良ã„ã¨æ€ã†ã‘ã©å®Ÿè£…ãŒé¢å€’ãªã®ã§å«ã¾ã‚Œãªã„ @@ -558,7 +624,7 @@ describe('Timelines', () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('following/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi' }); @@ -566,15 +632,15 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('ミュートã—ã¦ã„るユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' }); @@ -582,8 +648,8 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®ãƒªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -591,7 +657,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -599,8 +665,8 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーãŒè¡Œã£ãŸãƒŸãƒ¥ãƒ¼ãƒˆã—ã¦ã„るユーザーã®æŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -609,7 +675,7 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -617,15 +683,15 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); }); test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã‹ã‚‰ã®è‡ªåˆ†ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -633,8 +699,23 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); + + test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã®è‡ªåˆ†ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/local-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('[withReplies: true] 他人ã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -647,7 +728,7 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100, withReplies: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('[withFiles: true] ファイル付ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { @@ -661,8 +742,8 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100, withFiles: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }, 1000 * 10); }); @@ -676,7 +757,7 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã® visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -688,28 +769,28 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„ã‚‹ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã® visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã‹ã‚‰ã®è‡ªåˆ†ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -717,8 +798,64 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®ä»–人㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('following/create', { userId: carol.id }, bob); + await api('following/create', { userId: bob.id }, alice); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®è¡Œã£ãŸåˆ¥ã®ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: carol.id }, alice); + await api('following/create', { userId: carol.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); + }); + + test.concurrent('withReplies: true ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„るユーザーã®è‡ªåˆ†ã® visibility: followers ãªæŠ•ç¨¿ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await api('following/create', { userId: bob.id }, alice); + await api('following/create', { userId: alice.id }, bob); + await api('following/update', { userId: bob.id, withReplies: true }, alice); + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); }); test.concurrent('他人ã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -731,8 +868,8 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), true); }); test.concurrent('リモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -744,7 +881,7 @@ describe('Timelines', () => { const res = await api('notes/local-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザーã®ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -759,7 +896,7 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るリモートユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -774,7 +911,22 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); + + test.concurrent('withReplies: false ã§ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã®è‡ªåˆ†ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/local-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('[withReplies: true] 他人ã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -787,7 +939,7 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100, withReplies: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('[withFiles: true] ファイル付ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { @@ -801,8 +953,8 @@ describe('Timelines', () => { const res = await api('notes/hybrid-timeline', { limit: 100, withFiles: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }, 1000 * 10); }); @@ -812,14 +964,14 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi' }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -827,14 +979,14 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -842,14 +994,14 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -857,7 +1009,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -865,7 +1017,7 @@ describe('Timelines', () => { const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ãƒ¦ãƒ¼ã‚¶ãƒ¼è‡ªèº«ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -873,7 +1025,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); @@ -881,8 +1033,8 @@ describe('Timelines', () => { const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }); test.concurrent('withReplies: false ã§ãƒªã‚¹ã‚¤ãƒ³ã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã‹ã‚‰ã®è‡ªåˆ†ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -891,7 +1043,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); @@ -899,7 +1051,7 @@ describe('Timelines', () => { const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('withReplies: false ã§ãƒªã‚¹ã‚¤ãƒ³ã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -908,7 +1060,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -916,7 +1068,7 @@ describe('Timelines', () => { const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('withReplies: true ã§ãƒªã‚¹ã‚¤ãƒ³ã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„ãªã„ユーザーã®ä»–人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -925,7 +1077,7 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); @@ -933,7 +1085,7 @@ describe('Timelines', () => { const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: home ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -942,14 +1094,14 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('リスインã—ã¦ã„るフォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -958,15 +1110,15 @@ describe('Timelines', () => { await api('following/create', { userId: bob.id }, alice); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); }); test.concurrent('リスインã—ã¦ã„る自分㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -974,15 +1126,15 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); - await sleep(1000); + await setTimeout(1000); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); }); test.concurrent('リスインã—ã¦ã„るユーザーã®ãƒãƒ£ãƒ³ãƒãƒ«ãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -991,14 +1143,14 @@ describe('Timelines', () => { const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('[withFiles: true] リスインã—ã¦ã„るユーザーã®ãƒ•ã‚¡ã‚¤ãƒ«ä»˜ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { @@ -1014,8 +1166,8 @@ describe('Timelines', () => { const res = await api('notes/user-list-timeline', { listId: list.id, withFiles: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }, 1000 * 10); test.concurrent('リスインã—ã¦ã„るユーザーã®è‡ªèº«å®›ã¦ã® visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -1023,15 +1175,15 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); }); test.concurrent('リスインã—ã¦ã„るユーザーã®è‡ªèº«å®›ã¦ã§ã¯ãªã„ visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -1040,14 +1192,14 @@ describe('Timelines', () => { const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); await waitForPushToTl(); const res = await api('notes/user-list-timeline', { listId: list.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); }); @@ -1061,7 +1213,7 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„ãªã„ユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -1073,22 +1225,22 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('フォãƒãƒ¼ã—ã¦ã„るユーザー㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('following/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); await waitForPushToTl(); const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.find(note => note.id === bobNote.id)?.text, 'hi'); }); test.concurrent('自身㮠visibility: followers ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -1100,8 +1252,8 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: alice.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); - assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi'); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.find(note => note.id === aliceNote.id)?.text, 'hi'); }); test.concurrent('ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { @@ -1114,7 +1266,7 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('[withReplies: false] 他人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -1128,8 +1280,8 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); }); test.concurrent('[withReplies: true] 他人ã¸ã®è¿”ä¿¡ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -1143,8 +1295,8 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }); test.concurrent('[withReplies: true] 他人ã¸ã® visibility: specified ãªè¿”ä¿¡ãŒå«ã¾ã‚Œãªã„', async () => { @@ -1158,8 +1310,8 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), false); }); test.concurrent('[withFiles: true] ファイル付ãノートã®ã¿å«ã¾ã‚Œã‚‹', async () => { @@ -1173,8 +1325,8 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id, withFiles: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); }, 1000 * 10); test.concurrent('[withChannelNotes: true] ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -1187,7 +1339,7 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('[withChannelNotes: true] 他人ãŒå–å¾—ã—ãŸå ´åˆã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { @@ -1200,7 +1352,7 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('[withChannelNotes: true] 自分ãŒå–å¾—ã—ãŸå ´åˆã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–ãƒãƒ£ãƒ³ãƒãƒ«æŠ•ç¨¿ãŒå«ã¾ã‚Œã‚‹', async () => { @@ -1213,14 +1365,14 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id, withChannelNotes: true }, bob); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); test.concurrent('ミュートã—ã¦ã„るユーザーã«é–¢é€£ã™ã‚‹æŠ•ç¨¿ãŒå«ã¾ã‚Œãªã„', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); await api('mute/create', { userId: carol.id }, alice); - await sleep(1000); + await setTimeout(1000); const carolNote = await post(carol, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); @@ -1228,14 +1380,14 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); }); test.concurrent('ミュートã—ã¦ã„ã¦ã‚‚ userId ã«æŒ‡å®šã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æŠ•ç¨¿ãŒå«ã¾ã‚Œã‚‹', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); await api('mute/create', { userId: bob.id }, alice); - await sleep(1000); + await setTimeout(1000); const bobNote1 = await post(bob, { text: 'hi' }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); @@ -1244,9 +1396,9 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true); }); test.concurrent('自身㮠visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œã‚‹', async () => { @@ -1258,7 +1410,7 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: alice.id, withReplies: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); }); test.concurrent('visibleUserIds ã«æŒ‡å®šã•ã‚Œã¦ãªã„ visibility: specified ãªãƒŽãƒ¼ãƒˆãŒå«ã¾ã‚Œãªã„', async () => { @@ -1270,7 +1422,34 @@ describe('Timelines', () => { const res = await api('users/notes', { userId: bob.id, withReplies: true }, alice); - assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + }); + + /** @see https://github.com/misskey-dev/misskey/issues/14000 */ + test.concurrent('FTT: sinceId ã«ã‚ャッシュよりå¤ã„ノートを指定ã—ã¦ã‚‚ã€sinceId ã«ã‚ˆã‚‹çµžã‚Šè¾¼ã¿ãŒæ£ã—ã動作ã™ã‚‹', async () => { + const alice = await signup(); + const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); + const note1 = await post(alice, { text: '1' }); + const note2 = await post(alice, { text: '2' }); + await redisForTimelines.del('list:userTimeline:' + alice.id); + const note3 = await post(alice, { text: '3' }); + + const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id }); + assert.deepStrictEqual(res.body, [note1, note2, note3]); + }); + + test.concurrent('FTT: sinceId ã«ã‚ャッシュよりå¤ã„ノートを指定ã—ã¦ã‚‚ã€sinceId 㨠untilId ã«ã‚ˆã‚‹çµžã‚Šè¾¼ã¿ãŒæ£ã—ã動作ã™ã‚‹', async () => { + const alice = await signup(); + const noteSince = await post(alice, { text: 'Note where id will be `sinceId`.' }); + const note1 = await post(alice, { text: '1' }); + const note2 = await post(alice, { text: '2' }); + await redisForTimelines.del('list:userTimeline:' + alice.id); + const note3 = await post(alice, { text: '3' }); + const noteUntil = await post(alice, { text: 'Note where id will be `untilId`.' }); + await post(alice, { text: '4' }); + + const res = await api('users/notes', { userId: alice.id, sinceId: noteSince.id, untilId: noteUntil.id }); + assert.deepStrictEqual(res.body, [note3, note2, note1]); }); }); diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index 331e053935f81c081769452d7f8eb25b821df031..cc07c5ae71ac580bc50ec2e598bc6ca447edfce7 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -17,8 +17,8 @@ describe('users/notes', () => { beforeAll(async () => { alice = await signup({ username: 'alice' }); - const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); - const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); + const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg'); + const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.png'); jpgNote = await post(alice, { fileIds: [jpg.id], }); diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 89d48158ea90f939dfbb7ca206d87f0cd12f679a..eb56f7bebf35db3ee7270d064c0e2b464a2dc2d4 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -234,7 +234,7 @@ describe('ユーザー', () => { rolePublic = await role(root, { isPublic: true, name: 'Public Role' }); await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root); userRoleBadge = await signup({ username: 'userRoleBadge' }); - roleBadge = await role(root, { asBadge: true, name: 'Badge Role' }); + roleBadge = await role(root, { asBadge: true, name: 'Badge Role', isPublic: true }); await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root); userSilenced = await signup({ username: 'userSilenced' }); await post(userSilenced, { text: 'test' }); @@ -684,7 +684,16 @@ describe('ユーザー', () => { iconUrl: roleBadge.iconUrl, displayOrder: roleBadge.displayOrder, }]); - assert.deepStrictEqual(response.roles, []); // ãƒãƒƒãƒ‚ã ã‹ã‚‰ã¨ã„ã£ã¦rolesãŒå–れるã¨ã¯é™ã‚‰ãªã„ + assert.deepStrictEqual(response.roles, [{ + id: roleBadge.id, + name: roleBadge.name, + color: roleBadge.color, + iconUrl: roleBadge.iconUrl, + description: roleBadge.description, + isModerator: roleBadge.isModerator, + isAdministrator: roleBadge.isAdministrator, + displayOrder: roleBadge.displayOrder, + }]); }); test('ã‚’ID指定ã®ãƒªã‚¹ãƒˆå½¢å¼ã§å–å¾—ã™ã‚‹ã“ã¨ãŒã§ãる(空)', async () => { const parameters = { userIds: [] }; diff --git a/packages/backend/test/eslint.config.js b/packages/backend/test/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..a0f43babadfba82f0a8329e3e6bbd80052ec70c0 --- /dev/null +++ b/packages/backend/test/eslint.config.js @@ -0,0 +1,22 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/backend/test/prelude/maybe.ts b/packages/backend/test/prelude/maybe.ts deleted file mode 100644 index 16e92216d483ae249701471d485782745a1088b0..0000000000000000000000000000000000000000 --- a/packages/backend/test/prelude/maybe.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as assert from 'assert'; -import { just, nothing } from '../../src/misc/prelude/maybe.js'; - -describe('just', () => { - test('has a value', () => { - assert.deepStrictEqual(just(3).isJust(), true); - }); - - test('has the inverse called get', () => { - assert.deepStrictEqual(just(3).get(), 3); - }); -}); - -describe('nothing', () => { - test('has no value', () => { - assert.deepStrictEqual(nothing().isJust(), false); - }); -}); diff --git a/packages/backend/test/resources/192.jpg b/packages/backend/test/resources/192.jpg new file mode 100644 index 0000000000000000000000000000000000000000..76374628e0acf2e3b85152d55c11c9a763027810 Binary files /dev/null and b/packages/backend/test/resources/192.jpg differ diff --git a/packages/backend/test/resources/192.png b/packages/backend/test/resources/192.png new file mode 100644 index 0000000000000000000000000000000000000000..15fd1e3731210ad53f3948c1551a5be1f5ab1396 Binary files /dev/null and b/packages/backend/test/resources/192.png differ diff --git a/packages/backend/test/resources/Lenna.jpg b/packages/backend/test/resources/Lenna.jpg deleted file mode 100644 index 6b5b32281c1ffd5893d23392169cc368d72e0823..0000000000000000000000000000000000000000 Binary files a/packages/backend/test/resources/Lenna.jpg and /dev/null differ diff --git a/packages/backend/test/resources/Lenna.png b/packages/backend/test/resources/Lenna.png deleted file mode 100644 index 59ef68aabd033341028ccd2b1f6c5871c02b9d7b..0000000000000000000000000000000000000000 Binary files a/packages/backend/test/resources/Lenna.png and /dev/null differ diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts new file mode 100644 index 0000000000000000000000000000000000000000..e971659070ea3087d1494e4dbdb10d088d8adf9b --- /dev/null +++ b/packages/backend/test/unit/AbuseReportNotificationService.ts @@ -0,0 +1,343 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; +import { + AbuseReportNotificationRecipientRepository, + MiAbuseReportNotificationRecipient, + MiSystemWebhook, + MiUser, + SystemWebhooksRepository, + UserProfilesRepository, + UsersRepository, +} from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { IdService } from '@/core/IdService.js'; +import { EmailService } from '@/core/EmailService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; +import { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { randomString } from '../utils.js'; + +process.env.NODE_ENV = 'test'; + +describe('AbuseReportNotificationService', () => { + let app: TestingModule; + let service: AbuseReportNotificationService; + + // -------------------------------------------------------------------------------------- + + let usersRepository: UsersRepository; + let userProfilesRepository: UserProfilesRepository; + let systemWebhooksRepository: SystemWebhooksRepository; + let abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository; + let idService: IdService; + let roleService: jest.Mocked<RoleService>; + let emailService: jest.Mocked<EmailService>; + let webhookService: jest.Mocked<SystemWebhookService>; + + // -------------------------------------------------------------------------------------- + + let root: MiUser; + let alice: MiUser; + let bob: MiUser; + let systemWebhook1: MiSystemWebhook; + let systemWebhook2: MiSystemWebhook; + + // -------------------------------------------------------------------------------------- + + 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; + } + + async function createWebhook(data: Partial<MiSystemWebhook> = {}) { + return systemWebhooksRepository + .insert({ + id: idService.gen(), + name: randomString(), + on: ['abuseReport'], + url: 'https://example.com', + secret: randomString(), + ...data, + }) + .then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0])); + } + + async function createRecipient(data: Partial<MiAbuseReportNotificationRecipient> = {}) { + return abuseReportNotificationRecipientRepository + .insert({ + id: idService.gen(), + isActive: true, + name: randomString(), + ...data, + }) + .then(x => abuseReportNotificationRecipientRepository.findOneByOrFail(x.identifiers[0])); + } + + // -------------------------------------------------------------------------------------- + + beforeAll(async () => { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + AbuseReportNotificationService, + IdService, + { + provide: RoleService, useFactory: () => ({ getModeratorIds: jest.fn() }), + }, + { + provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), + }, + { + provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), + }, + { + provide: MetaService, useFactory: () => ({ fetch: jest.fn() }), + }, + { + provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), + }, + { + provide: GlobalEventService, useFactory: () => ({ publishAdminStream: jest.fn() }), + }, + ], + }) + .compile(); + + usersRepository = app.get(DI.usersRepository); + userProfilesRepository = app.get(DI.userProfilesRepository); + systemWebhooksRepository = app.get(DI.systemWebhooksRepository); + abuseReportNotificationRecipientRepository = app.get(DI.abuseReportNotificationRecipientRepository); + + service = app.get(AbuseReportNotificationService); + idService = app.get(IdService); + roleService = app.get(RoleService) as jest.Mocked<RoleService>; + emailService = app.get<EmailService>(EmailService) as jest.Mocked<EmailService>; + webhookService = app.get<SystemWebhookService>(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 }); + bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false }); + systemWebhook1 = await createWebhook(); + systemWebhook2 = await createWebhook(); + + roleService.getModeratorIds.mockResolvedValue([root.id, alice.id, bob.id]); + }); + + afterEach(async () => { + emailService.sendEmail.mockClear(); + webhookService.enqueueSystemWebhook.mockClear(); + + await usersRepository.delete({}); + await userProfilesRepository.delete({}); + await systemWebhooksRepository.delete({}); + await abuseReportNotificationRecipientRepository.delete({}); + }); + + afterAll(async () => { + await app.close(); + }); + + // -------------------------------------------------------------------------------------- + + describe('createRecipient', () => { + test('作æˆæˆåŠŸ1', async () => { + const params = { + isActive: true, + name: randomString(), + method: 'email' as RecipientMethod, + userId: alice.id, + systemWebhookId: null, + }; + + const recipient1 = await service.createRecipient(params, root); + expect(recipient1).toMatchObject(params); + }); + + test('作æˆæˆåŠŸ2', async () => { + const params = { + isActive: true, + name: randomString(), + method: 'webhook' as RecipientMethod, + userId: null, + systemWebhookId: systemWebhook1.id, + }; + + const recipient1 = await service.createRecipient(params, root); + expect(recipient1).toMatchObject(params); + }); + }); + + describe('updateRecipient', () => { + test('æ›´æ–°æˆåŠŸ1', async () => { + const recipient1 = await createRecipient({ + method: 'email', + userId: alice.id, + }); + + const params = { + id: recipient1.id, + isActive: false, + name: randomString(), + method: 'email' as RecipientMethod, + userId: bob.id, + systemWebhookId: null, + }; + + const recipient2 = await service.updateRecipient(params, root); + expect(recipient2).toMatchObject(params); + }); + + test('æ›´æ–°æˆåŠŸ2', async () => { + const recipient1 = await createRecipient({ + method: 'webhook', + systemWebhookId: systemWebhook1.id, + }); + + const params = { + id: recipient1.id, + isActive: false, + name: randomString(), + method: 'webhook' as RecipientMethod, + userId: null, + systemWebhookId: systemWebhook2.id, + }; + + const recipient2 = await service.updateRecipient(params, root); + expect(recipient2).toMatchObject(params); + }); + }); + + describe('deleteRecipient', () => { + test('削除æˆåŠŸ1', async () => { + const recipient1 = await createRecipient({ + method: 'email', + userId: alice.id, + }); + + await service.deleteRecipient(recipient1.id, root); + + await expect(abuseReportNotificationRecipientRepository.findOneBy({ id: recipient1.id })).resolves.toBeNull(); + }); + }); + + describe('fetchRecipients', () => { + async function create() { + const recipient1 = await createRecipient({ + method: 'email', + userId: alice.id, + }); + const recipient2 = await createRecipient({ + method: 'email', + userId: bob.id, + }); + + const recipient3 = await createRecipient({ + method: 'webhook', + systemWebhookId: systemWebhook1.id, + }); + const recipient4 = await createRecipient({ + method: 'webhook', + systemWebhookId: systemWebhook2.id, + }); + + return [recipient1, recipient2, recipient3, recipient4]; + } + + test('フィルタãªã—', async () => { + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({}); + expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); + }); + + test('フィルタãªã—(éžãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ã¯é™¤å¤–ã•ã‚Œã‚‹)', async () => { + roleService.getModeratorIds.mockClear(); + roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]); + + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({}); + // aliceã¯ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ã§ã¯ãªã„ã®ã§é™¤å¤–ã•ã‚Œã‚‹ + expect(recipients).toEqual([recipient2, recipient3, recipient4]); + }); + + test('フィルタãªã—(éžãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ã§ã‚‚除外ã•ã‚Œãªã„オプションè¨å®š)', async () => { + roleService.getModeratorIds.mockClear(); + roleService.getModeratorIds.mockResolvedValue([root.id, bob.id]); + + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({}, { removeUnauthorized: false }); + expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); + }); + + test('emailã®ã¿', async () => { + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({ method: ['email'] }); + expect(recipients).toEqual([recipient1, recipient2]); + }); + + test('webhookã®ã¿', async () => { + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({ method: ['webhook'] }); + expect(recipients).toEqual([recipient3, recipient4]); + }); + + test('ã™ã¹ã¦', async () => { + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({ method: ['email', 'webhook'] }); + expect(recipients).toEqual([recipient1, recipient2, recipient3, recipient4]); + }); + + test('ID指定', async () => { + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id] }); + expect(recipients).toEqual([recipient1, recipient3]); + }); + + test('ID指定(method=emailã§ã¯ãªã„IDãŒæ··ã–ã‚Šã“ã¾ãªã„)', async () => { + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['email'] }); + expect(recipients).toEqual([recipient1]); + }); + + test('ID指定(method=webhookã§ã¯ãªã„IDãŒæ··ã–ã‚Šã“ã¾ãªã„)', async () => { + const [recipient1, recipient2, recipient3, recipient4] = await create(); + + const recipients = await service.fetchRecipients({ ids: [recipient1.id, recipient3.id], method: ['webhook'] }); + expect(recipients).toEqual([recipient3]); + }); + }); +}); diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts index 79cb81f5c9ff919bbd82f68b8bc360d7ef84c440..e81a321c9be3a042af021520e6a46602db1dd86f 100644 --- a/packages/backend/test/unit/ApMfmService.ts +++ b/packages/backend/test/unit/ApMfmService.ts @@ -23,10 +23,10 @@ describe('ApMfmService', () => { describe('getNoteHtml', () => { test('Do not provide _misskey_content for simple text', () => { - const note: MiNote = { + const note = { text: 'テã‚スト #ã‚¿ã‚° @mention 🊠:emoji: https://example.com', mentionedRemoteUsers: '[]', - } as any; + }; const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); @@ -35,10 +35,10 @@ describe('ApMfmService', () => { }); test('Provide _misskey_content for MFM', () => { - const note: MiNote = { + const note = { text: '$[tada foo]', mentionedRemoteUsers: '[]', - } as any; + }; const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index bfe7143aa4567d10aaaae9376ccae6df772fd442..cf54ebd909e74d260776608ac5a31b785a1838af 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -12,7 +12,7 @@ import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import { afterAll, beforeAll, describe, test } from '@jest/globals'; import { GlobalModule } from '@/GlobalModule.js'; -import { FileInfoService } from '@/core/FileInfoService.js'; +import { FileInfo, FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; import { LoggerService } from '@/core/LoggerService.js'; import type { TestingModule } from '@nestjs/testing'; @@ -27,6 +27,15 @@ const moduleMocker = new ModuleMocker(global); describe('FileInfoService', () => { let app: TestingModule; let fileInfoService: FileInfoService; + const strip = (fileInfo: FileInfo): Omit<Partial<FileInfo>, 'warnings' | 'blurhash' | 'sensitive' | 'porn'> => { + const fi: Partial<FileInfo> = fileInfo; + delete fi.warnings; + delete fi.sensitive; + delete fi.blurhash; + delete fi.porn; + + return fi; + } beforeAll(async () => { app = await Test.createTestingModule({ @@ -58,11 +67,7 @@ describe('FileInfoService', () => { test('Empty file', async () => { const path = `${resources}/emptyfile`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 0, md5: 'd41d8cd98f00b204e9800998ecf8427e', @@ -78,32 +83,24 @@ describe('FileInfoService', () => { describe('IMAGE', () => { test('Generic JPEG', async () => { - const path = `${resources}/Lenna.jpg`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const path = `${resources}/192.jpg`; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { - size: 25360, - md5: '091b3f259662aa31e2ffef4519951168', + size: 5131, + md5: '8c9ed0677dd2b8f9f7472c3af247e5e3', type: { mime: 'image/jpeg', ext: 'jpg', }, - width: 512, - height: 512, + width: 192, + height: 192, orientation: undefined, }); }); test('Generic APNG', async () => { const path = `${resources}/anime.png`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 1868, md5: '08189c607bea3b952704676bb3c979e0', @@ -119,11 +116,7 @@ describe('FileInfoService', () => { test('Generic AGIF', async () => { const path = `${resources}/anime.gif`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 2248, md5: '32c47a11555675d9267aee1a86571e7e', @@ -139,11 +132,7 @@ describe('FileInfoService', () => { test('PNG with alpha', async () => { const path = `${resources}/with-alpha.png`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 3772, md5: 'f73535c3e1e27508885b69b10cf6e991', @@ -159,11 +148,7 @@ describe('FileInfoService', () => { test('Generic SVG', async () => { const path = `${resources}/image.svg`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 505, md5: 'b6f52b4b021e7b92cdd04509c7267965', @@ -180,11 +165,7 @@ describe('FileInfoService', () => { test('SVG with XML definition', async () => { // https://github.com/misskey-dev/misskey/issues/4413 const path = `${resources}/with-xml-def.svg`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 544, md5: '4b7a346cde9ccbeb267e812567e33397', @@ -200,11 +181,7 @@ describe('FileInfoService', () => { test('Dimension limit', async () => { const path = `${resources}/25000x25000.png`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 75933, md5: '268c5dde99e17cf8fe09f1ab3f97df56', @@ -220,11 +197,7 @@ describe('FileInfoService', () => { test('Rotate JPEG', async () => { const path = `${resources}/rotate.jpg`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); assert.deepStrictEqual(info, { size: 12624, md5: '68d5b2d8d1d1acbbce99203e3ec3857e', @@ -242,11 +215,7 @@ describe('FileInfoService', () => { describe('AUDIO', () => { test('MP3', async () => { const path = `${resources}/kick_gaba7.mp3`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); delete info.width; delete info.height; delete info.orientation; @@ -262,11 +231,7 @@ describe('FileInfoService', () => { test('WAV', async () => { const path = `${resources}/kick_gaba7.wav`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); delete info.width; delete info.height; delete info.orientation; @@ -282,11 +247,7 @@ describe('FileInfoService', () => { test('AAC', async () => { const path = `${resources}/kick_gaba7.aac`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); delete info.width; delete info.height; delete info.orientation; @@ -302,11 +263,7 @@ describe('FileInfoService', () => { test('FLAC', async () => { const path = `${resources}/kick_gaba7.flac`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); delete info.width; delete info.height; delete info.orientation; @@ -322,11 +279,7 @@ describe('FileInfoService', () => { test('MPEG-4 AUDIO (M4A)', async () => { const path = `${resources}/kick_gaba7.m4a`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); delete info.width; delete info.height; delete info.orientation; @@ -342,11 +295,7 @@ describe('FileInfoService', () => { test('WEBM AUDIO', async () => { const path = `${resources}/kick_gaba7.webm`; - const info = await fileInfoService.getFileInfo(path) as any; - delete info.warnings; - delete info.blurhash; - delete info.sensitive; - delete info.porn; + const info = strip(await fileInfoService.getFileInfo(path)); delete info.width; delete info.height; delete info.orientation; diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index ec441735d7e627188c7868ce8c20eb900ee74ba8..b6cbe4c520d1ff20924509943349442ddb4b97b7 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -3,17 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { UserEntityService } from '@/core/entities/UserEntityService.js'; - process.env.NODE_ENV = 'test'; +import { setTimeout } from 'node:timers/promises'; import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; -import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; +import { + MiRole, + MiRoleAssignment, + MiUser, + RoleAssignmentsRepository, + RolesRepository, + UsersRepository, +} from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { MetaService } from '@/core/MetaService.js'; import { genAidx } from '@/misc/id/aidx.js'; @@ -23,7 +29,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { NotificationService } from '@/core/NotificationService.js'; import { RoleCondFormulaValue } from '@/models/Role.js'; -import { sleep } from '../utils.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { TestingModule } from '@nestjs/testing'; import type { MockFunctionMetadata } from 'jest-mock'; @@ -39,27 +45,27 @@ describe('RoleService', () => { let notificationService: jest.Mocked<NotificationService>; let clock: lolex.InstalledClock; - function createUser(data: Partial<MiUser> = {}) { + async function createUser(data: Partial<MiUser> = {}) { const un = secureRndstr(16); - return usersRepository.insert({ + const x = await usersRepository.insert({ id: genAidx(Date.now()), username: un, usernameLower: un, ...data, - }) - .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + }); + return await usersRepository.findOneByOrFail(x.identifiers[0]); } - function createRole(data: Partial<MiRole> = {}) { - return rolesRepository.insert({ + async function createRole(data: Partial<MiRole> = {}) { + const x = await rolesRepository.insert({ id: genAidx(Date.now()), updatedAt: new Date(), lastUsedAt: new Date(), name: '', description: '', ...data, - }) - .then(x => rolesRepository.findOneByOrFail(x.identifiers[0])); + }); + return await rolesRepository.findOneByOrFail(x.identifiers[0]); } function createConditionalRole(condFormula: RoleCondFormulaValue, data: Partial<MiRole> = {}) { @@ -71,6 +77,20 @@ describe('RoleService', () => { }); } + async function assignRole(args: Partial<MiRoleAssignment>) { + const id = genAidx(Date.now()); + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 1); + + await roleAssignmentsRepository.insert({ + id, + expiresAt, + ...args, + }); + + return await roleAssignmentsRepository.findOneByOrFail({ id }); + } + function aidx() { return genAidx(Date.now()); } @@ -258,13 +278,103 @@ describe('RoleService', () => { // ストリーミング経由ã§åæ˜ ã•ã‚Œã‚‹ã¾ã§ã¡ã‚‡ã£ã¨å¾…㤠clock.uninstall(); - await sleep(100); + await setTimeout(100); const resultAfter25hAgain = await roleService.getUserPolicies(user.id); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); }); }); + describe('getModeratorIds', () => { + test('includeAdmins = false, excludeExpire = false', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), + ]); + + const result = await roleService.getModeratorIds(false, false); + expect(result).toEqual([modeUser1.id, modeUser2.id]); + }); + + test('includeAdmins = false, excludeExpire = true', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), + ]); + + const result = await roleService.getModeratorIds(false, true); + expect(result).toEqual([modeUser1.id]); + }); + + test('includeAdmins = true, excludeExpire = false', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), + ]); + + const result = await roleService.getModeratorIds(true, false); + expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]); + }); + + test('includeAdmins = true, excludeExpire = true', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), + ]); + + const result = await roleService.getModeratorIds(true, true); + expect(result).toEqual([adminUser1.id, modeUser1.id]); + }); + }); + describe('conditional role', () => { test('~ã‹ã¤ï½ž', async () => { const [user1, user2, user3, user4] = await Promise.all([ @@ -697,7 +807,7 @@ describe('RoleService', () => { await roleService.assign(user.id, role.id); clock.uninstall(); - await sleep(100); + await setTimeout(100); const assignments = await roleAssignmentsRepository.find({ where: { @@ -725,7 +835,7 @@ describe('RoleService', () => { await roleService.assign(user.id, role.id); clock.uninstall(); - await sleep(100); + await setTimeout(100); const assignments = await roleAssignmentsRepository.find({ where: { diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts new file mode 100644 index 0000000000000000000000000000000000000000..790cd1490eb5c050b5b74c23e7fdacc18e3d6b85 --- /dev/null +++ b/packages/backend/test/unit/SystemWebhookService.ts @@ -0,0 +1,516 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setTimeout } from 'node:timers/promises'; +import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MiUser } from '@/models/User.js'; +import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; +import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { ModerationLogService } from '@/core/ModerationLogService.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 { SystemWebhookService } from '@/core/SystemWebhookService.js'; +import { randomString } from '../utils.js'; + +describe('SystemWebhookService', () => { + let app: TestingModule; + let service: SystemWebhookService; + + // -------------------------------------------------------------------------------------- + + let usersRepository: UsersRepository; + let systemWebhooksRepository: SystemWebhooksRepository; + 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<MiSystemWebhook> = {}) { + return systemWebhooksRepository + .insert({ + id: idService.gen(), + name: randomString(), + on: ['abuseReport'], + url: 'https://example.com', + secret: randomString(), + ...data, + }) + .then(x => systemWebhooksRepository.findOneByOrFail(x.identifiers[0])); + } + + // -------------------------------------------------------------------------------------- + + async function beforeAllImpl() { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + SystemWebhookService, + IdService, + LoggerService, + GlobalEventService, + { + provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }), + }, + { + provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), + }, + ], + }) + .compile(); + + usersRepository = app.get(DI.usersRepository); + systemWebhooksRepository = app.get(DI.systemWebhooksRepository); + + service = app.get(SystemWebhookService); + 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 systemWebhooksRepository.delete({}); + } + + // -------------------------------------------------------------------------------------- + + describe('アプリを毎回作り直ã™å¿…è¦ã®ãªã„グループ', () => { + beforeAll(beforeAllImpl); + afterAll(afterAllImpl); + beforeEach(beforeEachImpl); + afterEach(afterEachImpl); + + describe('fetchSystemWebhooks', () => { + test('フィルタãªã—', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + const webhook3 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + const webhook4 = await createWebhook({ + isActive: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchSystemWebhooks(); + expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]); + }); + + test('activeã®ã¿', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + const webhook3 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + const webhook4 = await createWebhook({ + isActive: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchSystemWebhooks({ isActive: true }); + expect(fetchedWebhooks).toEqual([webhook1, webhook3]); + }); + + test('特定ã®ã‚¤ãƒ™ãƒ³ãƒˆã®ã¿', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + const webhook3 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + const webhook4 = await createWebhook({ + isActive: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'] }); + expect(fetchedWebhooks).toEqual([webhook1, webhook2]); + }); + + test('activeãªç‰¹å®šã®ã‚¤ãƒ™ãƒ³ãƒˆã®ã¿', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + const webhook3 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + const webhook4 = await createWebhook({ + isActive: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchSystemWebhooks({ on: ['abuseReport'], isActive: true }); + expect(fetchedWebhooks).toEqual([webhook1]); + }); + + test('ID指定', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + const webhook3 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + const webhook4 = await createWebhook({ + isActive: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id] }); + expect(fetchedWebhooks).toEqual([webhook1, webhook4]); + }); + + test('ID指定(ä»–æ¡ä»¶ã¨ANDã«ãªã‚‹ã‹è¦‹ãŸã„)', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + const webhook2 = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + const webhook3 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + const webhook4 = await createWebhook({ + isActive: false, + on: [], + }); + + const fetchedWebhooks = await service.fetchSystemWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false }); + expect(fetchedWebhooks).toEqual([webhook4]); + }); + }); + + describe('createSystemWebhook', () => { + test('作æˆæˆåŠŸ ', async () => { + const params = { + isActive: true, + name: randomString(), + on: ['abuseReport'] as SystemWebhookEventType[], + url: 'https://example.com', + secret: randomString(), + }; + + const webhook = await service.createSystemWebhook(params, root); + expect(webhook).toMatchObject(params); + }); + }); + + describe('updateSystemWebhook', () => { + test('æ›´æ–°æˆåŠŸ', async () => { + const webhook = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + + const params = { + id: webhook.id, + isActive: false, + name: randomString(), + on: ['abuseReport'] as SystemWebhookEventType[], + url: randomString(), + secret: randomString(), + }; + + const updatedWebhook = await service.updateSystemWebhook(params, root); + expect(updatedWebhook).toMatchObject(params); + }); + }); + + describe('deleteSystemWebhook', () => { + test('削除æˆåŠŸ', async () => { + const webhook = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + + await service.deleteSystemWebhook(webhook.id, root); + + await expect(systemWebhooksRepository.findOneBy({ id: webhook.id })).resolves.toBeNull(); + }); + }); + }); + + describe('アプリを毎回作り直ã™å¿…è¦ãŒã‚るグループ', () => { + beforeEach(async () => { + await beforeAllImpl(); + await beforeEachImpl(); + }); + + afterEach(async () => { + await afterEachImpl(); + await afterAllImpl(); + }); + + describe('enqueueSystemWebhook', () => { + test('ã‚ューã«è¿½åŠ æˆåŠŸ', async () => { + const webhook = await createWebhook({ + isActive: true, + on: ['abuseReport'], + }); + await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' }); + + expect(queueService.systemWebhookDeliver).toHaveBeenCalled(); + }); + + test('éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªWebhookã¯ã‚ューã«è¿½åŠ ã•ã‚Œãªã„', async () => { + const webhook = await createWebhook({ + isActive: false, + on: ['abuseReport'], + }); + await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' }); + + expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); + }); + + test('未許å¯ã®ã‚¤ãƒ™ãƒ³ãƒˆç¨®åˆ¥ãŒæ¸¡ã•ã‚ŒãŸå ´åˆã¯Webhookã¯ã‚ューã«è¿½åŠ ã•ã‚Œãªã„', async () => { + const webhook1 = await createWebhook({ + isActive: true, + on: [], + }); + const webhook2 = await createWebhook({ + isActive: true, + on: ['abuseReportResolved'], + }); + await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' }); + await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' }); + + expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled(); + }); + }); + + describe('fetchActiveSystemWebhooks', () => { + describe('systemWebhookCreated', () => { + test('ActiveãªWebhookãŒè¿½åŠ ã•ã‚ŒãŸæ™‚ã€ã‚ャッシュã«è¿½åŠ ã•ã‚Œã¦ã„ã‚‹', async () => { + const webhook = await service.createSystemWebhook( + { + isActive: true, + name: randomString(), + on: ['abuseReport'], + url: 'https://example.com', + secret: randomString(), + }, + root, + ); + + // redisã§ã®é…信経由ã§æ›´æ–°ã•ã‚Œã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ await setTimeout(500); + + const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); + expect(fetchedWebhooks).toEqual([webhook]); + }); + + test('NotActiveãªWebhookãŒè¿½åŠ ã•ã‚ŒãŸæ™‚ã€ã‚ャッシュã«è¿½åŠ ã•ã‚Œã¦ã„ãªã„', async () => { + const webhook = await service.createSystemWebhook( + { + isActive: false, + name: randomString(), + on: ['abuseReport'], + url: 'https://example.com', + secret: randomString(), + }, + root, + ); + + // redisã§ã®é…信経由ã§æ›´æ–°ã•ã‚Œã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ await setTimeout(500); + + const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); + expect(fetchedWebhooks).toEqual([]); + }); + }); + + describe('systemWebhookUpdated', () => { + test('ActiveãªWebhookãŒç·¨é›†ã•ã‚ŒãŸæ™‚ã€ã‚ャッシュã«åæ˜ ã•ã‚Œã¦ã„ã‚‹', async () => { + const id = idService.gen(); + await createWebhook({ id }); + // ã‚ãƒ£ãƒƒã‚·ãƒ¥ä½œæˆ + const webhook1 = await service.fetchActiveSystemWebhooks(); + // èªã¿è¾¼ã¾ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’ãƒã‚§ãƒƒã‚¯ + expect(webhook1.length).toEqual(1); + expect(webhook1[0].id).toEqual(id); + + const webhook2 = await service.updateSystemWebhook( + { + id, + isActive: true, + name: randomString(), + on: ['abuseReport'], + url: 'https://example.com', + secret: randomString(), + }, + root, + ); + + // redisã§ã®é…信経由ã§æ›´æ–°ã•ã‚Œã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ await setTimeout(500); + + const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); + expect(fetchedWebhooks).toEqual([webhook2]); + }); + + test('NotActiveãªWebhookãŒç·¨é›†ã•ã‚ŒãŸæ™‚ã€ã‚ャッシュã«è¿½åŠ ã•ã‚Œãªã„', async () => { + const id = idService.gen(); + await createWebhook({ id, isActive: false }); + // ã‚ãƒ£ãƒƒã‚·ãƒ¥ä½œæˆ + const webhook1 = await service.fetchActiveSystemWebhooks(); + // èªã¿è¾¼ã¾ã‚Œã¦ã„ãªã„ã“ã¨ã‚’ãƒã‚§ãƒƒã‚¯ + expect(webhook1.length).toEqual(0); + + const webhook2 = await service.updateSystemWebhook( + { + id, + isActive: false, + name: randomString(), + on: ['abuseReport'], + url: 'https://example.com', + secret: randomString(), + }, + root, + ); + + // redisã§ã®é…信経由ã§æ›´æ–°ã•ã‚Œã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ await setTimeout(500); + + const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); + expect(fetchedWebhooks.length).toEqual(0); + }); + + test('NotActiveãªWebhookãŒActiveã«ã•ã‚ŒãŸæ™‚ã€ã‚ャッシュã«è¿½åŠ ã•ã‚Œã¦ã„ã‚‹', async () => { + const id = idService.gen(); + const baseWebhook = await createWebhook({ id, isActive: false }); + // ã‚ãƒ£ãƒƒã‚·ãƒ¥ä½œæˆ + const webhook1 = await service.fetchActiveSystemWebhooks(); + // èªã¿è¾¼ã¾ã‚Œã¦ã„ãªã„ã“ã¨ã‚’ãƒã‚§ãƒƒã‚¯ + expect(webhook1.length).toEqual(0); + + const webhook2 = await service.updateSystemWebhook( + { + ...baseWebhook, + isActive: true, + }, + root, + ); + + // redisã§ã®é…信経由ã§æ›´æ–°ã•ã‚Œã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ await setTimeout(500); + + const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); + expect(fetchedWebhooks).toEqual([webhook2]); + }); + + test('ActiveãªWebhookãŒNotActiveã«ã•ã‚ŒãŸæ™‚ã€ã‚ャッシュã‹ã‚‰å‰Šé™¤ã•ã‚Œã¦ã„ã‚‹', async () => { + const id = idService.gen(); + const baseWebhook = await createWebhook({ id, isActive: true }); + // ã‚ãƒ£ãƒƒã‚·ãƒ¥ä½œæˆ + const webhook1 = await service.fetchActiveSystemWebhooks(); + // èªã¿è¾¼ã¾ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’ãƒã‚§ãƒƒã‚¯ + expect(webhook1.length).toEqual(1); + expect(webhook1[0].id).toEqual(id); + + const webhook2 = await service.updateSystemWebhook( + { + ...baseWebhook, + isActive: false, + }, + root, + ); + + // redisã§ã®é…信経由ã§æ›´æ–°ã•ã‚Œã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ await setTimeout(500); + + const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); + expect(fetchedWebhooks.length).toEqual(0); + }); + }); + + describe('systemWebhookDeleted', () => { + test('ã‚ャッシュã‹ã‚‰å‰Šé™¤ã•ã‚Œã¦ã„ã‚‹', async () => { + const id = idService.gen(); + const baseWebhook = await createWebhook({ id, isActive: true }); + // ã‚ãƒ£ãƒƒã‚·ãƒ¥ä½œæˆ + const webhook1 = await service.fetchActiveSystemWebhooks(); + // èªã¿è¾¼ã¾ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’ãƒã‚§ãƒƒã‚¯ + expect(webhook1.length).toEqual(1); + expect(webhook1[0].id).toEqual(id); + + const webhook2 = await service.deleteSystemWebhook( + id, + root, + ); + + // redisã§ã®é…信経由ã§æ›´æ–°ã•ã‚Œã‚‹ã®ã§ã¡ã‚‡ã£ã¨å¾…㤠+ await setTimeout(500); + + const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); + expect(fetchedWebhooks.length).toEqual(0); + }); + }); + }); + }); +}); diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ea325d4209bef8643b6cbc7e1dc31a1bbfd644e --- /dev/null +++ b/packages/backend/test/unit/UserSearchService.ts @@ -0,0 +1,265 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { describe, jest, test } from '@jest/globals'; +import { In } from 'typeorm'; +import { UserSearchService } from '@/core/UserSearchService.js'; +import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; + +describe('UserSearchService', () => { + let app: TestingModule; + let service: UserSearchService; + + let usersRepository: UsersRepository; + let followingsRepository: FollowingsRepository; + let idService: IdService; + let userProfilesRepository: UserProfilesRepository; + + let root: MiUser; + let alice: MiUser; + let alyce: MiUser; + let alycia: MiUser; + let alysha: MiUser; + let alyson: MiUser; + let alyssa: MiUser; + let bob: MiUser; + let bobbi: MiUser; + let bobbie: MiUser; + let bobby: 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; + } + + async function createFollowings(follower: MiUser, followees: MiUser[]) { + for (const followee of followees) { + await followingsRepository.insert({ + id: idService.gen(), + followerId: follower.id, + followeeId: followee.id, + }); + } + } + + async function setActive(users: MiUser[]) { + for (const user of users) { + await usersRepository.update(user.id, { + updatedAt: new Date(), + }); + } + } + + async function setInactive(users: MiUser[]) { + for (const user of users) { + await usersRepository.update(user.id, { + updatedAt: new Date(0), + }); + } + } + + async function setSuspended(users: MiUser[]) { + for (const user of users) { + await usersRepository.update(user.id, { + isSuspended: true, + }); + } + } + + beforeAll(async () => { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + UserSearchService, + { + provide: UserEntityService, useFactory: jest.fn(() => ({ + // ã¨ã‚Šã‚ãˆãšIDãŒè¿”ã‚Œã°ç¢ºèªãŒå‡ºæ¥ã‚‹ã®ã§ + packMany: (value: any) => value, + })), + }, + IdService, + ], + }) + .compile(); + + await app.init(); + + usersRepository = app.get(DI.usersRepository); + userProfilesRepository = app.get(DI.userProfilesRepository); + followingsRepository = app.get(DI.followingsRepository); + + service = app.get(UserSearchService); + idService = app.get(IdService); + }); + + beforeEach(async () => { + root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true }); + alice = await createUser({ username: 'Alice', usernameLower: 'alice' }); + alyce = await createUser({ username: 'Alyce', usernameLower: 'alyce' }); + alycia = await createUser({ username: 'Alycia', usernameLower: 'alycia' }); + alysha = await createUser({ username: 'Alysha', usernameLower: 'alysha' }); + alyson = await createUser({ username: 'Alyson', usernameLower: 'alyson', host: 'example.com' }); + alyssa = await createUser({ username: 'Alyssa', usernameLower: 'alyssa', host: 'example.com' }); + bob = await createUser({ username: 'Bob', usernameLower: 'bob' }); + bobbi = await createUser({ username: 'Bobbi', usernameLower: 'bobbi' }); + bobbie = await createUser({ username: 'Bobbie', usernameLower: 'bobbie', host: 'example.com' }); + bobby = await createUser({ username: 'Bobby', usernameLower: 'bobby', host: 'example.com' }); + }); + + afterEach(async () => { + await usersRepository.delete({}); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('search', () => { + test('フォãƒãƒ¼ä¸ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ユーザã®ã†ã¡ã€"al"ã‹ã‚‰å§‹ã¾ã‚‹äººãŒå…¨å“¡ãƒ’ットã™ã‚‹', async () => { + await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + await setActive([alice, alyce, alyssa, bob, bobbi, bobbie, bobby]); + await setInactive([alycia, alysha, alyson]); + + const result = await service.search( + { username: 'al' }, + { limit: 100 }, + root, + ); + + // alycia, alysha, alysonã¯éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§å¾Œã‚ã«è¡Œã + expect(result).toEqual([alice, alyce, alyssa, alycia, alysha, alyson].map(x => x.id)); + }); + + test('フォãƒãƒ¼ä¸ã®éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ユーザã®ã†ã¡ã€"al"ã‹ã‚‰å§‹ã¾ã‚‹äººãŒå…¨å“¡ãƒ’ットã™ã‚‹', async () => { + await createFollowings(root, [alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + + const result = await service.search( + { username: 'al' }, + { limit: 100 }, + root, + ); + + // alice, alyceã¯ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„ãªã„ã®ã§å¾Œã‚ã«è¡Œã + expect(result).toEqual([alycia, alysha, alyson, alyssa, alice, alyce].map(x => x.id)); + }); + + test('フォãƒãƒ¼ã—ã¦ã„ãªã„アクティブユーザã®ã†ã¡ã€"al"ã‹ã‚‰å§‹ã¾ã‚‹äººãŒå…¨å“¡ãƒ’ットã™ã‚‹', async () => { + await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + await setInactive([alice, alyce, alycia]); + + const result = await service.search( + { username: 'al' }, + { limit: 100 }, + root, + ); + + // alice, alyce, alyciaã¯éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§å¾Œã‚ã«è¡Œã + expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id)); + }); + + test('フォãƒãƒ¼ã—ã¦ã„ãªã„éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ユーザã®ã†ã¡ã€"al"ã‹ã‚‰å§‹ã¾ã‚‹äººãŒå…¨å“¡ãƒ’ットã™ã‚‹', async () => { + await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + + const result = await service.search( + { username: 'al' }, + { limit: 100 }, + root, + ); + + expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id)); + }); + + test('フォãƒãƒ¼ï¼ˆã‚¢ã‚¯ãƒ†ã‚£ãƒ–)ã€ãƒ•ã‚©ãƒãƒ¼ï¼ˆéžã‚¢ã‚¯ãƒ†ã‚£ãƒ–)ã€éžãƒ•ã‚©ãƒãƒ¼ï¼ˆã‚¢ã‚¯ãƒ†ã‚£ãƒ–)ã€éžãƒ•ã‚©ãƒãƒ¼ï¼ˆéžã‚¢ã‚¯ãƒ†ã‚£ãƒ–)混在時ã®å„ªå…ˆé †ä½åº¦ç¢ºèª', async () => { + await createFollowings(root, [alyson, alyssa, bob, bobbi, bobbie]); + await setActive([root, alyssa, bob, bobbi, alyce, alycia]); + await setInactive([alyson, alice, alysha, bobbie, bobby]); + + const result = await service.search( + { }, + { limit: 100 }, + root, + ); + + // 見る用 + // const users = await usersRepository.findBy({ id: In(result) }).then(it => new Map(it.map(x => [x.id, x]))); + // console.log(result.map(x => users.get(x as any)).map(it => it?.username)); + + // フォãƒãƒ¼ã—ã¦ã¦ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§å…ˆé : alyssa, bob, bobbi + // フォãƒãƒ¼ã—ã¦ã¦éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§æ¬¡: alyson, bobbie + // フォãƒãƒ¼ã—ã¦ãªã„ã‘ã©ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§æ¬¡: alyce, alycia, root(ã‚¢ãƒ«ãƒ•ã‚¡ãƒ™ãƒƒãƒˆé †çš„ã«ã“ã“ã«ãªã‚‹) + // フォãƒãƒ¼ã—ã¦ãªã„ã—éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§æœ€å¾Œ: alice, alysha, bobby + expect(result).toEqual([alyssa, bob, bobbi, alyson, bobbie, alyce, alycia, root, alice, alysha, bobby].map(x => x.id)); + }); + + test('[éžãƒã‚°ã‚¤ãƒ³] アクティブユーザã®ã†ã¡ã€"al"ã‹ã‚‰å§‹ã¾ã‚‹äººãŒå…¨å“¡ãƒ’ットã™ã‚‹', async () => { + await setActive([alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + await setInactive([alice, alyce, alycia]); + + const result = await service.search( + { username: 'al' }, + { limit: 100 }, + ); + + // alice, alyce, alyciaã¯éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã®ã§å¾Œã‚ã«è¡Œã + expect(result).toEqual([alysha, alyson, alyssa, alice, alyce, alycia].map(x => x.id)); + }); + + test('[éžãƒã‚°ã‚¤ãƒ³] éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ユーザã®ã†ã¡ã€"al"ã‹ã‚‰å§‹ã¾ã‚‹äººãŒå…¨å“¡ãƒ’ットã™ã‚‹', async () => { + await setInactive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + + const result = await service.search( + { username: 'al' }, + { limit: 100 }, + ); + + expect(result).toEqual([alice, alyce, alycia, alysha, alyson, alyssa].map(x => x.id)); + }); + + test('フォãƒãƒ¼ä¸ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ユーザã®ã†ã¡ã€"al"ã‹ã‚‰å§‹ã¾ã‚Š"example.com"ã«ã„る人ãŒå…¨å“¡ãƒ’ットã™ã‚‹', async () => { + await createFollowings(root, [alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + + const result = await service.search( + { username: 'al', host: 'exam' }, + { limit: 100 }, + root, + ); + + expect(result).toEqual([alyson, alyssa].map(x => x.id)); + }); + + test('サスペンド済ã¿ãƒ¦ãƒ¼ã‚¶ã¯å‡ºãªã„', async () => { + await setActive([alice, alyce, alycia, alysha, alyson, alyssa, bob, bobbi, bobbie, bobby]); + await setSuspended([alice, alyce, alycia]); + + const result = await service.search( + { username: 'al' }, + { limit: 100 }, + root, + ); + + expect(result).toEqual([alysha, alyson, alyssa].map(x => x.id)); + }); + }); +}); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 2e80b64a9fa313682349f6ed9b14ccb1930fa34d..889a6ac53f6b4effac727931ca66aa10e6a79ea0 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -20,7 +20,8 @@ import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; -import { MiMeta, MiNote } from '@/models/_.js'; +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'; @@ -86,6 +87,7 @@ async function createRandomRemoteUser( } describe('ActivityPub', () => { + let userProfilesRepository: UserProfilesRepository; let imageService: ApImageService; let noteService: ApNoteService; let personService: ApPersonService; @@ -127,6 +129,8 @@ describe('ActivityPub', () => { await app.init(); app.enableShutdownHooks(); + userProfilesRepository = app.get(DI.userProfilesRepository); + noteService = app.get<ApNoteService>(ApNoteService); personService = app.get<ApPersonService>(ApPersonService); rendererService = app.get<ApRendererService>(ApRendererService); @@ -205,6 +209,53 @@ describe('ActivityPub', () => { }); }); + describe('Collection visibility', () => { + test('Public following/followers', async () => { + const actor = createRandomActor(); + actor.following = { + id: `${actor.id}/following`, + type: 'OrderedCollection', + totalItems: 0, + first: `${actor.id}/following?page=1`, + }; + actor.followers = `${actor.id}/followers`; + + resolver.register(actor.id, actor); + resolver.register(actor.followers, { + id: actor.followers, + type: 'OrderedCollection', + totalItems: 0, + first: `${actor.followers}?page=1`, + }); + + const user = await personService.createPerson(actor.id, resolver); + const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id }); + + assert.deepStrictEqual(userProfile.followingVisibility, 'public'); + assert.deepStrictEqual(userProfile.followersVisibility, 'public'); + }); + + test('Private following/followers', async () => { + const actor = createRandomActor(); + actor.following = { + id: `${actor.id}/following`, + type: 'OrderedCollection', + totalItems: 0, + // first: … + }; + actor.followers = `${actor.id}/followers`; + + resolver.register(actor.id, actor); + //resolver.register(actor.followers, { … }); + + const user = await personService.createPerson(actor.id, resolver); + const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id }); + + assert.deepStrictEqual(userProfile.followingVisibility, 'private'); + assert.deepStrictEqual(userProfile.followersVisibility, 'private'); + }); + }); + describe('Renderer', () => { test('Render an announce with visibility: followers', () => { rendererService.renderAnnounce('https://example.com/notes/00example', { diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 86814fffe0e4d7548256b20f80fd7fba52b2a0b2..26de19eaf199fd7172c904c8293e24fce6d67a9c 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -12,11 +12,14 @@ import WebSocket, { ClientOptions } from 'ws'; import fetch, { File, RequestInit, type Headers } from 'node-fetch'; import { DataSource } from 'typeorm'; import { JSDOM } from 'jsdom'; -import { DEFAULT_POLICIES } from '@/core/RoleService.js'; -import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; +import { type Response } from 'node-fetch'; +import Fastify from 'fastify'; import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; +import { ApiError } from '@/server/api/error.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; @@ -25,11 +28,23 @@ export interface UserToken { bearer?: boolean; } +export type SystemWebhookPayload = { + server: string; + hookId: string; + eventId: string; + createdAt: string; + type: string; + body: any; +} + const config = loadConfig(); export const port = config.port; export const origin = config.url; export const host = new URL(config.url).host; +export const WEBHOOK_HOST = 'http://localhost:15080'; +export const WEBHOOK_PORT = 15080; + export const cookie = (me: UserToken): string => { return `token=${me.token};`; }; @@ -47,27 +62,28 @@ export const successfulApiCall = async <E extends keyof misskey.Endpoints, P ext const res = await api(endpoint, parameters, user); const status = assertion.status ?? (res.body == null ? 204 : 200); assert.strictEqual(res.status, status, inspect(res.body, { depth: 5, colors: true })); - return res.body; + + return res.body as misskey.api.SwitchCaseResponseType<E, P>; }; -export const failedApiCall = async <T, E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: { +export const failedApiCall = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(request: ApiRequest<E, P>, assertion: { status: number, code: string, id: string -}): Promise<T> => { +}): Promise<void> => { const { endpoint, parameters, user } = request; const { status, code, id } = assertion; const res = await api(endpoint, parameters, user); assert.strictEqual(res.status, status, inspect(res.body)); - assert.strictEqual(res.body.error.code, code, inspect(res.body)); - assert.strictEqual(res.body.error.id, id, inspect(res.body)); - return res.body; + assert.ok(res.body); + assert.strictEqual(castAsError(res.body as any).error.code, code, inspect(res.body)); + assert.strictEqual(castAsError(res.body as any).error.id, id, inspect(res.body)); }; -export const api = async <E extends keyof misskey.Endpoints>(path: E, params: misskey.Endpoints[E]['req'], me?: UserToken): Promise<{ +export const api = async <E extends keyof misskey.Endpoints, P extends misskey.Endpoints[E]['req']>(path: E, params: P, me?: UserToken): Promise<{ status: number, headers: Headers, - body: any + body: misskey.api.SwitchCaseResponseType<E, P> }> => { const bodyAuth: Record<string, string> = {}; const headers: Record<string, string> = { @@ -88,13 +104,14 @@ export const api = async <E extends keyof misskey.Endpoints>(path: E, params: mi }); const body = res.headers.get('content-type') === 'application/json; charset=utf-8' - ? await res.json() + ? await res.json() as misskey.api.SwitchCaseResponseType<E, P> : null; return { status: res.status, headers: res.headers, - body, + // FIXME: removing this non-null assertion: requires better typing around empty response. + body: body!, }; }; @@ -140,7 +157,8 @@ export const post = async (user: UserToken, params: misskey.Endpoints['notes/cre const res = await api('notes/create', q, user); - return res.body ? res.body.createdNote : null; + // FIXME: the return type should reflect this fact. + return (res.body ? res.body.createdNote : null)!; }; export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => { @@ -296,7 +314,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO body: misskey.entities.DriveFile | null }> => { const absPath = path == null - ? new URL('resources/Lenna.jpg', import.meta.url) + ? new URL('resources/192.jpg', import.meta.url) : isAbsolute(path.toString()) ? new URL(path) : new URL(path, new URL('resources/', import.meta.url)); @@ -454,7 +472,7 @@ export type SimpleGetResponse = { type: string | null, location: string | null }; -export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise<SimpleGetResponse> => { +export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined, bodyExtractor: (res: Response) => Promise<string | null> = _ => Promise.resolve(null)): Promise<SimpleGetResponse> => { const res = await relativeFetch(path, { headers: { Accept: accept, @@ -482,7 +500,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde const body = jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : - null; + await bodyExtractor(res); return { status: res.status, @@ -604,14 +622,6 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { return db; } -export function sleep(msec: number) { - return new Promise<void>(res => { - setTimeout(() => { - res(); - }, msec); - }); -} - export async function sendEnvUpdateRequest(params: { key: string, value?: string }) { const res = await fetch( `http://localhost:${port + 1000}/env`, @@ -642,3 +652,43 @@ export async function sendEnvResetRequest() { throw new Error('server env update failed.'); } } + +// 与ãˆã‚‰ã‚ŒãŸå€¤ã‚’強制的ã«ã‚¨ãƒ©ãƒ¼ã¨ã¿ãªã™ã€‚ã“ã®é–¢æ•°ã¯åž‹å®‰å…¨æ€§ã‚’ç ´å£Šã™ã‚‹ãŸã‚ã€ç•°å¸¸ç³»ã®ã‚¢ã‚µãƒ¼ã‚·ãƒ§ãƒ³ä»¥å¤–ã§ç”¨ã„られるã¹ãã§ã¯ãªã„。 +// FIXME(misskey-js): misskey-jsãŒã‚¨ãƒ©ãƒ¼æƒ…å ±ã‚’å…¬é–‹ã™ã‚‹ã‚ˆã†ã«ãªã£ãŸã‚‰ã“ã®é–¢æ•°ã‚’廃æ¢ã™ã‚‹ +export function castAsError(obj: Record<string, unknown>): { error: ApiError } { + return obj as { error: ApiError }; +} + +export async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>, port = WEBHOOK_PORT): Promise<T> { + const fastify = Fastify(); + + let timeoutHandle: NodeJS.Timeout | null = null; + const result = await new Promise<string>(async (resolve, reject) => { + fastify.all('/', async (req, res) => { + timeoutHandle && clearTimeout(timeoutHandle); + + const body = JSON.stringify(req.body); + res.status(200).send('ok'); + await fastify.close(); + resolve(body); + }); + + await fastify.listen({ port }); + + timeoutHandle = setTimeout(async () => { + await fastify.close(); + reject(new Error('timeout')); + }, 3000); + + try { + await postAction(); + } catch (e) { + await fastify.close(); + reject(e); + } + }); + + await fastify.close(); + + return JSON.parse(result) as T; +} diff --git a/packages/frontend/.eslintrc.cjs b/packages/frontend/.eslintrc.cjs deleted file mode 100644 index 20f88dc0786ca3e218994e0461c50c8a1c81bed6..0000000000000000000000000000000000000000 --- a/packages/frontend/.eslintrc.cjs +++ /dev/null @@ -1,82 +0,0 @@ -module.exports = { - root: true, - env: { - 'node': false, - }, - parser: 'vue-eslint-parser', - parserOptions: { - 'parser': '@typescript-eslint/parser', - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - extraFileExtensions: ['.vue'], - }, - extends: [ - '../shared/.eslintrc.js', - 'plugin:vue/vue3-recommended', - ], - 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-destructure': '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'], - }, - globals: { - // 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, - }, -}; diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts index 7c70972e1e3d8d1ea610bb641771779099fb1fa8..1299910499c8906fe23ff40adfee5a0fead34850 100644 --- a/packages/frontend/.storybook/changes.ts +++ b/packages/frontend/.storybook/changes.ts @@ -47,14 +47,12 @@ await fs.readFile( ) ) .map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, '')) - .map((path) => (path.startsWith('.') ? path : `./${path}`)) ); if ( micromatch(Array.from(modules), [ '../../assets/**', '../../fluent-emojis/**', '../../locales/ja-JP.yml', - '../../misskey-assets/**', 'assets/**', 'public/**', '../../pnpm-lock.yaml', diff --git a/packages/frontend/.storybook/charts.ts b/packages/frontend/.storybook/charts.ts new file mode 100644 index 0000000000000000000000000000000000000000..5015012a8246586ec1d20fa93e87e4dde43d83cc --- /dev/null +++ b/packages/frontend/.storybook/charts.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { DefaultBodyType, HttpResponse, HttpResponseResolver, JsonBodyType, PathParams, http } from 'msw'; +import seedrandom from 'seedrandom'; +import { action } from '@storybook/addon-actions'; + +function getChartArray(seed: string, limit: number, option?: { accumulate?: boolean, mul?: number }): number[] { + const rng = seedrandom(seed); + const max = Math.floor(option?.mul ?? 250 * rng()); + let accumulation = 0; + const array: number[] = []; + for (let i = 0; i < limit; i++) { + const num = Math.floor((max + 1) * rng()); + if (option?.accumulate) { + accumulation += num; + array.unshift(accumulation); + } else { + array.push(num); + } + } + return array; +} + +export function getChartResolver(fields: string[], option?: { accumulate?: boolean, mulMap?: Record<string, number> }): HttpResponseResolver<PathParams, DefaultBodyType, JsonBodyType> { + return ({ request }) => { + action(`GET ${request.url}`)(); + const limitParam = new URL(request.url).searchParams.get('limit'); + const limit = limitParam ? parseInt(limitParam) : 30; + const res = {}; + for (const field of fields) { + const layers = field.split('.'); + let current = res; + while (layers.length > 1) { + const currentKey = layers.shift()!; + if (current[currentKey] == null) current[currentKey] = {}; + current = current[currentKey]; + } + current[layers[0]] = getChartArray(field, limit, { + accumulate: option?.accumulate, + mul: option?.mulMap != null && field in option.mulMap ? option.mulMap[field] : undefined, + }); + } + return HttpResponse.json(res); + }; +} diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index a2325e69c6b72e1178088457789e43141e08c150..758827c196d659d15f65fb23067f8c4691d1de48 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { AISCRIPT_VERSION } from '@syuilo/aiscript'; import type { entities } from 'misskey-js' export function abuseUserReport() { @@ -22,6 +23,55 @@ export function abuseUserReport() { }; } +export function channel(id = 'somechannelid', name = 'Some Channel', bannerUrl: string | null = 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true'): entities.Channel { + return { + id, + createdAt: '2016-12-28T22:49:51.000Z', + lastNotedAt: '2016-12-28T22:49:51.000Z', + name, + description: null, + userId: null, + bannerUrl, + pinnedNoteIds: [], + color: '#000', + isArchived: false, + usersCount: 1, + notesCount: 1, + isSensitive: false, + allowRenoteToExternal: false, + }; +} + +export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip { + return { + id, + createdAt: '2016-12-28T22:49:51.000Z', + lastClippedAt: null, + userId: 'someuserid', + user: userLite(), + notesCount: undefined, + name, + description: 'Some clip description', + isPublic: false, + favoritedCount: 0, + }; +} + +export function emojiDetailed(id = 'someemojiid', name = 'some_emoji'): entities.EmojiDetailed { + return { + id, + aliases: ['alias1', 'alias2'], + name, + category: 'emojiCategory', + host: null, + url: '/client-assets/about-icon.png', + license: null, + isSensitive: false, + localOnly: false, + roleIdsThatCanBeUsedThisEmojiAsReaction: ['roleId1', 'roleId2'], + }; +} + export function galleryPost(isSensitive = false) { return { id: 'somepostid', @@ -65,7 +115,99 @@ export function file(isSensitive = false) { }; } -export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed { +const script = `/// @ ${AISCRIPT_VERSION} + +var name = "" + +Ui:render([ + Ui:C:textInput({ + label: "Your name" + onInput: @(v) { name = v } + }) + Ui:C:button({ + text: "Hello" + onClick: @() { + Mk:dialog(null, \`Hello, {name}!\`) + } + }) +]) +`; + +export function flash(): entities.Flash { + return { + id: 'someflashid', + createdAt: '2016-12-28T22:49:51.000Z', + updatedAt: '2016-12-28T22:49:51.000Z', + userId: 'someuserid', + user: userLite(), + title: 'Some Play title', + summary: 'Some Play summary', + script, + visibility: 'public', + likedCount: 0, + isLiked: false, + }; +} + +export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder { + return { + id, + createdAt: '2016-12-28T22:49:51.000Z', + name, + parentId, + }; +} + +export function federationInstance(): entities.FederationInstance { + return { + id: 'someinstanceid', + firstRetrievedAt: '2021-01-01T00:00:00.000Z', + host: 'misskey-hub.net', + usersCount: 10, + notesCount: 20, + followingCount: 5, + followersCount: 15, + isNotResponding: false, + isSuspended: false, + suspensionState: 'none', + isBlocked: false, + softwareName: 'misskey', + softwareVersion: '2024.5.0', + openRegistrations: false, + name: 'Misskey Hub', + description: '', + maintainerName: '', + maintainerEmail: '', + isSilenced: false, + iconUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/about-icon.png?raw=true', + faviconUrl: '', + themeColor: '', + infoUpdatedAt: '', + latestRequestReceivedAt: '', + }; +} + +export function note(id = 'somenoteid'): entities.Note { + return { + id, + createdAt: '2016-12-28T22:49:51.000Z', + deletedAt: null, + text: 'some note', + cw: null, + userId: 'someuserid', + user: userLite(), + visibility: 'public', + reactionAcceptance: 'nonSensitiveOnly', + reactionEmojis: {}, + reactions: {}, + myReaction: null, + reactionCount: 0, + renoteCount: 0, + repliesCount: 0, + }; +} + +export function userLite(id = 'someuserid', username = 'miskist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'Misskey User'): entities.UserLite { return { id, username, @@ -76,6 +218,12 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay', avatarDecorations: [], emojis: {}, + }; +} + +export function userDetailed(id = 'someuserid', username = 'miskist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'Misskey User'): entities.UserDetailed { + return { + ...userLite(id, username, host, name), bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog', bannerUrl: 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true', birthday: '2014-06-20', @@ -127,7 +275,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi movedTo: null, alsoKnownAs: null, notify: 'none', - memo: null + memo: null, }; } diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index d74c83a500079bc0254aedfd2ea9c09f12779bc6..490a441b70553924ede8753a8f3e7873de5c1ff7 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -397,13 +397,15 @@ function toStories(component: string): Promise<string> { const globs = await Promise.all([ glob('src/components/global/Mk*.vue'), glob('src/components/global/RouterView.vue'), - glob('src/components/Mk{A,B}*.vue'), - glob('src/components/MkDigitalClock.vue'), + glob('src/components/Mk[A-E]*.vue'), + glob('src/components/MkFlashPreview.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkUserSetupDialog.vue'), glob('src/components/MkUserSetupDialog.*.vue'), + glob('src/components/MkInstanceCardMini.vue'), glob('src/components/MkInviteCode.vue'), + glob('src/pages/search.vue'), glob('src/pages/user/home.vue'), ]); const components = globs.flat(); diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts index d3822942cd467aa7871cf7394b4d8440878ab330..9f318cf449b9a38979362495bc12583d31ac22d1 100644 --- a/packages/frontend/.storybook/main.ts +++ b/packages/frontend/.storybook/main.ts @@ -15,6 +15,7 @@ const _dirname = fileURLToPath(new URL('.', import.meta.url)); const config = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + staticDirs: [{ from: '../assets', to: '/client-assets' }], addons: [ getAbsolutePath('@storybook/addon-essentials'), getAbsolutePath('@storybook/addon-interactions'), diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts index 982a2979ac8ceac74cbb816e923746136b50baf5..d000a282328fe13dd10eb64afd51178ee8201320 100644 --- a/packages/frontend/.storybook/preview.ts +++ b/packages/frontend/.storybook/preview.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { FORCE_REMOUNT } from '@storybook/core-events'; +import { FORCE_RE_RENDER, FORCE_REMOUNT } from '@storybook/core-events'; import { addons } from '@storybook/preview-api'; import { type Preview, setup } from '@storybook/vue3'; import isChromatic from 'chromatic/isChromatic'; -import { initialize, mswDecorator } from 'msw-storybook-addon'; +import { initialize, mswLoader } from 'msw-storybook-addon'; import { userDetailed } from './fakes.js'; import locale from './locale.js'; import { commonHandlers, onUnhandledRequest } from './mocks.js'; @@ -16,7 +16,7 @@ import '../src/style.scss'; const appInitialized = Symbol(); -let lastStory = null; +let lastStory: string | null = null; let moduleInitialized = false; let unobserve = () => {}; let misskeyOS = null; @@ -110,7 +110,7 @@ const preview = { }).catch(() => {}); Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => { initLocalStorage(); - channel.emit(FORCE_REMOUNT, { storyId: context.id }); + channel.emit(FORCE_RE_RENDER, { storyId: context.id }); }); } const story = Story(); @@ -122,7 +122,6 @@ const preview = { } return story; }, - mswDecorator, (Story, context) => { return { setup() { @@ -137,6 +136,7 @@ const preview = { }; }, ], + loaders: [mswLoader], parameters: { controls: { exclude: /^__/, diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..dd8f03dac5102ecb648e0beb6516a40c5421a621 --- /dev/null +++ b/packages/frontend/eslint.config.js @@ -0,0 +1,95 @@ +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'; + +export default [ + ...sharedConfig, + { + files: ['src/**/*.vue'], + ...pluginMisskey.configs.typescript, + }, + ...pluginVue.configs['flat/recommended'], + { + files: ['src/**/*.{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'], + }, + }, +]; diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts index 948cc8b2c91dc01041ba2df38f957fe2617b845f..5d8cf05fff879efd1c740b942641a53799540cf0 100644 --- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts +++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts @@ -63,7 +63,7 @@ import { M as MkContainer } from './MkContainer-!~{03M}~.js'; import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js'; import './photoswipe-!~{003}~.js'; -const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ph-image-square ph-bold ph-lg" }, null, -1); +const _hoisted_1 = /* @__PURE__ */ createBaseVNode("i", { class: "ti ti-photo" }, null, -1); const _sfc_main = /* @__PURE__ */ defineComponent({ __name: "index.photos", props: { @@ -178,7 +178,7 @@ import {M as MkContainer} from './MkContainer-!~{03M}~.js'; import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js'; import './photoswipe-!~{003}~.js'; const _hoisted_1 = createBaseVNode("i", { - class: "ph-image-square ph-bold ph-lg" + class: "ti ti-photo" }, null, -1); const index_photos = defineComponent({ __name: "index.photos", @@ -345,7 +345,7 @@ const _sfc_main = defineComponent({ class: $style["date-1"] }, [ h("i", { - class: \`ph-caret-up ph-bold ph-lg \${$style["date-1-icon"]}\` + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` }), getDateText(item.createdAt) ]), @@ -354,7 +354,7 @@ const _sfc_main = defineComponent({ }, [ getDateText(props.items[i + 1].createdAt), h("i", { - class: \`ph-caret-down ph-bold ph-lg \${$style["date-2-icon"]}\` + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` }) ]) ])); @@ -511,11 +511,11 @@ const _sfc_main = defineComponent({ }, [h("span", { class: $style["date-1"] }, [h("i", { - class: \`ph-caret-up ph-bold ph-lg \${$style["date-1-icon"]}\` + class: \`ti ti-chevron-up \${$style["date-1-icon"]}\` }), getDateText(item.createdAt)]), h("span", { class: $style["date-2"] }, [getDateText(props.items[i + 1].createdAt), h("i", { - class: \`ph-caret-down ph-bold ph-lg \${$style["date-2-icon"]}\` + class: \`ti ti-chevron-down \${$style["date-2-icon"]}\` })])])); return [el, separator]; } else { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 648134b491d0b0261d4daa8e13dbf580dcf82615..35cb900d3bcaf9484a6a4a50db688170a4a516f0 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -23,26 +23,26 @@ "@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.5", + "@rollup/plugin-replace": "5.0.7", "@rollup/pluginutils": "5.1.0", "@transfem-org/sfm-js": "0.24.5", - "@syuilo/aiscript": "0.18.0", + "@syuilo/aiscript": "0.19.0", "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.0.4", - "@vue/compiler-sfc": "3.4.26", - "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9", + "@vitejs/plugin-vue": "5.1.0", + "@vue/compiler-sfc": "3.4.37", + "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11", "astring": "1.8.6", "broadcast-channel": "7.0.0", "buraha": "0.0.1", "canvas-confetti": "1.9.3", - "chart.js": "4.4.2", + "chart.js": "4.4.3", "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.3.0", - "compare-versions": "6.1.0", - "cropperjs": "2.0.0-beta.5", + "chromatic": "11.5.6", + "compare-versions": "6.1.1", + "cropperjs": "2.0.0-rc.1", "date-fns": "2.30.0", "escape-regexp": "0.0.1", "estree-walker": "3.0.3", @@ -56,87 +56,87 @@ "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", - "photoswipe": "5.4.3", + "photoswipe": "5.4.4", "punycode": "2.3.1", - "rollup": "4.17.2", + "rollup": "4.19.1", "sanitize-html": "2.13.0", - "sass": "1.76.0", - "shiki": "1.4.0", + "sass": "1.77.8", + "shiki": "1.12.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.164.1", - "throttle-debounce": "5.0.0", + "three": "0.167.0", + "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.8", + "tsc-alias": "1.8.10", "tsconfig-paths": "4.2.0", - "typescript": "5.4.5", - "uuid": "9.0.1", - "v-code-diff": "1.11.0", - "vite": "5.2.11", - "vue": "3.4.26", + "typescript": "5.5.4", + "uuid": "10.0.0", + "v-code-diff": "1.12.0", + "vite": "5.3.5", + "vue": "3.4.37", "vuedraggable": "next" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "1.0.0", "@misskey-dev/summaly": "5.1.0", - "@storybook/addon-actions": "8.0.9", - "@storybook/addon-essentials": "8.0.9", - "@storybook/addon-interactions": "8.0.9", - "@storybook/addon-links": "8.0.9", - "@storybook/addon-mdx-gfm": "8.0.9", - "@storybook/addon-storysource": "8.0.9", - "@storybook/blocks": "8.0.9", - "@storybook/components": "8.0.9", - "@storybook/core-events": "8.0.9", - "@storybook/manager-api": "8.0.9", - "@storybook/preview-api": "8.0.9", - "@storybook/react": "8.0.9", - "@storybook/react-vite": "8.0.9", - "@storybook/test": "8.0.9", - "@storybook/theming": "8.0.9", - "@storybook/types": "8.0.9", - "@storybook/vue3": "8.0.9", - "@storybook/vue3-vite": "8.0.9", - "@testing-library/vue": "8.0.3", + "@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", + "@testing-library/vue": "8.1.0", "@types/escape-regexp": "0.0.3", "@types/estree": "1.0.5", - "@types/matter-js": "0.19.6", - "@types/micromatch": "4.0.7", - "@types/node": "20.12.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/seedrandom": "3.0.8", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", - "@types/uuid": "9.0.8", - "@types/ws": "8.5.10", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", - "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.4.26", - "acorn": "8.11.3", + "@types/uuid": "10.0.0", + "@types/ws": "8.5.11", + "@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", + "acorn": "8.12.1", "cross-env": "7.0.3", - "cypress": "13.8.1", - "eslint": "8.57.0", + "cypress": "13.13.1", "eslint-plugin-import": "2.29.1", - "eslint-plugin-vue": "9.25.0", + "eslint-plugin-vue": "9.27.0", "fast-glob": "3.3.2", "happy-dom": "10.0.3", "intersection-observer": "0.12.2", - "micromatch": "4.0.5", - "msw": "2.2.14", - "msw-storybook-addon": "2.0.1", - "nodemon": "3.1.0", - "prettier": "3.2.5", + "micromatch": "4.0.7", + "msw": "2.3.4", + "msw-storybook-addon": "2.0.3", + "nodemon": "3.1.4", + "prettier": "3.3.3", "react": "18.3.1", "react-dom": "18.3.1", - "start-server-and-test": "2.0.3", - "storybook": "8.0.9", + "seedrandom": "3.0.5", + "start-server-and-test": "2.0.4", + "storybook": "8.2.6", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", - "vitest": "0.34.6", + "vitest": "1.6.0", "vitest-fetch-mock": "0.2.2", - "vue-component-type-helpers": "2.0.16", - "vue-eslint-parser": "9.4.2", - "vue-tsc": "2.0.16" + "vue-component-type-helpers": "2.0.29", + "vue-eslint-parser": "9.4.3", + "vue-tsc": "2.0.29" } } diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 90cc2e51c95982b9c58fc398e0bc39db51779200..4fdd51c33bbd35eaaea7299baf0a455014ebc40a 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -121,7 +121,7 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr res.json().then(done2, fail2); })) .then(async res => { - if (res.error) { + if ('error' in res) { if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { // SUSPENDED if (forceShowDialog || $i && (token === $i.token || id === $i.id)) { @@ -185,10 +185,12 @@ export async function refreshAccount() { export async function login(token: Account['token'], redirect?: string) { const showing = ref(true); - popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { success: false, showing: showing, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); if (_DEV_) console.log('logging as token ', token); const me = await fetchAccount(token, undefined, true) .catch(reason => { @@ -224,21 +226,23 @@ export async function openAccountMenu(opts: { if (!$i) return; function showSigninDialog() { - popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { done: res => { addAccount(res.id, res.i); success(); }, - }, 'closed'); + closed: () => dispose(), + }); } function createAccount() { - popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { done: res => { addAccount(res.id, res.i); switchAccountWithToken(res.i); }, - }, 'closed'); + closed: () => dispose(), + }); } async function switchAccount(account: Misskey.entities.UserDetailed) { @@ -293,7 +297,7 @@ export async function openAccountMenu(opts: { avatar: $i, }, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { type: 'parent' as const, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.addAccount, children: [{ text: i18n.ts.existingAccount, @@ -304,7 +308,7 @@ export async function openAccountMenu(opts: { }], }, { type: 'link' as const, - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', text: i18n.ts.manageAccounts, to: '/settings/accounts', }, { diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index e7c2ef94496f65a7ba06ff631e0fa4f852e62df4..c10930a03820bc7a20e52a1e28bbdfefc224ae10 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -5,6 +5,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 { i18n } from '@/i18n.js'; import { alert, confirm, popup, post, toast } from '@/os.js'; @@ -13,7 +14,6 @@ import * as sound from '@/scripts/sound.js'; import { $i, signout, updateAccount } from '@/account.js'; import { instance } from '@/instance.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; -import { makeHotkey } from '@/scripts/hotkey.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; @@ -22,6 +22,7 @@ import { deckStore } from '@/ui/deck/deck-store.js'; 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'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -36,7 +37,9 @@ export async function mainBoot() { emojiPicker.init(); if (isClientUpdated && $i) { - popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, { + closed: () => dispose(), + }); } const stream = useStream(); @@ -66,14 +69,6 @@ export async function mainBoot() { }); } - const hotkeys = { - 'd': (): void => { - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': (): void => { - mainRouter.push('/search'); - }, - }; try { if (defaultStore.state.enableSeasonalScreenEffect) { const month = new Date().getMonth() + 1; @@ -102,29 +97,34 @@ export async function mainBoot() { } if ($i) { - // only add post shortcuts if logged in - hotkeys['p|n'] = post; - defaultStore.loaded.then(() => { if (defaultStore.state.accountSetupWizard !== -1) { - popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { + closed: () => dispose(), + }); } }); for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) { - popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { announcement, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } - stream.on('announcementCreated', (ev) => { + function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) { const announcement = ev.announcement; if (announcement.display === 'dialog') { - popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { announcement, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } - }); + } + + stream.on('announcementCreated', onAnnouncementCreated); if ($i.isDeleted) { alert({ @@ -230,29 +230,34 @@ export async function mainBoot() { claimAchievement('client60min'); }, 1000 * 60 * 60); - const lastUsed = miLocalStorage.getItem('lastUsed'); - if (lastUsed) { - const lastUsedDate = parseInt(lastUsed, 10); - // 二時間以上å‰ãªã‚‰ - if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { - toast(i18n.tsx.welcomeBackWithName({ - name: $i.name || $i.username, - }), true); - } - } - miLocalStorage.setItem('lastUsed', Date.now().toString()); + // é‚ªé” + //const lastUsed = miLocalStorage.getItem('lastUsed'); + //if (lastUsed) { + // const lastUsedDate = parseInt(lastUsed, 10); + // // 二時間以上å‰ãªã‚‰ + // if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { + // toast(i18n.tsx.welcomeBackWithName({ + // name: $i.name || $i.username, + // }), true); + // } + //} + //miLocalStorage.setItem('lastUsed', Date.now().toString()); const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { - popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, { + closed: () => dispose(), + }); } } const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://activitypub.software/TransFem-org/Sharkey/') { - popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, { + closed: () => dispose(), + }); } if ('Notification' in window) { @@ -288,7 +293,7 @@ export async function mainBoot() { main.on('unreadNotification', () => { attemptShowNotificationDot(); - + const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1; updateAccount({ hasUnreadNotification: true, @@ -325,6 +330,9 @@ export async function mainBoot() { updateAccount({ hasUnreadAnnouncement: false }); }); + // 個人宛ã¦ãŠçŸ¥ã‚‰ã›ãŒç™ºè¡Œã•ã‚ŒãŸã¨ã + main.on('announcementCreated', onAnnouncementCreated); + // トークンãŒå†ç”Ÿæˆã•ã‚ŒãŸã¨ã // ã“ã®ã¾ã¾ã§ã¯MisskeyãŒåˆ©ç”¨ã§ããªã„ã®ã§å¼·åˆ¶çš„ã«ã‚µã‚¤ãƒ³ã‚¢ã‚¦ãƒˆã•ã›ã‚‹ main.on('myTokenRegenerated', () => { @@ -333,7 +341,19 @@ export async function mainBoot() { } // shortcut - document.addEventListener('keydown', makeHotkey(hotkeys)); + const keymap = { + 'p|n': () => { + if ($i == null) return; + post(); + }, + 'd': () => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': () => { + mainRouter.push('/search'); + }, + } as const satisfies Keymap; + document.addEventListener('keydown', makeHotkey(keymap), { passive: false }); initializeSw(); } diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts index 017457822bc6d8feced606440a07b13151ba73df..35c84d5568cb2be7bb22a8d0c1785310a41af2a4 100644 --- a/packages/frontend/src/boot/sub-boot.ts +++ b/packages/frontend/src/boot/sub-boot.ts @@ -5,9 +5,12 @@ import { createApp, defineAsyncComponent } from 'vue'; import { common } from './common.js'; +import { emojiPicker } from '@/scripts/emoji-picker.js'; export async function subBoot() { const { isClientUpdated } = await common(() => createApp( defineAsyncComponent(() => import('@/ui/minimum.vue')), )); + + emojiPicker.init(); } diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue index f228df85a64bbb1d43adeff23db475bd261d096f..a634a748e936c1373cfc85b3db07b2ce8452bb05 100644 --- a/packages/frontend/src/components/MkAbuseReportWindow.vue +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkWindow ref="uiWindow" :initialWidth="400" :initialHeight="500" :canResize="true" @closed="emit('closed')"> <template #header> - <i class="ph-warning-circle ph-bold ph-lg" style="margin-right: 0.5em;"></i> + <i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i> <I18n :src="i18n.ts.reportAbuseOf" tag="span"> <template #name> <b><MkAcct :user="user"/></b> @@ -39,7 +39,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ - user: Misskey.entities.UserDetailed; + user: Misskey.entities.UserLite; initialComment?: string; }>(); diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 83283a70736a76b6ba33f8f9a7cc22af205a09c2..6c0774b63426239e3c75edec379226a2fbc32651 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div v-if="user" :class="$style.root"> - <i class="ph-airplane-takeoff ph-bold ph-lg" style="margin-right: 8px;"></i> + <i class="ti ti-plane-departure" style="margin-right: 8px;"></i> {{ i18n.ts.accountMoved }} <MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/> </div> diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index 8ec3ec0505ffcffff1d0b6c592bc39da353b3c1f..ab7bafc47abdb49a9da524fb81fc84b8b4673f69 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -153,7 +153,7 @@ onMounted(() => { background: linear-gradient(0deg, #ffee20, #eb7018); } - &:before { + &::before { content: ""; display: block; position: absolute; @@ -173,7 +173,7 @@ onMounted(() => { background: linear-gradient(0deg, #e1e1e1, #7c7c7c); } - &:before { + &::before { content: ""; display: block; position: absolute; diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue index 032a815ee61b2af06185d0911a0906a15ea0df71..c81fea175cbc7f6ea66cd51e9cebab52fb9a2512 100644 --- a/packages/frontend/src/components/MkAnnouncementDialog.vue +++ b/packages/frontend/src/components/MkAnnouncementDialog.vue @@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div ref="rootEl" :class="$style.root"> <div :class="$style.header"> <span :class="$style.icon"> - <i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i> - <i v-else-if="announcement.icon === 'warning'" class="ph-warning-circle ph-bold ph-lg" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ph-seal-warning ph-bold ph-lg" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ph-check-circle ph-bold ph-lg" style="color: var(--success);"></i> + <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> </span> <span :class="$style.title">{{ announcement.title }}</span> </div> diff --git a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..1749e07a4ea4c87cc6a65e367f25020aa6d77fe4 --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkAntennaEditor from './MkAntennaEditor.vue'; +export const Default = { + render(args) { + return { + components: { + MkAntennaEditor, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + created: action('created'), + updated: action('updated'), + deleted: action('deleted'), + }; + }, + }, + template: '<MkAntennaEditor v-bind="props" v-on="events" />', + }; + }, + args: { + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/antennas/create', async ({ request }) => { + action('POST /api/antennas/create')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/update', async ({ request }) => { + action('POST /api/antennas/update')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/delete', async ({ request }) => { + action('POST /api/antennas/delete')(await request.json()); + return HttpResponse.json(); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkAntennaEditor>; diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/components/MkAntennaEditor.vue similarity index 64% rename from packages/frontend/src/pages/my-antennas/editor.vue rename to packages/frontend/src/components/MkAntennaEditor.vue index 51dbb3b08ff3301258448fafecda8090acc17c43..cb7ee3d6ca38abd27d4c49101225d1d829d77ca4 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -41,8 +41,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch> </div> <div :class="$style.actions"> - <MkButton inline primary @click="saveAntenna()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <div class="_buttons"> + <MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="initialAntenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> </div> </div> </MkSpacer> @@ -59,28 +61,53 @@ import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { deepMerge } from '@/scripts/merge.js'; +import type { DeepPartial } from '@/scripts/merge.js'; + +type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & { + id?: string; + createdAt?: string; + updatedAt?: string; +}; const props = defineProps<{ - antenna: Misskey.entities.Antenna + antenna?: DeepPartial<PartialAllowedAntenna>; }>(); +const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, { + name: '', + src: 'all', + userListId: null, + users: [], + keywords: [], + excludeKeywords: [], + excludeBots: false, + withReplies: false, + caseSensitive: false, + localOnly: false, + withFile: false, + isActive: true, + hasUnreadNote: false, + notify: false, +}); + const emit = defineEmits<{ - (ev: 'created'): void, - (ev: 'updated'): void, + (ev: 'created', newAntenna: Misskey.entities.Antenna): void, + (ev: 'updated', editedAntenna: Misskey.entities.Antenna): void, (ev: 'deleted'): void, }>(); -const name = ref<string>(props.antenna.name); -const src = ref<Misskey.entities.AntennasCreateRequest['src']>(props.antenna.src); -const userListId = ref<string | null>(props.antenna.userListId); -const users = ref<string>(props.antenna.users.join('\n')); -const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('\n')); -const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); -const caseSensitive = ref<boolean>(props.antenna.caseSensitive); -const localOnly = ref<boolean>(props.antenna.localOnly); -const excludeBots = ref<boolean>(props.antenna.excludeBots); -const withReplies = ref<boolean>(props.antenna.withReplies); -const withFile = ref<boolean>(props.antenna.withFile); +const name = ref<string>(initialAntenna.name); +const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src); +const userListId = ref<string | null>(initialAntenna.userListId); +const users = ref<string>(initialAntenna.users.join('\n')); +const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n')); +const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n')); +const caseSensitive = ref<boolean>(initialAntenna.caseSensitive); +const localOnly = ref<boolean>(initialAntenna.localOnly); +const excludeBots = ref<boolean>(initialAntenna.excludeBots); +const withReplies = ref<boolean>(initialAntenna.withReplies); +const withFile = ref<boolean>(initialAntenna.withFile); const userLists = ref<Misskey.entities.UserList[] | null>(null); watch(() => src.value, async () => { @@ -104,24 +131,26 @@ async function saveAntenna() { excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')), }; - if (props.antenna.id == null) { - await os.apiWithDialog('antennas/create', antennaData); - emit('created'); + if (initialAntenna.id == null) { + const res = await os.apiWithDialog('antennas/create', antennaData); + emit('created', res); } else { - await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: props.antenna.id }); - emit('updated'); + const res = await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: initialAntenna.id }); + emit('updated', res); } } async function deleteAntenna() { + if (initialAntenna.id == null) return; + const { canceled } = await os.confirm({ type: 'warning', - text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }), + text: i18n.tsx.removeAreYouSure({ x: initialAntenna.name }), }); if (canceled) return; await misskeyApi('antennas/delete', { - antennaId: props.antenna.id, + antennaId: initialAntenna.id, }); os.success(); diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c6ca83b47a53eab21835ab62bf33af3888d5f29 --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue'; +export const Default = { + render(args) { + return { + components: { + MkAntennaEditorDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + created: action('created'), + updated: action('updated'), + deleted: action('deleted'), + closed: action('closed'), + }; + }, + }, + template: '<MkAntennaEditorDialog v-bind="props" v-on="events" />', + }; + }, + args: { + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/antennas/create', async ({ request }) => { + action('POST /api/antennas/create')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/update', async ({ request }) => { + action('POST /api/antennas/update')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/delete', async ({ request }) => { + action('POST /api/antennas/delete')(await request.json()); + return HttpResponse.json(); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkAntennaEditorDialog>; diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.vue b/packages/frontend/src/components/MkAntennaEditorDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..6d815d29f31cf1eee43990602350254443f4b383 --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditorDialog.vue @@ -0,0 +1,63 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :withOkButton="false" + :width="500" + :height="550" + @close="close()" + @closed="emit('closed')" +> + <template #header>{{ antenna == null ? i18n.ts.createAntenna : i18n.ts.editAntenna }}</template> + <XAntennaEditor + :antenna="antenna" + @created="onAntennaCreated" + @updated="onAntennaUpdated" + @deleted="onAntennaDeleted" + /> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import XAntennaEditor from '@/components/MkAntennaEditor.vue'; +import { i18n } from '@/i18n.js'; + +defineProps<{ + antenna?: Misskey.entities.Antenna; +}>(); + +const emit = defineEmits<{ + (ev: 'created', newAntenna: Misskey.entities.Antenna): void, + (ev: 'updated', editedAntenna: Misskey.entities.Antenna): void, + (ev: 'deleted'): void, + (ev: 'closed'): void, +}>(); + +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); + +function onAntennaCreated(newAntenna: Misskey.entities.Antenna) { + emit('created', newAntenna); + dialog.value?.close(); +} + +function onAntennaUpdated(editedAntenna: Misskey.entities.Antenna) { + emit('updated', editedAntenna); + dialog.value?.close(); +} + +function onAntennaDeleted() { + emit('deleted'); + dialog.value?.close(); +} + +function close() { + dialog.value?.close(); +} +</script> diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index c8d2797e168f251b8418ea41d54d2b47fefb7969..f968fc586109aaf2228a52e22a2e459b6231299a 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :type="type" :name="name" :value="value" + :disabled="disabled" @click="emit('click', $event)" @mousedown="onMousedown" > @@ -55,6 +56,7 @@ const props = defineProps<{ asLike?: boolean; name?: string; value?: string; + disabled?: boolean; }>(); const emit = defineEmits<{ @@ -248,7 +250,6 @@ function onMousedown(evt: MouseEvent): void { } &:focus-visible { - outline: solid 2px var(--focus); outline-offset: 2px; } diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index c64bb47e771b517edb383ffac5e22dc0894250bc..c5b6e0caede7f712c6408f54c35e9357377524b7 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -104,7 +104,6 @@ async function requestRender() { }); } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { const { default: Widget } = await import('@mcaptcha/vanilla-glue'); - // @ts-expect-error avoid typecheck error new Widget({ siteKey: { instanceUrl: new URL(props.instanceUrl), diff --git a/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9770670dcdda40825f60565b6a2b56abb4c7553 --- /dev/null +++ b/packages/frontend/src/components/MkChannelFollowButton.stories.impl.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { expect, userEvent, within } from '@storybook/test'; +import { channel } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkChannelFollowButton from './MkChannelFollowButton.vue'; +import { i18n } from '@/i18n.js'; + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export const Default = { + render(args) { + return { + components: { + MkChannelFollowButton, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChannelFollowButton v-bind="props" />', + }; + }, + args: { + channel: channel(), + full: true, + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + await expect(buttonElement).toHaveTextContent(i18n.ts.follow); + await userEvent.click(buttonElement); + await sleep(1000); + await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow); + await userEvent.click(buttonElement); + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/channels/follow', async ({ request }) => { + action('POST /api/channels/follow')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/channels/unfollow', async ({ request }) => { + action('POST /api/channels/unfollow')(await request.json()); + return HttpResponse.json({}); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkChannelFollowButton>; diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 07732d9205e92ce6c09daf2efa7591fbb7e1e30f..6dace43fdef12c1abbf2dbbce0bdfe3612f2d919 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only > <template v-if="!wait"> <template v-if="isFollowing"> - <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ph-minus ph-bold ph-lg"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i> </template> <template v-else> - <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ph-plus ph-bold ph-lg"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> </template> </template> <template v-else> @@ -26,17 +26,18 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref } from 'vue'; +import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ - channel: Record<string, any>; + channel: Misskey.entities.Channel; full?: boolean; }>(), { full: false, }); -const isFollowing = ref<boolean>(props.channel.isFollowing); +const isFollowing = ref(props.channel.isFollowing); const wait = ref(false); async function onClick() { @@ -86,17 +87,7 @@ async function onClick() { } &:focus-visible { - &:after { - content: ""; - pointer-events: none; - position: absolute; - top: -5px; - right: -5px; - bottom: -5px; - left: -5px; - border: 2px solid var(--focus); - border-radius: var(--radius-xl); - } + outline-offset: 2px; } &:hover { diff --git a/packages/frontend/src/components/MkChannelList.stories.impl.ts b/packages/frontend/src/components/MkChannelList.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..f69b20c04967d2daefbaae35b47e16f685822149 --- /dev/null +++ b/packages/frontend/src/components/MkChannelList.stories.impl.ts @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { channel } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkChannelList from './MkChannelList.vue'; +export const Default = { + render(args) { + return { + components: { + MkChannelList, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChannelList v-bind="props" />', + }; + }, + args: { + pagination: { + endpoint: 'channels/search', + limit: 10, + }, + }, + parameters: { + chromatic: { + // NOTE: ãƒãƒ¼ãƒ‰ãŒçµ‚ã‚ã‚‹ã¾ã§å¾…㤠+ delay: 3000, + }, + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/channels/search', async ({ request, params }) => { + action('POST /api/channels/search')(await request.json()); + return HttpResponse.json(params.untilId === 'lastchannel' ? [] : [ + channel(), + channel('lastchannel', 'Last Channel', null), + ]); + }), + ], + }, + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>', + }), + ], +} satisfies StoryObj<typeof MkChannelList>; diff --git a/packages/frontend/src/components/MkChannelPreview.stories.impl.ts b/packages/frontend/src/components/MkChannelPreview.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..de0193c78f86cf7b815bbd22a033861e77da2bb8 --- /dev/null +++ b/packages/frontend/src/components/MkChannelPreview.stories.impl.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { channel } from '../../.storybook/fakes.js'; +import MkChannelPreview from './MkChannelPreview.vue'; +export const Default = { + render(args) { + return { + components: { + MkChannelPreview, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChannelPreview v-bind="props" />', + }; + }, + args: { + channel: channel(), + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>', + }), + ], +} satisfies StoryObj<typeof MkChannelPreview>; diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index 1bac59d6dfb51fa2fa089d9ed0c37027441637f0..a50416befce239dfc9a413aa84b55420e0d855e4 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -5,14 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div style="position: relative;"> - <MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt"> + <MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" @click="updateLastReadedAt"> <div class="banner" :style="bannerStyle"> <div class="fade"></div> - <div class="name"><i class="ph-television ph-bold ph-lg"></i> {{ channel.name }}</div> + <div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div> <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div> <div class="status"> <div> - <i class="ph-users ph-bold ph-lg"></i> + <i class="ti ti-users ti-fw"></i> <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"> <template #n> <b>{{ channel.usersCount }}</b> @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> </div> <div> - <i class="ph-pencil-simple ph-bold ph-lg"></i> + <i class="ti ti-pencil ti-fw"></i> <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"> <template #n> <b>{{ channel.notesCount }}</b> @@ -80,6 +80,7 @@ const bannerStyle = computed(() => { <style lang="scss" scoped> .eftoefju { display: block; + position: relative; overflow: hidden; width: 100%; @@ -87,6 +88,22 @@ const bannerStyle = computed(() => { text-decoration: none; } + &:focus-within { + outline: none; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: inherit; + pointer-events: none; + box-shadow: inset 0 0 0 2px var(--focus); + } + } + > .banner { position: relative; width: 100%; diff --git a/packages/frontend/src/components/MkChart.stories.impl.ts b/packages/frontend/src/components/MkChart.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..1bcb9c30d8333d6716f38495f2eaacfd322b41db --- /dev/null +++ b/packages/frontend/src/components/MkChart.stories.impl.ts @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { http } from 'msw'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import { getChartResolver } from '../../.storybook/charts.js'; +import MkChart from './MkChart.vue'; + +const Base = { + render(args) { + return { + components: { + MkChart, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkChart v-bind="props" />', + }; + }, + args: { + src: 'federation', + span: 'hour', + nowForChromatic: 1716263640000, + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.get('/api/charts/federation', getChartResolver( + ['deliveredInstances', 'inboxInstances', 'stalled', 'sub', 'pub', 'pubsub', 'subActive', 'pubActive'], + )), + http.get('/api/charts/notes', getChartResolver( + ['local.total', 'remote.total'], + { accumulate: true }, + )), + http.get('/api/charts/drive', getChartResolver( + ['local.incSize', 'local.decSize', 'remote.incSize', 'remote.decSize'], + { mulMap: { 'local.incSize': 1e7, 'local.decSize': 5e6, 'remote.incSize': 1e6, 'remote.decSize': 5e5 } }, + )), + ], + }, + }, +} satisfies StoryObj<typeof MkChart>; +export const FederationChart = { + ...Base, + args: { + ...Base.args, + src: 'federation', + }, +} satisfies StoryObj<typeof MkChart>; +export const NotesTotalChart = { + ...Base, + args: { + ...Base.args, + src: 'notes-total', + }, +} satisfies StoryObj<typeof MkChart>; +export const DriveChart = { + ...Base, + args: { + ...Base.args, + src: 'drive', + }, +} satisfies StoryObj<typeof MkChart>; diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 04b6d2f29c0e81b403d6778cd8e3000386e75a07..4b2456224991c22f3ce57c912c56c9e65570f82b 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -19,8 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only 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, PropType } from 'vue'; +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'; @@ -34,44 +35,63 @@ import MkChartLegend from '@/components/MkChartLegend.vue'; initChart(); -const props = defineProps({ - src: { - type: String, - required: true, - }, - args: { - type: Object, - required: false, - }, - limit: { - type: Number, - required: false, - default: 90, - }, - span: { - type: String as PropType<'hour' | 'day'>, - required: true, - }, - detailed: { - type: Boolean, - required: false, - default: false, - }, - stacked: { - type: Boolean, - required: false, - default: false, - }, - bar: { - type: Boolean, - required: false, - default: false, - }, - aspectRatio: { - type: Number, - required: false, - default: null, - }, +type ChartSrc = + | 'federation' + | 'ap-request' + | 'users' + | 'users-total' + | 'active-users' + | 'notes' + | 'local-notes' + | 'remote-notes' + | 'notes-total' + | 'drive' + | 'drive-files' + | 'instance-requests' + | 'instance-users' + | 'instance-users-total' + | 'instance-notes' + | 'instance-notes-total' + | 'instance-ff' + | 'instance-ff-total' + | 'instance-drive-usage' + | 'instance-drive-usage-total' + | 'instance-drive-files' + | 'instance-drive-files-total' + | 'per-user-notes' + | 'per-user-pv' + | 'per-user-following' + | 'per-user-followers' + | 'per-user-drive' + +const props = withDefaults(defineProps<{ + src: ChartSrc; + args?: { + host?: string; + user?: Misskey.entities.UserLite; + withoutAll?: boolean; + }; + limit?: number; + span: 'hour' | 'day'; + detailed?: boolean; + stacked?: boolean; + bar?: boolean; + aspectRatio?: number | null; + nowForChromatic?: number; +}>(), { + args: undefined, + limit: 90, + detailed: false, + stacked: false, + bar: false, + aspectRatio: null, + + /** + * @desc Overwrites current date to fix background lines of chart. + * @ignore Only used for Chromatic. Don't use this for production. + * @see https://github.com/misskey-dev/misskey/pull/13830#issuecomment-2155886151 + */ + nowForChromatic: undefined, }); const legendEl = shallowRef<InstanceType<typeof MkChartLegend>>(); @@ -94,7 +114,8 @@ const getColor = (i) => { return colorSets[i % colorSets.length]; }; -const now = new Date(); +// eslint-disable-next-line vue/no-setup-props-reactivity-loss +const now = props.nowForChromatic != null ? new Date(props.nowForChromatic) : new Date(); let chartInstance: Chart | null = null; let chartData: { series: { diff --git a/packages/backend/src/misc/prelude/math.ts b/packages/frontend/src/components/MkChartLegend.stories.impl.ts similarity index 53% rename from packages/backend/src/misc/prelude/math.ts rename to packages/frontend/src/components/MkChartLegend.stories.impl.ts index 38556def2dcfd52daee7d4a770aa2fcdd91d008a..06146e20e47884f41b790e9f04833443c77d3d2a 100644 --- a/packages/backend/src/misc/prelude/math.ts +++ b/packages/frontend/src/components/MkChartLegend.stories.impl.ts @@ -3,6 +3,5 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export function gcd(a: number, b: number): number { - return b === 0 ? a : gcd(b, a % b); -} +import MkChartLegend from './MkChartLegend.vue'; +void MkChartLegend; diff --git a/packages/frontend/src/components/MkChartTooltip.stories.impl.ts b/packages/frontend/src/components/MkChartTooltip.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..289a9e9f2788248e5e0e59908f870a7df3061003 --- /dev/null +++ b/packages/frontend/src/components/MkChartTooltip.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkChartTooltip from './MkChartTooltip.vue'; +void MkChartTooltip; diff --git a/packages/frontend/src/components/MkClickerGame.stories.impl.ts b/packages/frontend/src/components/MkClickerGame.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..36313f965daa944116f14eef02b05a83204aee16 --- /dev/null +++ b/packages/frontend/src/components/MkClickerGame.stories.impl.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { expect, userEvent, within } from '@storybook/test'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkClickerGame from './MkClickerGame.vue'; + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export const Default = { + render(args) { + return { + components: { + MkClickerGame, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkClickerGame v-bind="props" />', + }; + }, + async play({ canvasElement }) { + await sleep(1000); + const canvas = within(canvasElement); + const count = canvas.getByTestId('count'); + await expect(count).toHaveTextContent('0'); + const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + await userEvent.click(buttonElement); + await expect(count).toHaveTextContent('1'); + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/i/registry/get', async ({ request }) => { + action('POST /api/i/registry/get')(await request.json()); + return HttpResponse.json({ + error: { + message: 'No such key.', + code: 'NO_SUCH_KEY', + id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', + }, + }, { + status: 400, + }); + }), + http.post('/api/i/registry/set', async ({ request }) => { + action('POST /api/i/registry/set')(await request.json()); + return HttpResponse.json(undefined, { status: 204 }); + }), + http.post('/api/i/claim-achievement', async ({ request }) => { + action('POST /api/i/claim-achievement')(await request.json()); + return HttpResponse.json(undefined, { status: 204 }); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkClickerGame>; diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 892ad31b09a241f6b83c98169b30430e443bf4e1..00506fb73513d6e3dba787706f6d409e94da9ad4 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <div v-if="game.ready" :class="$style.game"> <div :class="$style.cps" class="">{{ number(cps) }}cps</div> - <div :class="$style.count" class=""><i class="ph-cookie ph-bold ph-lg" style="font-size: 70%;"></i> {{ number(cookies) }}</div> + <div :class="$style.count" class="" data-testid="count"><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> <button v-click-anime class="_button" @click="onClick"> <img src="/client-assets/cookie.png" :class="$style.img"> </button> @@ -35,7 +35,9 @@ const prevCookies = ref(0); function onClick(ev: MouseEvent) { const x = ev.clientX; const y = ev.clientY; - os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkPlusOneEffect, { x, y }, { + end: () => dispose(), + }); saveData.value!.cookies++; saveData.value!.totalCookies++; diff --git a/packages/frontend/src/components/MkClipPreview.stories.impl.ts b/packages/frontend/src/components/MkClipPreview.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..62503fb98a031c700c8e3f9c2f847835f03d24d4 --- /dev/null +++ b/packages/frontend/src/components/MkClipPreview.stories.impl.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { clip } from '../../.storybook/fakes.js'; +import MkClipPreview from './MkClipPreview.vue'; +export const Default = { + render(args) { + return { + components: { + MkClipPreview, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkClipPreview v-bind="props" />', + }; + }, + args: { + clip: clip(), + noUserInfo: false, + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>', + }), + ], +} satisfies StoryObj<typeof MkClipPreview>; diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue index 6299a28e9f68b6857ae52bd5a018396c3768dd73..dd550733cb213e27f7be2d883cf1a4d6c77fa3b4 100644 --- a/packages/frontend/src/components/MkClipPreview.vue +++ b/packages/frontend/src/components/MkClipPreview.vue @@ -12,10 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div> <div v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div> </div> - <div :class="$style.divider"></div> - <div> - <MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/> - </div> + <template v-if="!props.noUserInfo"> + <div :class="$style.divider"></div> + <div> + <MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/> + </div> + </template> </div> </MkA> </template> @@ -27,9 +29,12 @@ import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import number from '@/filters/number.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ clip: Misskey.entities.Clip; -}>(); + noUserInfo?: boolean; +}>(), { + noUserInfo: false, +}); const remaining = computed(() => { return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown; @@ -40,6 +45,14 @@ const remaining = computed(() => { .link { display: block; + &:focus-visible { + outline: none; + + .root { + box-shadow: inset 0 0 0 2px var(--focus); + } + } + &:hover { text-decoration: none; color: var(--accent); diff --git a/packages/frontend/src/components/MkCode.core.stories.impl.ts b/packages/frontend/src/components/MkCode.core.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..91990fffd5093f6a57935dd026ef78a4c75a7099 --- /dev/null +++ b/packages/frontend/src/components/MkCode.core.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkCode_core from './MkCode.core.vue'; +void MkCode_core; diff --git a/packages/frontend/src/components/MkCode.stories.impl.ts b/packages/frontend/src/components/MkCode.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7e53e8e350338c6248e3c312d666a4f22a1c976 --- /dev/null +++ b/packages/frontend/src/components/MkCode.stories.impl.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import MkCode from './MkCode.vue'; +const code = `for (let i, 100) { + <: if (i % 15 == 0) "FizzBuzz" + elif (i % 3 == 0) "Fizz" + elif (i % 5 == 0) "Buzz" + else i +}`; +export const Default = { + render(args) { + return { + components: { + MkCode, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkCode v-bind="props" />', + }; + }, + args: { + code, + lang: 'is', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCode>; diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue index e5589813e12aeac40ad7573c81d8b9cb53831d78..2475e3dc89c75a72c9b69a3af10a10b0fc490068 100644 --- a/packages/frontend/src/components/MkCode.vue +++ b/packages/frontend/src/components/MkCode.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.codeBlockRoot"> <button :class="$style.codeBlockCopyButton" class="_button" @click="copy"> - <i class="ph-copy ph-bold ph-lg"></i> + <i class="ti ti-copy"></i> </button> <Suspense> <template #fallback> @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre> <button v-else :class="$style.codePlaceholderRoot" @click="show = true"> <div :class="$style.codePlaceholderContainer"> - <div><i class="ph-code ph-bold ph-lg"></i> {{ i18n.ts.code }}</div> + <div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div> <div>{{ i18n.ts.clickToShow }}</div> </div> </button> @@ -30,7 +30,7 @@ import * as os from '@/os.js'; import MkLoading from '@/components/global/MkLoading.vue'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ code: string; diff --git a/packages/frontend/src/components/MkCodeEditor.stories.impl.ts b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c410c48864256867e9480ec98173d7d07f9d199 --- /dev/null +++ b/packages/frontend/src/components/MkCodeEditor.stories.impl.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { action } from '@storybook/addon-actions'; +import MkCodeEditor from './MkCodeEditor.vue'; +const code = `for (let i, 100) { + <: if (i % 15 == 0) "FizzBuzz" + elif (i % 3 == 0) "Fizz" + elif (i % 5 == 0) "Buzz" + else i +}`; +export const Default = { + render(args) { + return { + components: { + MkCodeEditor, + }, + data() { + return { + code, + }; + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'change': action('change'), + 'keydown': action('keydown'), + 'enter': action('enter'), + 'update:modelValue': action('update:modelValue'), + }; + }, + }, + template: '<MkCodeEditor v-model="code" v-bind="props" v-on="events" />', + }; + }, + args: { + lang: 'aiscript', + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><Suspense><story/></Suspense></div></div>', + }), + ], +} satisfies StoryObj<typeof MkCodeEditor>; diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue index 30e518f8f037279b61ef9977ad03f5bc019fdabc..b233189ab0f987d675b2fbf757b0dff17284cfed 100644 --- a/packages/frontend/src/components/MkCodeEditor.vue +++ b/packages/frontend/src/components/MkCodeEditor.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div :class="$style.caption"><slot name="caption"></slot></div> - <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> diff --git a/packages/frontend/src/components/MkCodeInline.stories.impl.ts b/packages/frontend/src/components/MkCodeInline.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..51d4d106ff87d7851b850ddf31ce5e6181119d68 --- /dev/null +++ b/packages/frontend/src/components/MkCodeInline.stories.impl.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import MkCodeInline from './MkCodeInline.vue'; +export const Default = { + render(args) { + return { + components: { + MkCodeInline, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkCodeInline v-bind="props"/>', + }; + }, + args: { + code: '<: "Hello, world!"', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCodeInline>; diff --git a/packages/frontend/src/components/MkColorInput.stories.impl.ts b/packages/frontend/src/components/MkColorInput.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..61383e2caee7fc28d5294439cf6f910eb8a0adcb --- /dev/null +++ b/packages/frontend/src/components/MkColorInput.stories.impl.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { action } from '@storybook/addon-actions'; +import MkColorInput from './MkColorInput.vue'; +export const Default = { + render(args) { + return { + components: { + MkColorInput, + }, + data() { + return { + color: '#cccccc', + }; + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'update:modelValue': action('update:modelValue'), + }; + }, + }, + template: '<MkColorInput v-model="color" v-bind="props" v-on="events" />', + }; + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 800px; width: 100%; margin: 3rem"><story/></div></div>', + }), + ], +} satisfies StoryObj<typeof MkColorInput>; diff --git a/packages/frontend/src/components/MkContainer.stories.impl.ts b/packages/frontend/src/components/MkContainer.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..72a7659521331b728510b1a738928fc2be7c806f --- /dev/null +++ b/packages/frontend/src/components/MkContainer.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkContainer from './MkContainer.vue'; +void MkContainer; diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 95188c335e330e1b07f5cd93ba15dba15ded6055..d1fe5dbdcf267e9ee8f83432f3e873aa37669a9c 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -13,8 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.headerSub"> <slot name="func" :buttonStyleClass="$style.headerButton"></slot> <button v-if="foldable" :class="$style.headerButton" class="_button" @click="() => showBody = !showBody"> - <template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template> - <template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template> + <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> + <template v-else><i class="ti ti-chevron-down"></i></template> </button> </div> </header> diff --git a/packages/frontend/src/components/MkContextMenu.stories.impl.ts b/packages/frontend/src/components/MkContextMenu.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ff0f51bd4ddce4dc6c03ffd8f968f6f04a0a44a --- /dev/null +++ b/packages/frontend/src/components/MkContextMenu.stories.impl.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { userEvent, within } from '@storybook/test'; +import MkContextMenu from './MkContextMenu.vue'; +import * as os from '@/os.js'; +export const Empty = { + render(args) { + return { + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + methods: { + onContextmenu(ev: MouseEvent) { + os.contextMenu(args.items, ev); + }, + }, + template: '<div @contextmenu.stop="onContextmenu">Right Click Here</div>', + }; + }, + args: { + items: [], + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const target = canvas.getByText('Right Click Here'); + await userEvent.pointer({ keys: '[MouseRight>]', target }); + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkContextMenu>; +export const SomeTabs = { + ...Empty, + args: { + items: [ + { + text: 'Home', + icon: 'ti ti-home', + action() {}, + }, + ], + }, +} satisfies StoryObj<typeof MkContextMenu>; diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index a807742bb9ba815501760d86d4857d02f045132a..8ea8fa6cf3a3a97af6be81c68104bdf4d200aafe 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''" > <div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}"> - <MkMenu :items="items" :align="'left'" @close="$emit('closed')"/> + <MkMenu :items="items" :align="'left'" @close="emit('closed')"/> </div> </Transition> </template> diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce13093975a09bb388b41a5166425ae9794f8eb3 --- /dev/null +++ b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { action } from '@storybook/addon-actions'; +import { file } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkCropperDialog from './MkCropperDialog.vue'; +export const Default = { + render(args) { + return { + components: { + MkCropperDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'ok': action('ok'), + 'cancel': action('cancel'), + 'closed': action('closed'), + }; + }, + }, + template: '<MkCropperDialog v-bind="props" v-on="events" />', + }; + }, + args: { + file: file(), + aspectRatio: NaN, + }, + parameters: { + chromatic: { + // NOTE: ãƒãƒ¼ãƒ‰ãŒçµ‚ã‚ã‚‹ã¾ã§å¾…㤠+ delay: 3000, + }, + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.get('/proxy/image.webp', async ({ request }) => { + const url = new URL(request.url).searchParams.get('url'); + if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') { + const image = await (await fetch('client-assets/fedi.jpg')).blob(); + return new HttpResponse(image, { + headers: { + 'Content-Type': 'image/jpeg', + }, + }); + } else { + return new HttpResponse(null, { status: 404 }); + } + }), + http.post('/api/drive/files/create', async ({ request }) => { + action('POST /api/drive/files/create')(await request.formData()); + return HttpResponse.json(file()); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkCropperDialog>; diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a05e063111628656eee90c7cb747b2001e93a01 --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.stories.impl.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { emojiDetailed } from '../../.storybook/fakes.js'; +import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue'; +export const Default = { + render(args) { + return { + components: { + MkCustomEmojiDetailedDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkCustomEmojiDetailedDialog v-bind="props" />', + }; + }, + args: { + emoji: emojiDetailed(), + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCustomEmojiDetailedDialog>; diff --git a/packages/frontend/src/components/MkCwButton.stories.impl.ts b/packages/frontend/src/components/MkCwButton.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d6ea56da9f95d8406d8f892befd929af4a8fd57 --- /dev/null +++ b/packages/frontend/src/components/MkCwButton.stories.impl.ts @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-default-export */ +import { StoryObj } from '@storybook/vue3'; +import { action } from '@storybook/addon-actions'; +import { expect, userEvent, within } from '@storybook/test'; +import { file } from '../../.storybook/fakes.js'; +import MkCwButton from './MkCwButton.vue'; +import { i18n } from '@/i18n.js'; + +export const Default = { + render(args) { + return { + components: { + MkCwButton, + }, + data() { + return { + showContent: false, + }; + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + 'update:modelValue': action('update:modelValue'), + }; + }, + }, + template: '<MkCwButton v-model="showContent" v-bind="props" v-on="events" />', + }; + }, + args: { + text: 'Some CW content', + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); + await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); + await userEvent.click(buttonElement); + await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); + await userEvent.click(buttonElement); + }, + parameters: { + chromatic: { + // NOTE: テストãŒçµ‚ã‚ã‚‹ã¾ã§å¾…㤠+ delay: 5000, + }, + layout: 'centered', + }, +} satisfies StoryObj<typeof MkCwButton>; +export const IncludesTextAndDriveFile = { + ...Default, + args: { + text: 'Some CW content', + files: [file()], + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); + await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.chars({ count: 15 })); + await expect(buttonElement).toHaveTextContent(' / '); + await expect(buttonElement).toHaveTextContent(i18n.tsx._cw.files({ count: 1 })); + }, +} satisfies StoryObj<typeof MkCwButton>; diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue index a2cb3185f44701b5d55c41dc51acc6fb51bd1525..b5f6e78b6c71f10ccfbf90c6f33cf3e0f3cb184e 100644 --- a/packages/frontend/src/components/MkCwButton.vue +++ b/packages/frontend/src/components/MkCwButton.vue @@ -45,11 +45,11 @@ function toggle() { .label { margin-left: 4px; - &:before { + &::before { content: '('; } - &:after { + &::after { content: ')'; } } diff --git a/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts b/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e5635754c4060259e5625cc49f7aca8bd660d3e --- /dev/null +++ b/packages/frontend/src/components/MkDateSeparatedList.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkDateSeparatedList from './MkDateSeparatedList.vue'; +void MkDateSeparatedList; diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index 475fbcb397abf287f225c7eb79d48e09e390eead..9976cd00c9f498f2062b72e2bf0768c1521c33bb 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -76,7 +76,7 @@ export default defineComponent({ class: $style['date-1'], }, [ h('i', { - class: `ph-caret-up ph-bold ph-lg ${$style['date-1-icon']}`, + class: `ti ti-chevron-up ${$style['date-1-icon']}`, }), getDateText(item.createdAt), ]), @@ -85,7 +85,7 @@ export default defineComponent({ }, [ getDateText(props.items[i + 1].createdAt), h('i', { - class: `ph-caret-down ph-bold ph-lg ${$style['date-2-icon']}`, + class: `ti ti-chevron-down ${$style['date-2-icon']}`, }), ]), ])); @@ -130,7 +130,7 @@ export default defineComponent({ el.style.left = ''; } - // eslint-disable-next-line vue/no-setup-props-destructure + // eslint-disable-next-line vue/no-setup-props-reactivity-loss const classes = { [$style['date-separated-list']]: true, [$style['date-separated-list-nogap']]: props.noGap, diff --git a/packages/frontend/src/components/MkDialog.stories.impl.ts b/packages/frontend/src/components/MkDialog.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d8d3661f2e35e3eb681d2f95f495a1c374da208 --- /dev/null +++ b/packages/frontend/src/components/MkDialog.stories.impl.ts @@ -0,0 +1,159 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { action } from '@storybook/addon-actions'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; +import { StoryObj } from '@storybook/vue3'; +import { i18n } from '@/i18n.js'; +import MkDialog from './MkDialog.vue'; +const Base = { + render(args) { + return { + components: { + MkDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + done: action('done'), + closed: action('closed'), + }; + }, + }, + template: '<MkDialog v-bind="props" v-on="events" />', + }; + }, + args: { + text: 'Hello, world!', + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkDialog>; +export const Success = { + ...Base, + args: { + ...Base.args, + type: 'success', + }, +} satisfies StoryObj<typeof MkDialog>; +export const Error = { + ...Base, + args: { + ...Base.args, + type: 'error', + }, +} satisfies StoryObj<typeof MkDialog>; +export const Warning = { + ...Base, + args: { + ...Base.args, + type: 'warning', + }, +} satisfies StoryObj<typeof MkDialog>; +export const Info = { + ...Base, + args: { + ...Base.args, + type: 'info', + }, +} satisfies StoryObj<typeof MkDialog>; +export const Question = { + ...Base, + args: { + ...Base.args, + type: 'question', + }, +} satisfies StoryObj<typeof MkDialog>; +export const Waiting = { + ...Base, + args: { + ...Base.args, + type: 'waiting', + }, +} satisfies StoryObj<typeof MkDialog>; +export const DialogWithActions = { + ...Question, + args: { + ...Question.args, + text: i18n.ts.areYouSure, + actions: [ + { + text: i18n.ts.yes, + primary: true, + callback() { + action('YES')(); + }, + }, + { + text: i18n.ts.no, + callback() { + action('NO')(); + }, + }, + ], + }, +} satisfies StoryObj<typeof MkDialog>; +export const DialogWithDangerActions = { + ...Warning, + args: { + ...Warning.args, + text: i18n.ts.resetAreYouSure, + actions: [ + { + text: i18n.ts.yes, + danger: true, + primary: true, + callback() { + action('YES')(); + }, + }, + { + text: i18n.ts.no, + callback() { + action('NO')(); + }, + }, + ], + }, +} satisfies StoryObj<typeof MkDialog>; +export const DialogWithInput = { + ...Question, + args: { + ...Question.args, + title: 'Hello, world!', + text: undefined, + input: { + placeholder: i18n.ts.inputMessageHere, + type: 'text', + default: null, + minLength: 2, + maxLength: 3, + }, + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 })); + const okButton = canvas.getByRole('button', { name: i18n.ts.ok }); + await expect(okButton).toBeDisabled(); + const input = canvas.getByRole<HTMLInputElement>('combobox'); + await waitFor(() => userEvent.hover(input)); + await waitFor(() => userEvent.click(input)); + await waitFor(() => userEvent.type(input, 'M')); + await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 1, min: 2 })); + await waitFor(() => userEvent.type(input, 'i')); + await expect(okButton).toBeEnabled(); + }, +} satisfies StoryObj<typeof MkDialog>; diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index b534ae4c56cf4a319d9889fce0c00f20f1a42a48..825c1d0513662e7bf972014752d6ecbca3fd26d8 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')"> +<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')" @esc="cancel()"> <div :class="$style.root"> <div v-if="icon" :class="$style.icon"> <i :class="icon"></i> @@ -18,17 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.type_info]: type === 'info', }]" > - <i v-if="type === 'success'" :class="$style.iconInner" class="ph-check ph-bold ph-lg"></i> - <i v-else-if="type === 'error'" :class="$style.iconInner" class="ph-x-circle ph-bold ph-lg"></i> - <i v-else-if="type === 'warning'" :class="$style.iconInner" class="ph-warning ph-bold ph-lg"></i> - <i v-else-if="type === 'info'" :class="$style.iconInner" class="ph-info ph-bold ph-lg"></i> - <i v-else-if="type === 'question'" :class="$style.iconInner" class="ph-question ph-bold ph-lg"></i> + <i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i> + <i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i> + <i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i> + <i v-else-if="type === 'info'" :class="$style.iconInner" class="ti ti-info-circle"></i> + <i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-help-circle"></i> <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> <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="ph-lock ph-bold ph-lg"></i></template> + <template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template> <template #caption> <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/> <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/> @@ -36,7 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkSelect v-if="select" v-model="selectedValue" autofocus> <template v-if="select.items"> - <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> + <template v-for="item in select.items"> + <optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle"> + <option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option> + </optgroup> + <option v-else :value="item.value">{{ item.text }}</option> + </template> </template> </MkSelect> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> @@ -51,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onBeforeUnmount, onMounted, ref, shallowRef, computed } from 'vue'; +import { ref, shallowRef, computed } from 'vue'; import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -67,11 +72,16 @@ type Input = { maxLength?: number; }; +type SelectItem = { + value: any; + text: string; +}; + type Select = { - items: { - value: any; - text: string; - }[]; + items: (SelectItem | { + sectionTitle: string; + items: SelectItem[]; + })[]; default: string | null; }; @@ -156,10 +166,6 @@ function onBgClick() { if (props.cancelableByBgClick) cancel(); } */ -function onKeydown(evt: KeyboardEvent) { - if (evt.key === 'Escape') cancel(); -} - function onInputKeydown(evt: KeyboardEvent) { if (evt.key === 'Enter' && okButtonDisabledReason.value === null) { evt.preventDefault(); @@ -167,14 +173,6 @@ function onInputKeydown(evt: KeyboardEvent) { ok(); } } - -onMounted(() => { - document.addEventListener('keydown', onKeydown); -}); - -onBeforeUnmount(() => { - document.removeEventListener('keydown', onKeydown); -}); </script> <style lang="scss" module> diff --git a/packages/backend/src/misc/prelude/symbol.ts b/packages/frontend/src/components/MkDivider.stories.impl.ts similarity index 64% rename from packages/backend/src/misc/prelude/symbol.ts rename to packages/frontend/src/components/MkDivider.stories.impl.ts index 7e8d39bdb6b9777f24bac245ae3d7500cd79f81e..a593111987b3a6b489b415042dfeffa09bbc14a8 100644 --- a/packages/backend/src/misc/prelude/symbol.ts +++ b/packages/frontend/src/components/MkDivider.stories.impl.ts @@ -3,4 +3,5 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export const fallback = Symbol('fallback'); +import MkDivider from './MkDivider.vue'; +void MkDivider; diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue new file mode 100644 index 0000000000000000000000000000000000000000..e4e3af99e42997c2d1918dd76f02115981431f9f --- /dev/null +++ b/packages/frontend/src/components/MkDivider.vue @@ -0,0 +1,32 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div + class="default" :style="[ + marginTopBottom ? { marginTop: marginTopBottom, marginBottom: marginTopBottom } : {}, + marginLeftRight ? { marginLeft: marginLeftRight, marginRight: marginLeftRight } : {}, + borderStyle ? { borderStyle: borderStyle } : {}, + borderWidth ? { borderWidth: borderWidth } : {}, + borderColor ? { borderColor: borderColor } : {}, + ]" +/> +</template> + +<script setup lang="ts"> +defineProps<{ + marginTopBottom?: string; + marginLeftRight?: string; + borderStyle?: string; + borderWidth?: string; + borderColor?: string; +}>(); +</script> + +<style scoped lang="scss"> +.default { + border-top: solid 0.5px var(--divider); +} +</style> diff --git a/packages/frontend/src/components/MkDonation.stories.impl.ts b/packages/frontend/src/components/MkDonation.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..27d6b7df6cb49a3ed773522de769b913defae9ce --- /dev/null +++ b/packages/frontend/src/components/MkDonation.stories.impl.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { onBeforeUnmount } from 'vue'; +import MkDonation from './MkDonation.vue'; +import { instance } from '@/instance.js'; +export const Default = { + render(args) { + return { + components: { + MkDonation, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + closed: action('closed'), + }; + }, + }, + template: '<MkDonation v-bind="props" v-on="events" />', + }; + }, + args: { + // @ts-expect-error name is used for mocking instance + name: 'Misskey Hub', + }, + decorators: [ + (_, { args }) => ({ + setup() { + // @ts-expect-error name is used for mocking instance + instance.name = args.name; + onBeforeUnmount(() => instance.name = null); + }, + template: '<story/>', + }), + ], + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkDonation>; diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue index a2780ddfe93ec5286b7e0490a1f3bf76bc254cbd..930f0f54cc29f94dc84ba06b3262b944e2e43327 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </I18n> <div style="margin-top: 0.2em;"> - <MkLink target="_blank" url="https://ko-fi.com/transfem">{{ i18n.ts.learnMore }}</MkLink> + <MkLink target="_blank" url="https://opencollective.com/sharkey">{{ i18n.ts.learnMore }}</MkLink> </div> </div> <div v-if="instance.donationUrl" :class="$style.text"> @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton @click="neverShow">{{ i18n.ts.neverShow }}</MkButton> </div> </div> - <button class="_button" :class="$style.close" @click="close"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button> </div> </template> diff --git a/packages/frontend/src/components/MkDrive.file.stories.impl.ts b/packages/frontend/src/components/MkDrive.file.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f6e6a0667235abe8cb35fcf562727b7ec2829b2 --- /dev/null +++ b/packages/frontend/src/components/MkDrive.file.stories.impl.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import MkDrive_file from './MkDrive.file.vue'; +import { file } from '../../.storybook/fakes.js'; +export const Default = { + render(args) { + return { + components: { + MkDrive_file, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + chosen: action('chosen'), + dragstart: action('dragstart'), + dragend: action('dragend'), + }; + }, + }, + template: '<MkDrive_file v-bind="props" v-on="events" />', + }; + }, + args: { + file: file(), + }, + parameters: { + chromatic: { + // NOTE: ãƒãƒ¼ãƒ‰ãŒçµ‚ã‚ã‚‹ã¾ã§å¾…㤠+ delay: 3000, + }, + layout: 'centered', + }, +} satisfies StoryObj<typeof MkDrive_file>; diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 13a2a2126cd484d08fe83d539e5e0eaf2e71bcca..20ad2984d8f28ee520b53e2b4283c10561f44ae8 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -119,14 +119,14 @@ function onDragend() { background: rgba(#000, 0.05); > .label { - &:before, - &:after { + &::before, + &::after { background: #0b65a5; } &.red { - &:before, - &:after { + &::before, + &::after { background: #c12113; } } @@ -137,14 +137,14 @@ function onDragend() { background: rgba(#000, 0.1); > .label { - &:before, - &:after { + &::before, + &::after { background: #0b588c; } &.red { - &:before, - &:after { + &::before, + &::after { background: #ce2212; } } @@ -163,8 +163,8 @@ function onDragend() { } > .label { - &:before, - &:after { + &::before, + &::after { display: none; } } @@ -185,8 +185,8 @@ function onDragend() { left: 0; pointer-events: none; - &:before, - &:after { + &::before, + &::after { content: ""; display: block; position: absolute; @@ -194,14 +194,14 @@ function onDragend() { background: #0c7ac9; } - &:before { + &::before { top: 0; left: 57px; width: 28px; height: 8px; } - &:after { + &::after { top: 57px; left: 0; width: 8px; @@ -209,8 +209,8 @@ function onDragend() { } &.red { - &:before, - &:after { + &::before, + &::after { background: #c12113; } } diff --git a/packages/frontend/src/components/MkDrive.folder.stories.impl.ts b/packages/frontend/src/components/MkDrive.folder.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f8ef485200ce973ac93740acad2c9b1fe23e3b2 --- /dev/null +++ b/packages/frontend/src/components/MkDrive.folder.stories.impl.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { http, HttpResponse } from 'msw'; +import * as Misskey from 'misskey-js'; +import MkDrive_folder from './MkDrive.folder.vue'; +import { folder } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +export const Default = { + render(args) { + return { + components: { + MkDrive_folder, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + chosen: action('chosen'), + move: action('move'), + upload: action('upload'), + removeFile: action('removeFile'), + removeFolder: action('removeFolder'), + dragstart: action('dragstart'), + dragend: action('dragend'), + }; + }, + }, + template: '<MkDrive_folder v-bind="props" v-on="events" />', + }; + }, + args: { + folder: folder(), + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/drive/folders/delete', async ({ request }) => { + action('POST /api/drive/folders/delete')(await request.json()); + return HttpResponse.json(undefined, { status: 204 }); + }), + http.post('/api/drive/folders/update', async ({ request }) => { + const req = await request.json() as Misskey.entities.DriveFoldersUpdateRequest; + action('POST /api/drive/folders/update')(req); + return HttpResponse.json({ + ...folder(), + id: req.folderId, + name: req.name ?? folder().name, + parentId: req.parentId ?? folder().parentId, + }); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkDrive_folder>; diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 945f45c01237ec9434ef32b5084810998b659d2b..3990d0b861691fe5f7b01e47d27095e05523777c 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -20,14 +20,16 @@ SPDX-License-Identifier: AGPL-3.0-only @dragend="onDragend" > <p :class="$style.name"> - <template v-if="hover"><i :class="$style.icon" class="ph-folder ph-bold ph-lg ti-fw"></i></template> - <template v-if="!hover"><i :class="$style.icon" class="ph-folder ph-bold ph-lg ti-fw"></i></template> + <template v-if="hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> + <template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template> {{ folder.name }} </p> <p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload"> {{ i18n.ts.uploadFolder }} </p> - <button v-if="selectMode" class="_button" :class="[$style.checkbox, { [$style.checked]: isSelected }]" @click.prevent.stop="checkboxClicked"></button> + <button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked"> + <div :class="[$style.checkbox, { [$style.checked]: isSelected }]"></div> + </button> </div> </template> @@ -39,7 +41,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ @@ -53,6 +55,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'chosen', v: Misskey.entities.DriveFolder): void; + (ev: 'unchose', v: Misskey.entities.DriveFolder): void; (ev: 'move', v: Misskey.entities.DriveFolder): void; (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder); (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void; @@ -68,7 +71,11 @@ const isDragging = ref(false); const title = computed(() => props.folder.name); function checkboxClicked() { - emit('chosen', props.folder); + if (props.isSelected) { + emit('unchose', props.folder); + } else { + emit('chosen', props.folder); + } } function onClick() { @@ -222,6 +229,17 @@ function rename() { }); } +function move() { + os.selectDriveFolder(false).then(folder => { + if (folder[0] && folder[0].id === props.folder.id) return; + + misskeyApi('drive/folders/update', { + folderId: props.folder.id, + parentId: folder[0] ? folder[0].id : null, + }); + }); +} + function deleteFolder() { misskeyApi('drive/folders/delete', { folderId: props.folder.id, @@ -255,26 +273,31 @@ function onContextmenu(ev: MouseEvent) { let menu: MenuItem[]; menu = [{ text: i18n.ts.openInWindow, - icon: 'ph-app-window ph-bold ph-lg', + icon: 'ti ti-app-window', action: () => { - os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), { initialFolder: props.folder, }, { - }, 'closed'); + closed: () => dispose(), + }); }, }, { type: 'divider' }, { text: i18n.ts.rename, - icon: 'ph-textbox ph-bold ph-lg', + icon: 'ti ti-forms', action: rename, + }, { + text: i18n.ts.move, + icon: 'ti ti ti-folder-symlink', + action: move, }, { type: 'divider' }, { text: i18n.ts.delete, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: deleteFolder, }]; if (defaultStore.state.devMode) { menu = menu.concat([{ type: 'divider' }, { - icon: 'ph-identification-card ph-bold ph-lg', + icon: 'ti ti-id', text: i18n.ts.copyFolderId, action: () => { copyToClipboard(props.folder.id); @@ -295,7 +318,7 @@ function onContextmenu(ev: MouseEvent) { cursor: pointer; &.draghover { - &:after { + &::after { content: ""; pointer-events: none; position: absolute; @@ -309,17 +332,43 @@ function onContextmenu(ev: MouseEvent) { } } -.checkbox { +.checkboxWrapper { position: absolute; - bottom: 8px; - right: 8px; - width: 16px; - height: 16px; - background: #fff; - border: solid 1px #000; - - &.checked { - background: var(--accent); + border-radius: 50%; + bottom: 2px; + right: 2px; + padding: 8px; + box-sizing: border-box; + + > .checkbox { + position: relative; + width: 18px; + height: 18px; + background: #fff; + border: solid 2px var(--divider); + border-radius: 4px; + box-sizing: border-box; + + &.checked { + border-color: var(--accent); + background: var(--accent); + + &::after { + content: "\ea5e"; + font-family: 'tabler-icons'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #fff; + font-size: 12px; + line-height: 22px; + } + } + } + + &:hover { + background: var(--accentedBg); } } diff --git a/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts b/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d49f24fa43d72aab1d297b30f6c6e7502e4e6a4 --- /dev/null +++ b/packages/frontend/src/components/MkDrive.navFolder.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkDrive_navFolder from './MkDrive.navFolder.vue'; +void MkDrive_navFolder; diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue index d78c215328777f3e67eb973d1266a0f22d9563ee..8df3c86ebf6a63e24a15c016d275fe88e063db28 100644 --- a/packages/frontend/src/components/MkDrive.navFolder.vue +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only @dragleave="onDragleave" @drop.stop="onDrop" > - <i v-if="folder == null" class="ph-cloud ph-bold ph-lg" style="margin-right: 4px;"></i> + <i v-if="folder == null" class="ti ti-cloud" style="margin-right: 4px;"></i> <span>{{ folder == null ? i18n.ts.drive : folder.name }}</span> </div> </template> diff --git a/packages/frontend/src/components/MkDrive.stories.impl.ts b/packages/frontend/src/components/MkDrive.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe20e6141578e7e9e81defc662dcaeda5e14db80 --- /dev/null +++ b/packages/frontend/src/components/MkDrive.stories.impl.ts @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { http, HttpResponse } from 'msw'; +import * as Misskey from 'misskey-js'; +import MkDrive from './MkDrive.vue'; +import { file, folder } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +export const Default = { + render(args) { + return { + components: { + MkDrive, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + selected: action('selected'), + 'change-selection': action('change-selection'), + 'move-root': action('move-root'), + cd: action('cd'), + 'open-folder': action('open-folder'), + }; + }, + }, + template: '<MkDrive v-bind="props" v-on="events" />', + }; + }, + parameters: { + chromatic: { + // NOTE: ãƒãƒ¼ãƒ‰ãŒçµ‚ã‚ã‚‹ã¾ã§å¾…㤠+ delay: 3000, + }, + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/drive/files', async ({ request }) => { + action('POST /api/drive/files')(await request.json()); + return HttpResponse.json([file()]); + }), + http.post('/api/drive/folders', async ({ request }) => { + action('POST /api/drive/folders')(await request.json()); + return HttpResponse.json([folder(crypto.randomUUID())]); + }), + http.post('/api/drive/folders/create', async ({ request }) => { + const req = await request.json() as Misskey.entities.DriveFoldersCreateRequest; + action('POST /api/drive/folders/create')(req); + return HttpResponse.json(folder(crypto.randomUUID(), req.name, req.parentId)); + }), + http.post('/api/drive/folders/delete', async ({ request }) => { + action('POST /api/drive/folders/delete')(await request.json()); + return HttpResponse.json(undefined, { status: 204 }); + }), + http.post('/api/drive/folders/update', async ({ request }) => { + const req = await request.json() as Misskey.entities.DriveFoldersUpdateRequest; + action('POST /api/drive/folders/update')(req); + return HttpResponse.json({ + ...folder(), + id: req.folderId, + name: req.name ?? folder().name, + parentId: req.parentId ?? folder().parentId, + }); + }), + ] + }, + }, +} satisfies StoryObj<typeof MkDrive>; diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 2990ea6861a57310c2a7881dd0e65aedd2fa485c..2d1c7c95f0d16cce887b1ed17daef997ba0dfced 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only @removeFolder="removeFolder" /> <template v-for="f in hierarchyFolders"> - <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ph-caret-right ph-bold ph-lg"></i></span> + <span :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> <XNavFolder :folder="f" :parentFolder="folder" @@ -27,10 +27,17 @@ SPDX-License-Identifier: AGPL-3.0-only @removeFolder="removeFolder" /> </template> - <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ph-caret-right ph-bold ph-lg"></i></span> + <span v-if="folder != null" :class="[$style.navPathItem, $style.navSeparator]"><i class="ti ti-chevron-right"></i></span> <span v-if="folder != null" :class="[$style.navPathItem, $style.navCurrent]">{{ folder.name }}</span> </div> - <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button> + <div :class="$style.navMenu"> + <!-- "Search drive via alt text or file names" --> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" :placeholder="i18n.ts.driveSearchbarPlaceholder" @enter="fetch"> + <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + </MkInput> + + <button class="_button" :class="$style.navMenu" @click="showMenu"><i class="ti ti-dots"></i></button> + </div> </nav> <div ref="main" @@ -52,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only :selectMode="select === 'folder'" :isSelected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" + @unchose="unchoseFolder" @move="move" @upload="upload" @removeFile="removeFile" @@ -102,6 +110,7 @@ import type { MenuItem } from '@/types/menu.js'; import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XFolder from '@/components/MkDrive.folder.vue'; import XFile from '@/components/MkDrive.file.vue'; +import MkInput from '@/components/MkInput.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; @@ -110,6 +119,8 @@ import { i18n } from '@/i18n.js'; import { uploadFile, uploads } from '@/scripts/upload.js'; import { claimAchievement } from '@/scripts/achievements.js'; +const searchQuery = ref(''); + const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder; type?: string; @@ -428,6 +439,11 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) { } } +function unchoseFolder(folderToUnchose: Misskey.entities.DriveFolder) { + selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToUnchose.id); + emit('change-selection', selectedFolders.value); +} + function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) { if (!target) { goRoot(); @@ -540,6 +556,7 @@ async function fetch() { const foldersPromise = misskeyApi('drive/folders', { folderId: folder.value ? folder.value.id : null, limit: foldersMax + 1, + searchQuery: searchQuery.value.toString().trim(), }).then(fetchedFolders => { if (fetchedFolders.length === foldersMax + 1) { moreFolders.value = true; @@ -552,6 +569,7 @@ async function fetch() { folderId: folder.value ? folder.value.id : null, type: props.type, limit: filesMax + 1, + searchQuery: searchQuery.value.toString().trim(), }).then(fetchedFiles => { if (fetchedFiles.length === filesMax + 1) { moreFiles.value = true; @@ -578,6 +596,7 @@ function fetchMoreFolders() { type: props.type, untilId: folders.value.at(-1)?.id, limit: max + 1, + searchQuery: searchQuery.value.toString().trim(), }).then(folders => { if (folders.length === max + 1) { moreFolders.value = true; @@ -601,6 +620,7 @@ function fetchMoreFiles() { type: props.type, untilId: files.value.at(-1)?.id, limit: max + 1, + searchQuery: searchQuery.value.toString().trim(), }).then(files => { if (files.length === max + 1) { moreFiles.value = true; @@ -623,26 +643,26 @@ function getMenu() { type: 'label', }, { text: i18n.ts.upload, - icon: 'ph-upload ph-bold ph-lg', + icon: 'ti ti-upload', action: () => { selectLocalFile(); }, }, { text: i18n.ts.fromUrl, - icon: 'ph-link ph-bold ph-lg', + icon: 'ti ti-link', action: () => { urlUpload(); }, }, { type: 'divider' }, { text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label', }, folder.value ? { text: i18n.ts.renameFolder, - icon: 'ph-textbox ph-bold ph-lg', + icon: 'ti ti-forms', action: () => { if (folder.value) renameFolder(folder.value); }, } : undefined, folder.value ? { text: i18n.ts.deleteFolder, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, } : undefined, { text: i18n.ts.createFolder, - icon: 'ph-folder ph-bold ph-lg-plus', + icon: 'ti ti-folder-plus', action: () => { createFolder(); }, }]; @@ -747,8 +767,13 @@ onBeforeUnmount(() => { } .navMenu { + display: flex; margin-left: auto; - padding: 0 12px; + align-items: center; +} + +.navMenu > *:not(:last-child) { + padding-right: 12px; } .main { diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.stories.impl.ts b/packages/frontend/src/components/MkDriveFileThumbnail.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fa24d7edb3de04c1dff30659fe26ecdcaecab1d --- /dev/null +++ b/packages/frontend/src/components/MkDriveFileThumbnail.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 MkDriveFileThumbnail from './MkDriveFileThumbnail.vue'; +import { file } from '../../.storybook/fakes.js'; +export const Default = { + render(args) { + return { + components: { + MkDriveFileThumbnail, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkDriveFileThumbnail v-bind="props" />', + }; + }, + args: { + file: file(), + fit: 'contain', + }, + parameters: { + chromatic: { + // NOTE: ãƒãƒ¼ãƒ‰ãŒçµ‚ã‚ã‚‹ã¾ã§å¾…㤠+ delay: 3000, + }, + layout: 'centered', + }, +} satisfies StoryObj<typeof MkDriveFileThumbnail>; diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 2f1fef4ea658bc1fbc0d186bb24d7f7a0681470e..9a6d272113c258d060e46e6e045e41ca11677999 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div ref="thumbnail" :class="$style.root"> <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="ph-image-square ph-bold ph-lg" :class="$style.icon"></i> - <i v-else-if="is === 'video'" class="ph-video ph-bold ph-lg" :class="$style.icon"></i> - <i v-else-if="is === 'audio' || is === 'midi'" class="ph-file-audio ph-bold ph-lg" :class="$style.icon"></i> - <i v-else-if="is === 'csv'" class="ph-file-text ph-bold ph-lg" :class="$style.icon"></i> - <i v-else-if="is === 'pdf'" class="ph-file-text ph-bold ph-lg" :class="$style.icon"></i> - <i v-else-if="is === 'textfile'" class="ph-file-text ph-bold ph-lg" :class="$style.icon"></i> - <i v-else-if="is === 'archive'" class="ph-file-zip ph-bold ph-lg" :class="$style.icon"></i> - <i v-else class="ph-file ph-bold ph-lg" :class="$style.icon"></i> + <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> + <i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i> + <i v-else-if="is === 'csv'" class="ti ti-file-text" :class="$style.icon"></i> + <i v-else-if="is === 'pdf'" class="ti ti-file-text" :class="$style.icon"></i> + <i v-else-if="is === 'textfile'" class="ti ti-file-text" :class="$style.icon"></i> + <i v-else-if="is === 'archive'" class="ti ti-file-zip" :class="$style.icon"></i> + <i v-else class="ti ti-file" :class="$style.icon"></i> - <i v-if="isThumbnailAvailable && is === 'video'" class="ph-video ph-bold ph-lg" :class="$style.iconSub"></i> + <i v-if="isThumbnailAvailable && is === 'video'" class="ti ti-video" :class="$style.iconSub"></i> </div> </template> @@ -26,7 +26,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; const props = defineProps<{ file: Misskey.entities.DriveFile; - fit: string; + fit: 'cover' | 'contain'; }>(); const is = computed(() => { diff --git a/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts b/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe8f70516590396d493bd2d0d9007b0b3643c1ac --- /dev/null +++ b/packages/frontend/src/components/MkDriveSelectDialog.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkDriveSelectDialog from './MkDriveSelectDialog.vue'; +void MkDriveSelectDialog; diff --git a/packages/frontend/src/components/MkDriveWindow.stories.impl.ts b/packages/frontend/src/components/MkDriveWindow.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..faa1f7fd5f09a01ccfea1a412fc3533f03ace0c1 --- /dev/null +++ b/packages/frontend/src/components/MkDriveWindow.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkDriveWindow from './MkDriveWindow.vue'; +void MkDriveWindow; diff --git a/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts b/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..69aef577decc9ec4181669cd53b8843c3312eca4 --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPicker.section.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkEmojiPicker_section from './MkEmojiPicker.section.vue'; +void MkEmojiPicker_section; diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index a5839586b680173b59a0b636009d799498a3d88d..008613c27e74aa215afea0c84d880db8a2af42fa 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- フォルダã®ä¸ã«ã¯ã‚«ã‚¹ã‚¿ãƒ 絵文å—ã ã‘(Unicode絵文å—ã‚‚ã“ã£ã¡ï¼‰ --> <section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);"> <header class="_acrylic" @click="shown = !shown"> - <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-bold ph-lg"></i>:{{ emojis.length }}) + <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ph-smiley-sticker ph-bold ph-lg"></i>:{{ emojis.length }}) </header> <div v-if="shown" class="body"> <button @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- フォルダã®ä¸ã«ã¯ã‚«ã‚¹ã‚¿ãƒ 絵文å—やフォルダãŒã‚ã‚‹ --> <section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);"> <header class="_acrylic" @click="shown = !shown"> - <i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-folder ph-bold ph-lg"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }}) + <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }}) </header> <div v-if="shown" style="padding-left: 9px;"> <MkEmojiPickerSection diff --git a/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts b/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..d38d8de8089967ccf4d944da4b15ecefe30f87d9 --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPicker.stories.impl.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { action } from '@storybook/addon-actions'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; +import { StoryObj } from '@storybook/vue3'; +import { i18n } from '@/i18n.js'; +import MkEmojiPicker from './MkEmojiPicker.vue'; +export const Default = { + render(args) { + return { + components: { + MkEmojiPicker, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + chosen: action('chosen'), + }; + }, + }, + template: '<MkEmojiPicker v-bind="props" v-on="events" />', + }; + }, + async play({ canvasElement }) { + const canvas = within(canvasElement); + const faceSection = canvas.getByText(/face/i); + await waitFor(() => userEvent.click(faceSection)); + const grinning = canvasElement.querySelector('[data-emoji="😀"]'); + await expect(grinning).toBeInTheDocument(); + if (grinning == null) throw new Error(); // NOTE: not called + await waitFor(() => userEvent.click(grinning)); + const recentUsedSection = canvas.getByText(new RegExp(i18n.ts.recentUsed)).parentElement; + await expect(recentUsedSection).toBeInTheDocument(); + if (recentUsedSection == null) throw new Error(); // NOTE: not called + await expect(within(recentUsedSection).getByAltText('😀')).toBeInTheDocument(); + await expect(within(recentUsedSection).queryByAltText('😬')).toEqual(null); + }, + parameters: { + layout: 'centered', + }, +} satisfies StoryObj<typeof MkEmojiPicker>; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 53fca97fc0c0ef43d6f1398d276a07cd444383c9..297d5f899eba549fb11b74932e36a2e3d3755cb3 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -5,7 +5,19 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> - <input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter"> + <input + ref="searchEl" + :value="q" + class="search" + data-prevent-emoji-insert + :class="{ filled: q != null && q != '' }" + :placeholder="i18n.ts.search" + type="search" + autocapitalize="off" + @input="input()" + @paste.stop="paste" + @keydown="onKeydown" + > <!-- Firefoxã®TabフォーカスãŒæƒ³å®šå¤–ã®æŒ™å‹•ã¨ãªã‚‹ãŸã‚tabindex="-1"ã‚’è¿½åŠ https://github.com/misskey-dev/misskey/issues/10744 --> <div ref="emojisEl" class="emojis" tabindex="-1"> <section class="result"> @@ -56,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only </section> <section> - <header class="_acrylic"><i class="ph-clock ph-bold ph-lg ti-fw"></i> {{ i18n.ts.recentUsed }}</header> + <header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header> <div class="body"> <button v-for="emoji in recentlyUsedEmojisDef" @@ -94,10 +106,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div class="tabs"> - <button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ph-asterisk ph-bold ph-lg ti-fw"></i></button> - <button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ph-smiley ph-bold ph-lg ti-fw"></i></button> - <button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ph-leaf ph-bold ph-lg ti-fw"></i></button> - <button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ph-hash ph-bold ph-lg ti-fw"></i></button> + <button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ti ti-asterisk ti-fw"></i></button> + <button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ti ti-mood-happy ti-fw"></i></button> + <button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ti ti-leaf ti-fw"></i></button> + <button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ti ti-hash ti-fw"></i></button> </div> </div> </template> @@ -139,6 +151,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'chosen', v: string): void; + (ev: 'esc'): void; }>(); const searchEl = shallowRef<HTMLInputElement>(); @@ -402,7 +415,9 @@ function chosen(emoji: any, ev?: MouseEvent) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } const key = getKey(emoji); @@ -431,9 +446,18 @@ function paste(event: ClipboardEvent): void { } } -function onEnter(ev: KeyboardEvent) { +function onKeydown(ev: KeyboardEvent) { if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return; - done(); + if (ev.key === 'Enter') { + ev.preventDefault(); + ev.stopPropagation(); + done(); + } + if (ev.key === 'Escape') { + ev.preventDefault(); + ev.stopPropagation(); + emit('esc'); + } } function done(query?: string): boolean | void { @@ -700,11 +724,6 @@ defineExpose({ border-radius: var(--radius-xs); font-size: 24px; - &:focus-visible { - outline: solid 2px var(--focus); - z-index: 1; - } - &:hover { background: rgba(0, 0, 0, 0.05); } diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.stories.impl.ts b/packages/frontend/src/components/MkEmojiPickerDialog.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..131087ad459b278b511b820a7c47a00bd9b60159 --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPickerDialog.stories.impl.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import MkEmojiPickerDialog from './MkEmojiPickerDialog.vue'; +void MkEmojiPickerDialog; diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index c6b38969894789710fd768ffcf5687d7319d22bd..0ec0679393faded61d150a1a3a82bf359c202dcd 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -9,10 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only v-slot="{ type, maxHeight }" :zPriority="'middle'" :preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'" + :hasInteractionWithOtherFocusTrappedEls="true" :transparentBg="true" :manualShowing="manualShowing" :src="src" @click="modal?.close()" + @esc="modal?.close()" @opening="opening" @close="emit('close')" @closed="emit('closed')" @@ -28,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only :asDrawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen" + @esc="modal?.close()" /> </MkModal> </template> diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue index 39551e6b3c78f9b3d080d11d5ee727f081c3f981..8754c72b7bbc4742a296004efad18d093c5e6150 100644 --- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue +++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header>{{ i18n.ts.describeFile }}</template> <MkSpacer :marginMin="20" :marginMax="28"> <MkDriveFileThumbnail :file="file" fit="contain" style="height: 193px; margin-bottom: 16px;"/> - <MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription"> + <MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription" @keydown="onKeydown($event)"> <template #label>{{ i18n.ts.caption }}</template> </MkTextarea> </MkSpacer> @@ -46,6 +46,15 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const caption = ref(props.default); +function onKeydown(ev: KeyboardEvent) { + if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) ok(); + + if (ev.key === 'Escape') { + emit('closed'); + dialog.value?.close(); + } +} + async function ok() { emit('done', caption.value); dialog.value?.close(); diff --git a/packages/frontend/src/components/MkFlashPreview.stories.impl.ts b/packages/frontend/src/components/MkFlashPreview.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa5288b73da00cf30b6188b3f56608a00ab4fb59 --- /dev/null +++ b/packages/frontend/src/components/MkFlashPreview.stories.impl.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import MkFlashPreview from './MkFlashPreview.vue'; +import { flash } from './../../.storybook/fakes.js'; +export const Public = { + render(args) { + return { + components: { + MkFlashPreview, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkFlashPreview v-bind="props" />', + }; + }, + args: { + flash: { + ...flash(), + visibility: 'public', + }, + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '<div style="display: flex; align-items: center; justify-content: center; height: 100vh"><div style="max-width: 700px; width: 100%; margin: 3rem"><story/></div></div>', + }), + ], +} satisfies StoryObj<typeof MkFlashPreview>; +export const Private = { + ...Public, + args: { + flash: { + ...flash(), + visibility: 'private', + }, + }, +} satisfies StoryObj<typeof MkFlashPreview>; diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue index c5dd87797105aa160490c040d7c452e648cafc80..8a2a4386240a44ad39ff4ca4f45c960e90e4832a 100644 --- a/packages/frontend/src/components/MkFlashPreview.vue +++ b/packages/frontend/src/components/MkFlashPreview.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1"> +<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" :class="[{ gray: flash.visibility === 'private' }]"> <article> <header> <h1 :title="flash.title">{{ flash.title }}</h1> @@ -22,11 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; +import * as Misskey from 'misskey-js'; import { userName } from '@/filters/user.js'; const props = defineProps<{ - //flash: Misskey.entities.Flash; - flash: any; + flash: Misskey.entities.Flash; }>(); </script> @@ -39,6 +39,10 @@ const props = defineProps<{ color: var(--accent); } + &:focus-visible { + outline-offset: -2px; + } + > article { padding: 16px; @@ -87,6 +91,12 @@ const props = defineProps<{ } } + &:global(.gray) { + --c: var(--bg); + background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); + background-size: 16px 16px; + } + @media (max-width: 700px) { } diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue index 51bcafd1c22be333eef4dc5c81e6369a21b204fa..f10d58b38a6237ba84889749e17c77518cb79424 100644 --- a/packages/frontend/src/components/MkFoldableSection.vue +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.title"><div><slot name="header"></slot></div></div> <div :class="$style.divider"></div> <button class="_button" :class="$style.button"> - <template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template> - <template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template> + <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> + <template v-else><i class="ti ti-chevron-down"></i></template> </button> </header> <Transition diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index 64d390f52b4b8f9ecbef7fb69f50f268d5b802b5..229cd59056778eb83d7d5d0623119d238a1b80f1 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -7,10 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened"> <MkStickyContainer> <template #header> - <div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle"> + <button :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle"> <div :class="$style.headerIcon"><slot name="icon"></slot></div> <div :class="$style.headerText"> - <div> + <div :class="$style.headerTextMain"> <MkCondensedLine :minScale="2 / 3"><slot name="label"></slot></MkCondensedLine> </div> <div :class="$style.headerTextSub"> @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.headerRight"> <span :class="$style.headerRightText"><slot name="suffix"></slot></span> - <i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i> - <i v-else class="ph-caret-down ph-bold ph-lg icon"></i> + <i v-if="opened" class="ti ti-chevron-up icon"></i> + <i v-else class="ti ti-chevron-down icon"></i> </div> - </div> + </button> </template> <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened"> @@ -147,6 +147,10 @@ onMounted(() => { background: var(--buttonHoverBg); } + &:focus-within { + outline-offset: 2px; + } + &.active { color: var(--accent); background: var(--buttonHoverBg); @@ -190,6 +194,12 @@ onMounted(() => { padding-right: 12px; } +.headerTextMain, +.headerTextSub { + width: fit-content; + max-width: 100%; +} + .headerTextSub { color: var(--fgTransparentWeak); font-size: .85em; diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index c0a625c69fad1862a2cde1a033c3f9b2b73aee37..3fdf673eb3667253d143656a8b4fa6f853d97054 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -12,20 +12,20 @@ SPDX-License-Identifier: AGPL-3.0-only > <template v-if="!wait"> <template v-if="hasPendingFollowRequestFromYou && user.isLocked"> - <span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ph-hourglass ph-bold ph-lg"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i> </template> <template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- ã¤ã¾ã‚Šãƒªãƒ¢ãƒ¼ãƒˆãƒ•ã‚©ãƒãƒ¼ã®å ´åˆã€‚ --> <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="ph-minus ph-bold ph-lg"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</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="ph-plus ph-bold ph-lg"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i> </template> <template v-else-if="!isFollowing && !user.isLocked"> - <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ph-plus ph-bold ph-lg"></i> + <span v-if="full" :class="$style.text">{{ i18n.ts.follow }}</span><i class="ti ti-plus"></i> </template> </template> <template v-else> @@ -42,6 +42,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; 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 { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; @@ -63,7 +65,7 @@ const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFro const wait = ref(false); const connection = useStream().useChannel('main'); -if (props.user.isFollowing == null) { +if (props.user.isFollowing == null && $i) { misskeyApi('users/show', { userId: props.user.id, }) @@ -78,6 +80,8 @@ function onFollowChange(user: Misskey.entities.UserDetailed) { } async function onClick() { + pleaseLogin(undefined, { type: 'web', path: `/@${props.user.username}@${props.user.host ?? host}` }); + wait.value = true; try { @@ -121,6 +125,8 @@ async function onClick() { }); hasPendingFollowRequestFromYou.value = true; + if ($i == null) return; + claimAchievement('following1'); if ($i.followingCount >= 10) { @@ -183,17 +189,7 @@ onBeforeUnmount(() => { } &:focus-visible { - &:after { - content: ""; - pointer-events: none; - position: absolute; - top: -5px; - right: -5px; - bottom: -5px; - left: -5px; - border: 2px solid var(--focus); - border-radius: var(--radius-xl); - } + outline-offset: 2px; } &:hover { diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 47cccd9b7cf544b86d187b3c00d0c1cef47f8eb6..2bb5b8762a199100934b86692956d1ced90fc062 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -83,7 +83,7 @@ function leaveHover(): void { > article { > footer { - &:before { + &::before { opacity: 1; } } @@ -139,7 +139,7 @@ function leaveHover(): void { text-shadow: 0 0 8px #000; background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); - &:before { + &::before { content: ""; display: block; position: absolute; diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue index d1809d1073fd7a98a5678abd28d2a843f1f515ec..54c7585f18d16a3a01914b2734edade472796ab5 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> <input v-model="query" :class="$style.input" type="search" :placeholder="q"> - <button :class="$style.button" @click="search"><i class="ph-magnifying-glass ph-bold ph-lg"></i> {{ i18n.ts.searchByGoogle }}</button> + <button :class="$style.button" @click="search"><i class="ti ti-search"></i> {{ i18n.ts.searchByGoogle }}</button> </div> </template> diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 4e3fafe845fc5ee8e136908a0c4ff4d08c309b6b..8d301f16bd4e4e279d46af23b6cc802350f9d93b 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only :enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined" :leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined" > - <canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined"/> - <img v-show="!hide" key="img" ref="img" :height="imgHeight" :width="imgWidth" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async"/> + <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"/> </TransitionGroup> </div> </template> @@ -151,22 +151,26 @@ function drawImage(bitmap: CanvasImageSource) { } function drawAvg() { - if (!canvas.value || !props.hash) return; + 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 = extractAvgColorFromBlurhash(props.hash) ?? '#888'; + ctx.fillStyle = color; ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value); } async function draw() { - if (props.hash == null) return; + if (import.meta.env.MODE === 'test' && props.hash == null) return; drawAvg(); + if (props.hash == null) return; + if (props.onlyAvgColor) return; const work = await canvasPromise; diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 9a5874b5c08f9508d8a3449b37ba848b1d2cbe4f..46c2db4b1fc74dffae8c33fe775db9b709e058bc 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="[$style.root, { [$style.warn]: warn }]"> - <i v-if="warn" class="ph-warning ph-bold ph-lg" :class="$style.i"></i> - <i v-else class="ph-info ph-bold ph-lg" :class="$style.i"></i> + <i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i> + <i v-else class="ti ti-info-circle" :class="$style.i"></i> <div><slot></slot></div> - <button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ph-x ph-bold ph-lg"></i></button> + <button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ti ti-x"></i></button> </div> </template> diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 201b409a050df84d97b731e614c5214b11492d0d..06389b40130b3f6baeb6dd1f631f95c4e00598b7 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.caption"><slot name="caption"></slot></div> - <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </template> @@ -79,7 +79,7 @@ const props = defineProps<{ const emit = defineEmits<{ (ev: 'change', _ev: KeyboardEvent): void; (ev: 'keydown', _ev: KeyboardEvent): void; - (ev: 'enter'): void; + (ev: 'enter', _ev: KeyboardEvent): void; (ev: 'update:modelValue', value: string | number): void; }>(); @@ -111,7 +111,7 @@ const onKeydown = (ev: KeyboardEvent) => { emit('keydown', ev); if (ev.code === 'Enter') { - emit('enter'); + emit('enter', ev); } }; diff --git a/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts b/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e8de9d8789cd0fcbbae545055801944dcecdf2d --- /dev/null +++ b/packages/frontend/src/components/MkInstanceCardMini.stories.impl.ts @@ -0,0 +1,65 @@ +/* + * 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 { HttpResponse, http } from 'msw'; +import { federationInstance } from '../../.storybook/fakes.js'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import { getChartResolver } from '../../.storybook/charts.js'; +import MkInstanceCardMini from './MkInstanceCardMini.vue'; + +export const Default = { + render(args) { + return { + components: { + MkInstanceCardMini, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<MkInstanceCardMini v-bind="props" />', + }; + }, + args: { + instance: federationInstance(), + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.get('/undefined/preview.webp', async ({ request }) => { + const urlStr = new URL(request.url).searchParams.get('url'); + if (urlStr == null) { + return new HttpResponse(null, { status: 404 }); + } + const url = new URL(urlStr); + + if (url.href.startsWith('https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/')) { + const image = await (await fetch(`client-assets/${url.pathname.split('/').pop()}`)).blob(); + return new HttpResponse(image, { + headers: { + 'Content-Type': 'image/jpeg', + }, + }); + } else { + return new HttpResponse(null, { status: 404 }); + } + }), + http.get('/api/charts/instance', getChartResolver(['requests.received'])), + ], + }, + }, +} satisfies StoryObj<typeof MkInstanceCardMini>; diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index feb62415aa1b2d3fd13268897a343567efbbd0c1..10b390e7f926ee8dc437e3f31560f86da5154680 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -29,8 +29,8 @@ const chartValues = ref<number[] | null>(null); misskeyApiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => { // 今日ã®ã¶ã‚“ã®å€¤ã¯ã¾ã 途ä¸ã®å€¤ã§ã‚ã‚Šã€ãれもå«ã‚ã‚‹ã¨å¤§æŠµã®å ´åˆå‰æ—¥ã‚ˆã‚Šã‚‚下é™ã—ã¦ã„るよã†ãªã‚°ãƒ©ãƒ•ã«ãªã£ã¦ã—ã¾ã†ãŸã‚今日ã¯å¼¾ã - res['requests.received'].splice(0, 1); - chartValues.value = res['requests.received']; + res.requests.received.splice(0, 1); + chartValues.value = res.requests.received; }); function getInstanceIcon(instance): string { diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue index b095df20e584b0c975268444cfed40b82ac5a7a2..de51a98789dcda264cb29d19407a9f0f84473093 100644 --- a/packages/frontend/src/components/MkInviteCode.vue +++ b/packages/frontend/src/components/MkInviteCode.vue @@ -50,8 +50,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div :class="$style.buttons"> - <MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton> - <MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <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> @@ -62,7 +62,7 @@ import { computed } from 'vue'; import * as Misskey from 'misskey-js'; import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue index 2175c0e888de6d38898bb9949e9fd412dafff3a7..50c9e16e5ebe4361cc3baf682b6c71027cb7a0e3 100644 --- a/packages/frontend/src/components/MkKeyValue.vue +++ b/packages/frontend/src/components/MkKeyValue.vue @@ -10,14 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.value"> <slot name="value"></slot> - <button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ph-copy ph-bold ph-lg"></i></button> + <button v-if="copy" v-tooltip="i18n.ts.copy" class="_textButton" style="margin-left: 0.5em;" @click="copy_"><i class="ti ti-copy"></i></button> </div> </div> </template> <script lang="ts" setup> import { } from 'vue'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index e232b4d66f79777e9ddee2cff33097d02f4f1d8c..e0880ec3e769a1210c73e338269884772ed8260c 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')"> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()"> <div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }"> <div class="main"> <template v-for="item in items" :key="item.text"> diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 49cbacd1e8321cf75ccfa299b4cee909ac385534..07cf9e0c37e0caa82a38c533ac170ed48d2780f1 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only @click.stop > <slot></slot> - <i v-if="target === '_blank'" class="ph-arrow-square-out ph-bold ph-lg" :class="$style.icon"></i> + <i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i> </component> </template> @@ -38,11 +38,13 @@ const el = ref<HTMLElement | { $el: HTMLElement }>(); if (isEnabledUrlPreview.value) { useTooltip(el, (showing) => { - os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { showing, url: props.url, source: el.value instanceof HTMLElement ? el.value : el.value?.$el, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } </script> diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index 41425facc31bada2c42533a5ce56dc59c1ffb0df..93affd930f106f740eca65bf9acd39f1dbfc4d6c 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -15,10 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.stop @keydown.stop > - <button v-if="hide" :class="$style.hidden" @click="hide = false"> + <button v-if="hide" :class="$style.hidden" @click="show"> <div :class="$style.hiddenTextWrapper"> - <b v-if="audio.isSensitive" style="display: block;"><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> + <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> <span style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </button> @@ -39,28 +39,42 @@ SPDX-License-Identifier: AGPL-3.0-only <audio ref="audioEl" preload="metadata" + @keydown.prevent="() => {}" > <source :src="audio.url"> </audio> <div :class="[$style.controlsChild, $style.controlsLeft]"> - <button class="_button" :class="$style.controlButton" @click="togglePlayPause"> - <i v-if="isPlaying" class="ph-pause ph-bold ph-lg"></i> - <i v-else class="ph-play ph-bold ph-lg"></i> + <button + :class="['_button', $style.controlButton]" + tabindex="-1" + @click.stop="togglePlayPause" + > + <i v-if="isPlaying" class="ti ti-player-pause-filled"></i> + <i v-else class="ti ti-player-play-filled"></i> </button> </div> <div :class="[$style.controlsChild, $style.controlsRight]"> <a class="_button" :class="$style.controlButton" :href="audio.url" :download="audio.name" target="_blank"> <i class="ph-download ph-bold ph-lg"></i> </a> - <button class="_button" :class="$style.controlButton" @click="showMenu"> - <i class="ph-gear ph-bold ph-lg"></i> + <button + :class="['_button', $style.controlButton]" + tabindex="-1" + @click.stop="() => {}" + @mousedown.prevent.stop="showMenu" + > + <i class="ti ti-settings"></i> </button> </div> <div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div> <div :class="[$style.controlsChild, $style.controlsVolume]"> - <button class="_button" :class="$style.controlButton" @click="toggleMute"> - <i v-if="volume === 0" class="ph-speaker-x ph-bold ph-lg"></i> - <i v-else class="ph-speaker-high ph-bold ph-lg"></i> + <button + :class="['_button', $style.controlButton]" + tabindex="-1" + @click.stop="toggleMute" + > + <i v-if="volume === 0" class="ti ti-volume-3"></i> + <i v-else class="ti ti-volume"></i> </button> <MkMediaRange v-model="volume" @@ -83,6 +97,7 @@ import type { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { type Keymap } from '@/scripts/hotkey.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; @@ -93,32 +108,44 @@ const props = defineProps<{ }>(); const keymap = { - 'up': () => { - if (hasFocus() && audioEl.value) { - volume.value = Math.min(volume.value + 0.1, 1); - } + 'up': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + volume.value = Math.min(volume.value + 0.1, 1); + } + }, }, - 'down': () => { - if (hasFocus() && audioEl.value) { - volume.value = Math.max(volume.value - 0.1, 0); - } + 'down': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + volume.value = Math.max(volume.value - 0.1, 0); + } + }, }, - 'left': () => { - if (hasFocus() && audioEl.value) { - audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0); - } + 'left': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0); + } + }, }, - 'right': () => { - if (hasFocus() && audioEl.value) { - audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration); - } + 'right': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration); + } + }, }, 'space': () => { if (hasFocus()) { togglePlayPause(); } }, -}; +} as const satisfies Keymap; // PlayerElã‚‚ã—ãã¯ãã®åè¦ç´ ã«ãƒ•ã‚©ãƒ¼ã‚«ã‚¹ãŒã‚ã‚‹ã‹ã©ã†ã‹ function hasFocus() { @@ -129,9 +156,21 @@ function hasFocus() { const playerEl = shallowRef<HTMLDivElement>(); const audioEl = shallowRef<HTMLAudioElement>(); -// eslint-disable-next-line vue/no-setup-props-destructure +// eslint-disable-next-line vue/no-setup-props-reactivity-loss const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore')); +async function show() { + if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.sensitiveMediaRevealConfirm, + }); + if (canceled) return; + } + + hide.value = false; +} + // Menu const menuShowing = ref(false); @@ -143,13 +182,13 @@ function showMenu(ev: MouseEvent) { { type: 'switch', text: i18n.ts._mediaControls.loop, - icon: 'ph ph-repeat', + icon: 'ti ti-repeat', ref: loop, }, { type: 'radio', text: i18n.ts._mediaControls.playbackRate, - icon: 'ph ph-gauge', + icon: 'ti ti-clock-play', ref: speed, options: { '0.25x': 0.25, @@ -166,7 +205,7 @@ function showMenu(ev: MouseEvent) { }, { text: i18n.ts.hide, - icon: 'ph-eye-closed ph-bold ph-lg', + icon: 'ti ti-eye-off', action: () => { hide.value = true; }, @@ -176,7 +215,7 @@ function showMenu(ev: MouseEvent) { if (iAmModerator) { menu.push({ text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: props.audio.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', + icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', danger: true, action: () => toggleSensitive(props.audio), }); @@ -188,7 +227,7 @@ function showMenu(ev: MouseEvent) { }, { type: 'link' as const, text: i18n.ts._fileViewer.title, - icon: 'ph ph-info', + icon: 'ti ti-info-circle', to: `/my/drive/file/${props.audio.id}`, }); } @@ -361,7 +400,7 @@ onDeactivated(() => { border-radius: var(--radius); overflow: clip; - &:focus { + &:focus-visible { outline: none; } } @@ -427,6 +466,10 @@ onDeactivated(() => { color: var(--accent); background-color: var(--accentedBg); } + + &:focus-visible { + outline: none; + } } } diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 605c1a4c80681a114cff11b8d997d8ab6311dae6..ed8d43273fbcb94545a97382c1fb7e66d1855dfe 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> - <div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false"> - <span style="font-size: 1.6em;"><i class="ph-warning ph-bold ph-lg"></i></span> + <div v-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> @@ -17,31 +17,37 @@ SPDX-License-Identifier: AGPL-3.0-only :title="media.name" :download="media.name" > - <span style="font-size: 1.6em;"><i class="ph-download ph-bold ph-lg"></i></span> + <span style="font-size: 1.6em;"><i class="ti ti-download"></i></span> <b>{{ media.name }}</b> </a> </div> </template> <script lang="ts" setup> -import { shallowRef, watch, ref } from 'vue'; +import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; +import { defaultStore } from '@/store.js'; +import * as os from '@/os.js'; import MkMediaAudio from '@/components/MkMediaAudio.vue'; -const props = withDefaults(defineProps<{ +const props = defineProps<{ media: Misskey.entities.DriveFile; -}>(), { -}); +}>(); -const audioEl = shallowRef<HTMLAudioElement>(); const hide = ref(true); -watch(audioEl, () => { - if (audioEl.value) { - audioEl.value.volume = 0.3; +async function show() { + if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.sensitiveMediaRevealConfirm, + }); + if (canceled) return; } -}); + + hide.value = false; +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index f1fb4f5b44c0056eef8bff761f32d40cf47d674e..cac419d42bd0298b3c058d0a3f7fea9febf07fb5 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="hide"> <div :class="$style.hiddenText"> <div :class="$style.hiddenTextWrapper"> - <b v-if="image.isSensitive" style="display: block;"><i class="ph-eye-closed ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ph-image-square ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> + <b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b> <span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </div> @@ -42,11 +42,11 @@ SPDX-License-Identifier: AGPL-3.0-only <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="ph-eye-closed ph-bold ph-lg"></i></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 v-if="!image.comment" :class="$style.indicator" title="Image lacks descriptive text"><i class="ph-pencil-simple ph-bold ph-lg-off"></i></div> </div> - <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ph-dots-three ph-bold ph-lg" style="vertical-align: middle;"></i></button> - <i class="ph-eye-slash ph-bold ph-lg" :class="$style.hide" @click.stop="hide = true"></i> + <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button> + <i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i> </template> </div> </template> @@ -84,11 +84,21 @@ const url = computed(() => (props.raw || defaultStore.state.loadRawImages) : props.image.thumbnailUrl, ); -function onclick() { +async function onclick(ev: MouseEvent) { if (!props.controls) { return; } + if (hide.value) { + ev.stopPropagation(); + if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.sensitiveMediaRevealConfirm, + }); + if (canceled) return; + } + hide.value = false; } } @@ -104,13 +114,13 @@ watch(() => props.image, () => { function showMenu(ev: MouseEvent) { os.popupMenu([{ text: i18n.ts.hide, - icon: 'ph-eye-slash ph-bold ph-lg', + icon: 'ti ti-eye-off', action: () => { hide.value = true; }, }, ...(iAmModerator ? [{ text: i18n.ts.markAsSensitive, - icon: 'ph-eye-closed ph-bold ph-lg', + icon: 'ti ti-eye-exclamation', danger: true, action: () => { os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); @@ -120,7 +130,7 @@ function showMenu(ev: MouseEvent) { }, { type: 'link' as const, text: i18n.ts._fileViewer.title, - icon: 'ph ph-info', + icon: 'ti ti-info-circle', to: `/my/drive/file/${props.image.id}`, }] : [])], ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 3bf44aea8ed8fbb04dc068b8140b56897f3ceaa2..4bc2b9fba7df8ed506dde94b321d78a8a08fa739 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -37,10 +37,11 @@ import 'photoswipe/style.css'; import XBanner from '@/components/MkMediaBanner.vue'; import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; -import XModPlayer from '@/components/MkModPlayer.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 { defaultStore } from '@/store.js'; +import { focusParent } from '@/scripts/focus.js'; const props = defineProps<{ mediaList: Misskey.entities.DriveFile[]; @@ -51,7 +52,9 @@ const gallery = shallowRef<HTMLDivElement>(); const pswpZIndex = os.claimZIndex('middle'); document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString()); const count = computed(() => props.mediaList.filter(media => previewable(media)).length); -let lightbox: PhotoSwipeLightbox | null; +let lightbox: PhotoSwipeLightbox | null = null; + +let activeEl: HTMLElement | null = null; const popstateHandler = (): void => { if (lightbox?.pswp && lightbox.pswp.isOpen === true) { @@ -62,7 +65,7 @@ const popstateHandler = (): void => { async function calcAspectRatio() { if (!gallery.value) return; - let img = props.mediaList[0]; + const img = props.mediaList[0]; if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) { gallery.value.style.aspectRatio = ''; @@ -139,18 +142,17 @@ onMounted(() => { bgOpacity: 1, showAnimationDuration: 100, hideAnimationDuration: 100, + returnFocus: false, pswpModule: PhotoSwipe, }); - lightbox.on('itemData', (ev) => { - const { itemData } = ev; - + lightbox.addFilter('itemData', (itemData) => { // element is children const { element } = itemData; const id = element?.dataset.id; const file = props.mediaList.find(media => media.id === id); - if (!file) return; + if (!file) return itemData; itemData.src = file.url; itemData.w = Number(file.properties.width); @@ -162,50 +164,60 @@ onMounted(() => { itemData.alt = file.comment ?? undefined; itemData.comment = file.comment; itemData.thumbCropped = true; + + return itemData; }); lightbox.on('uiRegister', () => { lightbox?.pswp?.ui?.registerElement({ name: 'altText', - className: 'pwsp__alt-text-container', + className: 'pswp__alt-text-container', appendTo: 'wrapper', - onInit: (el, pwsp) => { - let textBox = document.createElement('p'); - textBox.className = 'pwsp__alt-text _acrylic'; + onInit: (el, pswp) => { + const textBox = document.createElement('p'); + textBox.className = 'pswp__alt-text _acrylic'; el.appendChild(textBox); - pwsp.on('change', (a) => { - if (pwsp.currSlide?.data.comment) { + pswp.on('change', () => { + if (pswp.currSlide?.data.comment) { textBox.style.display = ''; } else { textBox.style.display = 'none'; } - textBox.textContent = pwsp.currSlide?.data.comment; + textBox.textContent = pswp.currSlide?.data.comment; }); }, }); }); - lightbox.init(); - - window.addEventListener('popstate', popstateHandler); - - lightbox.on('beforeOpen', () => { + lightbox.on('afterInit', () => { + activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null; + focusParent(activeEl, true, true); + lightbox?.pswp?.element?.focus({ + preventScroll: true, + }); history.pushState(null, '', '#pswp'); }); - lightbox.on('close', () => { + lightbox.on('destroy', () => { + focusParent(activeEl, true, false); + activeEl = null; if (window.location.hash === '#pswp') { history.back(); } }); + + window.addEventListener('popstate', popstateHandler); + + lightbox.init(); }); onUnmounted(() => { window.removeEventListener('popstate', popstateHandler); lightbox?.destroy(); lightbox = null; + activeEl = null; }); const previewable = (file: Misskey.entities.DriveFile): boolean => { @@ -214,6 +226,16 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => { if (isModule(file)) return true; return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type); }; + +const openGallery = () => { + if (props.mediaList.filter(media => previewable(media)).length > 0) { + lightbox?.loadAndOpen(0); + } +}; + +defineExpose({ + openGallery, +}); </script> <style lang="scss" module> @@ -317,7 +339,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => { backdrop-filter: var(--modalBgFilter); } -.pwsp__alt-text-container { +.pswp__alt-text-container { display: flex; flex-direction: row; align-items: center; @@ -331,7 +353,7 @@ const previewable = (file: Misskey.entities.DriveFile): boolean => { max-width: 800px; } -.pwsp__alt-text { +.pswp__alt-text { color: var(--fg); margin: 0 auto; text-align: center; diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index dbf76bc33d3d7d5b0b502500c859bd2424cd80a0..1c3c9a312b0aa05f7fdfe11b1b849a4125f7f97a 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -18,10 +18,10 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.stop @keydown.stop > - <button v-if="hide" :class="$style.hidden" @click="hide = false"> + <button v-if="hide" :class="$style.hidden" @click="show"> <div :class="$style.hiddenTextWrapper"> - <b v-if="video.isSensitive" style="display: block;"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> - <b v-else style="display: block;"><i class="ph-film-strip ph-bold ph-lg"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> + <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> + <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> <span style="display: block;">{{ i18n.ts.clickToShow }}</span> </div> </button> @@ -39,10 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only > <source :src="video.url"> </video> - <i class="ph-eye-closed ph-bold ph-lg" :class="$style.hide" @click="hide = true"></i> + <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i> <div :class="$style.indicators"> <div v-if="video.comment" :class="$style.indicator">ALT</div> - <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ph-warning ph-bold ph-lg"></i></div> + <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> </div> </div> @@ -60,20 +60,20 @@ SPDX-License-Identifier: AGPL-3.0-only > <source :src="video.url"> </video> - <button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ph-play ph-bold ph-lg"></i></button> + <button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ti ti-player-play-filled"></i></button> <div v-else-if="!isActuallyPlaying" :class="$style.videoLoading"> <MkLoading/> </div> - <i class="ph-eye-closed ph-bold ph-lg" :class="$style.hide" @click="hide = true"></i> + <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i> <div :class="$style.indicators"> <div v-if="video.comment" :class="$style.indicator">ALT</div> - <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ph-warning ph-bold ph-lg"></i></div> + <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div> </div> <div :class="$style.videoControls" @click.self="togglePlayPause"> <div :class="[$style.controlsChild, $style.controlsLeft]"> <button class="_button" :class="$style.controlButton" @click="togglePlayPause"> - <i v-if="isPlaying" class="ph-pause ph-bold ph-lg"></i> - <i v-else class="ph-play ph-bold ph-lg"></i> + <i v-if="isPlaying" class="ti ti-player-pause-filled"></i> + <i v-else class="ti ti-player-play-filled"></i> </button> </div> <div :class="[$style.controlsChild, $style.controlsRight]"> @@ -81,18 +81,18 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ph-download ph-bold ph-lg"></i> </a> <button class="_button" :class="$style.controlButton" @click="showMenu"> - <i class="ph-gear ph-bold ph-lg"></i> + <i class="ti ti-settings"></i> </button> <button class="_button" :class="$style.controlButton" @click="toggleFullscreen"> - <i v-if="isFullscreen" class="ph-arrows-in ph-bold ph-lg"></i> - <i v-else class="ph-arrows-out ph-bold ph-lg"></i> + <i v-if="isFullscreen" class="ti ti-arrows-minimize"></i> + <i v-else class="ti ti-arrows-maximize"></i> </button> </div> <div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div> <div :class="[$style.controlsChild, $style.controlsVolume]"> <button class="_button" :class="$style.controlButton" @click="toggleMute"> - <i v-if="volume === 0" class="ph-speaker-x ph-bold ph-lg"></i> - <i v-else class="ph-speaker-high ph-bold ph-lg"></i> + <i v-if="volume === 0" class="ti ti-volume-3"></i> + <i v-else class="ti ti-volume"></i> </button> <MkMediaRange v-model="volume" @@ -115,6 +115,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu.js'; +import { type Keymap } from '@/scripts/hotkey.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import { defaultStore } from '@/store.js'; @@ -130,32 +131,44 @@ const props = defineProps<{ }>(); const keymap = { - 'up': () => { - if (hasFocus() && videoEl.value) { - volume.value = Math.min(volume.value + 0.1, 1); - } + 'up': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + volume.value = Math.min(volume.value + 0.1, 1); + } + }, }, - 'down': () => { - if (hasFocus() && videoEl.value) { - volume.value = Math.max(volume.value - 0.1, 0); - } + 'down': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + volume.value = Math.max(volume.value - 0.1, 0); + } + }, }, - 'left': () => { - if (hasFocus() && videoEl.value) { - videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0); - } + 'left': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0); + } + }, }, - 'right': () => { - if (hasFocus() && videoEl.value) { - videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration); - } + 'right': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration); + } + }, }, 'space': () => { if (hasFocus()) { togglePlayPause(); } }, -}; +} as const satisfies Keymap; // PlayerElã‚‚ã—ãã¯ãã®åè¦ç´ ã«ãƒ•ã‚©ãƒ¼ã‚«ã‚¹ãŒã‚ã‚‹ã‹ã©ã†ã‹ function hasFocus() { @@ -163,9 +176,21 @@ function hasFocus() { return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement); } -// eslint-disable-next-line vue/no-setup-props-destructure +// eslint-disable-next-line vue/no-setup-props-reactivity-loss const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore')); +async function show() { + if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.ts.sensitiveMediaRevealConfirm, + }); + if (canceled) return; + } + + hide.value = false; +} + // Menu const menuShowing = ref(false); @@ -177,13 +202,13 @@ function showMenu(ev: MouseEvent) { { type: 'switch', text: i18n.ts._mediaControls.loop, - icon: 'ph ph-repeat', + icon: 'ti ti-repeat', ref: loop, }, { type: 'radio', text: i18n.ts._mediaControls.playbackRate, - icon: 'ph ph-gauge', + icon: 'ti ti-clock-play', ref: speed, options: { '0.25x': 0.25, @@ -197,7 +222,7 @@ function showMenu(ev: MouseEvent) { }, ...(document.pictureInPictureEnabled ? [{ text: i18n.ts._mediaControls.pip, - icon: 'ph ph-picture-in-picture', + icon: 'ti ti-picture-in-picture', action: togglePictureInPicture, }] : []), { @@ -205,7 +230,7 @@ function showMenu(ev: MouseEvent) { }, { text: i18n.ts.hide, - icon: 'ph-eye-closed ph-bold ph-lg', + icon: 'ti ti-eye-off', action: () => { hide.value = true; }, @@ -215,7 +240,7 @@ function showMenu(ev: MouseEvent) { if (iAmModerator) { menu.push({ text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: props.video.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', + icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', danger: true, action: () => toggleSensitive(props.video), }); @@ -227,7 +252,7 @@ function showMenu(ev: MouseEvent) { }, { type: 'link' as const, text: i18n.ts._fileViewer.title, - icon: 'ph ph-info', + icon: 'ti ti-info-circle', to: `/my/drive/file/${props.video.id}`, }); } @@ -471,7 +496,7 @@ onDeactivated(() => { position: relative; overflow: clip; - &:focus { + &:focus-visible { outline: none; } } @@ -578,6 +603,10 @@ onDeactivated(() => { border-radius: 99rem; font-size: 1.1rem; + + &:focus-visible { + outline: none; + } } .videoLoading { @@ -641,6 +670,10 @@ onDeactivated(() => { &:hover { background-color: var(--accent); } + + &:focus-visible { + outline: none; + } } } diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue index dfb6d346182ef56d1f1dee0154938b0c5318cbf7..235790556c5bb667f8ab92b3f53c21b06603e24f 100644 --- a/packages/frontend/src/components/MkMenu.child.vue +++ b/packages/frontend/src/components/MkMenu.child.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; +import { nextTick, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue'; import MkMenu from './MkMenu.vue'; import { MenuItem } from '@/types/menu.js'; @@ -19,7 +19,6 @@ const props = defineProps<{ targetElement: HTMLElement; rootElement: HTMLElement; width?: number; - viaKeyboard?: boolean; }>(); const emit = defineEmits<{ @@ -27,6 +26,8 @@ const emit = defineEmits<{ (ev: 'actioned'): void; }>(); +provide('isNestingMenu', true); + const el = shallowRef<HTMLElement>(); const align = 'left'; diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 6ced2fecc28a7312c9857680d5af248e1b3121ba..0537f4f98873b8c1e66bf9ec97b7025836bf2f95 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -4,23 +4,42 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div role="menu"> +<div role="menu" @focusin.passive.stop="() => {}"> <div - ref="itemsEl" v-hotkey="keymap" + ref="itemsEl" + v-hotkey="keymap" + tabindex="0" class="_popup _shadow" - :class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]" - :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" - @contextmenu.self="e => e.preventDefault()" + :class="{ + [$style.root]: true, + [$style.center]: align === 'center', + [$style.asDrawer]: asDrawer, + }" + :style="{ + width: (width && !asDrawer) ? `${width}px` : '', + maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)', + }" + @keydown.stop="() => {}" + @contextmenu.self.prevent="() => {}" > - <template v-for="(item, i) in (items2 ?? [])"> - <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div> - <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]"> + <template v-for="item in (items2 ?? [])"> + <div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div> + <span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]"> <span style="opacity: 0.7;">{{ item.text }}</span> </span> - <span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]"> + <span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]"> <span><MkEllipsis/></span> </span> - <MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <MkA + v-else-if="item.type === 'link'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item]" + :to="item.to" + @click.passive="close(true)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> @@ -28,20 +47,49 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> </div> </MkA> - <a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <a + v-else-if="item.type === 'a'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item]" + :href="item.href" + :target="item.target" + :rel="item.target === '_blank' ? 'noopener noreferrer' : undefined" + :download="item.download" + @click.passive="close(true)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <div :class="$style.item_content"> <span :class="$style.item_content_text">{{ item.text }}</span> <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> </div> </a> - <button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <button + v-else-if="item.type === 'user'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item, { [$style.active]: item.active }]" + @click.prevent="item.active ? close(false) : clicked(item.action, $event)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> <div v-if="item.indicate" :class="$style.item_content"> <span :class="$style.indicator"><i class="_indicatorCircle"></i></span> </div> </button> - <button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <button + v-else-if="item.type === 'switch'" + role="menuitemcheckbox" + tabindex="0" + :class="['_button', $style.item]" + :disabled="unref(item.disabled)" + @click.prevent="switchItem(item)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> <div :class="$style.item_content"> @@ -49,29 +97,61 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> </div> </button> - <button v-else-if="item.type === 'radio'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showRadioOptions(item, $event)" @click="!preferClick ? null : showRadioOptions(item, $event)"> + <button + v-else-if="item.type === 'radio'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]" + :disabled="unref(item.disabled)" + @mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)" + @keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)" + @click.prevent="!preferClick ? null : showRadioOptions(item, $event)" + > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <div :class="$style.item_content"> <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> - <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg"></i></span> + <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> </div> </button> - <button v-else-if="item.type === 'radioOption'" :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.radioActive]: item.active }]" @click="clicked(item.action, $event, false)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <button + v-else-if="item.type === 'radioOption'" + role="menuitemradio" + tabindex="0" + :class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]" + @click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > <div :class="$style.icon"> - <span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span> + <span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span> </div> <div :class="$style.item_content"> <span :class="$style.item_content_text">{{ item.text }}</span> </div> </button> - <button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)"> + <button + v-else-if="item.type === 'parent'" + role="menuitem" + tabindex="0" + :class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]" + @mouseenter.prevent="preferClick ? null : showChildren(item, $event)" + @keydown.enter.prevent="preferClick ? null : showChildren(item, $event)" + @click.prevent="!preferClick ? null : showChildren(item, $event)" + > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <div :class="$style.item_content"> <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span> - <span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span> + <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> </div> </button> - <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> + <button + v-else role="menuitem" + tabindex="0" + :class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]" + @click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)" + @mouseenter.passive="onItemMouseEnter" + @mouseleave.passive="onItemMouseLeave" + > <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <div :class="$style.item_content"> @@ -80,24 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </button> </template> - <span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]"> + <span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]"> <span>{{ i18n.ts.none }}</span> </span> </div> <div v-if="childMenu"> - <XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/> + <XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/> </div> </div> </template> <script lang="ts"> -import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; -import { focusPrev, focusNext } from '@/scripts/focus.js'; +import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue'; import MkSwitchButton from '@/components/MkSwitch.button.vue'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { isTouchUsing } from '@/scripts/touch.js'; +import { type Keymap } from '@/scripts/hotkey.js'; +import { isFocusable } from '@/scripts/focus.js'; +import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; const childrenCache = new WeakMap<MenuParent, MenuItem[]>(); </script> @@ -107,7 +189,6 @@ const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue')); const props = defineProps<{ items: MenuItem[]; - viaKeyboard?: boolean; asDrawer?: boolean; align?: 'center' | string; width?: number; @@ -119,17 +200,28 @@ const emit = defineEmits<{ (ev: 'hide'): void; }>(); -const itemsEl = shallowRef<HTMLDivElement>(); +const isNestingMenu = inject<boolean>('isNestingMenu', false); + +const itemsEl = shallowRef<HTMLElement>(); const items2 = ref<InnerMenuItem[]>(); const child = shallowRef<InstanceType<typeof XChild>>(); -const keymap = computed(() => ({ - 'up|k|shift+tab': focusUp, - 'down|j|tab': focusDown, - 'esc': close, -})); +const keymap = { + 'up|k|shift+tab': { + allowRepeat: true, + callback: () => focusUp(), + }, + 'down|j|tab': { + allowRepeat: true, + callback: () => focusDown(), + }, + 'esc': { + allowRepeat: true, + callback: () => close(false), + }, +} as const satisfies Keymap; const childShowingItem = ref<MenuItem | null>(); @@ -167,25 +259,19 @@ function childActioned() { close(true); } -const onGlobalMousedown = (event: MouseEvent) => { - if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return; - if (child.value && child.value.checkHit(event)) return; - closeChild(); -}; - let childCloseTimer: null | number = null; -function onItemMouseEnter(item) { +function onItemMouseEnter() { childCloseTimer = window.setTimeout(() => { closeChild(); }, 300); } -function onItemMouseLeave(item) { +function onItemMouseLeave() { if (childCloseTimer) window.clearTimeout(childCloseTimer); } -async function showRadioOptions(item: MenuRadio, ev: MouseEvent) { +async function showRadioOptions(item: MenuRadio, ev: Event) { const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => { const value = item.options[key]; return { @@ -200,7 +286,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) { if (props.asDrawer) { os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => { - emit('close'); + close(false); }); emit('hide'); } else { @@ -210,7 +296,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) { } } -async function showChildren(item: MenuParent, ev: MouseEvent) { +async function showChildren(item: MenuParent, ev: Event) { const children: MenuItem[] = await (async () => { if (childrenCache.has(item)) { return childrenCache.get(item)!; @@ -227,7 +313,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) { if (props.asDrawer) { os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => { - emit('close'); + close(false); }); emit('hide'); } else { @@ -246,41 +332,87 @@ function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) { } function close(actioned = false) { - emit('close', actioned); + disposeHandlers(); + nextTick(() => { + closeChild(); + emit('close', actioned); + }); +} + +function switchItem(item: MenuSwitch & { ref: any }) { + if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return; + item.ref = !item.ref; } function focusUp() { - focusPrev(document.activeElement); + if (disposed) return; + if (!itemsEl.value?.contains(document.activeElement)) return; + + const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable); + const activeIndex = focusableElements.findIndex(el => el === document.activeElement); + const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1); + const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value; + + targetElement.focus(); } function focusDown() { - focusNext(document.activeElement); -} + if (disposed) return; + if (!itemsEl.value?.contains(document.activeElement)) return; -function switchItem(item: MenuSwitch & { ref: any }) { - if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return; - item.ref = !item.ref; -} + const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable); + const activeIndex = focusableElements.findIndex(el => el === document.activeElement); + const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0; + const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value; -function getValue<T>(item?: ComputedRef<T> | T) { - return isRef(item) ? item.value : item; + targetElement.focus(); } -onMounted(() => { - if (props.viaKeyboard) { - nextTick(() => { - if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false); - }); - } +const onGlobalFocusin = (ev: FocusEvent) => { + if (disposed) return; + if (itemsEl.value?.parentElement?.contains(getNodeOrNull(ev.target))) return; + nextTick(() => { + if (itemsEl.value != null && isFocusable(itemsEl.value)) { + itemsEl.value.focus({ preventScroll: true }); + nextTick(() => focusDown()); + } + }); +}; - // TODO: アクティブãªè¦ç´ ã¾ã§ã‚¹ã‚¯ãƒãƒ¼ãƒ« - //itemsEl.scrollTo(); +const onGlobalMousedown = (ev: MouseEvent) => { + if (disposed) return; + if (childTarget.value?.contains(getNodeOrNull(ev.target))) return; + if (child.value?.checkHit(ev)) return; + closeChild(); +}; +const setupHandlers = () => { + if (!isNestingMenu) { + document.addEventListener('focusin', onGlobalFocusin, { passive: true }); + } document.addEventListener('mousedown', onGlobalMousedown, { passive: true }); +}; + +let disposed = false; + +const disposeHandlers = () => { + disposed = true; + if (!isNestingMenu) { + document.removeEventListener('focusin', onGlobalFocusin); + } + document.removeEventListener('mousedown', onGlobalMousedown); +}; + +onMounted(() => { + setupHandlers(); + + if (!isNestingMenu) { + nextTick(() => itemsEl.value?.focus({ preventScroll: true })); + } }); onBeforeUnmount(() => { - document.removeEventListener('mousedown', onGlobalMousedown); + disposeHandlers(); }); </script> @@ -293,6 +425,10 @@ onBeforeUnmount(() => { overflow: auto; overscroll-behavior: contain; + &:focus-visible { + outline: none; + } + &.center { > .item { text-align: center; @@ -310,7 +446,7 @@ onBeforeUnmount(() => { font-size: 1em; padding: 12px 24px; - &:before { + &::before { width: calc(100% - 24px); border-radius: var(--radius); } @@ -340,8 +476,10 @@ onBeforeUnmount(() => { text-align: left; overflow: hidden; text-overflow: ellipsis; + text-decoration: none !important; + color: var(--menuFg, var(--fg)); - &:before { + &::before { content: ""; display: block; position: absolute; @@ -355,56 +493,56 @@ onBeforeUnmount(() => { border-radius: var(--radius-sm); } - &:not(:disabled):hover { - color: var(--accent); - text-decoration: none; + &:focus-visible { + outline: none; - &:before { - background: var(--accentedBg); + &:not(:hover):not(:active)::before { + outline: var(--focus) solid 2px; + outline-offset: -2px; } } - &.danger { - color: #ff2a2a; - - &:hover { - color: #fff; + &:not(:disabled) { + &:hover, + &:focus-visible:active, + &:focus-visible.active { + color: var(--menuHoverFg, var(--accent)); - &:before { - background: #ff4242; + &::before { + background-color: var(--menuHoverBg, var(--accentedBg)); } } - &:active { - color: #fff; + &:not(:focus-visible):active, + &:not(:focus-visible).active { + color: var(--menuActiveFg, var(--fgOnAccent)); - &:before { - background: #d42e2e !important; + &::before { + background-color: var(--menuActiveBg, var(--accent)); } } } - &:active, - &.active { - color: var(--fgOnAccent) !important; - opacity: 1; - - &:before { - background: var(--accent) !important; - } + &:disabled { + cursor: not-allowed; } - &.radioActive { - color: var(--accent) !important; - opacity: 1; + &.danger { + --menuFg: #ff2a2a; + --menuHoverFg: #fff; + --menuHoverBg: #ff4242; + --menuActiveFg: #fff; + --menuActiveBg: #d42e2e; + } - &:before { - background-color: var(--accentedBg) !important; - } + &.radio { + --menuActiveFg: var(--accent); + --menuActiveBg: var(--accentedBg); } - &:not(:active):focus-visible { - box-shadow: 0 0 0 2px var(--focus) inset; + &.parent { + --menuActiveFg: var(--accent); + --menuActiveBg: var(--accentedBg); } &.label { @@ -422,22 +560,6 @@ onBeforeUnmount(() => { pointer-events: none; opacity: 0.7; } - - &.parent { - pointer-events: auto; - display: flex; - align-items: center; - cursor: default; - - &.childShowing { - color: var(--accent); - text-decoration: none; - - &:before { - background: var(--accentedBg); - } - } - } } .item_content { @@ -456,18 +578,6 @@ onBeforeUnmount(() => { overflow: hidden; } -.switch { - position: relative; - display: flex; - transition: all 0.2s ease; - user-select: none; - cursor: pointer; -} - -.switchDisabled { - cursor: not-allowed; -} - .switchButton { margin-left: -2px; --height: 1.35em; @@ -479,14 +589,6 @@ onBeforeUnmount(() => { text-overflow: ellipsis; } -.switchInput { - position: absolute; - width: 0; - height: 0; - opacity: 0; - margin: 0; -} - .icon { margin-right: 8px; line-height: 1; @@ -515,12 +617,12 @@ onBeforeUnmount(() => { border-top: solid 0.5px var(--divider); } -.radio { +.radioIcon { display: inline-block; position: relative; width: 1em; height: 1em; - vertical-align: -.125em; + vertical-align: -0.125em; border-radius: 50%; border: solid 2px var(--divider); background-color: var(--panel); diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 9e69ab2207f58d4f472c07d9912f0bd1ca23449b..f8032f9b431bb547ce7249fb2fff2b518700d5ef 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -30,9 +30,9 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.transition_modal_leaveTo]: transitionName === 'modal', [$style.transition_send_leaveTo]: transitionName === 'send', })" - :duration="transitionDuration" appear @afterLeave="emit('closed')" @enter="emit('opening')" @afterEnter="onOpened" + :duration="transitionDuration" appear @afterLeave="onClosed" @enter="emit('opening')" @afterEnter="onOpened" > - <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> + <div v-show="manualShowing != null ? manualShowing : showing" ref="modalRootEl" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> <div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div ref="content" :class="[$style.content, { [$style.fixed]: fixed }]" :style="{ zIndex }" @click.self="onBgClick"> <slot :max-height="maxHeight" :type="type"></slot> @@ -47,6 +47,9 @@ import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; import { defaultStore } from '@/store.js'; import { deviceKind } from '@/scripts/device-kind.js'; +import { type Keymap } from '@/scripts/hotkey.js'; +import { focusTrap } from '@/scripts/focus-trap.js'; +import { focusParent } from '@/scripts/focus.js'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -68,6 +71,8 @@ const props = withDefaults(defineProps<{ zPriority?: 'low' | 'middle' | 'high'; noOverlap?: boolean; transparentBg?: boolean; + hasInteractionWithOtherFocusTrappedEls?: boolean; + returnFocusTo?: HTMLElement | null; }>(), { manualShowing: null, src: null, @@ -76,6 +81,8 @@ const props = withDefaults(defineProps<{ zPriority: 'low', noOverlap: true, transparentBg: false, + hasInteractionWithOtherFocusTrappedEls: false, + returnFocusTo: null, }); const emit = defineEmits<{ @@ -93,6 +100,7 @@ const maxHeight = ref<number>(); const fixed = ref(false); const transformOrigin = ref('center'); const showing = ref(true); +const modalRootEl = shallowRef<HTMLElement>(); const content = shallowRef<HTMLElement>(); const zIndex = os.claimZIndex(props.zPriority); const useSendAnime = ref(false); @@ -131,6 +139,7 @@ const transitionDuration = computed((() => : 0 )); +let releaseFocusTrap: (() => void) | null = null; let contentClicking = false; function close(opts: { useSendAnimation?: boolean } = {}) { @@ -154,8 +163,11 @@ if (type.value === 'drawer') { } const keymap = { - 'esc': () => emit('esc'), -}; + 'esc': { + allowRepeat: true, + callback: () => emit('esc'), + }, +} as const satisfies Keymap; const MARGIN = 16; const SCROLLBAR_THICKNESS = 16; @@ -292,6 +304,10 @@ const onOpened = () => { }, { passive: true }); }; +const onClosed = () => { + emit('closed'); +}; + const alignObserver = new ResizeObserver((entries, observer) => { align(); }); @@ -309,6 +325,20 @@ onMounted(() => { align(); }, { immediate: true }); + watch([showing, () => props.manualShowing], ([showing, manualShowing]) => { + if (manualShowing === true || (manualShowing == null && showing === true)) { + if (modalRootEl.value != null) { + const { release } = focusTrap(modalRootEl.value, props.hasInteractionWithOtherFocusTrappedEls); + + releaseFocusTrap = release; + modalRootEl.value.focus(); + } + } else { + releaseFocusTrap?.(); + focusParent(props.returnFocusTo ?? props.src, true, false); + } + }, { immediate: true }); + nextTick(() => { alignObserver.observe(content.value!); }); diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index fc634176c73e6f828e968317f6078af273d56343..c3c78120362194363f7cee980503fda2c3017223 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -4,15 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="$emit('closed')"> - <div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }" @keydown="onKeydown"> +<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')"> + <div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }"> <div ref="headerEl" :class="$style.header"> - <button v-if="withOkButton" :class="$style.headerButton" class="_button" @click="$emit('close')"><i class="ph-x ph-bold ph-lg"></i></button> + <button v-if="withOkButton && withCloseButton" :class="$style.headerButton" class="_button" @click="emit('close')"><i class="ti ti-x"></i></button> <span :class="$style.title"> <slot name="header"></slot> </span> - <button v-if="!withOkButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="$emit('close')"><i class="ph-x ph-bold ph-lg"></i></button> - <button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="$emit('ok')"><i class="ph-check ph-bold ph-lg"></i></button> + <button v-if="!withOkButton && withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button> + <button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="emit('ok')"><i class="ti ti-check"></i></button> </div> <div :class="$style.body"> <slot :width="bodyWidth" :height="bodyHeight"></slot> @@ -27,11 +27,13 @@ import MkModal from './MkModal.vue'; const props = withDefaults(defineProps<{ withOkButton: boolean; + withCloseButton: boolean; okButtonDisabled: boolean; width: number; height: number; }>(), { withOkButton: false, + withCloseButton: true, okButtonDisabled: false, width: 400, height: 500, @@ -42,6 +44,7 @@ const emit = defineEmits<{ (event: 'close'): void; (event: 'closed'): void; (event: 'ok'): void; + (event: 'esc'): void; }>(); const modal = shallowRef<InstanceType<typeof MkModal>>(); @@ -50,21 +53,13 @@ const headerEl = shallowRef<HTMLElement>(); const bodyWidth = ref(0); const bodyHeight = ref(0); -const close = () => { +function close() { modal.value?.close(); -}; +} -const onBgClick = () => { +function onBgClick() { emit('click'); -}; - -const onKeydown = (evt) => { - if (evt.which === 27) { // Esc - evt.preventDefault(); - evt.stopPropagation(); - close(); - } -}; +} const ro = new ResizeObserver((entries, observer) => { if (rootEl.value == null || headerEl.value == null) return; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index fe85307a52304cbb3cc6268ffd434afa1f0bb1d0..e2f0a4e492351ccd486424aaf16ad9f20ed26b1d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -10,23 +10,23 @@ SPDX-License-Identifier: AGPL-3.0-only ref="rootEl" v-hotkey="keymap" :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" - :tabindex="!isDeleted ? '-1' : undefined" + :tabindex="isDeleted ? '-1' : '0'" > <div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo"> <MkAvatar :class="$style.collapsedInReplyToAvatar" :user="appearNote.reply.user" link preview/> - <MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkA v-user-preview="appearNote.reply.userId" :class="$style.name" :to="userPage(appearNote.reply.user)"> <MkAcct :user="appearNote.reply.user"/> </MkA>: <Mfm :text="getNoteSummary(appearNote.reply)" :plain="true" :nowrap="true" :author="appearNote.reply.user" :nyaize="'respect'" :class="$style.collapsedInReplyToText" @click="inReplyToCollapsed = false"/> </div> <MkNoteSub v-if="appearNote.reply && !renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> - <div v-if="pinned" :class="$style.tip"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.pinnedNote }}</div> - <!--<div v-if="appearNote._prId_" class="tip"><i class="ph-megaphone ph-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div>--> - <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div>--> + <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> <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> - <i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> <I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText"> <template #user> <MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)"> @@ -35,17 +35,17 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </I18n> <div :class="$style.renoteInfo"> - <button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()"> - <i class="ph-dots-three ph-bold ph-lg" :class="$style.renoteMenu"></i> + <button ref="renoteTime" :class="$style.renoteTime" class="_button" @mousedown.prevent="showRenoteMenu()"> + <i class="ti ti-dots" :class="$style.renoteMenu"></i> <MkTime :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="ph-house ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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="ph-rocket ph-bold ph-lg"></i></span> - <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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> <span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span> </div> </div> @@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> </div> <div v-if="appearNote.files && appearNote.files.length > 0"> - <MkMediaList :mediaList="appearNote.files" @click.stop/> + <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/> <div v-if="isEnabledUrlPreview"> @@ -106,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> </button> </div> - <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> + <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </bdi> </div> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> @@ -116,7 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkReactionsViewer> <footer :class="$style.footer"> <button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> - <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> + <i class="ti ti-arrow-back-up"></i> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p> </button> <button @@ -126,13 +126,13 @@ SPDX-License-Identifier: AGPL-3.0-only class="_button" :style="renoted ? 'color: var(--accent) !important;' : ''" @click.stop - @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" + @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()" > - <i class="ph-rocket-launch ph-bold ph-lg"></i> + <i class="ti ti-repeat"></i> <p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p> </button> <button v-else :class="$style.footerButton" class="_button" disabled> - <i class="ph-prohibit ph-bold ph-lg"></i> + <i class="ti ti-ban"></i> </button> <button v-if="canRenote && !props.mock" @@ -148,17 +148,17 @@ 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="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i> - <i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i> - <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></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> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> - <i class="ph-paperclip ph-bold ph-lg"></i> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> + <i class="ti ti-paperclip"></i> </button> - <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()"> - <i class="ph-dots-three ph-bold ph-lg"></i> + <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()"> + <i class="ti ti-dots"></i> </button> </footer> </div> @@ -204,8 +204,7 @@ import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkButton from '@/components/MkButton.vue'; -import { pleaseLogin } from '@/scripts/please-login.js'; -import { focusPrev, focusNext } from '@/scripts/focus.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import number from '@/filters/number.js'; @@ -231,7 +230,11 @@ 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 { isEnabledUrlPreview } from '@/instance.js'; +import { type Keymap } from '@/scripts/hotkey.js'; +import { focusPrev, focusNext } from '@/scripts/focus.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -283,14 +286,7 @@ if (noteViewInterruptors.length > 0) { }); } -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 isRenote = Misskey.note.isPureRenote(note.value); const rootEl = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); @@ -301,8 +297,8 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); - +const appearNote = computed(() => getAppearNote(note.value)); +const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); @@ -328,6 +324,11 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state. const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + /* Overload Functionã«LintãŒå¯¾å¿œã—ã¦ã„ãªã„ã®ã§ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; @@ -348,15 +349,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string let renoting = false; const keymap = { - 'r': () => reply(true), - 'e|a|plus': () => react(true), - '(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, - 'up|k|shift+tab': focusBefore, - 'down|j|tab': focusAfter, - 'esc': blur, - 'm|o': () => showMenu(true), - 's': () => showContent.value !== showContent.value, -}; + 'r': () => { + if (renoteCollapsed.value) return; + reply(); + }, + 'e|a|plus': () => { + if (renoteCollapsed.value) return; + react(); + }, + 'q': () => { + if (renoteCollapsed.value) return; + if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); + }, + 'm': () => { + if (renoteCollapsed.value) return; + showMenu(); + }, + 'c': () => { + if (renoteCollapsed.value) return; + if (!defaultStore.state.showClipButtonInNoteFooter) return; + clip(); + }, + 'o': () => { + if (renoteCollapsed.value) return; + galleryEl.value?.openGallery(); + }, + 'v|enter': () => { + if (renoteCollapsed.value) { + renoteCollapsed.value = false; + } else if (appearNote.value.cw != null) { + showContent.value = !showContent.value; + } else if (isLong) { + collapsed.value = !collapsed.value; + } + }, + 'esc': { + allowRepeat: true, + callback: () => blur(), + }, + 'up|k|shift+tab': { + allowRepeat: true, + callback: () => focusBefore(), + }, + 'down|j|tab': { + allowRepeat: true, + callback: () => focusAfter(), + }, +} as const satisfies Keymap; provide('react', (reaction: string) => { misskeyApi('notes/reactions/create', { @@ -389,12 +428,14 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: renoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); useTooltip(quoteButton, async (showing) => { @@ -408,12 +449,14 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: quoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); if ($i) { @@ -438,13 +481,15 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkReactionsViewerDetails, { + const { dispose } = os.popup(MkReactionsViewerDetails, { showing, reaction: 'â¤ï¸', users, count: appearNote.value.reactionCount, targetElement: reactButton.value!, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } } @@ -460,7 +505,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); renoting = true; @@ -471,7 +516,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } if (!props.mock) { @@ -489,7 +536,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } if (!props.mock) { @@ -506,7 +555,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (props.mock) { return; @@ -529,7 +578,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -551,7 +602,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -560,22 +613,21 @@ function quote() { } } -function reply(viaKeyboard = false): void { - pleaseLogin(); +function reply(): void { + pleaseLogin(undefined, pleaseLoginContext.value); if (props.mock) { return; } os.post({ reply: appearNote.value, channel: appearNote.value.channel, - animation: !viaKeyboard, }).then(() => { focus(); }); } function like(): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.mock) { @@ -590,12 +642,14 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } function react(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -613,7 +667,9 @@ function react(viaKeyboard = false): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); @@ -667,7 +723,9 @@ function undoRenote(note) : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -706,15 +764,13 @@ function onContextmenu(ev: MouseEvent): void { } } -function showMenu(viaKeyboard = false): void { +function showMenu(): void { if (props.mock) { return; } const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); - os.popupMenu(menu, menuButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } async function menuVersions(viaKeyboard = false): Promise<void> { @@ -724,7 +780,7 @@ async function menuVersions(viaKeyboard = false): Promise<void> { }).then(focus).finally(cleanup); } -async function clip() { +async function clip(): Promise<void> { if (props.mock) { return; } @@ -732,7 +788,7 @@ async function clip() { os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } -function showRenoteMenu(viaKeyboard = false): void { +function showRenoteMenu(): void { if (props.mock) { return; } @@ -740,7 +796,7 @@ function showRenoteMenu(viaKeyboard = false): void { function getUnrenote(): MenuItem { return { text: i18n.ts.unrenote, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: () => { misskeyApi('notes/delete', { @@ -752,23 +808,19 @@ function showRenoteMenu(viaKeyboard = false): void { } if (isMyRenote) { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); os.popupMenu([ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), - ], renoteTime.value, { - viaKeyboard: viaKeyboard, - }); + ], renoteTime.value); } else { os.popupMenu([ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), ($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined, - ], renoteTime.value, { - viaKeyboard: viaKeyboard, - }); + ], renoteTime.value); } } @@ -793,11 +845,11 @@ function blur() { } function focusBefore() { - focusPrev(rootEl.value ?? null); + focusPrev(rootEl.value); } function focusAfter() { - focusNext(rootEl.value ?? null); + focusNext(rootEl.value); } function readPromo() { @@ -835,7 +887,7 @@ function emitUpdReaction(emoji: string, delta: number) { &:focus-visible { outline: none; - &:after { + &::after { content: ""; pointer-events: none; display: block; @@ -848,7 +900,7 @@ function emitUpdReaction(emoji: string, delta: number) { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: dashed 1px var(--focus); + border: dashed 2px var(--focus); border-radius: var(--radius); box-sizing: border-box; } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index f0b1ca82a459093c684c64a55893aa6c1e747216..64559ef2655d8bc5e6d0ad19ba545a95fb6e9943 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="rootEl" v-hotkey="keymap" :class="$style.root" + :tabindex="isDeleted ? '-1' : '0'" > <div v-if="appearNote.reply && appearNote.reply.replyId"> <div v-if="!conversationLoaded" style="padding: 16px"> @@ -20,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo" :expandAllCws="props.expandAllCws"/> <div v-if="isRenote" :class="$style.renote"> <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> - <i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> <span :class="$style.renoteText"> <I18n :src="i18n.ts.renotedBy" tag="span"> <template #user> @@ -31,16 +32,16 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> </span> <div :class="$style.renoteInfo"> - <button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()"> - <i v-if="isMyRenote" class="ph-dots-three ph-bold ph-lg" style="margin-right: 4px;"></i> + <button ref="renoteTime" class="_button" :class="$style.renoteTime" @mousedown.prevent="showRenoteMenu()"> + <i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i> <MkTime :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="ph-house ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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="ph-rocket ph-bold ph-lg"></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" @contextmenu.stop="onContextmenu"> @@ -54,12 +55,12 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span> <div :class="$style.noteHeaderInfo"> <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> - <i v-if="appearNote.visibility === 'home'" class="ph-house ph-bold ph-lg"></i> - <i v-else-if="appearNote.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span> - <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span> + <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> </div> <div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div> @@ -97,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> <div v-if="appearNote.files && appearNote.files.length > 0"> - <MkMediaList :mediaList="appearNote.files"/> + <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/> </div> <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/> <div v-if="isEnabledUrlPreview"> @@ -105,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div> </div> - <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> + <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> <footer :class="$style.footer"> <div :class="$style.noteFooterInfo"> @@ -118,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/> <button class="_button" :class="$style.noteFooterButton" @click="reply()"> - <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> + <i class="ti ti-arrow-back-up"></i> <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p> </button> <button @@ -127,13 +128,13 @@ SPDX-License-Identifier: AGPL-3.0-only class="_button" :class="$style.noteFooterButton" :style="renoted ? 'color: var(--accent) !important;' : ''" - @mousedown="renoted ? undoRenote() : boostVisibility()" + @mousedown.prevent="renoted ? undoRenote() : boostVisibility()" > - <i class="ph-rocket-launch ph-bold ph-lg"></i> + <i class="ti ti-repeat"></i> <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p> </button> <button v-else class="_button" :class="$style.noteFooterButton" disabled> - <i class="ph-prohibit ph-bold ph-lg"></i> + <i class="ti ti-ban"></i> </button> <button v-if="canRenote" @@ -148,23 +149,23 @@ 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="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i> - <i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i> - <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></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> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> - <i class="ph-paperclip ph-bold ph-lg"></i> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()"> + <i class="ti ti-paperclip"></i> </button> - <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()"> - <i class="ph-dots-three ph-bold ph-lg"></i> + <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()"> + <i class="ti ti-dots"></i> </button> </footer> </article> <div :class="$style.tabs"> - <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i> {{ i18n.ts.replies }}</button> - <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ph-rocket-launch ph-bold ph-lg"></i> {{ i18n.ts.renotes }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'quotes' }]" @click="tab = 'quotes'"><i class="ph-quotes ph-bold ph-lg"></i> {{ i18n.ts._notification._types.quote }}</button> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ph-smiley ph-bold ph-lg"></i> {{ i18n.ts.reactions }}</button> </div> @@ -236,7 +237,7 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import { notePage } from '@/filters/note.js'; @@ -249,6 +250,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 { 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'; @@ -264,6 +266,8 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; import { isEnabledUrlPreview } from '@/instance.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { type Keymap } from '@/scripts/hotkey.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -296,14 +300,7 @@ if (noteViewInterruptors.length > 0) { }); } -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 isRenote = Misskey.note.isPureRenote(note.value); const rootEl = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); @@ -314,7 +311,8 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const appearNote = computed(() => getAppearNote(note.value)); +const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); const isDeleted = ref(false); @@ -349,14 +347,31 @@ if ($i) { let renoting = false; +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + const keymap = { - 'r': () => reply(true), - 'e|a|plus': () => react(true), - '(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, - 'esc': blur, - 'm|o': () => showMenu(true), - 's': () => showContent.value !== showContent.value, -}; + 'r': () => reply(), + 'e|a|plus': () => react(), + 'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, + 'm': () => showMenu(), + 'c': () => { + if (!defaultStore.state.showClipButtonInNoteFooter) return; + clip(); + }, + 'o': () => galleryEl.value?.openGallery(), + 'v|enter': () => { + if (appearNote.value.cw != null) { + showContent.value = !showContent.value; + } + }, + 'esc': { + allowRepeat: true, + callback: () => blur(), + }, +} as const satisfies Keymap; provide('react', (reaction: string) => { misskeyApi('notes/reactions/create', { @@ -416,12 +431,14 @@ useTooltip(renoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: renoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); useTooltip(quoteButton, async (showing) => { @@ -435,12 +452,14 @@ useTooltip(quoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: quoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); function boostVisibility() { @@ -465,18 +484,20 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { if (users.length < 1) return; - os.popup(MkReactionsViewerDetails, { + const { dispose } = os.popup(MkReactionsViewerDetails, { showing, reaction: 'â¤ï¸', users, count: appearNote.value.reactionCount, targetElement: reactButton.value!, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); renoting = true; @@ -487,7 +508,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -503,7 +526,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -518,7 +543,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.channel) { @@ -538,7 +563,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -560,7 +587,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -569,20 +598,19 @@ function quote() { } } -function reply(viaKeyboard = false): void { - pleaseLogin(); +function reply(): void { + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); os.post({ reply: appearNote.value, channel: appearNote.value.channel, - animation: !viaKeyboard, }).then(() => { focus(); }); } -function react(viaKeyboard = false): void { - pleaseLogin(); +function react(): void { + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -591,12 +619,14 @@ function react(viaKeyboard = false): void { noteId: appearNote.value.id, override: defaultLike.value, }); - const el = reactButton.value as HTMLElement | null | undefined; + const el = reactButton.value; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); @@ -617,7 +647,7 @@ function react(viaKeyboard = false): void { } function like(): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -629,7 +659,9 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -654,7 +686,9 @@ function undoRenote() : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -687,30 +721,26 @@ function onContextmenu(ev: MouseEvent): void { } } -function showMenu(viaKeyboard = false): void { +function showMenu(): void { const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted }); - os.popupMenu(menu, menuButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } -async function menuVersions(viaKeyboard = false): Promise<void> { +async function menuVersions(): Promise<void> { const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); - os.popupMenu(menu, menuVersionsButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuVersionsButton.value).then(focus).finally(cleanup); } -async function clip() { +async function clip(): Promise<void> { os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus); } -function showRenoteMenu(viaKeyboard = false): void { +function showRenoteMenu(): void { if (!isMyRenote) return; - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); os.popupMenu([{ text: i18n.ts.unrenote, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: () => { misskeyApi('notes/delete', { @@ -718,9 +748,7 @@ function showRenoteMenu(viaKeyboard = false): void { }); isDeleted.value = true; }, - }], renoteTime.value, { - viaKeyboard: viaKeyboard, - }); + }], renoteTime.value); } function focus() { @@ -794,6 +822,28 @@ function animatedMFM() { transition: box-shadow 0.1s ease; overflow: clip; contain: content; + + &: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 { diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index e643590e86a8a11804477764981352e43e2aa547..fd5da0d68784509a5048559fc8869184eef8a499 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -24,13 +24,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTime :time="note.createdAt" colored/> </MkA> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> - <i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span> - <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span> - <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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> </header> </template> diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index a8853a8a5f830e7887015659fa8e7de582d8c417..7ccc2c0320f079a60cee4be1e07147d1b0834bfe 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> - <MkAvatar :class="$style.avatar" :user="user" link preview/> + <MkAvatar :class="$style.avatar" :user="user"/> <div :class="$style.main"> <div :class="$style.header"> <MkUserName :user="user" :nowrap="true"/> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 66d1e51a6c90cbafc463aed12735a38eaa900d62..9caed62ce2447a36d3004331c6ec42d7d466958a 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply"/> </template> <div v-else :class="$style.more"> - <MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA> + <MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA> </div> </div> <div v-else :class="$style.muted" @click="muted = false"> @@ -207,7 +207,9 @@ function react(viaKeyboard = false): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); @@ -238,7 +240,9 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -263,7 +267,9 @@ function undoRenote() : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -291,7 +297,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -307,7 +315,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -342,7 +352,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -364,7 +376,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 5240d646612605360585ab23f3f996b61e020867..15173fbd99704b84602c0631e0d6e3201d5436b2 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -15,7 +15,6 @@ SPDX-License-Identifier: AGPL-3.0-only <template #default="{ items: notes }"> <div :class="[$style.root, { [$style.noGap]: noGap }]"> <MkDateSeparatedList - v-if="defaultStore.state.noteDesign === 'misskey'" ref="notes" v-slot="{ item: note }" :items="notes" @@ -27,34 +26,25 @@ SPDX-License-Identifier: AGPL-3.0-only > <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> </MkDateSeparatedList> - <MkDateSeparatedList - v-else-if="defaultStore.state.noteDesign === 'sharkey'" - ref="notes" - v-slot="{ item: note }" - :items="notes" - :direction="pagination.reversed ? 'up' : 'down'" - :reversed="pagination.reversed" - :noGap="noGap" - :ad="true" - :class="$style.notes" - > - <SkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> - </MkDateSeparatedList> </div> </template> </MkPagination> </template> <script lang="ts" setup> -import { shallowRef, ref } from 'vue'; -import MkNote from '@/components/MkNote.vue'; -import SkNote from '@/components/SkNote.vue'; +import { defineAsyncComponent, shallowRef, ref } from 'vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); + const props = defineProps<{ pagination: Paging; noGap?: boolean; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index f849e94e939616237cad20e7d062f9839834c703..99486761983206e493ec450ad0c3c3edccf452dd 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> <div :class="$style.head"> - <MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/> + <MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> - <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div> + <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="notification.user" :class="$style.icon" :user="notification.user" link preview/> - <img v-else-if="notification.icon" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> + <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=""/> <div :class="[$style.subIcon, { [$style.t_follow]: notification.type === 'follow', @@ -29,18 +29,18 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_pollEnded]: notification.type === 'edited', }]" > <!-- we re-use t_pollEnded for "edited" instead of making an identical style --> - <i v-if="notification.type === 'follow'" class="ph-plus ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'renote'" class="ph-rocket-launch ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'reply'" class="ph-arrow-u-up-left ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'mention'" class="ph-at ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'pollEnded'" class="ph-chart-bar-horizontal ph-bold ph-lg"></i> - <i v-else-if="notification.type === 'achievementEarned'" class="ph-trophy ph-bold ph-lg"></i> + <i v-if="notification.type === 'follow'" class="ti ti-plus"></i> + <i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i> + <i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i> + <i v-else-if="notification.type === 'renote'" class="ti ti-repeat"></i> + <i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i> + <i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> + <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> <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="ph-seal-check ph-bold ph-lg"></i> + <i v-else class="ti ti-badges"></i> </template> <i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i> <!-- notification.reaction ㌠null ã«ãªã‚‹ã“ã¨ã¯ã¾ãšãªã„ãŒã€ã“ã“ã§optional chaining使ã†ã¨ä¸€éƒ¨ãƒ–ラウザã§åˆºã•ã‚‹ã®ã§å¿µã®ç‚º --> @@ -70,14 +70,14 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div> <MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + <i class="ti ti-quote" :class="$style.quote"></i> <Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/> - <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + <i class="ti ti-quote" :class="$style.quote"></i> </MkA> <MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> - <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + <i class="ti ti-quote" :class="$style.quote"></i> <Mfm :text="getNoteSummary(notification.note.renote)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.renote?.user"/> - <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + <i class="ti ti-quote" :class="$style.quote"></i> </MkA> <MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/> @@ -92,9 +92,9 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/> </MkA> <MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + <i class="ti ti-quote" :class="$style.quote"></i> <Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/> - <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + <i class="ti ti-quote" :class="$style.quote"></i> </MkA> <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text"> {{ notification.role.name }} @@ -109,8 +109,8 @@ SPDX-License-Identifier: AGPL-3.0-only <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"> - <MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ph-check ph-bold ph-lg"/> {{ i18n.ts.accept }}</MkButton> - <MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ph-x ph-bold ph-lg"/> {{ i18n.ts.reject }}</MkButton> + <MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton> + <MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton> </div> </template> <span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span> @@ -174,13 +174,13 @@ const props = withDefaults(defineProps<{ const followRequestDone = ref(false); const acceptFollowRequest = () => { - if (props.notification.user == null) return; + if (!('user' in props.notification)) return; followRequestDone.value = true; misskeyApi('following/requests/accept', { userId: props.notification.user.id }); }; const rejectFollowRequest = () => { - if (props.notification.user == null) return; + if (!('user' in props.notification)) return; followRequestDone.value = true; misskeyApi('following/requests/reject', { userId: props.notification.user.id }); }; @@ -353,7 +353,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) margin-right: 4px; position: relative; - &:before { + &::before { position: absolute; transform: rotate(180deg); } diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 68bf1bf3d8f252e43de75e4ec6aaa579d4d07c65..2dd6c21ef6d383256cbb3ff78e6142234d6b0106 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -14,26 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items: notifications }"> - <MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> + <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/> <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> </MkDateSeparatedList> - <MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/> - <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> - </MkDateSeparatedList> </template> </MkPagination> </MkPullToRefresh> </template> <script lang="ts" setup> -import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; +import { defineAsyncComponent, onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; -import MkNote from '@/components/MkNote.vue'; -import SkNote from '@/components/SkNote.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { notificationTypes } from '@/const.js'; @@ -42,6 +36,12 @@ import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import * as Misskey from 'misskey-js'; +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); + const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; }>(); diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue index f6dc00698ccb6bc9343ea85e63f902194cdbf3b5..8559d4b96e72158cd191087a882d2a2e8fef1474 100644 --- a/packages/frontend/src/components/MkPagePreview.vue +++ b/packages/frontend/src/components/MkPagePreview.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1"> +<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj"> <div v-if="page.eyeCatchingImage" class="thumbnail"> <MediaImage :image="page.eyeCatchingImage" @@ -50,12 +50,29 @@ const props = defineProps<{ <style lang="scss" scoped> .vhpxefrj { display: block; + position: relative; &:hover { text-decoration: none; color: var(--accent); } + &:focus-within { + outline: none; + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: var(--radius); + pointer-events: none; + box-shadow: inset 0 0 0 2px var(--focus); + } + } + > .thumbnail { & + article { border-radius: 0 0 var(--radius) var(--radius); diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index d664dc9731ccc5ca78ca9e1cfd1a9c61ca0c1648..8f6109ca04b782b4c5f9e508a1aee772a4bfbde0 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -33,7 +33,7 @@ import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue' 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { useScrollPositionManager } from '@/nirax.js'; import { i18n } from '@/i18n.js'; @@ -68,7 +68,7 @@ const buttonsLeft = computed(() => { if (history.value.length > 1) { buttons.push({ - icon: 'ph-arrow-left ph-bold ph-lg', + icon: 'ti ti-arrow-left', onClick: back, }); } @@ -77,11 +77,11 @@ const buttonsLeft = computed(() => { }); const buttonsRight = computed(() => { const buttons = [{ - icon: 'ph-arrows-clockwise ph-bold ph-lg', + icon: 'ti ti-reload', title: i18n.ts.reload, onClick: reload, }, { - icon: 'ph-eject ph-bold ph-lg', + icon: 'ti ti-player-eject', title: i18n.ts.showInPage, onClick: expand, }]; @@ -113,22 +113,22 @@ provide('forceSpacerMin', true); provide('shouldBackButton', false); const contextmenu = computed(() => ([{ - icon: 'ph-eject ph-bold ph-lg', + icon: 'ti ti-player-eject', text: i18n.ts.showInPage, action: expand, }, { - icon: 'ph-frame-corners ph-bold ph-lg', + icon: 'ti ti-window-maximize', text: i18n.ts.popout, action: popout, }, { - icon: 'ph-arrow-square-out ph-bold ph-lg', + icon: 'ti ti-external-link', text: i18n.ts.openInNewTab, action: () => { window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener'); windowEl.value?.close(); }, }, { - icon: 'ph-link ph-bold ph-lg', + icon: 'ti ti-link', text: i18n.ts.copyLink, action: () => { copyToClipboard(url + windowRouter.getCurrentPath()); diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 9a324849e2e701aee9f98f7a7bf27ee12cdfc5a9..4f2a4d270311f28a45029ea16a900045c8c5aafc 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue'; +import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch, type Ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -199,11 +199,17 @@ watch(error, (n, o) => { emit('status', n); }); +function getActualValue<T>(input: T|Ref<T>|undefined, defaultValue: T) : T { + if (!input) return defaultValue; + if (isRef(input)) return input.value; + return input; +} + 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 : {}; + const params = getActualValue<Paging['params']>(props.pagination.params, {}); await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { ...params, limit: props.pagination.limit ?? 10, @@ -239,8 +245,8 @@ const reload = (): Promise<void> => { 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 : {}; - const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false; + const params = getActualValue<Paging['params']>(props.pagination.params, {}); + const offsetMode = getActualValue(props.pagination.offsetMode, false); await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { ...params, limit: SECOND_FETCH_LIMIT, @@ -304,8 +310,8 @@ const fetchMore = async (): Promise<void> => { 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 : {}; - const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false; + const params = getActualValue<Paging['params']>(props.pagination.params, {}); + const offsetMode = getActualValue(props.pagination.offsetMode, false); await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { ...params, limit: SECOND_FETCH_LIMIT, diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue index 3a133269462a9666f22546f3c9d7b240785d83bb..e749725feaaacd71ad338658f93db7ab0f9f5d51 100644 --- a/packages/frontend/src/components/MkPasswordDialog.vue +++ b/packages/frontend/src/components/MkPasswordDialog.vue @@ -22,16 +22,16 @@ SPDX-License-Identifier: AGPL-3.0-only <form @submit.prevent="done"> <div class="_gaps"> <MkInput ref="passwordInput" v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" required :withPasswordToggle="true"> - <template #prefix><i class="ph-password ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-password"></i></template> </MkInput> <MkInput v-if="$i.twoFactorEnabled" 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="ph-keyhole ph-bold ph-lg"></i><i v-else class="ph-numpad ph-bold ph-lg"></i></template> + <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template> </MkInput> - <MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ph-lock ph-bold ph-lg-open"></i> {{ i18n.ts.continue }}</MkButton> + <MkButton :disabled="(password ?? '') == '' || ($i.twoFactorEnabled && (token ?? '') == '')" type="submit" primary rounded style="margin: 0 auto;"><i class="ti ti-lock-open"></i> {{ i18n.ts.continue }}</MkButton> </div> </form> </MkSpacer> diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 8c0804de049b6ad2731aebd151befcd03ec6c356..393ac4efbac0c10c323b252973dacbb47792c682 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <li v-for="(choice, i) in 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="ph-check ph-bold ph-lg" style="margin-right: 4px; color: var(--accent);"></i></template> + <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> <Mfm :text="choice.text" :plain="true"/> <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span> </span> @@ -36,7 +36,9 @@ 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'; const props = defineProps<{ noteId: string; @@ -62,6 +64,11 @@ const timer = computed(() => i18n.tsx._poll[ const showResult = ref(props.readOnly || isVoted.value); +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${props.noteId}`, +})); + // 期é™ä»˜ãアンケート if (props.poll.expiresAt) { const tick = () => { @@ -78,7 +85,7 @@ if (props.poll.expiresAt) { } const vote = async (id) => { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); if (props.readOnly || closed.value || isVoted.value) return; if (!props.poll.multiple) { diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue index 98fbf253701e9266241b0d59f0746ed1d3988d95..3726ddf822e3923cec478876ae454ad00b3aabc2 100644 --- a/packages/frontend/src/components/MkPollEditor.vue +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="zmdxowus"> <p v-if="choices.length < 2" class="caution"> - <i class="ph-warning ph-bold ph-lg"></i>{{ i18n.ts._poll.noOnlyOneChoice }} + <i class="ti ti-alert-triangle"></i>{{ i18n.ts._poll.noOnlyOneChoice }} </p> <ul> <li v-for="(choice, i) in choices" :key="i"> <MkInput class="input" small :modelValue="choice" :placeholder="i18n.tsx._poll.choiceN({ n: i + 1 })" @update:modelValue="onInput(i, $event)"> </MkInput> <button class="_button" @click="remove(i)"> - <i class="ph-x ph-bold ph-lg"></i> + <i class="ti ti-x"></i> </button> </li> </ul> @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </section> <section v-else-if="expiration === 'after'"> - <MkInput v-model="after" small type="number" class="input"> + <MkInput v-model="after" small type="number" min="1" class="input"> <template #label>{{ i18n.ts._poll.duration }}</template> </MkInput> <MkSelect v-model="unit" small> diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue index 3748f0cc642ce5ff5c18a468a752799cfaa13077..ff29b6619376dcc3d82c5329d7bf4499d94b278a 100644 --- a/packages/frontend/src/components/MkPopupMenu.vue +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed"> - <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/> +<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" :returnFocusTo="returnFocusTo" @click="click" @close="onModalClose" @closed="onModalClosed"> + <MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :returnFocusTo="returnFocusTo" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/> </MkModal> </template> @@ -19,8 +19,8 @@ defineProps<{ items: MenuItem[]; align?: 'center' | string; width?: number; - viaKeyboard?: boolean; src?: any; + returnFocusTo?: HTMLElement | null; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index cfaaeecc342dddf4df5bfc90c9fb1e530e245cd2..2bc607fbb68213309e1833fc7e913345a6d77c1b 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <header :class="$style.header"> <div :class="$style.headerLeft"> - <button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ph-x ph-bold ph-lg"></i></button> + <button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button> <button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu"> <MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/> </button> @@ -21,24 +21,24 @@ SPDX-License-Identifier: AGPL-3.0-only <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"> - <span v-if="visibility === 'public'"><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></span> - <span v-if="visibility === 'home'"><i class="ph-house ph-bold ph-lg"></i></span> - <span v-if="visibility === 'followers'"><i class="ph-lock ph-bold ph-lg"></i></span> - <span v-if="visibility === 'specified'"><i class="ph-envelope ph-bold ph-lg"></i></span> + <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> + <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> <span :class="$style.headerRightButtonText">{{ i18n.ts._visibility[visibility] }}</span> </button> <button v-else class="_button" :class="[$style.headerRightItem, $style.visibility]" disabled> - <span><i class="ph-television ph-bold ph-lg"></i></span> + <span><i class="ti ti-device-tv"></i></span> <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"> - <span v-if="!localOnly"><i class="ph-rocket-launch ph-bold ph-lg"></i></span> - <span v-else><i class="ph-rocket ph-bold ph-lg"></i></span> + <span v-if="!localOnly"><i class="ti ti-rocket"></i></span> + <span v-else><i class="ti ti-rocket-off"></i></span> </button> <button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance"> - <span v-if="reactionAcceptance === 'likeOnly'"><i class="ph-heart ph-bold ph-lg"></i></span> - <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ph-heart ph-bold ph-lg"></i></span> + <span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span> + <span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span> <span v-else><i class="ph-smiley ph-bold ph-lg"></i></span> </button> <button v-click-anime class="_button" :class="$style.submit" :disabled="!canPost" data-cy-open-post-form-submit @click="post"> @@ -46,22 +46,22 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="posted"></template> <template v-else-if="posting"><MkEllipsis/></template> <template v-else>{{ submitText }}</template> - <i style="margin-left: 6px;" :class="posted ? 'ph-check ph-bold ph-lg' : reply ? 'ph-arrow-u-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i> + <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : reply ? 'ti ti-arrow-back-up' : renote ? 'ti ti-quote' : 'ti ti-send'"></i> </div> </button> </div> </header> <MkNoteSimple v-if="reply" :class="$style.targetNote" :hideFiles="true" :note="reply"/> <MkNoteSimple v-if="renote" :class="$style.targetNote" :hideFiles="true" :note="renote"/> - <div v-if="quoteId" :class="$style.withQuote"><i class="ph-quotes ph-bold ph-lg"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ph-x ph-bold ph-lg"></i></button></div> + <div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div> <div v-if="visibility === 'specified'" :class="$style.toSpecified"> <span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span> <div :class="$style.visibleUsers"> <span v-for="u in visibleUsers" :key="u.id" :class="$style.visibleUser"> <MkAcct :user="u"/> - <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" style="padding: 4px 8px;" @click="removeVisibleUser(u)"><i class="ti ti-x"></i></button> </span> - <button class="_buttonPrimary" style="padding: 4px; border-radius: var(--radius-sm);" @click="addVisibleUser"><i class="ph-plus ph-bold ph-lg ti-fw"></i></button> + <button class="_buttonPrimary" style="padding: 4px; border-radius: var(--radius-sm);" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> </div> </div> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> @@ -79,19 +79,19 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <footer :class="$style.footer"> <div :class="$style.footerLeft"> - <button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ph-image-square ph-bold ph-lg-plus"></i></button> - <button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ph-chart-bar-horizontal ph-bold ph-lg"></i></button> - <button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ph-eye-slash ph-bold ph-lg"></i></button> - <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ph-at ph-bold ph-lg"></i></button> - <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ph-hash ph-bold ph-lg"></i></button> - <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ph-plug ph-bold ph-lg"></i></button> - <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button> - <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ph-palette ph-bold ph-lg"></i></button> + <button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button> + <button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button> + <button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button> + <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button> + <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button> + <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> + <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> + <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button> </div> <div :class="$style.footerRight"> - <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ph-eye ph-bold ph-lg"></i></button> + <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> <button v-tooltip="'MFM Cheatsheet'" class="_button" :class="$style.footerButton" @click="MFMWindow"><i class="ph-notebook ph-bold ph-lg"></i></button> - <!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ph-dots-three ph-bold ph-lg"></i></button>--> + <!--<button v-tooltip="i18n.ts.more" class="_button" :class="$style.footerButton" @click="showingOptions = !showingOptions"><i class="ti ti-dots"></i></button>--> </div> </footer> <datalist id="hashtags"> @@ -261,7 +261,7 @@ const canPost = computed((): boolean => { 1 <= files.value.length || poll.value != null || props.renote != null || - (props.reply != null && quoteId.value != null) + quoteId.value != null ) && (textLength.value <= maxTextLength.value) && (!poll.value || poll.value.choices.length >= 2); @@ -369,10 +369,17 @@ function watchForDraft() { watch(files, () => saveDraft(), { deep: true }); watch(visibility, () => saveDraft()); watch(localOnly, () => saveDraft()); + watch(quoteId, () => saveDraft()); + watch(reactionAcceptance, () => saveDraft()); } function MFMWindow() { - os.popup(defineAsyncComponent(() => import('@/components/MkMfmWindow.vue')), {}, {}, 'closed'); + const { dispose } = os.popup( + defineAsyncComponent(() => import('@/components/SkMfmWindow.vue')), + {}, + { + closed: () => dispose(), + }); } function checkMissingMention() { @@ -469,7 +476,7 @@ function setVisibility() { return; } - os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { currentVisibility: visibility.value, isSilenced: $i.isSilenced, localOnly: localOnly.value, @@ -482,7 +489,8 @@ function setVisibility() { defaultStore.set('visibility', visibility.value); } }, - }, 'closed'); + closed: () => dispose(), + }); } async function toggleLocalOnly() { @@ -575,6 +583,7 @@ function clear() { function onKeydown(ev: KeyboardEvent) { if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey) && canPost.value) post(); + if (ev.key === 'Escape') emit('esc'); } @@ -630,8 +639,8 @@ async function onPaste(ev: ClipboardEvent) { return; } - const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, "0"); - const file = new File([paste], `${fileName}.txt`, { type: "text/plain" }); + const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0'); + const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' }); upload(file, `${fileName}.txt`); }); } @@ -707,6 +716,8 @@ function saveDraft() { files: files.value, poll: poll.value, visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined, + quoteId: quoteId.value, + reactionAcceptance: reactionAcceptance.value, }, }; @@ -737,7 +748,9 @@ async function post(ev?: MouseEvent) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -773,11 +786,13 @@ async function post(ev?: MouseEvent) { visibility.value = 'home'; } } - + if (defaultStore.state.warnMissingAltText) { const filesData = toRaw(files.value); - const isMissingAltText = filesData.some(file => !file.comment); + const isMissingAltText = filesData.filter( + file => file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/') + ).some(file => !file.comment); if (isMissingAltText) { const { canceled, result } = await os.actions({ @@ -793,7 +808,7 @@ async function post(ev?: MouseEvent) { }); if (canceled) return; - if (result === 'cancel') return; + if (result === 'cancel') return; } } @@ -928,10 +943,23 @@ async function insertEmoji(ev: MouseEvent) { textAreaReadOnly.value = true; const target = ev.currentTarget ?? ev.target; if (target == null) return; + + // emojiPickerã¯ãƒ€ã‚¤ã‚¢ãƒã‚°ãŒé–‰ã˜ãšã«textareaã¨ã‚„ã‚Šã¨ã‚Šã™ã‚‹ã®ã§ã€ + // focustrapã‚’ã‹ã‘ã¦ã„ã‚‹ã¨insertTextAtCursorãŒåŠ¹ã‹ãªã„ + // ãã®ãŸã‚ã€æŠ•ç¨¿ãƒ•ã‚©ãƒ¼ãƒ ã®ãƒ†ã‚ストã«ç›´æŽ¥æ³¨å…¥ã™ã‚‹ + // See: https://github.com/misskey-dev/misskey/pull/14282 + // https://github.com/misskey-dev/misskey/issues/14274 + + let pos = textareaEl.value?.selectionStart ?? 0; + let posEnd = textareaEl.value?.selectionEnd ?? text.value.length; emojiPicker.show( target as HTMLElement, emoji => { - insertTextAtCursor(textareaEl.value, emoji); + const textBefore = text.value.substring(0, pos); + const textAfter = text.value.substring(posEnd); + text.value = textBefore + emoji + textAfter; + pos += emoji.length; + posEnd += emoji.length; }, () => { textAreaReadOnly.value = false; @@ -1024,6 +1052,8 @@ onMounted(() => { users.forEach(u => pushVisibleUser(u)); }); } + quoteId.value = draft.data.quoteId; + reactionAcceptance.value = draft.data.reactionAcceptance; } } @@ -1031,9 +1061,11 @@ onMounted(() => { if (props.initialNote) { const init = props.initialNote; text.value = init.text ? init.text : ''; - files.value = init.files ?? []; - cw.value = init.cw ?? null; useCw.value = init.cw != null; + cw.value = init.cw ?? null; + visibility.value = init.visibility; + localOnly.value = init.localOnly ?? false; + files.value = init.files ?? []; if (init.poll) { poll.value = { choices: init.poll.choices.map(x => x.text), @@ -1042,9 +1074,13 @@ onMounted(() => { expiredAfter: null, }; } - visibility.value = init.visibility; - localOnly.value = init.localOnly ?? false; + if (init.visibleUserIds) { + misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => { + users.forEach(u => pushVisibleUser(u)); + }); + } quoteId.value = init.renote ? init.renote.id : null; + reactionAcceptance.value = init.reactionAcceptance; } nextTick(() => watchForDraft()); @@ -1117,6 +1153,15 @@ defineExpose({ margin: 12px 12px 12px 6px; vertical-align: bottom; + &:focus-visible { + outline: none; + + .submitInner { + outline: 2px solid var(--fgOnAccent); + outline-offset: -4px; + } + } + &:disabled { opacity: 0.7; } diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 956dad802131cef6dd7bd88544b68a41fb5238fb..a3fb7c691fcf5989ae1dc86951a70e0887d2d2dc 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.file" @click="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="ph-eye-closed ph-bold ph-lg" style="margin: auto;"></i> + <i class="ti ti-eye-exclamation" style="margin: auto;"></i> </div> </div> </template> @@ -108,7 +108,7 @@ async function rename(file) { async function describe(file) { if (mock) return; - os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.comment !== null ? file.comment : '', file: file, }, { @@ -121,7 +121,8 @@ async function describe(file) { file.comment = comment; }); }, - }, 'closed'); + closed: () => dispose(), + }); } async function crop(file: Misskey.entities.DriveFile): Promise<void> { @@ -137,29 +138,29 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { const isImage = file.type.startsWith('image/'); os.popupMenu([{ text: i18n.ts.renameFile, - icon: 'ph-textbox ph-bold ph-lg', + icon: 'ti ti-forms', action: () => { rename(file); }, }, { text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: file.isSensitive ? 'ph-eye-closed ph-bold ph-lg' : 'ph-eye ph-bold ph-lg', + icon: file.isSensitive ? 'ti ti-eye-exclamation' : 'ti ti-eye', action: () => { toggleSensitive(file); }, }, { text: i18n.ts.describeFile, - icon: 'ph-text-indent ph-bold ph-lg', + icon: 'ti ti-text-caption', action: () => { describe(file); }, }, ...isImage ? [{ text: i18n.ts.cropImage, - icon: 'ph-crop ph-bold ph-lg', + icon: 'ti ti-crop', action: () : void => { crop(file); }, }] : [], { type: 'divider', }, { text: i18n.ts.attachCancel, - icon: 'ph-x-circle ph-bold ph-lg', + icon: 'ti ti-circle-x', action: () => { detachMedia(file.id); }, }, { text: i18n.ts.deleteFile, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: () => { detachAndDeleteMedia(file); }, }], ev.currentTarget ?? ev.target).then(() => menuShowing = false); diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index ad990e21db80d3eeb75279a3d145d3ea7aacd434..947c0ee4d065d003b60e1f9a988e2e70510297ee 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()"> +<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()"/> </MkModal> </template> diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue new file mode 100644 index 0000000000000000000000000000000000000000..649dee2fdb7146574b20906c6002b485d0aad4b3 --- /dev/null +++ b/packages/frontend/src/components/MkPreview.vue @@ -0,0 +1,150 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.preview"> + <div> + <MkInput v-model="text"> + <template #label>Text</template> + </MkInput> + <MkSwitch v-model="flag" :class="$style.preview__content1__switch_button"> + <span>Switch is now {{ flag ? 'on' : 'off' }}</span> + </MkSwitch> + <div :class="$style.preview__content1__input"> + <MkRadio v-model="radio" value="misskey">Misskey</MkRadio> + <MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio> + <MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio> + </div> + <div :class="$style.preview__content1__button"> + <MkButton inline>This is</MkButton> + <MkButton inline primary>the button</MkButton> + </div> + </div> + <div :class="$style.preview__content2" style="pointer-events: none;"> + <Mfm :text="mfm"/> + </div> + <div :class="$style.preview__content3"> + <MkButton inline primary @click="openMenu">Open menu</MkButton> + <MkButton inline primary @click="openDialog">Open dialog</MkButton> + <MkButton inline primary @click="openForm">Open form</MkButton> + <MkButton inline primary @click="openDrive">Open drive</MkButton> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +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 { $i } from '@/account.js'; + +const text = ref(''); +const flag = ref(true); +const radio = ref('misskey'); +const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`); + +const openDialog = async () => { + await os.alert({ + type: 'warning', + title: 'Oh my Aichan', + text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }); +}; + +const openForm = async () => { + await os.form('Example form', { + foo: { + type: 'boolean', + default: true, + label: 'This is a boolean property', + }, + bar: { + type: 'number', + default: 300, + label: 'This is a number property', + }, + baz: { + type: 'string', + default: 'Misskey makes you happy.', + label: 'This is a string property', + }, + }); +}; + +const openDrive = async () => { + await os.selectDriveFile(false); +}; + +const selectUser = async () => { + await os.selectUser(); +}; + +const openMenu = async (ev: Event) => { + os.popupMenu([{ + type: 'label', + text: 'Fruits', + }, { + text: 'Create some apples', + action: () => {}, + }, { + text: 'Read some oranges', + action: () => {}, + }, { + text: 'Update some melons', + action: () => {}, + }, { + text: 'Delete some bananas', + danger: true, + action: () => {}, + }], ev.currentTarget ?? ev.target); +}; +</script> + +<style lang="scss" module> +.preview { + padding: 16px; + + &__content1 { + + &__switch_button { + padding: 16px 0 8px 0; + } + + &__input { + padding: 8px 0 8px 0; + + div { + margin: 0 8px 8px 0; + } + } + + &__button { + padding: 4px 0 8px 0; + + button { + margin: 0 8px 8px 0; + } + } + } + + &__content2 { + padding: 8px 0 8px 0; + } + + &__content3 { + padding: 8px 0 8px 0; + + button { + margin: 0 8px 8px 0; + + } + } +} +</style> diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index b1ec440e42cec8a24ae7123f6915f8d9471f3b37..e0d0b561beb08f961fca6b1bb4a15928c6e66a4f 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`"> <div :class="$style.frameContent"> <MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/> - <i v-else class="ph-arrow-line-down ph-bold ph-lg" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i> + <i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i> <div :class="$style.text"> <template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template> <template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template> diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue index 0b4023f2542fda9f589bfdc7c56b109daa231cc6..e02f76a58fec0fcf916cc34959aa5615ce840bed 100644 --- a/packages/frontend/src/components/MkRadio.vue +++ b/packages/frontend/src/components/MkRadio.vue @@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]" :aria-checked="checked" :aria-disabled="disabled" + role="checkbox" @click="toggle" > <input @@ -69,6 +70,11 @@ function toggle(): void { border-color: var(--inputBorderHover) !important; } + &:focus-within { + outline: none; + box-shadow: 0 0 0 2px var(--focus); + } + &.checked { background-color: var(--accentedBg) !important; border-color: var(--accentedBg) !important; @@ -78,7 +84,7 @@ function toggle(): void { > .button { border-color: var(--accent); - &:after { + &::after { background-color: var(--accent); transform: scale(1); opacity: 1; @@ -104,7 +110,7 @@ function toggle(): void { border-radius: var(--radius-full); transition: inherit; - &:after { + &::after { content: ''; display: block; position: absolute; diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index 549438f61b35ec2f880f0331be69047134212e54..705c93f770c3f4e7972e1e037e0ff7e68cc864a5 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -29,6 +29,9 @@ export default defineComponent({ // ãªãœã‹Fragmentã«ãªã‚‹ã“ã¨ãŒã‚ã‚‹ãŸã‚ if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[]; + // vnodeã®ã†ã¡v-if=falseãªã‚‚ã®ã‚’除外ã™ã‚‹(trueã«ãªã‚‹ã‚‚ã®ã¯optionãªã©ä»–typeã«ãªã‚‹) + options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if')); + return () => h('div', { class: 'novjtcto', }, [ @@ -40,6 +43,7 @@ export default defineComponent({ }, options.map(option => h(MkRadio, { key: option.key as string, value: option.props?.value, + disabled: option.props?.disabled, modelValue: value.value, 'onUpdate:modelValue': _v => value.value = _v, }, () => option.children)), diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index 46d76e255165de3bec452c1ecb8fb4ed4fa858fd..244fcdceaeb391604537ab1ef954266d79a00307 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -101,17 +101,19 @@ const steps = computed(() => { } }); -const onMousedown = (ev: MouseEvent | TouchEvent) => { +function onMousedown(ev: MouseEvent | TouchEvent) { ev.preventDefault(); const tooltipShowing = ref(true); - os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { showing: tooltipShowing, text: computed(() => { return props.textConverter(finalValue.value); }), targetElement: thumbEl, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); const style = document.createElement('style'); style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); @@ -152,7 +154,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { window.addEventListener('touchmove', onDrag); window.addEventListener('mouseup', onMouseup, { once: true }); window.addEventListener('touchend', onMouseup, { once: true }); -}; +} </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue index 068a2968dbe7dd5aa63aa15283ea7e01a4fde865..c0cbd8a65d4047c28ed73c5114068ce53c4e2823 100644 --- a/packages/frontend/src/components/MkReactionIcon.vue +++ b/packages/frontend/src/components/MkReactionIcon.vue @@ -24,11 +24,13 @@ const elRef = shallowRef(); if (props.withTooltip) { useTooltip(elRef, (showing) => { - os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), { showing, reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'), targetElement: elRef.value.$el, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } </script> diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 8b5e6efdf3c690135e15dd2830a01a329c75cae5..60118fadd2a4f87c04f01afe2963d0cea949a664 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -81,6 +81,7 @@ function getReactionName(reaction: string): string { } .user { + display: flex; line-height: 24px; padding-top: 4px; white-space: nowrap; diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 2464d21b6a5e5f50d6e51b24f260aaaeb7d6e75a..6506035f8f8f2dbdf3acd41901308840f4697a0c 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -112,12 +112,14 @@ async function menu(ev) { os.popupMenu([{ text: i18n.ts.info, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', action: async () => { - os.popup(MkCustomEmojiDetailedDialog, { + const { dispose } = os.popup(MkCustomEmojiDetailedDialog, { emoji: await misskeyApiGet('emoji', { name: props.reaction.replace(/:/g, '').replace(/@\./, ''), }), + }, { + closed: () => dispose(), }); }, }], ev.currentTarget ?? ev.target); @@ -129,7 +131,9 @@ function anime() { const rect = buttonEl.value.getBoundingClientRect(); const x = rect.left + 16; const y = rect.top + (buttonEl.value.offsetHeight / 2); - os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end'); + const { dispose } = os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, { + end: () => dispose(), + }); } watch(() => props.count, (newCount, oldCount) => { @@ -151,13 +155,15 @@ if (!mock) { const users = reactions.map(x => x.user); - os.popup(XDetails, { + const { dispose } = os.popup(XDetails, { showing, reaction: props.reaction, users, count: props.count, targetElement: buttonEl.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }, 100); } </script> diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue index 5106cdfd6a7d92d19ee2428ff390381f8b99df28..2b59eab9d91c742aec67c6044fd778edd42d3db7 100644 --- a/packages/frontend/src/components/MkRemoteCaution.vue +++ b/packages/frontend/src/components/MkRemoteCaution.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.root"><i class="ph-warning ph-bold ph-lg" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div> +<div :class="$style.root"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a :class="$style.link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div> </template> <script lang="ts" setup> diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index f0343d499bedaee7bae426b9872d6ef3cbec12b6..ce17ae08e0f0bce795991e43dad38eb999171616 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -4,25 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkA v-adaptive-bg :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }"> - <div :class="$style.title"> - <span :class="$style.icon"> - <template v-if="role.iconUrl"> - <img :class="$style.badge" :src="role.iconUrl"/> - </template> - <template v-else> - <i v-if="role.isAdministrator" class="ph-crown ph-bold ph-lg" style="color: var(--accent);"></i> - <i v-else-if="role.isModerator" class="ph-shield ph-bold ph-lg" style="color: var(--accent);"></i> - <i v-else class="ph-user ph-bold ph-lg" style="opacity: 0.7;"></i> +<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }"> + <template v-if="forModeration"> + <i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i> + <i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i> + </template> + + <div v-adaptive-bg class="_panel" :class="$style.body"> + <div :class="$style.bodyTitle"> + <span :class="$style.bodyIcon"> + <template v-if="role.iconUrl"> + <img :class="$style.bodyBadge" :src="role.iconUrl"/> + </template> + <template v-else> + <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i> + <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i> + <i v-else class="ti ti-user" style="opacity: 0.7;"></i> + </template> + </span> + <span :class="$style.bodyName">{{ role.name }}</span> + <template v-if="detailed"> + <span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span> + <span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span> </template> - </span> - <span :class="$style.name">{{ role.name }}</span> - <template v-if="detailed"> - <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span> - <span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span> - </template> + </div> + <div :class="$style.bodyDescription">{{ role.description }}</div> </div> - <div :class="$style.description">{{ role.description }}</div> </MkA> </template> @@ -42,34 +49,44 @@ const props = withDefaults(defineProps<{ <style lang="scss" module> .root { + display: flex; + align-items: center; +} + +.icon { + margin: 0 12px; +} + +.body { display: block; padding: 16px 20px; + flex: 1; border-left: solid 6px var(--color); } -.title { +.bodyTitle { display: flex; } -.icon { +.bodyIcon { margin-right: 8px; } -.badge { +.bodyBadge { height: 1.3em; vertical-align: -20%; } -.name { +.bodyName { font-weight: bold; } -.users { +.bodyUsers { margin-left: auto; opacity: 0.7; } -.description { +.bodyDescription { opacity: 0.7; font-size: 85%; } diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index ecac99ae455b117b19f8fe3f209a82703019cd34..8254ac83cfe6a2be90d50e50dbd655da8764d8a9 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -6,28 +6,37 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <div :class="$style.label" @click="focus"><slot name="label"></slot></div> - <div ref="container" :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused }]" @mousedown.prevent="show"> + <div + ref="container" + tabindex="0" + :class="[$style.input, { [$style.inline]: inline, [$style.disabled]: disabled, [$style.focused]: focused || opening }]" + @focus="focused = true" + @blur="focused = false" + @mousedown.prevent="show" + @keydown.space.enter="show" + > <div ref="prefixEl" :class="$style.prefix"><slot name="prefix"></slot></div> <select ref="inputEl" v-model="v" v-adaptive-border + tabindex="-1" :class="$style.inputCore" :disabled="disabled" :required="required" :readonly="readonly" :placeholder="placeholder" - @focus="focused = true" - @blur="focused = false" @input="onInput" + @mousedown.prevent="() => {}" + @keydown.prevent="() => {}" > <slot></slot> </select> - <div ref="suffixEl" :class="$style.suffix"><i class="ph-caret-down ph-bold ph-lg" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> + <div ref="suffixEl" :class="$style.suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div> </div> <div :class="$style.caption"><slot name="caption"></slot></div> - <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> @@ -75,7 +84,7 @@ const height = props.large ? 39 : 36; -const focus = () => inputEl.value?.focus(); +const focus = () => container.value?.focus(); const onInput = (ev) => { changed.value = true; }; @@ -126,7 +135,9 @@ onMounted(() => { }); function show() { - focused.value = true; + if (opening.value) return; + focus(); + opening.value = true; const menu: MenuItem[] = []; @@ -173,8 +184,6 @@ function show() { onClosing: () => { opening.value = false; }, - }).then(() => { - focused.value = false; }); } </script> @@ -225,6 +234,10 @@ function show() { } } + &:focus { + outline: none; + } + &:hover { > .inputCore { border-color: var(--inputBorderHover) !important; diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 6f7994dccb1a13cfff7c6466f81af78d838f3413..42fa2bf4a78dbe4239b231d0d5a59a37c9f8a105 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -6,17 +6,30 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <form :class="{ signing, totpLogin }" @submit.prevent="onSubmit"> <div class="_gaps_m"> - <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div> + <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div> <MkInfo v-if="message"> {{ message }} </MkInfo> + <div v-if="openOnRemote" class="_gaps_m"> + <div class="_gaps_s"> + <MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)"> + {{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i> + </MkButton> + <button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)"> + {{ i18n.ts.specifyServerHost }} + </button> + </div> + <div :class="$style.orHr"> + <p :class="$style.orMsg">{{ i18n.ts.or }}</p> + </div> + </div> <div v-if="!totpLogin" class="normal-signin _gaps_m"> <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange"> <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> - <template #prefix><i class="ph-lock ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-lock"></i></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> </MkInput> <MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> @@ -28,17 +41,17 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.retry }} </MkButton> </div> - <div v-if="user && user.securityKeys" class="or-hr"> - <p class="or-msg">{{ i18n.ts.or }}</p> + <div v-if="user && user.securityKeys" :class="$style.orHr"> + <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="ph-lock ph-bold ph-lg"></i></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="ph-keyhole ph-bold ph-lg"></i><i v-else class="ph-numpad ph-bold ph-lg"></i></template> + <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template> <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template> </MkInput> <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> @@ -53,6 +66,7 @@ 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 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'; @@ -60,6 +74,7 @@ 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'; @@ -72,28 +87,22 @@ const host = ref(toUnicode(configHost)); const totpLogin = ref(false); const isBackupCode = ref(false); const queryingKey = ref(false); -const credentialRequest = ref<CredentialRequestOptions | null>(null); +let credentialRequest: CredentialRequestOptions | null = null; const emit = defineEmits<{ (ev: 'login', v: any): void; }>(); -const props = defineProps({ - withAvatar: { - type: Boolean, - required: false, - default: true, - }, - autoSet: { - type: Boolean, - required: false, - default: false, - }, - message: { - type: String, - required: false, - default: '', - }, +const props = withDefaults(defineProps<{ + withAvatar?: boolean; + autoSet?: boolean; + message?: string, + openOnRemote?: OpenOnRemoteOptions, +}>(), { + withAvatar: true, + autoSet: false, + message: '', + openOnRemote: undefined, }); function onUsernameChange(): void { @@ -113,14 +122,14 @@ function onLogin(res: any): Promise<void> | void { } async function queryKey(): Promise<void> { - if (credentialRequest.value == null) return; + if (credentialRequest == null) return; queryingKey.value = true; - await webAuthnRequest(credentialRequest.value) + await webAuthnRequest(credentialRequest) .catch(() => { queryingKey.value = false; return Promise.reject(null); }).then(credential => { - credentialRequest.value = null; + credentialRequest = null; queryingKey.value = false; signing.value = true; return misskeyApi('signin', { @@ -151,7 +160,7 @@ function onSubmit(): void { }).then(res => { totpLogin.value = true; signing.value = false; - credentialRequest.value = parseRequestOptionsFromJSON({ + credentialRequest = parseRequestOptionsFromJSON({ publicKey: res, }); }) @@ -218,8 +227,65 @@ function loginFailed(err: any): void { } function resetPassword(): void { - os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { - }, 'closed'); + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { + closed: () => dispose(), + }); +} + +function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void { + switch (options.type) { + case 'web': + case 'lookup': { + let _path: string; + + if (options.type === 'lookup') { + // TODO: v2024.7.0以é™ãŒæµ¸é€ã—ã¦ããŸã‚‰æ£å¼ãªURLã«å¤‰æ›´ã™ã‚‹â–¼ + // _path = `/lookup?uri=${encodeURIComponent(_path)}`; + _path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`; + } else { + _path = options.path; + } + + if (targetHost) { + window.open(`https://${targetHost}${_path}`, '_blank', 'noopener'); + } else { + window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener'); + } + break; + } + case 'share': { + const params = query(options.params); + if (targetHost) { + window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener'); + } else { + window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener'); + } + break; + } + } +} + +async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> { + const { canceled, result: hostTemp } = await os.inputText({ + title: i18n.ts.inputHostName, + placeholder: 'misskey.example.com', + }); + + if (canceled) return; + + let targetHost: string | null = hostTemp; + + // ドメイン部分ã ã‘ã‚’å–り出㙠+ targetHost = extractDomain(targetHost); + if (targetHost == null) { + os.alert({ + type: 'error', + title: i18n.ts.invalidValue, + text: i18n.ts.tryAgain, + }); + return; + } + openRemote(options, targetHost); } </script> @@ -233,4 +299,36 @@ function resetPassword(): void { background-size: cover; border-radius: var(--radius-full); } + +.instanceManualSelectButton { + display: block; + text-align: center; + opacity: .7; + font-size: .8em; + + &:hover { + text-decoration: underline; + } +} + +.orHr { + position: relative; + margin: .4em auto; + width: 100%; + height: 1px; + background: var(--divider); +} + +.orMsg { + position: absolute; + top: -.6em; + display: inline-block; + padding: 0 1em; + background: var(--panel); + font-size: 0.8em; + color: var(--fgOnPanel); + margin: 0; + left: 50%; + transform: translateX(-50%); +} </style> diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 33355bb99eb9c04280837ee409678872e2c9483c..524c62b4d3aaee8c469895dbd8d0e79c29716ea3 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -6,21 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkModalWindow ref="dialog" - :width="370" - :height="400" + :width="400" + :height="430" @close="onClose" @closed="emit('closed')" > <template #header>{{ i18n.ts.login }}</template> <MkSpacer :marginMin="20" :marginMax="28"> - <MkSignin :autoSet="autoSet" :message="message" @login="onLogin"/> + <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/> </MkSpacer> </MkModalWindow> </template> <script lang="ts" setup> import { shallowRef } from 'vue'; +import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import MkSignin from '@/components/MkSignin.vue'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; @@ -28,9 +29,11 @@ import { i18n } from '@/i18n.js'; withDefaults(defineProps<{ autoSet?: boolean; message?: string, + openOnRemote?: OpenOnRemoteOptions, }>(), { autoSet: false, message: '', + openOnRemote: undefined, }); const emit = defineEmits<{ diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index 7d03381a49c14eee64bab9b2d86864a50ae64c67..e673b6d5308a52949f5c379bc73ddfc3d39c664a 100644 --- a/packages/frontend/src/components/MkSignupDialog.form.vue +++ b/packages/frontend/src/components/MkSignupDialog.form.vue @@ -6,60 +6,60 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <div :class="$style.banner"> - <i class="ph-user-list ph-bold ph-lg"></i> + <i class="ti ti-user-edit"></i> </div> <MkSpacer :marginMin="20" :marginMax="32"> <form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit"> <MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required> <template #label>{{ i18n.ts.invitationCode }}</template> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> </MkInput> <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" autocomplete="username" required data-cy-signup-username @update:modelValue="onChangeUsername"> - <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question ph-bold ph-lg"></i></div></template> + <template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> <template #prefix>@</template> <template #suffix>@{{ host }}</template> <template #caption> - <div><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> + <div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div> <span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.error }}</span> - <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> - <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.tooShort }}</span> - <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.tooLong }}</span> + <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span> + <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span> + <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span> </template> </MkInput> <MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail"> - <template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question ph-bold ph-lg"></i></div></template> - <template #prefix><i class="ph-envelope ph-bold ph-lg"></i></template> + <template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template> + <template #prefix><i class="ti ti-mail"></i></template> <template #caption> <span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span> - <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.available }}</span> - <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> - <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> - <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> - <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> - <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> - <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> - <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.unavailable }}</span> - <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.error }}</span> + <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span> + <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span> + <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span> + <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span> + <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span> + <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span> + <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span> + <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span> + <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span> </template> </MkInput> <MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword"> <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ph-lock ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-lock"></i></template> <template #caption> - <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.weakPassword }}</span> - <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.normalPassword }}</span> - <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.strongPassword }}</span> + <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span> + <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span> + <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span> </template> </MkInput> <MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype"> <template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template> - <template #prefix><i class="ph-lock ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-lock"></i></template> <template #caption> - <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ph-check ph-bold ph-lg ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> - <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ph-warning ph-bold ph-lg ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> + <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span> + <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span> </template> </MkInput> <MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason> diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index c2435b308f54f7fd998b4ea3728957d907f89862..251c8054012b75a5c871278485f4df4c1977561f 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> <div :class="$style.banner"> - <i class="ph-check ph-bold ph-lglist"></i> + <i class="ti ti-checklist"></i> </div> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps_m"> @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="availableServerRules" :defaultOpen="true"> <template #label>{{ i18n.ts.serverRules }}</template> - <template #suffix><i v-if="agreeServerRules" class="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template> <ol class="_gaps_s" :class="$style.rules"> <li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="sanitizeHtml(item)"></div></li> @@ -32,10 +32,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true"> <template #label>{{ tosPrivacyPolicyLabel }}</template> - <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template> <div class="_gaps_s"> - <div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a></div> - <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a></div> + <div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div> + <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div> </div> <MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch> @@ -43,9 +43,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template> - <template #suffix><i v-if="agreeNote" class="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template> + <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template> - <a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a> + <a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> <MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch> </MkFolder> @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_buttonsCenter"> <MkButton inline rounded @click="emit('cancel')">{{ i18n.ts.cancel }}</MkButton> - <MkButton inline primary rounded gradate :disabled="!agreed" data-cy-signup-rules-continue @click="emit('done')">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton inline primary rounded gradate :disabled="!agreed" data-cy-signup-rules-continue @click="emit('done')">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </MkSpacer> diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue index 7b936b656c7abd589da05bf0da95c6cc6e708ba1..cd884f0b19c22574c5ac8b2f98addfd4bed5bd13 100644 --- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue +++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton @click="close">{{ i18n.ts.gotIt }}</MkButton> </div> </div> - <button class="_button" :class="$style.close" @click="close"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button> </div> </template> diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 2a7c72ccd95a319b9534ab59358095712927671a..041ae881095e4e90b8d6938e04787cf956c15422 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -10,15 +10,15 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="items"> <template v-for="(item, i) in group.items"> - <a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> + <a v-if="item.type === 'a'" :href="item.href" :target="item.target" class="_button item" :class="{ danger: item.danger, active: item.active }"> <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span> <span class="text">{{ item.text }}</span> </a> - <button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)"> + <button v-else-if="item.type === 'button'" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)"> <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span> <span class="text">{{ item.text }}</span> </button> - <MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }"> + <MkA v-else :to="item.to" class="_button item" :class="{ danger: item.danger, active: item.active }"> <span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span> <span class="text">{{ item.text }}</span> </MkA> @@ -67,6 +67,10 @@ defineProps<{ background: var(--panelHighlight); } + &:focus-visible { + outline-offset: -2px; + } + &.active { color: var(--accent); background: var(--accentedBg); diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index 5672c8e9f733d4b6b10454247448027ac4953450..a0994d9cc91dfca59d0599850e2096cab24d6f94 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -10,16 +10,16 @@ SPDX-License-Identifier: AGPL-3.0-only type="checkbox" :disabled="disabled" :class="$style.input" - @keydown.enter="toggle" + @click="toggle" > - <XButton :checked="checked" :disabled="disabled" @toggle="toggle"/> - <span :class="$style.body"> + <XButton :class="$style.toggle" :checked="checked" :disabled="disabled" @toggle="toggle"/> + <span v-if="!noBody" :class="$style.body"> <!-- TODO: ç„¡åslotã®æ–¹ã¯å»ƒæ¢ --> <span :class="$style.label"> <span @click="toggle"> <slot name="label"></slot><slot></slot> </span> - <span v-if="helpText" v-tooltip:dialog="helpText" class="_button _help" :class="$style.help"><i class="ph-question ph-bold ph-lg"></i></span> + <span v-if="helpText" v-tooltip:dialog="helpText" class="_button _help" :class="$style.help"><i class="ti ti-help-circle"></i></span> </span> <p :class="$style.caption"><slot name="caption"></slot></p> </span> @@ -34,16 +34,19 @@ const props = defineProps<{ modelValue: boolean | Ref<boolean>; disabled?: boolean; helpText?: string; + noBody?: boolean; }>(); const emit = defineEmits<{ (ev: 'update:modelValue', v: boolean): void; + (ev: 'change', v: boolean): void; }>(); const checked = toRefs(props).modelValue; const toggle = () => { if (props.disabled) return; emit('update:modelValue', !checked.value); + emit('change', !checked.value); }; </script> @@ -72,7 +75,13 @@ const toggle = () => { height: 0; opacity: 0; margin: 0; + + &:focus-visible ~ .toggle { + outline: 2px solid var(--focus); + outline-offset: 2px; + } } + .body { margin-left: 12px; margin-top: 2px; diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..69b8edd85aec7f2729b9e8a02f75d6839a0783c1 --- /dev/null +++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { defineAsyncComponent } from 'vue'; +import * as os from '@/os.js'; + +export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved'; + +export type MkSystemWebhookEditorProps = { + mode: 'create' | 'edit'; + id?: string; + requiredEvents?: SystemWebhookEventType[]; +}; + +export type MkSystemWebhookResult = { + id?: string; + isActive: boolean; + name: string; + on: SystemWebhookEventType[]; + url: string; + secret: string; +}; + +export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> { + const { result } = await new Promise<{ result: MkSystemWebhookResult | null }>(async resolve => { + const { dispose } = os.popup( + defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')), + props, + { + submitted: (ev: MkSystemWebhookResult) => { + resolve({ result: ev }); + }, + canceled: () => { + resolve({ result: null }); + }, + closed: () => { + dispose(); + }, + }, + ); + }); + + return result; +} diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue new file mode 100644 index 0000000000000000000000000000000000000000..f5c7a3160bb4d9df1b9dc7f74469d0aef48784d0 --- /dev/null +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -0,0 +1,238 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialogEl" + :width="450" + :height="590" + :canClose="true" + :withOkButton="false" + :okButtonDisabled="false" + @click="onCancelClicked" + @close="onCancelClicked" + @closed="emit('closed')" +> + <template #header> + {{ mode === 'create' ? i18n.ts._webhookSettings.createWebhook : i18n.ts._webhookSettings.modifyWebhook }} + </template> + + <div style="display: flex; flex-direction: column; min-height: 100%;"> + <MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;"> + <MkLoading v-if="loading !== 0"/> + <div v-else :class="$style.root" class="_gaps_m"> + <MkInput v-model="title"> + <template #label>{{ i18n.ts._webhookSettings.name }}</template> + </MkInput> + <MkInput v-model="url"> + <template #label>URL</template> + </MkInput> + <MkInput v-model="secret"> + <template #label>{{ i18n.ts._webhookSettings.secret }}</template> + </MkInput> + <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> + </MkFolder> + + <MkSwitch v-model="isActive"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </div> + </MkSpacer> + <div :class="$style.footer" class="_buttonsCenter"> + <MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked"> + <i class="ti ti-check"></i> + {{ i18n.ts.ok }} + </MkButton> + <MkButton rounded @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton> + </div> + </div> +</MkModalWindow> +</template> + +<script setup lang="ts"> +import { computed, onMounted, ref, shallowRef, toRefs } from 'vue'; +import MkInput from '@/components/MkInput.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import { + MkSystemWebhookEditorProps, + MkSystemWebhookResult, + SystemWebhookEventType, +} from '@/components/MkSystemWebhookEditor.impl.js'; +import { i18n } from '@/i18n.js'; +import MkButton from '@/components/MkButton.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import * as os from '@/os.js'; + +type EventType = { + abuseReport: boolean; + abuseReportResolved: boolean; + userCreated: boolean; +} + +const emit = defineEmits<{ + (ev: 'submitted', result: MkSystemWebhookResult): void; + (ev: 'canceled'): void; + (ev: 'closed'): void; +}>(); + +const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); + +const props = defineProps<MkSystemWebhookEditorProps>(); + +const { mode, id, requiredEvents } = toRefs(props); + +const loading = ref<number>(0); + +const title = ref<string>(''); +const url = ref<string>(''); +const secret = ref<string>(''); +const events = ref<EventType>({ + abuseReport: true, + abuseReportResolved: true, + userCreated: true, +}); +const isActive = ref<boolean>(true); + +const disabledEvents = ref<EventType>({ + abuseReport: false, + abuseReportResolved: false, + userCreated: false, +}); + +const disableSubmitButton = computed(() => { + if (!title.value) { + return true; + } + if (!url.value) { + return true; + } + if (!secret.value) { + return true; + } + + return false; +}); + +async function onSubmitClicked() { + await loadingScope(async () => { + const params = { + isActive: isActive.value, + name: title.value, + url: url.value, + secret: secret.value, + on: Object.keys(events.value).filter(ev => events.value[ev as keyof EventType]) as SystemWebhookEventType[], + }; + + try { + switch (mode.value) { + case 'create': { + const result = await misskeyApi('admin/system-webhook/create', params); + dialogEl.value?.close(); + emit('submitted', result); + break; + } + case 'edit': { + // eslint-disable-next-line + const result = await misskeyApi('admin/system-webhook/update', { id: id.value!, ...params }); + dialogEl.value?.close(); + emit('submitted', result); + break; + } + } + // eslint-disable-next-line + } catch (ex: any) { + const msg = ex.message ?? i18n.ts.internalServerErrorDescription; + await os.alert({ type: 'error', title: i18n.ts.error, text: msg }); + dialogEl.value?.close(); + emit('canceled'); + } + }); +} + +function onCancelClicked() { + dialogEl.value?.close(); + emit('canceled'); +} + +async function loadingScope<T>(fn: () => Promise<T>): Promise<T> { + loading.value++; + try { + return await fn(); + } finally { + loading.value--; + } +} + +onMounted(async () => { + await loadingScope(async () => { + switch (mode.value) { + case 'edit': { + if (!id.value) { + throw new Error('id is required'); + } + + try { + const res = await misskeyApi('admin/system-webhook/show', { id: id.value }); + + title.value = res.name; + url.value = res.url; + secret.value = res.secret; + isActive.value = res.isActive; + for (const ev of Object.keys(events.value)) { + events.value[ev] = res.on.includes(ev as SystemWebhookEventType); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (ex: any) { + const msg = ex.message ?? i18n.ts.internalServerErrorDescription; + await os.alert({ type: 'error', title: i18n.ts.error, text: msg }); + dialogEl.value?.close(); + emit('canceled'); + } + break; + } + } + + for (const ev of requiredEvents.value ?? []) { + disabledEvents.value[ev] = true; + } + }); +}); +</script> + +<style module lang="scss"> +.root { + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; +} + +.footer { + position: sticky; + z-index: 10000; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--divider); + background: var(--acrylicBg); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} +</style> diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue index 7b9fb3d8adc3ced3e375d0a0258ebc5361712753..72d6e1265651f8ce120df132c7760552b4dc1fc5 100644 --- a/packages/frontend/src/components/MkTextarea.vue +++ b/packages/frontend/src/components/MkTextarea.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="v" :isBlock="true" /> </div> - <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 0f7eb3b86cfcd4976eb9abe8b3c8f7171a8d9784..b69c19eb9ee9cf8231159b79acd576ffdb6dec53 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; +import type { BasicTimelineType } from '@/timelines.js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; @@ -29,7 +30,7 @@ import { defaultStore } from '@/store.js'; import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ - src: 'home' | 'local' | 'social' | 'bubble' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; + src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; list?: string; antenna?: string; channel?: string; diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue index 725cfcdc333be83ee4978811a37f6b55141339e7..cec7d699439b46bebcb12ab8f8b4084f6e02059f 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div> <MkNote :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/> <div class="_gaps_s"> - <div><i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <b>{{ i18n.ts.reply }}</b> … {{ i18n.ts._initialTutorial._note.reply }}</div> - <div><i class="ph-rocket-launch ph-bold ph-lg"></i> <b>{{ i18n.ts.renote }}</b> … {{ i18n.ts._initialTutorial._note.renote }}</div> + <div><i class="ti ti-arrow-back-up"></i> <b>{{ i18n.ts.reply }}</b> … {{ i18n.ts._initialTutorial._note.reply }}</div> + <div><i class="ti ti-repeat"></i> <b>{{ i18n.ts.renote }}</b> … {{ i18n.ts._initialTutorial._note.renote }}</div> <div><i class="ph-smiley ph-bold ph-lg"></i> <b>{{ i18n.ts.reaction }}</b> … {{ i18n.ts._initialTutorial._note.reaction }}</div> - <div><i class="ph-dots-three ph-bold ph-lg"></i> <b>{{ i18n.ts.menu }}</b> … {{ i18n.ts._initialTutorial._note.menu }}</div> + <div><i class="ti ti-dots"></i> <b>{{ i18n.ts.menu }}</b> … {{ i18n.ts._initialTutorial._note.menu }}</div> </div> </div> <div v-else-if="phase === 'howToReact'" class="_gaps"> @@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/> <div v-if="onceReacted"> - <b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br> + <b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br> <I18n :src="i18n.ts._initialTutorial._reaction.reactDone"> <template #undo> <i class="ph-minus ph-bold ph-lg"></i> diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue index b0561d4bae519a4451160c73fe1100a6b55ea99c..a9014d420245b494bdf0c2152b70a98b14105456 100644 --- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue +++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue @@ -11,16 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.visibility }}</template> <div class="_gaps"> <div>{{ i18n.ts._initialTutorial._postNote._visibility.description }}</div> - <div><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.public }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div> - <div><i class="ph-house ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.home }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div> - <div><i class="ph-lock ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.followers }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div> + <div><i class="ti ti-world"></i> <b>{{ i18n.ts._visibility.public }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div> + <div><i class="ti ti-home"></i> <b>{{ i18n.ts._visibility.home }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div> + <div><i class="ti ti-lock"></i> <b>{{ i18n.ts._visibility.followers }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div> <div class="_gaps_s"> - <div><i class="ph-envelope ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.specified }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div> + <div><i class="ti ti-mail"></i> <b>{{ i18n.ts._visibility.specified }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div> <MkInfo :warn="true"> <b>{{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect1 }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect2 }} </MkInfo> </div> - <div><i class="ph-rocket ph-bold ph-lg"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div> + <div><i class="ti ti-rocket-off"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div> </div> </MkFormSection> <MkFormSection> @@ -105,7 +105,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({ font-weight: bold; text-align: left; - &:before { + &::before { content: ""; display: block; width: calc(100% - 38px); diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue index f155ad7bcb56a95a6dd2dddd5bbc0999f51b0bee..322082f5a0d9c10efbdd110165245258add4891c 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only :initialNote="exampleNote" @fileChangeSensitive="doSucceeded" ></MkPostForm> - <div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> + <div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> <MkFolder> <template #label>{{ i18n.ts.previewNoteText }}</template> <MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote> @@ -115,7 +115,7 @@ const exampleNote = reactive<Misskey.entities.Note>({ font-weight: bold; text-align: left; - &:before { + &::before { content: ""; display: block; width: calc(100% - 38px); diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue index f5670c7ebdbe836130fa60e8f3678f7430a87e98..b900a30c85d3a783f94dfa3713163a1277cd44a7 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -7,10 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div> <div class="_gaps_s"> - <div><i class="ph-house ph-bold pg-lg"></i> <b>{{ i18n.ts._timelines.home }}</b> … {{ i18n.ts._initialTutorial._timeline.home }}</div> - <div><i class="ph-planet ph-bold pg-lg"></i> <b>{{ i18n.ts._timelines.local }}</b> … {{ i18n.ts._initialTutorial._timeline.local }}</div> - <div><i class="ph-rocket-launch ph-bold ph-lg"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div> - <div><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i> <b>{{ i18n.ts._timelines.global }}</b> … {{ i18n.ts._initialTutorial._timeline.global }}</div> + <div v-for="tl in basicTimelineTypes"> + <i :class="basicTimelineIconClass(tl)"></i> <b>{{ i18n.ts._timelines[tl] }}</b> … {{ i18n.ts._initialTutorial._timeline[tl] }} + </div> </div> <div class="_gaps_s"> <div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div> @@ -22,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only <a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a> </template> </I18n> - </div> </template> <script setup lang="ts"> import { i18n } from '@/i18n.js'; +import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; </script> <style lang="scss" module> @@ -56,7 +55,7 @@ import { i18n } from '@/i18n.js'; font-weight: bold; text-align: left; - &:before { + &::before { content: ""; display: block; width: calc(100% - 38px); diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue index 6cd7019fed0dd89db3eb2bf712a96a43fd291a53..9adc8d466c86f10befa98ce7310df9abee13450b 100644 --- a/packages/frontend/src/components/MkTutorialDialog.vue +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -11,11 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only @close="close(true)" @closed="emit('closed')" > - <template v-if="page === 1" #header><i class="ph-pencil-simple ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._note.title }}</template> - <template v-else-if="page === 2" #header><i class="ph-smiley ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template> - <template v-else-if="page === 3" #header><i class="ph-house ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template> - <template v-else-if="page === 4" #header><i class="ph-plus ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template> - <template v-else-if="page === 5" #header><i class="ph-eye-slash ph-bold pg-lg"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template> + <template v-if="page === 1" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template> + <template v-else-if="page === 2" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template> + <template v-else-if="page === 3" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template> + <template v-else-if="page === 4" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template> + <template v-else-if="page === 5" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template> <template v-else #header>{{ i18n.ts._initialTutorial.title }}</template> <div style="overflow-x: clip;"> @@ -31,10 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ph-confetti ph-bold pg-lg" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div> <div>{{ i18n.ts._initialTutorial._landing.description }}</div> - <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton> + <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton> <MkButton style="margin: 0 auto;" transparent rounded @click="close(true)">{{ i18n.ts.close }}</MkButton> </div> </MkSpacer> @@ -48,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton> + <MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -66,8 +66,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton> + <MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -81,8 +81,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton> + <MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -96,8 +96,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton> + <MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -114,8 +114,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold pg-lg"></i></MkButton> + <MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ph-check ph-bold pg-lg" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div> <I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;"> <template #link> @@ -135,7 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> <div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div> <div class="_buttonsCenter" style="margin-top: 16px;"> - <MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ph-arrow-left ph-bold pg-lg"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> <MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton> </div> </div> @@ -172,7 +172,7 @@ const emit = defineEmits<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -// eslint-disable-next-line vue/no-setup-props-destructure +// eslint-disable-next-line vue/no-setup-props-reactivity-loss const page = ref(props.initialPage ?? 0); watch(page, (to) => { diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 2e069fcdd248ca28daa222a5269d92bca200689d..a51b8785802f69fdb6043f014d44055886a91009 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only scrolling="no" :allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')" :class="$style.playerIframe" - :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" + :src="transformPlayerUrl(player.url)" :style="{ border: 0 }" ></iframe> <span v-else>invalid url</span> </div> <div :class="$style.action"> <MkButton :small="true" inline @click="playerEnabled = false"> - <i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.disablePlayer }} + <i class="ti ti-x"></i> {{ i18n.ts.disablePlayer }} </MkButton> </div> </template> @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.action"> <MkButton :small="true" inline @click="tweetExpanded = false"> - <i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.close }} + <i class="ti ti-x"></i> {{ i18n.ts.close }} </MkButton> </div> </template> @@ -67,15 +67,15 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="showActions"> <div v-if="tweetId" :class="$style.action"> <MkButton :small="true" inline @click="tweetExpanded = true"> - <i class="ph-twitter-logo ph-bold ph-lg"></i> {{ i18n.ts.expandTweet }} + <i class="ti ti-brand-x"></i> {{ i18n.ts.expandTweet }} </MkButton> </div> <div v-if="!playerEnabled && player.url" :class="$style.action"> <MkButton :small="true" inline @click="playerEnabled = true"> - <i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.enablePlayer }} + <i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }} </MkButton> <MkButton v-if="!isMobile" :small="true" inline @click="openPlayer()"> - <i class="ph-picture-in-picture ph-bold ph-lg"></i> {{ i18n.ts.openInWindow }} + <i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }} </MkButton> </div> </template> @@ -91,6 +91,7 @@ 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 { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; type SummalyResult = Awaited<ReturnType<typeof summaly>>; @@ -188,11 +189,13 @@ function adjustTweetHeight(message: any) { if (height) tweetHeight.value = height; } -const openPlayer = (): void => { - os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), { +function openPlayer(): void { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), { url: requestUrl.href, + }, { + // TODO }); -}; +} (window as any).addEventListener('message', adjustTweetHeight); diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index 13ab6fd76351a166fddc85fc5339c21b4d99f883..3c5f563aa000393ba435a94ff81a673a4080dc55 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkTextarea> <MkRadios v-model="icon"> <template #label>{{ i18n.ts.icon }}</template> - <option value="info"><i class="ph-info ph-bold ph-lg"></i></option> - <option value="warning"><i class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i></option> - <option value="error"><i class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i></option> - <option value="success"><i class="ph-check ph-bold ph-lg" style="color: var(--success);"></i></option> + <option value="info"><i class="ti ti-info-circle"></i></option> + <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option> + <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option> + <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option> </MkRadios> <MkRadios v-model="display"> <template #label>{{ i18n.ts.display }}</template> @@ -39,11 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} <template #caption>{{ i18n.ts._announcement.needConfirmationToReadDescription }}</template> </MkSwitch> - <MkButton v-if="announcement" danger @click="del()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </MkSpacer> <div :class="$style.footer"> - <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ph-check ph-bold ph-lg"></i> {{ props.announcement ? i18n.ts.update : i18n.ts.create }}</MkButton> + <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.announcement ? i18n.ts.update : i18n.ts.create }}</MkButton> </div> </div> </MkModalWindow> diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index 5658188c41f899de3faf900e57d41a41d85d7408..e528f04dfc15200d0423206b4ceefbd6b5841564 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_panel" :class="$style.root"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> + <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div> <MkAvatar :class="$style.avatar" :user="user" indicator/> <div :class="$style.title"> <MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only <p :class="$style.statusItemLabel">{{ i18n.ts.followers }}</p><span :class="$style.statusItemValue">{{ number(user.followersCount) }}</span> </div> </div> - <MkFollowButton v-if="$i && user.id != $i.id" :class="$style.follow" :user="user" mini/> + <MkFollowButton v-if="user.id != $i?.id" :class="$style.follow" :user="user" mini/> </div> </template> @@ -41,6 +41,8 @@ import { userPage } from '@/filters/user.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +import { defaultStore } from '@/store.js'; defineProps<{ user: Misskey.entities.UserDetailed; diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 2aee9181145f80b1650df40991f2d31912a5a1b8..c6f4699b3eacce06daf2e4760183bd284b934acc 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }"> <div v-if="user != null"> - <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"> + <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"> <span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span> <span v-if="user.isLocked && $i && $i.id != user.id && !user.isFollowing" :title="i18n.ts.isLocked" :class="$style.locked"><i class="ph-lock ph-bold ph-lg"></i></span> </div> @@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div>{{ number(user.followersCount) }}</div> </div> </div> - <button class="_button" :class="$style.menu" @click="showMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.menu" @click="showMenu"><i class="ti ti-dots"></i></button> <MkFollowButton v-if="$i && user.id != $i.id" v-model:user="user" :class="$style.follow" mini/> </div> <div v-else> @@ -79,6 +79,7 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; +import { getStaticImageUrl } from '@/scripts/media-proxy.js'; const props = defineProps<{ showing: boolean; diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index b76be051d842d45e6b75afbe54033e1ac81e0612..7d210a4385cd49c8a8323e8821791911fb76dbaf 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref } from 'vue'; +import { onMounted, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import MkInput from '@/components/MkInput.vue'; import FormSplit from '@/components/form/split.vue'; @@ -91,7 +91,7 @@ const host = ref(''); const users = ref<Misskey.entities.UserLite[]>([]); const recentUsers = ref<Misskey.entities.UserDetailed[]>([]); const selected = ref<Misskey.entities.UserLite | null>(null); -const dialogEl = ref(); +const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); function search() { if (username.value === '' && host.value === '') { @@ -123,7 +123,7 @@ async function ok() { }); emit('ok', user); - dialogEl.value.close(); + dialogEl.value?.close(); // 最近使ã£ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼æ›´æ–° let recents = defaultStore.state.recentlyUsedUsers; @@ -134,7 +134,7 @@ async function ok() { function cancel() { emit('cancel'); - dialogEl.value.close(); + dialogEl.value?.close(); } onMounted(() => { diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue index 6d2f0bbb995a53dac3e72274b6f6831887ede442..bc998d615883eed4851556f7fb4173286969aa69 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder> <template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template> - <template #icon><i class="ph-lock ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-lock"></i></template> <template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template> <MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch> @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder> <template #label>{{ i18n.ts.hideOnlineStatus }}</template> - <template #icon><i class="ph-eye-slash ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-eye-off"></i></template> <template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template> <MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch> @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder> <template #label>{{ i18n.ts.noCrawle }}</template> - <template #icon><i class="ph-planet ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-world-x"></i></template> <template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template> <MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch> diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue index efb1ed559313164c595914a0f2962cc44cece2b0..c80349d0347de5fea5005de82ce205ecdd176aff 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.User.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue @@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span> </div> <div :class="$style.footer"> - <MkButton v-if="!isFollowing" primary gradate rounded full @click="follow"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.follow }}</MkButton> - <div v-else style="opacity: 0.7; text-align: center;">{{ i18n.ts.youFollowing }} <i class="ph-check ph-bold ph-lg"></i></div> + <MkButton v-if="!isFollowing" primary gradate rounded full @click="follow"><i class="ti ti-plus"></i> {{ i18n.ts.follow }}</MkButton> + <div v-else style="opacity: 0.7; text-align: center;">{{ i18n.ts.youFollowing }} <i class="ti ti-check"></i></div> </div> </div> </template> diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index bd8949890c04241a990743cdfb6c1cdbd39fcb09..514350c930ff3085fea88e9acad00e6669362ac9 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only @close="close(true)" @closed="emit('closed')" > - <template v-if="page === 1" #header><i class="ph-user-list ph-bold ph-lg"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template> - <template v-else-if="page === 2" #header><i class="ph-lock ph-bold ph-lg"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template> - <template v-else-if="page === 3" #header><i class="ph-user-plus ph-bold ph-lg"></i> {{ i18n.ts.follow }}</template> - <template v-else-if="page === 4" #header><i class="ph-bell-ringing ph-bold ph-lg"></i> {{ i18n.ts.pushNotification }}</template> + <template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template> + <template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template> + <template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template> + <template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template> <template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template> <template v-else #header>{{ i18n.ts.initialAccountSetting }}</template> @@ -35,10 +35,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ph-confetti ph-bold ph-lg" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div> <div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div> - <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton> <MkButton style="margin: 0 auto;" transparent rounded @click="later(true)">{{ i18n.ts.later }}</MkButton> </div> </MkSpacer> @@ -52,8 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -81,8 +81,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.pageFooter"> <div class="_buttonsCenter"> - <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </div> @@ -91,13 +91,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.centerPage"> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ph-bell-ringing ph-bold ph-lg" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div> <div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div> <MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/> <div class="_buttonsCenter" style="margin-top: 16px;"> - <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </div> </MkSpacer> @@ -108,14 +108,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/> <MkSpacer :marginMin="20" :marginMax="28"> <div class="_gaps" style="text-align: center;"> - <i class="ph-check ph-bold ph-lg" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> + <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i> <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div> <div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div> <div class="_buttonsCenter" style="margin-top: 16px;"> - <MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton> </div> <div class="_buttonsCenter"> - <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> <MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton> </div> </div> @@ -148,7 +148,7 @@ const emit = defineEmits<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); -// eslint-disable-next-line vue/no-setup-props-destructure +// eslint-disable-next-line vue/no-setup-props-reactivity-loss const page = ref(defaultStore.state.accountSetupWizard); watch(page, () => { @@ -176,9 +176,11 @@ function setupComplete() { function launchTutorial() { setupComplete(); nextTick(() => { - os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), { initialPage: 1, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index bd6edad663231a18e152db5125644747c2ff72f1..3c3f9e94b6a01d3ff523f5e78b60af7c6b225ddd 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -4,34 +4,34 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')"> +<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()"> <div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }"> <div :class="[$style.label, $style.item]"> {{ i18n.ts.visibility }} </div> <button key="public" :disabled="isSilenced || isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')"> - <div :class="$style.icon"><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></div> + <div :class="$style.icon"><i class="ti ti-world"></i></div> <div :class="$style.body"> <span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span> </div> </button> <button key="home" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')"> - <div :class="$style.icon"><i class="ph-house ph-bold ph-lg"></i></div> + <div :class="$style.icon"><i class="ti ti-home"></i></div> <div :class="$style.body"> <span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span> </div> </button> <button key="followers" :disabled="isReplyVisibilitySpecified" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')"> - <div :class="$style.icon"><i class="ph-lock ph-bold ph-lg"></i></div> + <div :class="$style.icon"><i class="ti ti-lock"></i></div> <div :class="$style.body"> <span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span> </div> </button> <button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')"> - <div :class="$style.icon"><i class="ph-envelope ph-bold ph-lg"></i></div> + <div :class="$style.icon"><i class="ti ti-mail"></i></div> <div :class="$style.body"> <span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span> diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index b9024940255d2787c378834941820705b1f71531..6fb33044680fd38c39591fbddbb8da6b10322bf9 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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"/> - <button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button> + <button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button> <div :class="$style.mainFg"> <h1 :class="$style.mainTitle"> <!-- 背景色ã«ã‚ˆã£ã¦ã¯ãƒã‚´ãŒè¦‹ãˆãªããªã‚‹ã®ã§ã¨ã‚Šã‚ãˆãšç„¡åŠ¹ã« --> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_gaps_s" :class="$style.mainActions"> <MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton> - <MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton> + <MkButton :class="$style.mainAction" full rounded link to="https://joinsharkey.org/#findaninstance">{{ i18n.ts.exploreOtherServers }}</MkButton> <MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton> </div> </div> @@ -69,7 +69,8 @@ import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import MkNumber from '@/components/MkNumber.vue'; import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue'; -import { openInstanceMenu } from '@/ui/_common_/common'; +import { openInstanceMenu } from '@/ui/_common_/common.js'; +import type { MenuItem } from '@/types/menu.js'; const stats = ref<Misskey.entities.StatsResponse | null>(null); @@ -78,24 +79,24 @@ misskeyApi('stats', {}).then((res) => { }); function signin() { - os.popup(XSigninDialog, { + const { dispose } = os.popup(XSigninDialog, { autoSet: true, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } function signup() { - os.popup(XSignupDialog, { + const { dispose } = os.popup(XSignupDialog, { autoSet: true, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } -function showMenu(ev) { +function showMenu(ev: MouseEvent) { openInstanceMenu(ev); } - -function exploreOtherServers() { - window.open('https://joinsharkey.org/#findaninstance', '_blank', 'noopener'); -} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue index ad2105cc0bfaf9c0f8bd7d2f383b796c9dc39fb2..60b75b6d3029bad7eed02ea035369878b6f16a49 100644 --- a/packages/frontend/src/components/MkWaitingDialog.vue +++ b/packages/frontend/src/components/MkWaitingDialog.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')"> <div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]"> - <i v-if="success" :class="[$style.icon, $style.success]" class="ph-check ph-bold ph-lg"></i> + <i v-if="success" :class="[$style.icon, $style.success]" class="ti ti-check"></i> <MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/> <div v-if="text && !success" :class="$style.text">{{ text }}<MkEllipsis/></div> </div> diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 05a0f6e04eb0f19125254c1aa8e89834b46fb733..06879f5917b6a1397f6aee7ff76142b040ecb402 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.selectWidget }}</template> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option> </MkSelect> - <MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> + <MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> </header> <Sortable @@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only > <template #item="{element}"> <div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container> - <button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ph-gear ph-bold ph-lg"></i></button> - <button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ph-x ph-bold ph-lg"></i></button> + <button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button> + <button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button> <div class="handle"> <component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/> </div> @@ -120,7 +120,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { type: 'label', text: i18n.ts._widgets[widget.name], }, { - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts.settings, action: () => { configWidget(widget.id); diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index f13b53b005ec6c248bbc4570fe98ccdfaef9e8ad..303e49de00fa731dfc0604628239b80360e75f74 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -27,11 +27,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="!minimized"> <button v-for="button in buttonsRight" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button> </template> - <button v-if="canResize && minimized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMinimize()"><i class="ph-frame-corners ph-bold ph-lg"></i></button> - <button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMinimize" class="_button" :class="$style.headerButton" @click="minimize()"><i class="ph-arrows-in-simple ph-bold ph-lg"></i></button> - <button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ph-picture-in-picture ph-bold ph-lg"></i></button> - <button v-else-if="canResize && !maximized && !minimized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ph-frame-corners ph-bold ph-lg"></i></button> - <button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ph-x ph-bold ph-lg"></i></button> + <button v-if="canResize && minimized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMinimize()"><i class="ti ti-maximize"></i></button> + <button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMinimize" class="_button" :class="$style.headerButton" @click="minimize()"><i class="ti ti-minimize"></i></button> + <button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button> + <button v-else-if="canResize && !maximized && !minimized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button> + <button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button> </span> </div> <div :class="$style.content"> diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue index 3ad2a95bc3c7ca888c8cfdb0dcb3b824409efccf..e3711b3463f15465f516c62349f2a809625c3d87 100644 --- a/packages/frontend/src/components/MkYouTubePlayer.vue +++ b/packages/frontend/src/components/MkYouTubePlayer.vue @@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true"> <template #header> - <i class="icon ph-youtube-logo ph-bold ph-lg" style="margin-right: 0.5em;"></i> + <i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i> <span>{{ title ?? 'YouTube' }}</span> </template> <div class="poamfof"> <Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in"> <div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player"> - <iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/> + <iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe> </div> <span v-else>invalid url</span> </Transition> @@ -27,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import MkWindow from '@/components/MkWindow.vue'; import { versatileLang } from '@/scripts/intl-const.js'; +import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; import { defaultStore } from '@/store.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkFormula.vue b/packages/frontend/src/components/SkFormula.vue similarity index 100% rename from packages/frontend/src/components/MkFormula.vue rename to packages/frontend/src/components/SkFormula.vue diff --git a/packages/frontend/src/components/MkMfmWindow.vue b/packages/frontend/src/components/SkMfmWindow.vue similarity index 100% rename from packages/frontend/src/components/MkMfmWindow.vue rename to packages/frontend/src/components/SkMfmWindow.vue diff --git a/packages/frontend/src/components/MkModPlayer.vue b/packages/frontend/src/components/SkModPlayer.vue similarity index 100% rename from packages/frontend/src/components/MkModPlayer.vue rename to packages/frontend/src/components/SkModPlayer.vue diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index a193df4326b1d3f3bbe81e675d99d3b8d9c061e3..b02d902482582ceec95b83ee93e0efd4acf764fc 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -10,24 +10,24 @@ SPDX-License-Identifier: AGPL-3.0-only ref="rootEl" v-hotkey="keymap" :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" - :tabindex="!isDeleted ? '-1' : undefined" + :tabindex="isDeleted ? '-1' : '0'" > <SkNoteSub v-if="appearNote.reply && !renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> <div v-if="appearNote.reply && inReplyToCollapsed && !renoteCollapsed" :class="$style.collapsedInReplyTo"> <div :class="$style.collapsedInReplyToLine"></div> <MkAvatar :class="$style.collapsedInReplyToAvatar" :user="appearNote.reply.user" link preview/> - <MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkA v-user-preview="appearNote.reply.userId" :class="$style.name" :to="userPage(appearNote.reply.user)"> <MkAcct :user="appearNote.reply.user"/> </MkA>: <Mfm :text="getNoteSummary(appearNote.reply)" :plain="true" :nowrap="true" :author="appearNote.reply.user" :nyaize="'respect'" :class="$style.collapsedInReplyToText" @click="inReplyToCollapsed = false"/> </div> - <div v-if="pinned" :class="$style.tip"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.pinnedNote }}</div> - <!--<div v-if="appearNote._prId_" class="tip"><i class="ph-megaphone ph-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div>--> - <!--<div v-if="appearNote._featuredId_" class="tip"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div>--> + <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> <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> - <i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> <I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText"> <template #user> <MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)"> @@ -36,17 +36,17 @@ SPDX-License-Identifier: AGPL-3.0-only </template> </I18n> <div :class="$style.renoteInfo"> - <button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()"> - <i class="ph-dots-three ph-bold ph-lg" :class="$style.renoteMenu"></i> + <button ref="renoteTime" :class="$style.renoteTime" class="_button" @mousedown.prevent="showRenoteMenu()"> + <i class="ti ti-dots" :class="$style.renoteMenu"></i> <MkTime :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="ph-house ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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="ph-rocket ph-bold ph-lg"></i></span> - <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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> <span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span> </div> </div> @@ -94,7 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> </div> <div v-if="appearNote.files && appearNote.files.length > 0"> - <MkMediaList :mediaList="appearNote.files" @click.stop/> + <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/> <div v-if="isEnabledUrlPreview"> @@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span> </button> </div> - <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> + <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @click.stop @mockUpdateMyReaction="emitUpdReaction"> <template #more> @@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkReactionsViewer> <footer :class="$style.footer"> <button :class="$style.footerButton" class="_button" @click.stop @click="reply()"> - <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> + <i class="ti ti-arrow-back-up"></i> <p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.repliesCount) }}</p> </button> <button @@ -127,13 +127,13 @@ SPDX-License-Identifier: AGPL-3.0-only class="_button" :style="renoted ? 'color: var(--accent) !important;' : ''" @click.stop - @mousedown="renoted ? undoRenote(appearNote) : boostVisibility()" + @mousedown.prevent="renoted ? undoRenote(appearNote) : boostVisibility()" > - <i class="ph-rocket-launch ph-bold ph-lg"></i> + <i class="ti ti-repeat"></i> <p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.renoteCount) }}</p> </button> <button v-else :class="$style.footerButton" class="_button" disabled> - <i class="ph-prohibit ph-bold ph-lg"></i> + <i class="ti ti-ban"></i> </button> <button v-if="canRenote && !props.mock" @@ -149,17 +149,17 @@ 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="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i> - <i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i> - <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></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> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> - <i class="ph-paperclip ph-bold ph-lg"></i> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> + <i class="ti ti-paperclip"></i> </button> - <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()"> - <i class="ph-dots-three ph-bold ph-lg"></i> + <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()"> + <i class="ti ti-dots"></i> </button> </footer> </div> @@ -204,8 +204,7 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkButton from '@/components/MkButton.vue'; -import { pleaseLogin } from '@/scripts/please-login.js'; -import { focusPrev, focusNext } from '@/scripts/focus.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import number from '@/filters/number.js'; @@ -231,7 +230,11 @@ 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 { isEnabledUrlPreview } from '@/instance.js'; +import { type Keymap } from '@/scripts/hotkey.js'; +import { focusPrev, focusNext } from '@/scripts/focus.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -283,14 +286,7 @@ if (noteViewInterruptors.length > 0) { }); } -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 isRenote = Misskey.note.isPureRenote(note.value); const rootEl = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); @@ -301,8 +297,8 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); - +const appearNote = computed(() => getAppearNote(note.value)); +const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); @@ -328,6 +324,11 @@ const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state. const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null); const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false); +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + /* Overload Functionã«LintãŒå¯¾å¿œã—ã¦ã„ãªã„ã®ã§ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; @@ -348,15 +349,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string let renoting = false; const keymap = { - 'r': () => reply(true), - 'e|a|plus': () => react(true), - '(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, - 'up|k|shift+tab': focusBefore, - 'down|j|tab': focusAfter, - 'esc': blur, - 'm|o': () => showMenu(true), - 's': () => showContent.value !== showContent.value, -}; + 'r': () => { + if (renoteCollapsed.value) return; + reply(); + }, + 'e|a|plus': () => { + if (renoteCollapsed.value) return; + react(); + }, + 'q': () => { + if (renoteCollapsed.value) return; + if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); + }, + 'm': () => { + if (renoteCollapsed.value) return; + showMenu(); + }, + 'c': () => { + if (renoteCollapsed.value) return; + if (!defaultStore.state.showClipButtonInNoteFooter) return; + clip(); + }, + 'o': () => { + if (renoteCollapsed.value) return; + galleryEl.value?.openGallery(); + }, + 'v|enter': () => { + if (renoteCollapsed.value) { + renoteCollapsed.value = false; + } else if (appearNote.value.cw != null) { + showContent.value = !showContent.value; + } else if (isLong) { + collapsed.value = !collapsed.value; + } + }, + 'esc': { + allowRepeat: true, + callback: () => blur(), + }, + 'up|k|shift+tab': { + allowRepeat: true, + callback: () => focusBefore(), + }, + 'down|j|tab': { + allowRepeat: true, + callback: () => focusAfter(), + }, +} as const satisfies Keymap; provide('react', (reaction: string) => { misskeyApi('notes/reactions/create', { @@ -389,12 +428,14 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: renoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); useTooltip(quoteButton, async (showing) => { @@ -408,12 +449,14 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: quoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); if ($i) { @@ -438,13 +481,15 @@ if (!props.mock) { if (users.length < 1) return; - os.popup(MkReactionsViewerDetails, { + const { dispose } = os.popup(MkReactionsViewerDetails, { showing, reaction: 'â¤ï¸', users, count: appearNote.value.reactionCount, targetElement: reactButton.value!, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } } @@ -460,7 +505,7 @@ function boostVisibility() { } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); renoting = true; @@ -471,7 +516,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } if (!props.mock) { @@ -489,7 +536,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } if (!props.mock) { @@ -506,7 +555,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (props.mock) { return; @@ -529,7 +578,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -551,7 +602,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -560,22 +613,21 @@ function quote() { } } -function reply(viaKeyboard = false): void { - pleaseLogin(); +function reply(): void { + pleaseLogin(undefined, pleaseLoginContext.value); if (props.mock) { return; } os.post({ reply: appearNote.value, channel: appearNote.value.channel, - animation: !viaKeyboard, }).then(() => { focus(); }); } function like(): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); sound.playMisskeySfx('reaction'); if (props.mock) { @@ -590,12 +642,14 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } function react(viaKeyboard = false): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -613,7 +667,9 @@ function react(viaKeyboard = false): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); @@ -667,7 +723,9 @@ function undoRenote(note) : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -706,15 +764,13 @@ function onContextmenu(ev: MouseEvent): void { } } -function showMenu(viaKeyboard = false): void { +function showMenu(): void { if (props.mock) { return; } const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); - os.popupMenu(menu, menuButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } async function menuVersions(viaKeyboard = false): Promise<void> { @@ -724,7 +780,7 @@ async function menuVersions(viaKeyboard = false): Promise<void> { }).then(focus).finally(cleanup); } -async function clip() { +async function clip(): Promise<void> { if (props.mock) { return; } @@ -732,7 +788,7 @@ async function clip() { os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } -function showRenoteMenu(viaKeyboard = false): void { +function showRenoteMenu(): void { if (props.mock) { return; } @@ -740,7 +796,7 @@ function showRenoteMenu(viaKeyboard = false): void { function getUnrenote(): MenuItem { return { text: i18n.ts.unrenote, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: () => { misskeyApi('notes/delete', { @@ -752,23 +808,19 @@ function showRenoteMenu(viaKeyboard = false): void { } if (isMyRenote) { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); os.popupMenu([ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), - ], renoteTime.value, { - viaKeyboard: viaKeyboard, - }); + ], renoteTime.value); } else { os.popupMenu([ getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), { type: 'divider' }, getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), ($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined, - ], renoteTime.value, { - viaKeyboard: viaKeyboard, - }); + ], renoteTime.value); } } @@ -793,11 +845,11 @@ function blur() { } function focusBefore() { - focusPrev(rootEl.value ?? null); + focusPrev(rootEl.value); } function focusAfter() { - focusNext(rootEl.value ?? null); + focusNext(rootEl.value); } function readPromo() { @@ -835,7 +887,7 @@ function emitUpdReaction(emoji: string, delta: number) { &:focus-visible { outline: none; - &:after { + &::after { content: ""; pointer-events: none; display: block; @@ -848,7 +900,7 @@ function emitUpdReaction(emoji: string, delta: number) { margin: auto; width: calc(100% - 8px); height: calc(100% - 8px); - border: solid 1px var(--focus); + border: solid 2px var(--focus); border-radius: var(--radius); box-sizing: border-box; } diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 7a23d0aa73aee0794135dfe1ed0eb0e23819879a..cca6c7a40c86e15157734e7c6c2089a6191988ae 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -10,13 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only ref="rootEl" v-hotkey="keymap" :class="$style.root" + :tabindex="isDeleted ? '-1' : '0'" > <div v-if="appearNote.reply && appearNote.reply.replyId && !conversationLoaded" style="padding: 16px"> <MkButton style="margin: 0 auto;" primary rounded @click="loadConversation">{{ i18n.ts.loadConversation }}</MkButton> </div> <div v-if="isRenote" :class="$style.renote"> <MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/> - <i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i> + <i class="ti ti-repeat" style="margin-right: 4px;"></i> <span :class="$style.renoteText"> <I18n :src="i18n.ts.renotedBy" tag="span"> <template #user> @@ -27,16 +28,16 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> </span> <div :class="$style.renoteInfo"> - <button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()"> - <i v-if="isMyRenote" class="ph-dots-three ph-bold ph-lg" style="margin-right: 4px;"></i> + <button ref="renoteTime" class="_button" :class="$style.renoteTime" @mousedown.prevent="showRenoteMenu()"> + <i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i> <MkTime :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="ph-house ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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="ph-rocket ph-bold ph-lg"></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> <template v-if="appearNote.reply && appearNote.reply.replyId"> @@ -64,12 +65,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.noteHeaderBody"> <div :class="$style.noteHeaderInfo"> <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> - <i v-if="appearNote.visibility === 'home'" class="ph-house ph-bold ph-lg"></i> - <i v-else-if="appearNote.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span> - <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span> + <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div> <SkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> </div> @@ -105,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton> <MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton> <div v-if="appearNote.files && appearNote.files.length > 0"> - <MkMediaList :mediaList="appearNote.files"/> + <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/> </div> <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/> <div v-if="isEnabledUrlPreview"> @@ -113,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div> </div> - <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> + <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> <div :class="$style.noteFooterInfo"> <div v-if="appearNote.updatedAt"> @@ -126,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/> <footer :class="$style.footer"> <button class="_button" :class="$style.noteFooterButton" @click="reply()"> - <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> + <i class="ti ti-arrow-back-up"></i> <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p> </button> <button @@ -135,13 +136,13 @@ SPDX-License-Identifier: AGPL-3.0-only class="_button" :class="$style.noteFooterButton" :style="renoted ? 'color: var(--accent) !important;' : ''" - @mousedown="renoted ? undoRenote() : boostVisibility()" + @mousedown.prevent="renoted ? undoRenote() : boostVisibility()" > - <i class="ph-rocket-launch ph-bold ph-lg"></i> + <i class="ti ti-repeat"></i> <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p> </button> <button v-else class="_button" :class="$style.noteFooterButton" disabled> - <i class="ph-prohibit ph-bold ph-lg"></i> + <i class="ti ti-ban"></i> </button> <button v-if="canRenote" @@ -156,23 +157,23 @@ 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="ph-heart ph-bold ph-lg" style="color: var(--eventReactionHeart);"></i> - <i v-else-if="appearNote.myReaction != null" class="ph-minus ph-bold ph-lg" style="color: var(--accent);"></i> - <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i> + <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></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> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> </button> - <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> - <i class="ph-paperclip ph-bold ph-lg"></i> + <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()"> + <i class="ti ti-paperclip"></i> </button> - <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()"> - <i class="ph-dots-three ph-bold ph-lg"></i> + <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()"> + <i class="ti ti-dots"></i> </button> </footer> </article> <div :class="$style.tabs"> - <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i> {{ i18n.ts.replies }}</button> - <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ph-rocket-launch ph-bold ph-lg"></i> {{ i18n.ts.renotes }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button> + <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'quotes' }]" @click="tab = 'quotes'"><i class="ph-quotes ph-bold ph-lg"></i> {{ i18n.ts._notification._types.quote }}</button> <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ph-smiley ph-bold ph-lg"></i> {{ i18n.ts.reactions }}</button> </div> @@ -244,7 +245,7 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import SkInstanceTicker from '@/components/SkInstanceTicker.vue'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import number from '@/filters/number.js'; @@ -257,6 +258,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 { 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'; @@ -272,6 +274,8 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; import { isEnabledUrlPreview } from '@/instance.js'; +import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { type Keymap } from '@/scripts/hotkey.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -304,14 +308,7 @@ if (noteViewInterruptors.length > 0) { }); } -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 isRenote = Misskey.note.isPureRenote(note.value); const rootEl = shallowRef<HTMLElement>(); const noteEl = shallowRef<HTMLElement>(); @@ -323,7 +320,8 @@ const reactButton = shallowRef<HTMLElement>(); const quoteButton = shallowRef<HTMLElement>(); const clipButton = shallowRef<HTMLElement>(); const likeButton = shallowRef<HTMLElement>(); -const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const appearNote = computed(() => getAppearNote(note.value)); +const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); const isDeleted = ref(false); @@ -358,14 +356,31 @@ if ($i) { let renoting = false; +const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + const keymap = { - 'r': () => reply(true), - 'e|a|plus': () => react(true), - '(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, - 'esc': blur, - 'm|o': () => showMenu(true), - 's': () => showContent.value !== showContent.value, -}; + 'r': () => reply(), + 'e|a|plus': () => react(), + 'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, + 'm': () => showMenu(), + 'c': () => { + if (!defaultStore.state.showClipButtonInNoteFooter) return; + clip(); + }, + 'o': () => galleryEl.value?.openGallery(), + 'v|enter': () => { + if (appearNote.value.cw != null) { + showContent.value = !showContent.value; + } + }, + 'esc': { + allowRepeat: true, + callback: () => blur(), + }, +} as const satisfies Keymap; provide('react', (reaction: string) => { misskeyApi('notes/reactions/create', { @@ -425,12 +440,14 @@ useTooltip(renoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: renoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); useTooltip(quoteButton, async (showing) => { @@ -444,12 +461,14 @@ useTooltip(quoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: quoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); function boostVisibility() { @@ -474,18 +493,20 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { if (users.length < 1) return; - os.popup(MkReactionsViewerDetails, { + const { dispose } = os.popup(MkReactionsViewerDetails, { showing, reaction: 'â¤ï¸', users, count: appearNote.value.reactionCount, targetElement: reactButton.value!, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); renoting = true; @@ -496,7 +517,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -512,7 +535,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -527,7 +552,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.channel) { @@ -547,7 +572,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -569,7 +596,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -578,20 +607,19 @@ function quote() { } } -function reply(viaKeyboard = false): void { - pleaseLogin(); +function reply(): void { + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); os.post({ reply: appearNote.value, channel: appearNote.value.channel, - animation: !viaKeyboard, }).then(() => { focus(); }); } -function react(viaKeyboard = false): void { - pleaseLogin(); +function react(): void { + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -600,12 +628,14 @@ function react(viaKeyboard = false): void { noteId: appearNote.value.id, override: defaultLike.value, }); - const el = reactButton.value as HTMLElement | null | undefined; + const el = reactButton.value; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); @@ -626,7 +656,7 @@ function react(viaKeyboard = false): void { } function like(): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -638,7 +668,9 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -663,7 +695,9 @@ function undoRenote() : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -696,30 +730,26 @@ function onContextmenu(ev: MouseEvent): void { } } -function showMenu(viaKeyboard = false): void { +function showMenu(): void { const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted }); - os.popupMenu(menu, menuButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } -async function menuVersions(viaKeyboard = false): Promise<void> { +async function menuVersions(): Promise<void> { const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); - os.popupMenu(menu, menuVersionsButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuVersionsButton.value).then(focus).finally(cleanup); } -async function clip() { +async function clip(): Promise<void> { os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus); } -function showRenoteMenu(viaKeyboard = false): void { +function showRenoteMenu(): void { if (!isMyRenote) return; - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); os.popupMenu([{ text: i18n.ts.unrenote, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: () => { misskeyApi('notes/delete', { @@ -727,9 +757,7 @@ function showRenoteMenu(viaKeyboard = false): void { }); isDeleted.value = true; }, - }], renoteTime.value, { - viaKeyboard: viaKeyboard, - }); + }], renoteTime.value); } function focus() { @@ -829,6 +857,28 @@ onUnmounted(() => { transition: box-shadow 0.1s ease; overflow: clip; contain: content; + + &: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 { diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue index 2f177815eefabe1e861fb56544c885762985e76d..45218fafb697c1889329dcff8bf0561aabb3e320 100644 --- a/packages/frontend/src/components/SkNoteHeader.vue +++ b/packages/frontend/src/components/SkNoteHeader.vue @@ -61,13 +61,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTime :time="note.createdAt" colored/> </MkA> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> - <i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i> - <i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i> + <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.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil-simple ph-bold ph-lg"></i></span> - <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span> - <span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></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> </header> </template> diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index dc1d5b10b2f6540cdb25b13da32d93f29b7ed7fd..c2986b252453efa63c9529b46c7d11ed471d15ea 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only <SkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="[$style.reply, { [$style.single]: replies.length === 1 }]" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply" :isReply="props.isReply"/> </template> <div v-else :class="$style.more"> - <MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA> + <MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA> </div> </div> <div v-else :class="$style.muted" @click="muted = false"> @@ -221,7 +221,9 @@ function react(viaKeyboard = false): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); @@ -252,7 +254,9 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -277,7 +281,9 @@ function undoRenote() : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -305,7 +311,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -321,7 +329,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -356,7 +366,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -378,7 +390,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index 8c8a34301013ba26d0c1a31b55da65ca76c7f094..164606e1f9495b59fe5bbb9dd8a82dca71f3fd62 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.text"><slot></slot></span> <span :class="$style.suffix"> <span :class="$style.suffixText"><slot name="suffix"></slot></span> - <i class="ph-arrow-square-out ph-bold ph-lg"></i> + <i class="ti ti-external-link"></i> </span> </a> <a v-else-if="onClick" :class="[$style.main, { [$style.active]: active }]" class="_button" :behavior="behavior" @click="onClick"> @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span :class="$style.text"><slot></slot></span> <span :class="$style.suffix"> <span :class="$style.suffixText"><slot name="suffix"></slot></span> - <i class="ph-caret-right ph-bold ph-lg"></i> + <i class="ti ti-chevron-right"></i> </span> </MkA> </div> diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue index 54566dc13514d460729eff2f9c88ba1ed471f2e7..5226c61d68360afc43615cea4ef4f0dfee98119b 100644 --- a/packages/frontend/src/components/form/suspense.vue +++ b/packages/frontend/src/components/form/suspense.vue @@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else> <div :class="$style.error"> - <div><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</div> - <MkButton inline style="margin-top: 16px;" @click="retry"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.retry }}</MkButton> + <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div> + <MkButton inline style="margin-top: 16px;" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton> </div> </div> </template> diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts index c1d8cf0ca6b2fbcf02b5da9a1c284791e02a8b3b..02e5a7f98c1cba82060e3e10c868e7155006383a 100644 --- a/packages/frontend/src/components/global/MkA.stories.impl.ts +++ b/packages/frontend/src/components/global/MkA.stories.impl.ts @@ -35,12 +35,10 @@ export const Default = { // FIXME: 通るã‘ã©ãã®å¾Œè½ã¡ã‚‹ã®ã§ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆ // await expect(a.href).toMatch(/^https?:\/\/.*#test$/); await userEvent.pointer({ keys: '[MouseRight]', target: a }); - await tick(); const menu = canvas.getByRole('menu'); await expect(menu).toBeInTheDocument(); await userEvent.click(a); a.blur(); - await tick(); await expect(menu).not.toBeInTheDocument(); }, args: { diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 5b67c3f7bdcd0c674c316603e359b2a3080891a2..e0303dbb2734a456a7cf8bff75a58b56fe7bd1aa 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -16,7 +16,7 @@ export type MkABehavior = 'window' | 'browser' | null; <script lang="ts" setup> import { computed, inject, shallowRef } from 'vue'; import * as os from '@/os.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { i18n } from '@/i18n.js'; import { useRouter } from '@/router/supplier.js'; @@ -55,25 +55,25 @@ function onContextmenu(ev) { type: 'label', text: props.to, }, { - icon: 'ph-app-window ph-bold ph-lg', + icon: 'ti ti-app-window', text: i18n.ts.openInWindow, action: () => { os.pageWindow(props.to); }, }, { - icon: 'ph-eject ph-bold ph-lg', + icon: 'ti ti-player-eject', text: i18n.ts.showInPage, action: () => { router.push(props.to, 'forcePage'); }, }, { type: 'divider' }, { - icon: 'ph-arrow-square-out ph-bold ph-lg', + icon: 'ti ti-external-link', text: i18n.ts.openInNewTab, action: () => { window.open(props.to, '_blank', 'noopener'); }, }, { - icon: 'ph-link ph-bold ph-lg', + icon: 'ti ti-link', text: i18n.ts.copyLink, action: () => { copyToClipboard(`${url}${props.to}`); diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue index 8cb082585bc8895a7ce837e78573c8b8aedaf1f9..bbcb070803850d7c734d8b8f907e2e348aa50173 100644 --- a/packages/frontend/src/components/global/MkAcct.vue +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -21,7 +21,7 @@ import { host as hostRaw } from '@/config.js'; import { defaultStore } from '@/store.js'; defineProps<{ - user: Misskey.entities.User; + user: Misskey.entities.UserLite; detail?: boolean; }>(); diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts index aef26ab92d645796100ac1e1c789debf9108086c..8c0b7ef52fc1c9786115739f46445294d907e518 100644 --- a/packages/frontend/src/components/global/MkAd.stories.impl.ts +++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts @@ -9,12 +9,6 @@ import { StoryObj } from '@storybook/vue3'; import MkAd from './MkAd.vue'; import { i18n } from '@/i18n.js'; -let lock: Promise<undefined> | undefined; - -function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - const common = { render(args) { return { @@ -37,56 +31,41 @@ const common = { }; }, async play({ canvasElement, args }) { - if (lock) { - console.warn('This test is unexpectedly running twice in parallel, fix it!'); - console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267'); - await lock; + const canvas = within(canvasElement); + const a = canvas.getByRole<HTMLAnchorElement>('link'); + // FIXME: 通るã‘ã©ãã®å¾Œè½ã¡ã‚‹ã®ã§ã‚³ãƒ¡ãƒ³ãƒˆã‚¢ã‚¦ãƒˆ + // await expect(a.href).toMatch(/^https?:\/\/.*#test$/); + const img = within(a).getByRole('img'); + await expect(img).toBeInTheDocument(); + let buttons = canvas.getAllByRole<HTMLButtonElement>('button'); + await expect(buttons).toHaveLength(1); + const i = buttons[0]; + await expect(i).toBeInTheDocument(); + await userEvent.click(i); + await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back); + await expect(a).not.toBeInTheDocument(); + await expect(i).not.toBeInTheDocument(); + buttons = canvas.getAllByRole<HTMLButtonElement>('button'); + const hasReduceFrequency = args.specify?.ratio !== 0; + await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1); + const reduce = hasReduceFrequency ? buttons[0] : null; + const back = buttons[hasReduceFrequency ? 1 : 0]; + if (reduce) { + await expect(reduce).toBeInTheDocument(); + await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd); } - - let resolve: (value?: any) => void; - lock = new Promise(r => resolve = r); - - try { - // NOTE: sleep ã—ãªã„ã¨ä½•æ•…ã‹è½ã¡ã‚‹ - await sleep(100); - const canvas = within(canvasElement); - const a = canvas.getByRole<HTMLAnchorElement>('link'); - // await expect(a.href).toMatch(/^https?:\/\/.*#test$/); - const img = within(a).getByRole('img'); - await expect(img).toBeInTheDocument(); - let buttons = canvas.getAllByRole<HTMLButtonElement>('button'); - await expect(buttons).toHaveLength(1); - const i = buttons[0]; - await expect(i).toBeInTheDocument(); - await userEvent.click(i); - await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back); - await expect(a).not.toBeInTheDocument(); - await expect(i).not.toBeInTheDocument(); - buttons = canvas.getAllByRole<HTMLButtonElement>('button'); - const hasReduceFrequency = args.specify?.ratio !== 0; - await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1); - const reduce = hasReduceFrequency ? buttons[0] : null; - const back = buttons[hasReduceFrequency ? 1 : 0]; - if (reduce) { - await expect(reduce).toBeInTheDocument(); - await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd); - } - await expect(back).toBeInTheDocument(); - await expect(back).toHaveTextContent(i18n.ts._ad.back); - await userEvent.click(back); - await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy()); - if (reduce) { - await expect(reduce).not.toBeInTheDocument(); - } - await expect(back).not.toBeInTheDocument(); - const aAgain = canvas.getByRole<HTMLAnchorElement>('link'); - await expect(aAgain).toBeInTheDocument(); - const imgAgain = within(aAgain).getByRole('img'); - await expect(imgAgain).toBeInTheDocument(); - } finally { - resolve!(); - lock = undefined; + await expect(back).toBeInTheDocument(); + await expect(back).toHaveTextContent(i18n.ts._ad.back); + await userEvent.click(back); + await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy()); + if (reduce) { + await expect(reduce).not.toBeInTheDocument(); } + await expect(back).not.toBeInTheDocument(); + const aAgain = canvas.getByRole<HTMLAnchorElement>('link'); + await expect(aAgain).toBeInTheDocument(); + const imgAgain = within(aAgain).getByRole('img'); + await expect(imgAgain).toBeInTheDocument(); }, args: { prefer: [], diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index c01211443dd492e1b4a48a3a44fffced6c1a98bd..bee1d9ca4c10037171ea8a9b36b6446a02024bdb 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only }" > <img :src="chosen.imageUrl" :class="$style.img"> - <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ph-info ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button> </component> </div> <div v-else :class="$style.menu"> diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index 8ba4e396b72713ce5a3f16cf19ca67221da45c70..4cacc7b292eb8982e3f44333c10b8ec994eb7c76 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -31,7 +31,7 @@ import { defaultStore } from '@/store.js'; import { customEmojisMap } from '@/custom-emojis.js'; import * as os from '@/os.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +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'; @@ -91,7 +91,7 @@ function onClick(ev: MouseEvent) { text: `:${props.name}:`, }, { text: i18n.ts.copy, - icon: 'ph-copy ph-bold ph-lg', + icon: 'ti ti-copy', action: () => { copyToClipboard(`:${props.name}:`); os.success(); @@ -105,14 +105,14 @@ function onClick(ev: MouseEvent) { }, }] : []), { text: i18n.ts.info, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', action: async () => { - os.popup(MkCustomEmojiDetailedDialog, { + const { dispose } = os.popup(MkCustomEmojiDetailedDialog, { emoji: await misskeyApiGet('emoji', { name: customEmojiName.value, }), }, { - anchor: ev.target, + closed: () => dispose(), }); }, }], ev.currentTarget ?? ev.target); diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index b6b5aaba3284e9195ea4df5ecf0b044dc522a429..c485305376730932656cba26668f30ad21e5a1b4 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -14,7 +14,7 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath, char2tossfaceFilePath } 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; @@ -45,7 +45,7 @@ function onClick(ev: MouseEvent) { text: props.emoji, }, { text: i18n.ts.copy, - icon: 'ph-copy ph-bold ph-lg', + icon: 'ti ti-copy', action: () => { copyToClipboard(props.emoji); os.success(); diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue index 2976cd7be8229f877d2c6780f2445630925def99..64c828ba4a980216a0f73a6bf2e6f05aeeebf6ca 100644 --- a/packages/frontend/src/components/global/MkError.vue +++ b/packages/frontend/src/components/global/MkError.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear> <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> - <p :class="$style.text"><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.somethingHappened }}</p> + <p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p> <MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton> </div> </Transition> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 1bc33585f29e0a16bed791d926fca90f6314e2e7..a3a2b9f319ee7337e011c7b2a50228e67dc266f2 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -68,7 +68,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven 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; + return t.match(/^\-?[0-9.]+s$/) ? t : null; }; const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : props.isAnim ? true : false; @@ -80,7 +80,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const isBlock = props.isBlock ?? false; - const MkFormula = defineAsyncComponent(() => import('@/components/MkFormula.vue')); + const SkFormula = defineAsyncComponent(() => import('@/components/SkFormula.vue')); /** * Gen Vue Elements from MFM AST @@ -360,7 +360,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: var(--radius-ellipse); padding: 4px 10px 4px 6px;', }, [ h('i', { - class: 'ph-clock ph-bold ph-lg', + class: 'ti ti-clock', style: 'margin-right: 0.25em;', }), h(MkTime, { @@ -499,14 +499,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'mathInline': { - return [h('bdi', h(MkFormula, { + return [h('bdi', h(SkFormula, { formula: token.props.formula, block: false, }))]; } case 'mathBlock': { - return [h('bdi', { class: 'block' }, h(MkFormula, { + return [h('bdi', { class: 'block' }, h(SkFormula, { formula: token.props.formula, block: true, }))]; diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts index 25f50516483551f831787b8ca32e75bdc80a0681..1d079edd2c57d2d5dcc83713f2e8c98410aa105f 100644 --- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts +++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts @@ -59,7 +59,7 @@ export const Icon = { tabs: [ { ...OneTab.args.tabs[0], - icon: 'ph-house ph-bold ph-lg', + icon: 'ti ti-home', }, ], }, @@ -86,17 +86,17 @@ export const SomeTabs = { { key: 'princess', title: 'Princess', - icon: 'ph-crown ph-bold ph-lg', + icon: 'ti ti-crown', }, { key: 'fairy', title: 'Fairy', - icon: 'ph-snowflake ph-bold ph-lg', + icon: 'ti ti-snowflake', }, { key: 'angel', title: 'Angel', - icon: 'ph-feather ph-bold ph-lg', + icon: 'ti ti-feather', }, ], }, diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue index 89993e1b8ea716aee5bfe3c5391d5688168f697e..b12dc8cb31ad864b5249a694a5baf1bf00e6d0b9 100644 --- a/packages/frontend/src/components/global/MkStickyContainer.vue +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -8,7 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div ref="headerEl"> <slot name="header"></slot> </div> - <div ref="bodyEl" :data-sticky-container-header-height="headerHeight"> + <div + ref="bodyEl" + :data-sticky-container-header-height="headerHeight" + :data-sticky-container-footer-height="footerHeight" + > <slot></slot> </div> <div ref="footerEl"> diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 23fe99bd9c32dd5118c965e09b14e358b823418a..027b226f3fb0e8249f7965654463d1215be22a74 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -41,12 +41,12 @@ function getDateSafe(n: Date | string | number) { } } -// eslint-disable-next-line vue/no-setup-props-destructure +// 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-destructure +// 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*/); diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 19888ae146f5ccd57b21b11243e4e9b84b8ec0ce..15595ba51576f01e0905f9d286761ddf26b30126 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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="ph-arrow-square-out ph-bold ph-lg"></i> + <i v-if="target === '_blank'" :class="$style.icon" class="ti ti-external-link"></i> </component> </template> @@ -51,11 +51,13 @@ const el = ref(); if (props.showUrlPreview && isEnabledUrlPreview.value) { useTooltip(el, (showing) => { - os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { showing, url: props.url, source: el.value instanceof HTMLElement ? el.value : el.value?.$el, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index 06cb30eff1b1d4e2cf938e478d0de0eba3feb12a..19bd794a5d5d76a350f2ff56c0eaf21707901e9d 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -53,14 +53,14 @@ function resolveNested(current: Resolved, d = 0): Resolved | null { const current = resolveNested(router.current)!; const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); const currentPageProps = ref(current.props); -const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); +const key = ref(router.getCurrentKey() + JSON.stringify(Object.fromEntries(current.props))); function onChange({ resolved, key: newKey }) { const current = resolveNested(resolved); if (current == null || 'redirect' in current.route) return; currentPageComponent.value = current.route.component; currentPageProps.value = current.props; - key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props)); + key.value = newKey + JSON.stringify(Object.fromEntries(current.props)); nextTick(() => { // ページé·ç§»å®Œäº†å¾Œã«å†ã³ã‚ャッシュを有効化 diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue index 7f80f0c4553683fc6ff475b281845a237422167f..8c511a690da44f40697379412af4c7f0bfdd9bd0 100644 --- a/packages/frontend/src/components/page/page.dynamic.vue +++ b/packages/frontend/src/components/page/page.dynamic.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- 動的ページã®ãƒ–ãƒãƒƒã‚¯ã®ä»£æ›¿ã€‚利用ã§ããªã„ã¨ã„ã†ã“ã¨ã‚’表示ã™ã‚‹ --> <template> <div :class="$style.root"> - <div :class="$style.heading"><i class="ph ph-dice-5 ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.dynamic }}</div> + <div :class="$style.heading"><i class="ti ti-dice-5"></i> {{ i18n.ts._pages.blocks.dynamic }}</div> <I18n :src="i18n.ts._pages.blocks.dynamicDescription" tag="div" :class="$style.text"> <template #play> <MkA to="/play" class="_link">Play</MkA> diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 5109c34c02515d1310ef3c7bfd83c7b3ceb7f499..c94c0d440882f10581d3c0365fab3b7ed9596474 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -119,6 +119,7 @@ export const notificationTypes = [ 'roleAssigned', 'achievementEarned', 'app', + 'edited' ] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; @@ -140,6 +141,7 @@ export const ROLE_POLICIES = [ 'canHideAds', 'driveCapacityMb', 'alwaysMarkNsfw', + 'canUpdateBioMedia', 'pinLimit', 'antennaLimit', 'wordMuteLimit', diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts index b082b6edf2d4fd6316cfc8f428b03c2e4bea2fe5..0e5c7ede2404455fdce26c64493775b6a18d9d43 100644 --- a/packages/frontend/src/directives/hotkey.ts +++ b/packages/frontend/src/directives/hotkey.ts @@ -4,7 +4,7 @@ */ import { Directive } from 'vue'; -import { makeHotkey } from '../scripts/hotkey.js'; +import { makeHotkey } from '@/scripts/hotkey.js'; export default { mounted(el, binding) { @@ -13,9 +13,9 @@ export default { el._keyHandler = makeHotkey(binding.value); if (el._hotkey_global) { - document.addEventListener('keydown', el._keyHandler); + document.addEventListener('keydown', el._keyHandler, { passive: false }); } else { - el.addEventListener('keydown', el._keyHandler); + el.addEventListener('keydown', el._keyHandler, { passive: false }); } }, diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts index 2d724f771e402ec7f3b9171818da8ecef23f1dda..a043ff212dec0cf1fefdd6936f4955a3976e6c2d 100644 --- a/packages/frontend/src/directives/ripple.ts +++ b/packages/frontend/src/directives/ripple.ts @@ -17,7 +17,9 @@ export default { const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); }); }, }; diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts index b1c1b19907d6950bfb9339db9bc241a4e68193ed..251ce5675f426e52ced63cdb6b27c0fbca19998b 100644 --- a/packages/frontend/src/directives/tooltip.ts +++ b/packages/frontend/src/directives/tooltip.ts @@ -51,13 +51,15 @@ export default { if (self.text == null) return; const showing = ref(true); - popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { showing, text: self.text, asMfm: binding.modifiers.mfm, direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top', targetElement: el, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); self._close = () => { showing.value = false; diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts index 7a008a44865eb32842f65cc1e6578b254a081783..278d842d09fefd40248d122fd07973b8a87b3fce 100644 --- a/packages/frontend/src/directives/user-preview.ts +++ b/packages/frontend/src/directives/user-preview.ts @@ -35,7 +35,7 @@ export class UserPreview { const showing = ref(true); - popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), { showing, q: this.user, source: this.el, @@ -47,7 +47,8 @@ export class UserPreview { window.clearTimeout(this.showTimer); this.hideTimer = window.setTimeout(this.close, 500); }, - }, 'closed'); + closed: () => dispose(), + }); this.promise = { cancel: () => { diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts index b713d4178954e0b299684b6e2acd56e6054dc7ae..a87766764daed628c39e4b17dff106d8c330ec80 100644 --- a/packages/frontend/src/filters/user.ts +++ b/packages/frontend/src/filters/user.ts @@ -6,7 +6,7 @@ import * as Misskey from 'misskey-js'; import { url } from '@/config.js'; -export const acct = (user: misskey.Acct) => { +export const acct = (user: Misskey.Acct) => { return Misskey.acct.toString(user); }; @@ -14,6 +14,6 @@ export const userName = (user: Misskey.entities.User) => { return user.name || user.username; }; -export const userPage = (user: misskey.Acct, path?, absolute = false) => { +export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => { return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; }; diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts index cc9faddb2098ad4bf4c2495a0e0a30650e244f13..10d6adbcd0ca9d203c748505aa108a8fc401ccc9 100644 --- a/packages/frontend/src/i18n.ts +++ b/packages/frontend/src/i18n.ts @@ -11,6 +11,5 @@ import { I18n } from '@/scripts/i18n.js'; export const i18n = markRaw(new I18n<Locale>(locale)); export function updateI18n(newLocale: Locale) { - // @ts-expect-error -- private field i18n.locale = newLocale; } diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index b71c15d19f15a05d18fdf8601844306b482b3817..ccd7034968ce9a3b7d5cd18094dd229696f8cd81 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -18,7 +18,7 @@ import { unisonReload } from '@/scripts/unison-reload.js'; export const navbarItemDef = reactive({ notifications: { title: i18n.ts.notifications, - icon: 'ph-bell ph-bold ph-lg', + icon: 'ti ti-bell', show: computed(() => $i != null), indicated: computed(() => $i != null && $i.hasUnreadNotification), indicateValue: computed(() => { @@ -34,66 +34,66 @@ export const navbarItemDef = reactive({ }, drive: { title: i18n.ts.drive, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', show: computed(() => $i != null), to: '/my/drive', }, followRequests: { title: i18n.ts.followRequests, - icon: 'ph-user-plus ph-bold ph-lg', + icon: 'ti ti-user-plus', show: computed(() => $i != null && $i.isLocked), indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest), to: '/my/follow-requests', }, explore: { title: i18n.ts.explore, - icon: 'ph-hash ph-bold ph-lg', + icon: 'ti ti-hash', to: '/explore', }, announcements: { title: i18n.ts.announcements, - icon: 'ph-megaphone ph-bold ph-lg', + icon: 'ti ti-speakerphone', indicated: computed(() => $i != null && $i.hasUnreadAnnouncement), to: '/announcements', }, search: { title: i18n.ts.search, - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', to: '/search', }, lookup: { title: i18n.ts.lookup, - icon: 'ph-binoculars ph-bold ph-lg', + icon: 'ti ti-world-search', action: (ev) => { lookup(); }, }, lists: { title: i18n.ts.lists, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', show: computed(() => $i != null), to: '/my/lists', }, antennas: { title: i18n.ts.antennas, - icon: 'ph-flying-saucer ph-bold ph-lg', + icon: 'ti ti-antenna', show: computed(() => $i != null), to: '/my/antennas', }, favorites: { title: i18n.ts.favorites, - icon: 'ph-star ph-bold ph-lg', + icon: 'ti ti-star', show: computed(() => $i != null), to: '/my/favorites', }, pages: { title: i18n.ts.pages, - icon: 'ph-newspaper ph-bold ph-lg', + icon: 'ti ti-news', to: '/pages', }, play: { title: 'Play', - icon: 'ph-play ph-bold ph-lg', + icon: 'ti ti-player-play', to: '/play', }, gallery: { @@ -103,29 +103,29 @@ export const navbarItemDef = reactive({ }, clips: { title: i18n.ts.clip, - icon: 'ph-paperclip ph-bold ph-lg', + icon: 'ti ti-paperclip', show: computed(() => $i != null), to: '/my/clips', }, channels: { title: i18n.ts.channel, - icon: 'ph-television ph-bold ph-lg', + icon: 'ti ti-device-tv', to: '/channels', }, achievements: { title: i18n.ts.achievements, - icon: 'ph-trophy ph-bold ph-lg', + icon: 'ti ti-medal', show: computed(() => $i != null && instance.enableAchievements), to: '/my/achievements', }, games: { title: 'Games', - icon: 'ph-game-controller ph-bold ph-lg', + icon: 'ti ti-device-gamepad', to: '/games', }, ui: { title: i18n.ts.switchUi, - icon: 'ph-devices ph-bold ph-lg', + icon: 'ti ti-devices', action: (ev) => { os.popupMenu([{ text: i18n.ts.default, @@ -153,27 +153,27 @@ export const navbarItemDef = reactive({ }, about: { title: i18n.ts.about, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', action: (ev) => { openInstanceMenu(ev); }, }, reload: { title: i18n.ts.reload, - icon: 'ph-arrows-clockwise ph-bold ph-lg', + icon: 'ti ti-refresh', action: (ev) => { location.reload(); }, }, profile: { title: i18n.ts.profile, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', show: computed(() => $i != null), to: `/@${$i?.username}`, }, cacheClear: { title: i18n.ts.clearCache, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', action: (ev) => { clearCache(); }, diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 6adf2e590b0e2aa51fc45c3ea33b8152ee7f7481..f6f4d62d50f5657452abf76cc9fadaae610c9878 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -5,12 +5,13 @@ // TODO: ãªã‚“ã§ã‚‚ã‹ã‚“ã§ã‚‚os.tsã«çªã£è¾¼ã‚€ã®ã‚„ã‚ãŸã„ã®ã§ã‚ˆã—ãªã«åˆ†å‰²ã™ã‚‹ -import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; +import { Component, markRaw, Ref, ref, defineAsyncComponent, nextTick } from 'vue'; import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/scripts/form.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; +import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; @@ -22,8 +23,11 @@ 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 copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { pleaseLogin } from '@/scripts/please-login.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; +import { focusParent } from '@/scripts/focus.js'; export const openingWindowsCount = ref(0); @@ -123,11 +127,13 @@ export function promiseDialog<T extends Promise<any>>( }); // NOTE: dynamic importã™ã‚‹ã¨æŒ™å‹•ãŒãŠã‹ã—ããªã‚‹(showingã®å¤‰æ›´ãŒä¼æ’ã—ãªã„) - popup(MkWaitingDialog, { + const { dispose } = popup(MkWaitingDialog, { success: success, showing: showing, text: text, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); return promise; } @@ -173,28 +179,24 @@ type EmitsExtractor<T> = { [K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K]; }; -export async function popup<T extends Component>( +export function popup<T extends Component>( component: T, props: ComponentProps<T>, events: ComponentEmit<T> = {} as ComponentEmit<T>, - disposeEvent?: keyof ComponentEmit<T>, -): Promise<{ dispose: () => void }> { +): { dispose: () => void } { markRaw(component); const id = ++popupIdCount; const dispose = () => { // ã“ã®setTimeoutãŒç„¡ã„ã¨æŒ™å‹•ãŒãŠã‹ã—ããªã‚‹(autocompleteãŒé–‰ã˜ãªããªã‚‹)。Vueã®ãƒã‚°ï¼Ÿ window.setTimeout(() => { - popups.value = popups.value.filter(popup => popup.id !== id); + popups.value = popups.value.filter(p => p.id !== id); }, 0); }; const state = { component, props, - events: disposeEvent ? { - ...events, - [disposeEvent]: dispose, - } : events, + events, id, }; @@ -206,16 +208,20 @@ export async function popup<T extends Component>( } export function pageWindow(path: string) { - popup(MkPageWindow, { + const { dispose } = popup(MkPageWindow, { initialPath: path, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } export function toast(message: string, renderMfm = false) { - popup(MkToast, { + const { dispose } = popup(MkToast, { message, renderMfm, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } export function alert(props: { @@ -224,11 +230,12 @@ export function alert(props: { text?: string; }): Promise<void> { return new Promise(resolve => { - popup(MkDialog, props, { + const { dispose } = popup(MkDialog, props, { done: () => { resolve(); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -240,14 +247,15 @@ export function confirm(props: { cancelText?: string; }): Promise<{ canceled: boolean }> { return new Promise(resolve => { - popup(MkDialog, { + const { dispose } = popup(MkDialog, { ...props, showCancelButton: true, }, { done: result => { resolve(result ? result : { canceled: true }); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -269,7 +277,7 @@ export function actions<T extends { canceled: false; result: T[number]['value']; }> { return new Promise(resolve => { - popup(MkDialog, { + const { dispose } = popup(MkDialog, { ...props, actions: props.actions.map(a => ({ text: a.text, @@ -283,7 +291,8 @@ export function actions<T extends { done: result => { resolve(result ? result : { canceled: true }); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -331,7 +340,7 @@ export function inputText(props: { canceled: false; result: string | null; }> { return new Promise(resolve => { - popup(MkDialog, { + const { dispose } = popup(MkDialog, { title: props.title, text: props.text, input: { @@ -346,7 +355,8 @@ export function inputText(props: { done: result => { resolve(result ? result : { canceled: true }); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -385,7 +395,7 @@ export function inputNumber(props: { canceled: false; result: number | null; }> { return new Promise(resolve => { - popup(MkDialog, { + const { dispose } = popup(MkDialog, { title: props.title, text: props.text, input: { @@ -398,7 +408,8 @@ export function inputNumber(props: { done: result => { resolve(result ? result : { canceled: true }); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -413,7 +424,7 @@ export function inputDate(props: { canceled: false; result: Date; }> { return new Promise(resolve => { - popup(MkDialog, { + const { dispose } = popup(MkDialog, { title: props.title, text: props.text, input: { @@ -425,7 +436,8 @@ export function inputDate(props: { done: result => { resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true }); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -435,23 +447,29 @@ export function authenticateDialog(): Promise<{ canceled: false; result: { password: string; token: string | null; }; }> { return new Promise(resolve => { - popup(MkPasswordDialog, {}, { + const { dispose } = popup(MkPasswordDialog, {}, { done: result => { resolve(result ? { canceled: false, result } : { canceled: true, result: undefined }); }, - }, 'closed'); + closed: () => dispose(), + }); }); } +type SelectItem<C> = { + value: C; + text: string; +}; + // default ãŒæŒ‡å®šã•ã‚Œã¦ã„ãŸã‚‰ result 㯠null ã«ãªã‚Šå¾—ãªã„ã“ã¨ã‚’ä¿è¨¼ã™ã‚‹ overload function export function select<C = any>(props: { title?: string; text?: string; default: string; - items: { - value: C; - text: string; - }[]; + items: (SelectItem<C> | { + sectionTitle: string; + items: SelectItem<C>[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -461,10 +479,10 @@ export function select<C = any>(props: { title?: string; text?: string; default?: string | null; - items: { - value: C; - text: string; - }[]; + items: (SelectItem<C> | { + sectionTitle: string; + items: SelectItem<C>[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -474,28 +492,29 @@ export function select<C = any>(props: { title?: string; text?: string; default?: string | null; - items: { - value: C; - text: string; - }[]; + items: (SelectItem<C> | { + sectionTitle: string; + items: SelectItem<C>[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { canceled: false; result: C | null; }> { return new Promise(resolve => { - popup(MkDialog, { + const { dispose } = popup(MkDialog, { title: props.title, text: props.text, select: { - items: props.items, + items: props.items.filter(x => x !== undefined), default: props.default ?? null, }, }, { done: result => { resolve(result ? result : { canceled: true }); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -505,53 +524,57 @@ export function success(): Promise<void> { window.setTimeout(() => { showing.value = false; }, 1000); - popup(MkWaitingDialog, { + const { dispose } = popup(MkWaitingDialog, { success: true, showing: showing, }, { done: () => resolve(), - }, 'closed'); + closed: () => dispose(), + }); }); } export function waiting(): Promise<void> { return new Promise(resolve => { const showing = ref(true); - popup(MkWaitingDialog, { + const { dispose } = popup(MkWaitingDialog, { success: false, showing: showing, }, { done: () => resolve(), - }, 'closed'); + closed: () => dispose(), + }); }); } export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> { return new Promise(resolve => { - popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, { done: result => { resolve(result); }, - }, 'closed'); + closed: () => dispose(), + }); }); } export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> { return new Promise(resolve => { - popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), { includeSelf: opts.includeSelf, localOnly: opts.localOnly, }, { ok: user => { resolve(user); }, - }, 'closed'); + closed: () => dispose(), + }); }); } export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> { return new Promise(resolve => { - popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { type: 'file', multiple, }, { @@ -560,13 +583,14 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti resolve(files); } }, - }, 'closed'); + closed: () => dispose(), + }); }); } export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> { return new Promise(resolve => { - popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { type: 'folder', multiple, }, { @@ -575,20 +599,22 @@ export async function selectDriveFolder(multiple: boolean): Promise<Misskey.enti resolve(folders); } }, - }, 'closed'); + closed: () => dispose(), + }); }); } export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> { return new Promise(resolve => { - popup(MkEmojiPickerDialog, { + const { dispose } = popup(MkEmojiPickerDialog, { src, ...opts, }, { done: emoji => { resolve(emoji); }, - }, 'closed'); + closed: () => dispose(), + }); }); } @@ -597,7 +623,7 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: { uploadFolder?: string | null; }): Promise<Misskey.entities.DriveFile> { return new Promise(resolve => { - popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { file: image, aspectRatio: options.aspectRatio, uploadFolder: options.uploadFolder, @@ -605,73 +631,88 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: { ok: x => { resolve(x); }, - }, 'closed'); + closed: () => dispose(), + }); }); } export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: { align?: string; width?: number; - viaKeyboard?: boolean; onClosing?: () => void; }): Promise<void> { - return new Promise(resolve => { - let dispose; - popup(MkPopupMenu, { + let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(document.activeElement); + return new Promise(resolve => nextTick(() => { + const { dispose } = popup(MkPopupMenu, { items, src, width: options?.width, align: options?.align, - viaKeyboard: options?.viaKeyboard, + returnFocusTo, }, { closed: () => { resolve(); dispose(); + returnFocusTo = null; }, closing: () => { - if (options?.onClosing) options.onClosing(); + options?.onClosing?.(); }, - }).then(res => { - dispose = res.dispose; }); - }); + })); } export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> { + if ( + defaultStore.state.contextMenu === 'native' || + (defaultStore.state.contextMenu === 'appWithShift' && !ev.shiftKey) + ) { + return Promise.resolve(); + } + + let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement); ev.preventDefault(); - return new Promise(resolve => { - let dispose; - popup(MkContextMenu, { + return new Promise(resolve => nextTick(() => { + const { dispose } = popup(MkContextMenu, { items, ev, }, { closed: () => { resolve(); dispose(); + + // MkModalを通ã—ã¦ã„ãªã„ã®ã§ã“ã“ã§ãƒ•ã‚©ãƒ¼ã‚«ã‚¹ã‚’戻ã™å‡¦ç†ã‚’行ㆠ+ if (returnFocusTo != null) { + focusParent(returnFocusTo, true, false); + returnFocusTo = null; + } }, - }).then(res => { - dispose = res.dispose; }); - }); + })); } export function post(props: Record<string, any> = {}): Promise<void> { - showMovedDialog(); + pleaseLogin(undefined, (props.initialText || props.initialNote ? { + type: 'share', + params: { + text: props.initialText ?? props.initialNote.text, + visibility: props.initialVisibility ?? props.initialNote?.visibility, + localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0', + }, + } : undefined)); + showMovedDialog(); return new Promise(resolve => { // NOTE: MkPostFormDialogã‚’dynamic importã™ã‚‹ã¨iOSã§ãƒ†ã‚ストエリアã«è‡ªå‹•ãƒ•ã‚©ãƒ¼ã‚«ã‚¹ã§ããªã„ // NOTE: ãŸã ã€dynamic importã—ãªã„å ´åˆã€MkPostFormDialogインスタンスãŒä½¿ã„ã¾ã‚ã•ã‚Œã€ // VueãŒæ¸¡ã•ã‚ŒãŸã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã«å†…部的ã«__propsã¨ã„ã†ãƒ—ãƒãƒ‘ティを生やã™å½±éŸ¿ã§ã€ // 複数ã®post formã‚’é–‹ã„ãŸã¨ãã«å ´åˆã«ã‚ˆã£ã¦ã¯ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ // ã‚‚ã¡ã‚ん複数ã®post formã‚’é–‹ã‘ã‚‹ã“ã¨è‡ªä½“Misskeyサイドã®ãƒã‚°ãªã®ã ㌠- let dispose; - popup(MkPostFormDialog, props, { + const { dispose } = popup(MkPostFormDialog, props, { closed: () => { resolve(); dispose(); }, - }).then(res => { - dispose = res.dispose; }); }); } diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue index 6b1eff5bcb61f439e64df1f3c4c2e5c49c4199f8..dbc113a7664d2c45a035497a13886df03776944b 100644 --- a/packages/frontend/src/pages/_error_.vue +++ b/packages/frontend/src/pages/_error_.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-show="loaded" :class="$style.root"> <img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/> <div class="_gaps"> - <div><b><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.pageLoadError }}</b></div> + <div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div> <div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div> <div v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</div> <template v-else> @@ -69,7 +69,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.error, - icon: 'ph-warning ph-bold ph-lg', + icon: 'ti ti-alert-triangle', })); </script> diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue index f5a8fcac4233b20f5494b7e91e13cbcae05e8b6d..2d1a3111c24d9485f26ff8583ceb334381489bd3 100644 --- a/packages/frontend/src/pages/about-sharkey.vue +++ b/packages/frontend/src/pages/about-sharkey.vue @@ -33,11 +33,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }} </MkInfo> <FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-code"></i></template> {{ i18n.ts._aboutMisskey.source }} </FormLink> <FormLink v-if="instance.providesTarball" :to="`/tarball/sharkey-${version}.tar.gz`" external> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-download"></i></template> {{ i18n.ts._aboutMisskey.source }} <template #suffix>Tarball</template> </FormLink> @@ -53,17 +53,17 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original_sharkey }}) <template #suffix>GitLab</template> </FormLink> - <FormLink to="https://ko-fi.com/transfem" external> + <FormLink to="https://opencollective.com/sharkey" external> <template #icon><i class="ph-piggy-bank ph-bold ph-lg"></i></template> {{ i18n.ts._aboutMisskey.donate_sharkey }} - <template #suffix>Ko-Fi</template> + <template #suffix>OpenCollective</template> </FormLink> </div> </FormSection> <FormSection> <div class="_gaps_s"> <FormLink to="https://github.com/misskey-dev/misskey" external> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-code"></i></template> {{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original }}) <template #suffix>GitHub</template> </FormLink> @@ -77,18 +77,10 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template> <div :class="$style.contributors" style="margin-bottom: 8px;"> - <a href="https://activitypub.software/Amelia" target="_blank" :class="$style.contributor"> - <img src="https://activitypub.software/uploads/-/system/user/avatar/1/avatar.png?width=128" :class="$style.contributorAvatar"> - <span :class="$style.contributorUsername">@Amelia</span> - </a> <a href="https://activitypub.software/dakkar" target="_blank" :class="$style.contributor"> <img src="https://secure.gravatar.com/avatar/c71b315eed7c63ff94c42b1b3e8dbad1?s=192&d=identicon" :class="$style.contributorAvatar"> <span :class="$style.contributorUsername">@dakkar</span> </a> - <a href="https://activitypub.software/esm" target="_blank" :class="$style.contributor"> - <img src="https://secure.gravatar.com/avatar/00fd054610e2a9dcf97a2aa661b168d0?s=192&d=identicon" :class="$style.contributorAvatar"> - <span :class="$style.contributorUsername">@esm</span> - </a> <a href="https://activitypub.software/supakaity" target="_blank" :class="$style.contributor"> <img src="https://activitypub.software/uploads/-/system/user/avatar/65/avatar.png?width=40" :class="$style.contributorAvatar"> <span :class="$style.contributorUsername">@supakaity</span> @@ -109,6 +101,14 @@ SPDX-License-Identifier: AGPL-3.0-only <img src="https://activitypub.software/uploads/-/system/user/avatar/132/avatar.png?width=128" :class="$style.contributorAvatar"> <span :class="$style.contributorUsername">@tess</span> </a> + <a href="https://activitypub.software/marie" target="_blank" :class="$style.contributor"> + <img src="https://activitypub.software/uploads/-/system/user/avatar/2/avatar.png?width=128" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@Marie</span> + </a> + <a href="https://activitypub.software/luna" target="_blank" :class="$style.contributor"> + <img src="https://secure.gravatar.com/avatar/4faf37df86a3d93a6c19ed6abf8588eade4efb837410dbbc53021b4fd12eaae7?s=80&d=identicon" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@Luna</span> + </a> </div> <template #description><MkLink url="https://activitypub.software/TransFem-org/Sharkey/-/graphs/develop">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template> </FormSection> @@ -119,9 +119,9 @@ SPDX-License-Identifier: AGPL-3.0-only <img src="https://antani.cyou/proxy/avatar.webp?url=https%3A%2F%2Fantani.cyou%2Ffiles%2Fa2944119-024c-4abd-86e5-64bf0d30b26f&avatar=1" :class="$style.contributorAvatar"> <span :class="$style.contributorUsername">@lucent</span> </a> - <a href="https://karilaa.app/@karilaa" target="_blank" :class="$style.contributor"> - <img src="https://karilaa.app/proxy/avatar.webp?url=https%3A%2F%2Fkarilaa.app%2Ffiles%2Fc366e6f9-96d8-4d3b-b996-30e0a7cb3c5a&avatar=1" :class="$style.contributorAvatar"> - <span :class="$style.contributorUsername">@karilaa</span> + <a href="https://plasmatrap.com/@privateger" target="_blank" :class="$style.contributor"> + <img src="https://mediaproxy.plasmatrap.com/?url=https%3A%2F%2Fplasmatrap.com%2Ffiles%2F2cf35a8f-6520-4d4c-9611-bf22ee983293&avatar=1" :class="$style.contributorAvatar"> + <span :class="$style.contributorUsername">@privateger</span> </a> <a href="https://thetransagenda.gay/@phoenix_fairy" target="_blank" :class="$style.contributor"> <img src="https://thetransagenda.gay/proxy/avatar.webp?url=https%3A%2F%2Fs3.us-east-005.backblazeb2.com%2Ftranssharkey%2Fnull%2Fd93ac6dc-2020-4b5a-bce7-84b41e97a0ac.png&avatar=1" :class="$style.contributorAvatar"> diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index f37e9dbf9670d2b95aa928e3c45ab30c68b08366..d7d526f3bad7a6a6f6a9b5135d1d516c3b3360ec 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="query"> <MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> </MkInput> <!-- ãŸãã•ã‚“ã‚ã‚‹ã¨é‚ªé” diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index b8f2fcc8834f276a2f76751e78b04919192061c1..71353c7dfad5aa8edd0db9a55227c46981d3661d 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <div> <MkInput v-model="host" :debounce="true" class=""> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.host }}</template> </MkInput> <FormSplit style="margin-top: var(--margin);"> @@ -74,9 +74,9 @@ const pagination = { sort: sort.value, host: host.value !== '' ? host.value : null, ...( - state.value === 'federating' ? { federating: true } : - state.value === 'subscribing' ? { subscribing: true } : - state.value === 'publishing' ? { publishing: true } : + state.value === 'federating' ? { federating: true, suspended: false, blocked: false } : + state.value === 'subscribing' ? { subscribing: true, suspended: false, blocked: false } : + state.value === 'publishing' ? { publishing: true, suspended: false, blocked: false } : state.value === 'suspended' ? { suspended: true } : state.value === 'blocked' ? { blocked: true } : state.value === 'silenced' ? { silenced: true } : diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue new file mode 100644 index 0000000000000000000000000000000000000000..c378c0a0b8d29802ca4067c3296445b7c8029969 --- /dev/null +++ b/packages/frontend/src/pages/about.overview.vue @@ -0,0 +1,210 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<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"/> + <div :class="$style.bannerName"> + <b>{{ instance.name ?? host }}</b> + </div> + </div> + </div> + + <MkKeyValue> + <template #key>{{ i18n.ts.description }}</template> + <template #value><div v-html="sanitizeHtml(instance.description)"></div></template> + </MkKeyValue> + + <FormSection> + <div class="_gaps_m"> + <MkKeyValue :copy="version"> + <template #key>Sharkey</template> + <template #value>{{ version }}</template> + </MkKeyValue> + <div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })"> + </div> + <FormLink to="/about-sharkey"> + <template #icon><i class="ti ti-info-circle"></i></template> + {{ i18n.ts.aboutMisskey }} + </FormLink> + <FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/sharkey-${version}.tar.gz`" external> + <template #icon><i class="ti ti-code"></i></template> + {{ i18n.ts.sourceCode }} + </FormLink> + <MkInfo v-else warn> + {{ i18n.ts.sourceCodeIsNotYetProvided }} + </MkInfo> + </div> + </FormSection> + + <FormSection> + <div class="_gaps_m"> + <FormSplit> + <MkKeyValue :copy="instance.maintainerName"> + <template #key>{{ i18n.ts.administrator }}</template> + <template #value> + <template v-if="instance.maintainerName">{{ instance.maintainerName }}</template> + <span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span> + </template> + </MkKeyValue> + <MkKeyValue :copy="instance.maintainerEmail"> + <template #key>{{ i18n.ts.contact }}</template> + <template #value> + <template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template> + <span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span> + </template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.inquiry }}</template> + <template #value> + <MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink> + <span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span> + </template> + </MkKeyValue> + </FormSplit> + <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external> + <template #icon><i class="ti ti-user-shield"></i></template> + {{ i18n.ts.impressum }} + </FormLink> + <div class="_gaps_s"> + <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external> + <template #icon><i class="ti ti-user-shield"></i></template> + <template #default>{{ i18n.ts.impressum }}</template> + </FormLink> + <MkFolder v-if="instance.serverRules.length > 0"> + <template #icon><i class="ti ti-checkup-list"></i></template> + <template #label>{{ i18n.ts.serverRules }}</template> + <ol class="_gaps_s" :class="$style.rules"> + <li v-for="item in instance.serverRules" :key="item" :class="$style.rule"> + <div :class="$style.ruleText" v-html="sanitizeHtml(item)"></div> + </li> + </ol> + </MkFolder> + <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external> + <template #icon><i class="ti ti-license"></i></template> + <template #default>{{ i18n.ts.termsOfService }}</template> + </FormLink> + <FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external> + <template #icon><i class="ti ti-shield-lock"></i></template> + <template #default>{{ i18n.ts.privacyPolicy }}</template> + </FormLink> + <FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external> + <template #icon><i class="ti ti-message"></i></template> + <template #default>{{ i18n.ts.feedback }}</template> + </FormLink> + </div> + </div> + </FormSection> + + <FormSuspense v-slot="{ result: stats }" :p="initStats"> + <FormSection> + <template #label>{{ i18n.ts.statistics }}</template> + <FormSplit> + <MkKeyValue> + <template #key>{{ i18n.ts.users }}</template> + <template #value>{{ number(stats.originalUsersCount) }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.notes }}</template> + <template #value>{{ number(stats.originalNotesCount) }}</template> + </MkKeyValue> + </FormSplit> + </FormSection> + </FormSuspense> + + <FormSection> + <template #label>Well-known resources</template> + <div class="_gaps_s"> + <FormLink to="/.well-known/host-meta" external>host-meta</FormLink> + <FormLink to="/.well-known/host-meta.json" external>host-meta.json</FormLink> + <FormLink to="/.well-known/nodeinfo" external>nodeinfo</FormLink> + <FormLink to="/robots.txt" external>robots.txt</FormLink> + <FormLink to="/manifest.json" external>manifest.json</FormLink> + </div> + </FormSection> +</div> +</template> + +<script lang="ts" setup> +import sanitizeHtml from '@/scripts/sanitize-html.js'; +import { host, version } from '@/config.js'; +import { i18n } from '@/i18n.js'; +import { instance } from '@/instance.js'; +import number from '@/filters/number.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import FormLink from '@/components/form/link.vue'; +import FormSection from '@/components/form/section.vue'; +import FormSplit from '@/components/form/split.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import MkLink from '@/components/MkLink.vue'; + +const initStats = () => misskeyApi('stats', {}); +</script> + +<style lang="scss" module> +.banner { + text-align: center; + border-radius: var(--radius); + overflow: clip; + background-color: var(--panel); + background-size: cover; + background-position: center center; +} + +.bannerIcon { + display: block; + margin: 16px auto 0 auto; + height: 64px; + border-radius: var(--radius-sm);; +} + +.bannerName { + display: block; + padding: 16px; + color: #fff; + text-shadow: 0 0 8px #000; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); +} + +.rules { + counter-reset: item; + list-style: none; + padding: 0; + margin: 0; +} + +.rule { + display: flex; + gap: 8px; + word-break: break-word; + + &::before { + flex-shrink: 0; + display: flex; + position: sticky; + top: calc(var(--stickyTop, 0px) + 8px); + counter-increment: item; + content: counter(item); + width: 32px; + height: 32px; + line-height: 32px; + background-color: var(--accentedBg); + color: var(--accent); + font-size: 13px; + font-weight: bold; + align-items: center; + justify-content: center; + border-radius: var(--radius-ellipse); + } +} + +.ruleText { + padding-top: 6px; +} +</style> diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index 23960d39d9aaa8f6ba6ef76be0f540e3693c5a15..f35cbe8d5a4fd0f46e5b7aa360159f233257f5f3 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -8,113 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20"> - <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"/> - <div :class="$style.bannerName"> - <b>{{ instance.name ?? host }}</b> - </div> - </div> - </div> - - <MkKeyValue> - <template #key>{{ i18n.ts.description }}</template> - <template #value><div v-html="sanitizeHtml(instance.description)"></div></template> - </MkKeyValue> - - <FormSection> - <div class="_gaps_m"> - <MkKeyValue :copy="version"> - <template #key>Sharkey</template> - <template #value>{{ version }}</template> - </MkKeyValue> - <div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })"> - </div> - <FormLink to="/about-sharkey"> - <template #icon><i class="ph-info ph-bold ph-lg"></i></template> - {{ i18n.ts.aboutMisskey }} - </FormLink> - <FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/sharkey-${version}.tar.gz`" external> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> - {{ i18n.ts.sourceCode }} - </FormLink> - <MkInfo v-else warn> - {{ i18n.ts.sourceCodeIsNotYetProvided }} - </MkInfo> - </div> - </FormSection> - - <FormSection> - <div class="_gaps_m"> - <FormSplit> - <MkKeyValue> - <template #key>{{ i18n.ts.administrator }}</template> - <template #value>{{ instance.maintainerName }}</template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.contact }}</template> - <template #value>{{ instance.maintainerEmail }}</template> - </MkKeyValue> - </FormSplit> - <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external> - <template #icon><i class="ph-newspaper-clipping ph-bold ph-lg"></i></template> - {{ i18n.ts.impressum }} - </FormLink> - <div class="_gaps_s"> - <MkFolder v-if="instance.serverRules.length > 0"> - <template #label> - <i class="ph-list-checks ph-bold ph-lg"></i> - {{ i18n.ts.serverRules }} - </template> - - <ol class="_gaps_s" :class="$style.rules"> - <li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="sanitizeHtml(item)"></div></li> - </ol> - </MkFolder> - <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external> - <template #icon><i class="ph-notebook ph-bold ph-lg"></i></template> - {{ i18n.ts.termsOfService }} - </FormLink> - <FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external> - <template #icon><i class="ph-shield ph-bold ph-lg"></i></template> - {{ i18n.ts.privacyPolicy }} - </FormLink> - <FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external> - <template #icon><i class="ph-envelope ph-bold ph-lg"></i></template> - {{ i18n.ts.feedback }} - </FormLink> - </div> - </div> - </FormSection> - - <FormSuspense :p="initStats"> - <FormSection> - <template #label>{{ i18n.ts.statistics }}</template> - <FormSplit> - <MkKeyValue> - <template #key>{{ i18n.ts.users }}</template> - <template #value>{{ number(stats.originalUsersCount) }}</template> - </MkKeyValue> - <MkKeyValue> - <template #key>{{ i18n.ts.notes }}</template> - <template #value>{{ number(stats.originalNotesCount) }}</template> - </MkKeyValue> - </FormSplit> - </FormSection> - </FormSuspense> - - <FormSection> - <template #label>Well-known resources</template> - <div class="_gaps_s"> - <FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> - <FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> - <FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> - <FormLink :to="`/robots.txt`" external>robots.txt</FormLink> - <FormLink :to="`/manifest.json`" external>manifest.json</FormLink> - </div> - </FormSection> - </div> + <XOverview/> </MkSpacer> <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20"> <XEmojis/> @@ -130,27 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import sanitizeHtml from '@/scripts/sanitize-html.js'; -import { computed, watch, ref } from 'vue'; -import * as Misskey from 'misskey-js'; -import XEmojis from './about.emojis.vue'; -import XFederation from './about.federation.vue'; -import { version, host } from '@/config.js'; -import FormLink from '@/components/form/link.vue'; -import FormSection from '@/components/form/section.vue'; -import FormSuspense from '@/components/form/suspense.vue'; -import FormSplit from '@/components/form/split.vue'; -import MkFolder from '@/components/MkFolder.vue'; -import MkKeyValue from '@/components/MkKeyValue.vue'; -import MkInfo from '@/components/MkInfo.vue'; -import MkInstanceStats from '@/components/MkInstanceStats.vue'; -import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import number from '@/filters/number.js'; +import { computed, defineAsyncComponent, ref, watch } from 'vue'; import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; import { claimAchievement } from '@/scripts/achievements.js'; -import { instance } from '@/instance.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; + +const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue')); +const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue')); +const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue')); +const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue')); const props = withDefaults(defineProps<{ initialTab?: string; @@ -158,7 +41,6 @@ const props = withDefaults(defineProps<{ initialTab: 'overview', }); -const stats = ref<Misskey.entities.StatsResponse | null>(null); const tab = ref(props.initialTab); watch(tab, () => { @@ -167,11 +49,6 @@ watch(tab, () => { } }); -const initStats = () => misskeyApi('stats', { -}).then((res) => { - stats.value = res; -}); - const headerActions = computed(() => []); const headerTabs = computed(() => [{ @@ -184,76 +61,15 @@ const headerTabs = computed(() => [{ }, { key: 'federation', title: i18n.ts.federation, - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', + icon: 'ti ti-whirl', }, { key: 'charts', title: i18n.ts.charts, - icon: 'ph-chart-line ph-bold ph-lg', + icon: 'ti ti-chart-line', }]); definePageMetadata(() => ({ title: i18n.ts.instanceInfo, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', })); </script> - -<style lang="scss" module> -.banner { - text-align: center; - border-radius: var(--radius); - overflow: clip; - background-size: cover; - background-position: center center; -} - -.bannerIcon { - display: block; - margin: 16px auto 0 auto; - height: 64px; - border-radius: var(--radius-sm); -} - -.bannerName { - display: block; - padding: 16px; - color: #fff; - text-shadow: 0 0 8px #000; - background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); -} - -.rules { - counter-reset: item; - list-style: none; - padding: 0; - margin: 0; -} - -.rule { - display: flex; - gap: 8px; - word-break: break-word; - - &::before { - flex-shrink: 0; - display: flex; - position: sticky; - top: calc(var(--stickyTop, 0px) + 8px); - counter-increment: item; - content: counter(item); - width: 32px; - height: 32px; - line-height: 32px; - background-color: var(--accentedBg); - color: var(--accent); - font-size: 13px; - font-weight: bold; - align-items: center; - justify-content: center; - border-radius: var(--radius-ellipse); - } -} - -.ruleText { - padding-top: 6px; -} -</style> diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue index 4e496c3c6cf42acbaf7ac5a922d3d737d5a08a6e..77ab473ea2561f4fe285a3f907e8f71538dd8085 100644 --- a/packages/frontend/src/pages/achievements.vue +++ b/packages/frontend/src/pages/achievements.vue @@ -50,7 +50,7 @@ onDeactivated(() => { definePageMetadata(() => ({ title: i18n.ts.achievements, - icon: 'ph-trophy ph-bold ph-lg', + icon: 'ti ti-medal', })); </script> diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index b8f7e2c163df855478ed2652e0e6022446cbf7d2..d8311186ab84676851af70999185278b9d3bf81c 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div> - <MkButton danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> <div v-else-if="tab === 'ip' && info" class="_gaps_m"> @@ -120,7 +120,7 @@ async function toggleIsSensitive(v) { const headerActions = computed(() => [{ text: i18n.ts.openInNewTab, - icon: 'ph-arrow-square-out ph-bold ph-lg', + icon: 'ti ti-external-link', handler: () => { window.open(file.value.url, '_blank', 'noopener'); }, @@ -129,20 +129,20 @@ const headerActions = computed(() => [{ const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', }, iAmModerator ? { key: 'ip', title: 'IP', - icon: 'ph-password ph-bold ph-lg', + icon: 'ti ti-password', } : null, { key: 'raw', title: 'Raw data', - icon: 'ph-code ph-bold ph-lg', + icon: 'ti ti-code', }]); definePageMetadata(() => ({ title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file, - icon: 'ph-file ph-bold ph-lg', + icon: 'ti ti-file', })); </script> diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 07df36bd11e6f6ed2289eee4eab202de4f436349..187ec66b423a5c1cda81de3469502909331a8fb7 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="user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo> + <MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo> <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink> @@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="display: flex; flex-direction: column; gap: 1em;"> <MkKeyValue oneline> <template #key>{{ i18n.ts.instanceInfo }}</template> - <template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ph-caret-right ph-bold ph-lg"></i></MkA></template> + <template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ti ti-chevron-right"></i></MkA></template> </MkKeyValue> <MkKeyValue oneline> <template #key>{{ i18n.ts.updatedAt }}</template> @@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> </div> - <MkButton @click="updateRemoteUser"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.updateRemoteUser }}</MkButton> + <MkButton @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</MkButton> </div> </FormSection> @@ -83,7 +83,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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="ph-key ph-bold ph-lg"></i> {{ i18n.ts.resetPassword }}</MkButton> + <MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton> </div> <MkFolder> @@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-password ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-password"></i></template> <template #label>IP</template> <MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> <MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo> @@ -110,8 +110,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <div> - <MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ph-user-circle ph-bold ph-lg"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton> - <MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ph-image ph-bold ph-lg"></i> {{ i18n.ts.unsetUserBanner }}</MkButton> + <MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton> + <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> @@ -120,14 +120,14 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'roles'" class="_gaps"> - <MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton> + <MkButton v-if="user.host == null" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <div v-for="role in info.roles" :key="role.id"> <div :class="$style.roleItemMain"> <MkRolePreview :class="$style.role" :role="role" :forModeration="true"/> - <button class="_button" @click="toggleRoleItem(role)"><i class="ph-caret-down ph-bold ph-lg"></i></button> - <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ph-x ph-bold ph-lg"></i></button> - <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ph-prohibit ph-bold ph-lg"></i></button> + <button class="_button" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button> + <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> + <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> </div> <div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub"> <div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div> @@ -138,17 +138,17 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'announcements'" class="_gaps"> - <MkButton primary rounded @click="createAnnouncement"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.new }}</MkButton> + <MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts.new }}</MkButton> <MkPagination :pagination="announcementsPagination"> <template #default="{ items }"> <div class="_gaps_s"> <div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)"> <span style="margin-right: 0.5em;"> - <i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i> - <i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i> + <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> </span> <span>{{ announcement.title }}</span> <span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span> @@ -464,7 +464,7 @@ async function assignRole() { async function unassignRole(role, ev) { os.popupMenu([{ text: i18n.ts.unassign, - icon: 'ph-x ph-bold ph-lg', + icon: 'ti ti-x', danger: true, action: async () => { await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.value.id }); @@ -482,16 +482,20 @@ function toggleRoleItem(role) { } function createAnnouncement() { - os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { user: user.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } function editAnnouncement(announcement) { - os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { user: user.value, announcement, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } watch(() => props.userId, () => { @@ -513,32 +517,32 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', }, { key: 'roles', title: i18n.ts.roles, - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badges', }, { key: 'announcements', title: i18n.ts.announcements, - icon: 'ph-megaphone ph-bold ph-lg', + icon: 'ti ti-speakerphone', }, { key: 'drive', title: i18n.ts.drive, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', }, { key: 'chart', title: i18n.ts.charts, - icon: 'ph-chart-line ph-bold ph-lg', + icon: 'ti ti-chart-line', }, { key: 'raw', title: 'Raw', - icon: 'ph-code ph-bold ph-lg', + icon: 'ti ti-code', }]); definePageMetadata(() => ({ title: user.value ? acct(user.value) : i18n.ts.userInfo, - icon: 'ph-warning-circle ph-bold ph-lg', + icon: 'ti ti-user-exclamation', })); </script> diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index de1f6646bb0a7d063f422349231634818d3f24c5..f001a4ac20310198b541962d65b548b1f4f3cd6b 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -28,10 +28,10 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="not">{{ i18n.ts._role._condition.not }}</option> </MkSelect> <button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle"> - <i class="ph-list ph-bold ph-lg-2"></i> + <i class="ti ti-menu-2"></i> </button> <button v-if="draggable" class="_button" :class="$style.remove" @click="removeSelf"> - <i class="ph-x ph-bold ph-lg"></i> + <i class="ti ti-x"></i> </button> </div> @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> </Sortable> - <MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> + <MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> </div> <div v-else-if="type === 'not'" :class="$style.item"> diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue index 5c9c32c964fce73d4efc1e35c2e0f40899bbc256..61dc4f854921bf43acb0466c1440f149ba9a5f66 100644 --- a/packages/frontend/src/pages/admin/_header_.vue +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="buttons right"> <template v-if="actions"> <template v-for="action in actions"> - <MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton> - <button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> + <MkButton v-if="action.asFullButton" class="fullButton" primary :disabled="action.disabled" @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton> + <button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" :disabled="action.disabled" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> </template> </template> </div> @@ -56,6 +56,7 @@ const props = defineProps<{ text: string; icon: string; asFullButton?: boolean; + disabled?: boolean; handler: (ev: MouseEvent) => void; }[]; thin?: boolean; diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue new file mode 100644 index 0000000000000000000000000000000000000000..827e22e8ae319989c3d2983b1f8a6473c4a26d99 --- /dev/null +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue @@ -0,0 +1,321 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialogEl" + :width="400" + :height="490" + :withOkButton="false" + :okButtonDisabled="false" + @close="onCancelClicked" + @closed="emit('closed')" +> + <template #header> + {{ mode === 'create' ? i18n.ts._abuseReport._notificationRecipient.createRecipient : i18n.ts._abuseReport._notificationRecipient.modifyRecipient }} + </template> + <div v-if="loading === 0" style="display: flex; flex-direction: column; min-height: 100%;"> + <MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;"> + <div :class="$style.root" class="_gaps_m"> + <MkInput v-model="title"> + <template #label>{{ i18n.ts.title }}</template> + </MkInput> + <MkSelect v-model="method"> + <template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template> + <option value="email">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option> + <option value="webhook">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option> + <template #caption> + {{ methodCaption }} + </template> + </MkSelect> + <div> + <MkSelect v-if="method === 'email'" v-model="userId"> + <template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedUser }}</template> + <option v-for="user in moderators" :key="user.id" :value="user.id"> + {{ user.name ? `${user.name}(${user.username})` : user.username }} + </option> + </MkSelect> + <div v-else-if="method === 'webhook'" :class="$style.systemWebhook"> + <MkSelect v-model="systemWebhookId" style="flex: 1"> + <template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template> + <option v-for="webhook in systemWebhooks" :key="webhook.id ?? undefined" :value="webhook.id"> + {{ webhook.name }} + </option> + </MkSelect> + <MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked"> + <span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/> + <span v-else class="ti ti-settings" style="line-height: normal"/> + </MkButton> + </div> + </div> + + <MkDivider/> + + <MkSwitch v-model="isActive"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </div> + </MkSpacer> + + <div :class="$style.footer" class="_buttonsCenter"> + <MkButton primary rounded :disabled="disableSubmitButton" @click="onSubmitClicked"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton> + <MkButton rounded @click="onCancelClicked"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton> + </div> + </div> + <div v-else> + <MkLoading/> + </div> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { computed, onMounted, ref, shallowRef, toRefs } from 'vue'; +import { entities } from 'misskey-js'; +import MkButton from '@/components/MkButton.vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import { i18n } from '@/i18n.js'; +import MkInput from '@/components/MkInput.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import MkSelect from '@/components/MkSelect.vue'; +import { MkSystemWebhookResult, showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js'; +import MkSwitch from '@/components/MkSwitch.vue'; +import MkDivider from '@/components/MkDivider.vue'; +import * as os from '@/os.js'; + +type NotificationRecipientMethod = 'email' | 'webhook'; + +const emit = defineEmits<{ + (ev: 'submitted'): void; + (ev: 'canceled'): void; + (ev: 'closed'): void; +}>(); + +const props = defineProps<{ + mode: 'create' | 'edit'; + id?: string; +}>(); + +const { mode, id } = toRefs(props); + +const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>(); + +const loading = ref<number>(0); + +const title = ref<string>(''); +const method = ref<NotificationRecipientMethod>('email'); +const userId = ref<string | null>(null); +const systemWebhookId = ref<string | null>(null); +const isActive = ref<boolean>(true); + +const moderators = ref<entities.User[]>([]); +const systemWebhooks = ref<(entities.SystemWebhook | { id: null, name: string })[]>([]); + +const methodCaption = computed(() => { + switch (method.value) { + case 'email': { + return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.mail; + } + case 'webhook': { + return i18n.ts._abuseReport._notificationRecipient._recipientType._captions.webhook; + } + default: { + return ''; + } + } +}); + +const disableSubmitButton = computed(() => { + if (!title.value) { + return true; + } + + switch (method.value) { + case 'email': { + return userId.value === null; + } + case 'webhook': { + return systemWebhookId.value === null; + } + default: { + return true; + } + } +}); + +async function onSubmitClicked() { + await loadingScope(async () => { + const _userId = (method.value === 'email') ? userId.value : null; + const _systemWebhookId = (method.value === 'webhook') ? systemWebhookId.value : null; + const params = { + isActive: isActive.value, + name: title.value, + method: method.value, + userId: _userId ?? undefined, + systemWebhookId: _systemWebhookId ?? undefined, + }; + + try { + switch (mode.value) { + case 'create': { + await misskeyApi('admin/abuse-report/notification-recipient/create', params); + break; + } + case 'edit': { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await misskeyApi('admin/abuse-report/notification-recipient/update', { id: id.value!, ...params }); + break; + } + } + + dialogEl.value?.close(); + emit('submitted'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (ex: any) { + const msg = ex.message ?? i18n.ts.internalServerErrorDescription; + await os.alert({ type: 'error', title: i18n.ts.error, text: msg }); + dialogEl.value?.close(); + emit('canceled'); + } + }); +} + +function onCancelClicked() { + dialogEl.value?.close(); + emit('canceled'); +} + +async function onEditSystemWebhookClicked() { + let result: MkSystemWebhookResult | null; + if (systemWebhookId.value === null) { + result = await showSystemWebhookEditorDialog({ + mode: 'create', + }); + } else { + result = await showSystemWebhookEditorDialog({ + mode: 'edit', + id: systemWebhookId.value, + }); + } + if (!result) { + return; + } + + await fetchSystemWebhooks(); + systemWebhookId.value = result.id ?? null; +} + +async function fetchSystemWebhooks() { + await loadingScope(async () => { + systemWebhooks.value = [ + { id: null, name: i18n.ts.createNew }, + ...await misskeyApi('admin/system-webhook/list', { }), + ]; + }); +} + +async function fetchModerators() { + await loadingScope(async () => { + const users = Array.of<entities.User>(); + for (; ;) { + const res = await misskeyApi('admin/show-users', { + limit: 100, + state: 'adminOrModerator', + origin: 'local', + offset: users.length, + }); + + if (res.length === 0) { + break; + } + + users.push(...res); + } + + moderators.value = users; + }); +} + +async function loadingScope<T>(fn: () => Promise<T>): Promise<T> { + loading.value++; + try { + return await fn(); + } finally { + loading.value--; + } +} + +onMounted(async () => { + await loadingScope(async () => { + await fetchModerators(); + await fetchSystemWebhooks(); + + if (mode.value === 'edit') { + if (!id.value) { + throw new Error('id is required'); + } + + try { + const res = await misskeyApi('admin/abuse-report/notification-recipient/show', { id: id.value }); + + title.value = res.name; + method.value = res.method; + userId.value = res.userId ?? null; + systemWebhookId.value = res.systemWebhookId ?? null; + isActive.value = res.isActive; + // eslint-disable-next-line + } catch (ex: any) { + const msg = ex.message ?? i18n.ts.internalServerErrorDescription; + await os.alert({ type: 'error', title: i18n.ts.error, text: msg }); + dialogEl.value?.close(); + emit('canceled'); + } + } else { + userId.value = moderators.value[0]?.id ?? null; + systemWebhookId.value = systemWebhooks.value[0]?.id ?? null; + } + }); +}); + +</script> + +<style lang="scss" module> +.root { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; +} + +.footer { + position: sticky; + z-index: 10000; + bottom: 0; + left: 0; + padding: 12px; + border-top: solid 0.5px var(--divider); + background: var(--acrylicBg); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} + +.systemWebhook { + display: flex; + flex-direction: row; + justify-content: stretch; + align-items: flex-end; + gap: 8px; +} + +.systemWebhookEditButton { + min-width: 0; + min-height: 0; + width: 34px; + height: 34px; + flex-shrink: 0; + box-sizing: border-box; + margin: 1px 0; + padding: 6px; +} +</style> diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue new file mode 100644 index 0000000000000000000000000000000000000000..0b86808fafb10124cbd48cb47b1c7b7fb07c2c39 --- /dev/null +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue @@ -0,0 +1,114 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.root" class="_panel _gaps_s"> + <div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"/> {{ methodName }}</div> + <div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div> + <div :class="$style.rightDivider" style="flex: 1"> + <div v-if="method === 'email' && user"> + {{ + `${i18n.ts._abuseReport._notificationRecipient.notifiedUser}: ` + ((user.name) ? `${user.name}(${user.username})` : user.username) + }} + </div> + <div v-if="method === 'webhook' && systemWebhook"> + {{ `${i18n.ts._abuseReport._notificationRecipient.notifiedWebhook}: ` + systemWebhook.name }} + </div> + </div> + <div :class="$style.recipientButtons" style="margin-left: auto"> + <button :class="$style.recipientButton" @click="onEditButtonClicked()"> + <span class="ti ti-settings"/> + </button> + <button :class="$style.recipientButton" @click="onDeleteButtonClicked()"> + <span class="ti ti-trash"/> + </button> + </div> +</div> +</template> + +<script setup lang="ts"> +import { entities } from 'misskey-js'; +import { computed, toRefs } from 'vue'; +import { i18n } from '@/i18n.js'; + +const emit = defineEmits<{ + (ev: 'edit', id: entities.AbuseReportNotificationRecipient['id']): void; + (ev: 'delete', id: entities.AbuseReportNotificationRecipient['id']): void; +}>(); + +const props = defineProps<{ + entity: entities.AbuseReportNotificationRecipient; +}>(); + +const { entity } = toRefs(props); + +const method = computed(() => entity.value.method); +const user = computed(() => entity.value.user); +const systemWebhook = computed(() => entity.value.systemWebhook); +const methodIcon = computed(() => { + switch (entity.value.method) { + case 'email': + return 'ti-mail'; + case 'webhook': + return 'ti-webhook'; + default: + return 'ti-help'; + } +}); +const methodName = computed(() => { + switch (entity.value.method) { + case 'email': + return i18n.ts._abuseReport._notificationRecipient._recipientType.mail; + case 'webhook': + return i18n.ts._abuseReport._notificationRecipient._recipientType.webhook; + default: + return 'ä¸æ˜Ž'; + } +}); + +function onEditButtonClicked() { + emit('edit', entity.value.id); +} + +function onDeleteButtonClicked() { + emit('delete', entity.value.id); +} +</script> + +<style module lang="scss"> +.root { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 4px 8px; +} + +.rightDivider { + border-right: 0.5px solid var(--divider); +} + +.recipientButtons { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin-right: -4; +} + +.recipientButton { + background-color: transparent; + border: none; + border-radius: 9999px; + box-sizing: border-box; + margin-top: -2px; + margin-bottom: -2px; + padding: 8px; + + &:hover { + background-color: var(--buttonBg); + } +} +</style> diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue new file mode 100644 index 0000000000000000000000000000000000000000..f5249261be19838e5a080c74ea96a6c1a583c183 --- /dev/null +++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue @@ -0,0 +1,177 @@ +<!-- +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="900"> + <div :class="$style.root" class="_gaps_m"> + <div :class="$style.addButton"> + <MkButton primary @click="onAddButtonClicked"> + <span class="ti ti-plus"/> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }} + </MkButton> + </div> + <div :class="$style.subMenus" class="_gaps_s"> + <MkSelect v-model="filterMethod" style="flex: 1"> + <template #label>{{ i18n.ts._abuseReport._notificationRecipient.recipientType }}</template> + <option :value="null">-</option> + <option :value="'email'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.mail }}</option> + <option :value="'webhook'">{{ i18n.ts._abuseReport._notificationRecipient._recipientType.webhook }}</option> + </MkSelect> + <MkInput v-model="filterText" type="search" style="flex: 1"> + <template #label>{{ i18n.ts._abuseReport._notificationRecipient.keywords }}</template> + </MkInput> + </div> + + <MkDivider/> + + <div :class="$style.recipients" class="_gaps_s"> + <XRecipient + v-for="r in filteredRecipients" + :key="r.id" + :entity="r" + @edit="onEditButtonClicked" + @delete="onDeleteButtonClicked" + /> + </div> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script setup lang="ts"> +import { entities } from 'misskey-js'; +import { computed, defineAsyncComponent, onMounted, ref } from 'vue'; +import XRecipient from './notification-recipient.item.vue'; +import XHeader from '@/pages/admin/_header_.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkButton from '@/components/MkButton.vue'; +import * as os from '@/os.js'; +import MkDivider from '@/components/MkDivider.vue'; +import { i18n } from '@/i18n.js'; + +const recipients = ref<entities.AbuseReportNotificationRecipient[]>([]); + +const filterMethod = ref<string | null>(null); +const filterText = ref<string>(''); + +const filteredRecipients = computed(() => { + const method = filterMethod.value; + const text = filterText.value.trim().length === 0 ? null : filterText.value; + + return recipients.value.filter(it => { + if (method ?? text) { + if (text) { + const keywords = [it.name, it.systemWebhook?.name, it.user?.name, it.user?.username]; + if (keywords.filter(k => k?.includes(text)).length !== 0) { + return true; + } + } + + if (method) { + return it.method.includes(method); + } + + return false; + } + + return true; + }); +}); +const headerActions = computed(() => []); +const headerTabs = computed(() => []); + +async function onAddButtonClicked() { + await showEditor('create'); +} + +async function onEditButtonClicked(id: string) { + await showEditor('edit', id); +} + +async function onDeleteButtonClicked(id: string) { + const res = await os.confirm({ + type: 'warning', + title: i18n.ts._abuseReport._notificationRecipient.deleteConfirm, + }); + if (!res.canceled) { + await misskeyApi('admin/abuse-report/notification-recipient/delete', { id: id }); + await fetchRecipients(); + } +} + +async function showEditor(mode: 'create' | 'edit', id?: string) { + const { needLoad } = await new Promise<{ needLoad: boolean }>(async resolve => { + const { dispose } = os.popup( + defineAsyncComponent(() => import('./notification-recipient.editor.vue')), + { + mode, + id, + }, + { + submitted: () => { + resolve({ needLoad: true }); + }, + canceled: () => { + resolve({ needLoad: false }); + }, + closed: () => { + dispose(); + }, + }, + ); + }); + + if (needLoad) { + await fetchRecipients(); + } +} + +async function fetchRecipients() { + const result = await misskeyApi('admin/abuse-report/notification-recipient/list', { + method: ['email', 'webhook'], + }); + + recipients.value = result.sort((a, b) => (a.method + a.id).localeCompare(b.method + b.id)); +} + +onMounted(async () => { + await fetchRecipients(); +}); +</script> + +<style module lang="scss"> +.root { + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; +} + +.addButton { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.subMenus { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-end; +} + +.recipients { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; +} +</style> diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue index 6e1f5f9cecafcf3509ee02632a0e2e84553c8299..c8a9ca71126bfecd8081082aaf554827ef3acfd1 100644 --- a/packages/frontend/src/pages/admin/abuses.vue +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -7,30 +7,33 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="900"> - <div> - <div class="reports"> - <div class=""> - <div class="inputs" style="display: flex;"> - <MkSelect v-model="state" style="margin: 0; flex: 1;"> - <template #label>{{ i18n.ts.state }}</template> - <option value="all">{{ i18n.ts.all }}</option> - <option value="unresolved">{{ i18n.ts.unresolved }}</option> - <option value="resolved">{{ i18n.ts.resolved }}</option> - </MkSelect> - <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> - <template #label>{{ i18n.ts.reporteeOrigin }}</template> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> - </MkSelect> - <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> - <template #label>{{ i18n.ts.reporterOrigin }}</template> - <option value="combined">{{ i18n.ts.all }}</option> - <option value="local">{{ i18n.ts.local }}</option> - <option value="remote">{{ i18n.ts.remote }}</option> - </MkSelect> - </div> - <!-- TODO + <div :class="$style.root" class="_gaps"> + <div :class="$style.subMenus" class="_gaps"> + <MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton> + </div> + + <div :class="$style.inputs" class="_gaps"> + <MkSelect v-model="state" style="margin: 0; flex: 1;"> + <template #label>{{ i18n.ts.state }}</template> + <option value="all">{{ i18n.ts.all }}</option> + <option value="unresolved">{{ i18n.ts.unresolved }}</option> + <option value="resolved">{{ i18n.ts.resolved }}</option> + </MkSelect> + <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> + <template #label>{{ i18n.ts.reporteeOrigin }}</template> + <option value="combined">{{ i18n.ts.all }}</option> + <option value="local">{{ i18n.ts.local }}</option> + <option value="remote">{{ i18n.ts.remote }}</option> + </MkSelect> + <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> + <template #label>{{ i18n.ts.reporterOrigin }}</template> + <option value="combined">{{ i18n.ts.all }}</option> + <option value="local">{{ i18n.ts.local }}</option> + <option value="remote">{{ i18n.ts.remote }}</option> + </MkSelect> + </div> + + <!-- TODO <div class="inputs" style="display: flex; padding-top: 1.2em;"> <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" :spellcheck="false"> <span>{{ i18n.ts.username }}</span> @@ -41,11 +44,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> --> - <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);"> - <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> - </MkPagination> - </div> - </div> + <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);"> + <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> + </MkPagination> </div> </MkSpacer> </MkStickyContainer> @@ -60,6 +61,7 @@ import MkPagination from '@/components/MkPagination.vue'; import XAbuseReport from '@/components/MkAbuseReport.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkButton from '@/components/MkButton.vue'; const reports = shallowRef<InstanceType<typeof MkPagination>>(); @@ -80,7 +82,7 @@ const pagination = { }; function resolved(reportId) { - reports.value.removeItem(reportId); + reports.value?.removeItem(reportId); } const headerActions = computed(() => []); @@ -89,6 +91,29 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.abuseReports, - icon: 'ph-warning-circle ph-bold ph-lg', + icon: 'ti ti-exclamation-circle', })); </script> + +<style module lang="scss"> +.root { + display: flex; + flex-direction: column; + justify-content: center; + align-items: stretch; +} + +.subMenus { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; +} + +.inputs { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} +</style> diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index 6ec5abd2f2eef83ee53427f0dbc6b595a32c5d50..bd442ccc691a4b4dcf0b0d39b854c52168847f42 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -68,16 +68,16 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="buttons"> <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"> <i - class="ph-floppy-disk ph-bold ph-lg" + class="ti ti-device-floppy" ></i> {{ i18n.ts.save }} </MkButton> <MkButton class="button" inline danger @click="remove(ad)"> - <i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }} + <i class="ti ti-trash"></i> {{ i18n.ts.remove }} </MkButton> </div> </div> <MkButton class="button" @click="more()"> - <i class="ph-arrow-clockwise ph-bold ph-lg"></i>{{ i18n.ts.more }} + <i class="ti ti-reload"></i>{{ i18n.ts.more }} </MkButton> </div> </MkSpacer> @@ -248,7 +248,7 @@ refresh(); const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.add, handler: add, }]); @@ -257,7 +257,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.ads, - icon: 'ph-flag ph-bold ph-lg', + icon: 'ti ti-ad', })); </script> diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index a8832b99fd4892b062c954c1e109d6f5a394e267..b9e09c8d03a6c7be47441bc00bba2d462ab484fb 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -11,70 +11,83 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo> <MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo> - <MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null"> - <template #label>{{ announcement.title }}</template> - <template #icon> - <i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i> - <i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i> - </template> - <template #caption>{{ announcement.text }}</template> - - <div class="_gaps_m"> - <MkInput v-model="announcement.title"> - <template #label>{{ i18n.ts.title }}</template> - </MkInput> - <MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true"> - <template #label>{{ i18n.ts.text }}</template> - </MkTextarea> - <MkInput v-model="announcement.imageUrl" type="url"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <MkRadios v-model="announcement.icon"> - <template #label>{{ i18n.ts.icon }}</template> - <option value="info"><i class="ph-info ph-bold ph-lg"></i></option> - <option value="warning"><i class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i></option> - <option value="error"><i class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i></option> - <option value="success"><i class="ph-check ph-bold ph-lg" style="color: var(--success);"></i></option> - </MkRadios> - <MkRadios v-model="announcement.display"> - <template #label>{{ i18n.ts.display }}</template> - <option value="normal">{{ i18n.ts.normal }}</option> - <option value="banner">{{ i18n.ts.banner }}</option> - <option value="dialog">{{ i18n.ts.dialog }}</option> - </MkRadios> - <MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo> - <MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription"> - {{ i18n.ts._announcement.forExistingUsers }} - </MkSwitch> - <MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription"> - {{ i18n.ts._announcement.silence }} - </MkSwitch> - <MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription"> - {{ 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="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton> - <MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkSelect v-model="announcementsStatus"> + <template #label>{{ i18n.ts.filter }}</template> + <option value="active">{{ i18n.ts.active }}</option> + <option value="archived">{{ i18n.ts.archived }}</option> + </MkSelect> + + <MkLoading v-if="loading"/> + + <template v-else> + <MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null"> + <template #label>{{ announcement.title }}</template> + <template #icon> + <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + </template> + <template #caption>{{ announcement.text }}</template> + + <div class="_gaps_m"> + <MkInput v-model="announcement.title"> + <template #label>{{ i18n.ts.title }}</template> + </MkInput> + <MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true"> + <template #label>{{ i18n.ts.text }}</template> + </MkTextarea> + <MkInput v-model="announcement.imageUrl" type="url"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <MkRadios v-model="announcement.icon"> + <template #label>{{ i18n.ts.icon }}</template> + <option value="info"><i class="ti ti-info-circle"></i></option> + <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option> + <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option> + <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option> + </MkRadios> + <MkRadios v-model="announcement.display"> + <template #label>{{ i18n.ts.display }}</template> + <option value="normal">{{ i18n.ts.normal }}</option> + <option value="banner">{{ i18n.ts.banner }}</option> + <option value="dialog">{{ i18n.ts.dialog }}</option> + </MkRadios> + <MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo> + <MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription"> + {{ i18n.ts._announcement.forExistingUsers }} + </MkSwitch> + <MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription"> + {{ i18n.ts._announcement.silence }} + </MkSwitch> + <MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription"> + {{ 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> - </div> - </MkFolder> - <MkButton class="button" @click="more()"> - <i class="ph-arrow-clockwise ph-bold ph-lg"></i>{{ i18n.ts.more }} - </MkButton> + </MkFolder> + <MkLoading v-if="loadingMore"/> + <MkButton class="button" @click="more()"> + <i class="ti ti-reload"></i>{{ i18n.ts.more }} + </MkButton> + </template> </div> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -85,11 +98,22 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkFolder from '@/components/MkFolder.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +const announcementsStatus = ref<'active' | 'archived'>('active'); + +const loading = ref(true); +const loadingMore = ref(false); + const announcements = ref<any[]>([]); -misskeyApi('admin/announcements/list').then(announcementResponse => { - announcements.value = announcementResponse; -}); +watch(announcementsStatus, (to) => { + loading.value = true; + misskeyApi('admin/announcements/list', { + status: to, + }).then(announcementResponse => { + announcements.value = announcementResponse; + loading.value = false; + }); +}, { immediate: true }); function add() { announcements.value.unshift({ @@ -125,6 +149,14 @@ async function archive(announcement) { refresh(); } +async function unarchive(announcement) { + await os.apiWithDialog('admin/announcements/update', { + ...announcement, + isActive: true, + }); + refresh(); +} + async function save(announcement) { if (announcement.id == null) { await os.apiWithDialog('admin/announcements/create', announcement); @@ -135,30 +167,38 @@ async function save(announcement) { } function more() { - misskeyApi('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => { + loadingMore.value = true; + misskeyApi('admin/announcements/list', { + status: announcementsStatus.value, + untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id + }).then(announcementResponse => { announcements.value = announcements.value.concat(announcementResponse); + loadingMore.value = false; }); } function refresh() { - misskeyApi('admin/announcements/list').then(announcementResponse => { + loading.value = true; + misskeyApi('admin/announcements/list', { + status: announcementsStatus.value, + }).then(announcementResponse => { announcements.value = announcementResponse; + loading.value = false; }); } -refresh(); - const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.add, handler: add, + disabled: announcementsStatus.value === 'archived', }]); const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.announcements, - icon: 'ph-megaphone ph-bold ph-lg', + icon: 'ti ti-speakerphone', })); </script> diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 052d2f0bc266d3bd169adbafe0079e5bc05e1935..73c5e1919f41af20678e745ad0e39743846688b9 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -17,11 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="provider === 'hcaptcha'"> <MkInput v-model="hcaptchaSiteKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.hcaptchaSiteKey }}</template> </MkInput> <MkInput v-model="hcaptchaSecretKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.hcaptchaSecretKey }}</template> </MkInput> <FormSlot> @@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template v-else-if="provider === 'mcaptcha'"> <MkInput v-model="mcaptchaSiteKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template> </MkInput> <MkInput v-model="mcaptchaSecretKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template> </MkInput> <MkInput v-model="mcaptchaInstanceUrl"> - <template #prefix><i class="ph-globe-simple ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template> </MkInput> <FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl"> @@ -49,11 +49,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template v-else-if="provider === 'recaptcha'"> <MkInput v-model="recaptchaSiteKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.recaptchaSiteKey }}</template> </MkInput> <MkInput v-model="recaptchaSecretKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.recaptchaSecretKey }}</template> </MkInput> <FormSlot v-if="recaptchaSiteKey"> @@ -63,11 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template v-else-if="provider === 'turnstile'"> <MkInput v-model="turnstileSiteKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.turnstileSiteKey }}</template> </MkInput> <MkInput v-model="turnstileSecretKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.turnstileSecretKey }}</template> </MkInput> <FormSlot> @@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSlot> </template> - <MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </FormSuspense> </div> diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 9310f52bfb94ba74139ee501109598b612e01126..2e14aef0b946e63c7d14206d4b5ec8c98f91a03c 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSuspense :p="init"> <div class="_gaps_m"> <MkInput v-model="iconUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts._serverSettings.iconUrl }}</template> </MkInput> <MkInput v-model="app192IconUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template> <template #caption> <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div> @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkInput v-model="app512IconUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template> <template #caption> <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div> @@ -38,12 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkInput v-model="bannerUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.bannerUrl }}</template> </MkInput> <MkInput v-model="backgroundImageUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.backgroundImageUrl }}</template> </MkInput> @@ -55,17 +55,17 @@ SPDX-License-Identifier: AGPL-3.0-only </FromSlot> <MkInput v-model="notFoundImageUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.notFoundDescription }}</template> </MkInput> <MkInput v-model="infoImageUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.nothing }}</template> </MkInput> <MkInput v-model="serverErrorImageUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.somethingHappened }}</template> </MkInput> @@ -84,12 +84,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkTextarea> <MkInput v-model="repositoryUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.repositoryUrl }}</template> </MkInput> <MkInput v-model="feedbackUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.feedbackUrl }}</template> </MkInput> @@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #footer> <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> </template> @@ -200,7 +200,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.branding, - icon: 'ph-paint-roller ph-bold ph-lg', + icon: 'ti ti-paint', })); </script> diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue index a64e07b4c771745f162eb0d00ff9d86b83c9f4e0..e092efd92c23a2755708f89a0ce2efc44856a492 100644 --- a/packages/frontend/src/pages/admin/database.vue +++ b/packages/frontend/src/pages/admin/database.vue @@ -35,6 +35,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.database, - icon: 'ph-database ph-bold ph-lg', + icon: 'ti ti-database', })); </script> diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index 1cbfdab094affa7af2d6df7a32d6e48e42386d22..4a858887f353a0c0774e39ef25d36df4383fad3f 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -54,8 +54,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <div class="_buttons"> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton rounded @click="testEmail"><i class="ph-paper-plane-tilt ph-bold ph-lg"></i> {{ i18n.ts.testEmail }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton rounded @click="testEmail"><i class="ti ti-send"></i> {{ i18n.ts.testEmail }}</MkButton> </div> </MkSpacer> </div> @@ -132,7 +132,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.emailServer, - icon: 'ph-envelope ph-bold ph-lg', + icon: 'ti ti-mail', })); </script> diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index 728cdc60b037bfc1949942f1420cddd913fec8ca..e4308e6030a0c0dfd2f7e4beaf328332d41b4b94 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <MkInput v-model="deeplAuthKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>DeepL Auth Key</template> </MkInput> <MkSwitch v-model="deeplIsPro"> @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #footer> <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> </template> diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index c1f958cece44fae65baf5c064c98f643acaa746a..1902c97724be64387f8d8c69cb8eaf19552b859e 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <div> <MkInput v-model="host" :debounce="true" class=""> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.host }}</template> </MkInput> <FormSplit style="margin-top: var(--margin);"> @@ -81,9 +81,9 @@ const pagination = { sort: sort.value, host: host.value !== '' ? host.value : null, ...( - state.value === 'federating' ? { federating: true } : - state.value === 'subscribing' ? { subscribing: true } : - state.value === 'publishing' ? { publishing: true } : + state.value === 'federating' ? { federating: true, suspended: false, blocked: false } : + state.value === 'subscribing' ? { subscribing: true, suspended: false, blocked: false } : + state.value === 'publishing' ? { publishing: true, suspended: false, blocked: false } : state.value === 'suspended' ? { suspended: true } : state.value === 'blocked' ? { blocked: true } : state.value === 'silenced' ? { silenced: true } : @@ -117,7 +117,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.federation, - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', + icon: 'ti ti-whirl', })); </script> diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue index 29b5bc5f8886572abdc378febf4032162922d4e9..5132b85c64ed7842557c631878a480b2b619ed8f 100644 --- a/packages/frontend/src/pages/admin/files.vue +++ b/packages/frontend/src/pages/admin/files.vue @@ -75,11 +75,11 @@ function clear() { const headerActions = computed(() => [{ text: i18n.ts.lookup, - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', handler: lookupFile, }, { text: i18n.ts.clearCachedFiles, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', handler: clear, }]); @@ -87,6 +87,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.files, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', })); </script> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index fae86246eeb0ac5472a21a4e62d2ebff3d321ef7..f547bedacb12d358921871b0b61dde16cfcb1349 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> <div v-if="!narrow || currentPage?.route.name == null" class="nav"> <MkSpacer :contentMax="700" :marginMin="16"> - <div class="lxpfedzu"> + <div class="lxpfedzu _gaps"> <div class="banner"> <img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/> </div> @@ -50,7 +50,7 @@ const router = useRouter(); const indexInfo = { title: i18n.ts.controlPanel, - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', hideHeader: true, }; @@ -62,10 +62,10 @@ const narrow = ref(false); const view = ref(null); const el = ref<HTMLDivElement | null>(null); const pageProps = ref({}); -let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail); -let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableMcaptcha && !instance.enableTurnstile; -let noEmailServer = !instance.enableEmail; -let noInquiryUrl = isEmpty(instance.inquiryUrl); +const noMaintainerInformation = computed(() => isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail)); +const noBotProtection = computed(() => !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile && !instance.enableMcaptcha); +const noEmailServer = computed(() => !instance.enableEmail); +const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl)); const thereIsUnresolvedAbuseReport = ref(false); const pendingUserApprovals = ref(false); const currentPage = computed(() => router.currentRef.value.child); @@ -95,29 +95,29 @@ const menuDef = computed(() => [{ title: i18n.ts.quickAction, items: [{ type: 'button', - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', text: i18n.ts.lookup, action: adminLookup, }, ...(instance.disableRegistration ? [{ type: 'button', - icon: 'ph-user-plus ph-bold ph-lg', + icon: 'ti ti-user-plus', text: i18n.ts.createInviteCode, action: invite, }] : [])], }, { title: i18n.ts.administration, items: [{ - icon: 'ph-gauge ph-bold ph-lg', + icon: 'ti ti-dashboard', text: i18n.ts.dashboard, to: '/admin/overview', active: currentPage.value?.route.name === 'overview', }, { - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', text: i18n.ts.users, to: '/admin/users', active: currentPage.value?.route.name === 'users', }, { - icon: 'ph-user-plus ph-bold ph-lg', + icon: 'ti ti-user-plus', text: i18n.ts.invite, to: '/admin/invites', active: currentPage.value?.route.name === 'invites', @@ -127,7 +127,7 @@ const menuDef = computed(() => [{ to: '/admin/approvals', active: currentPage.value?.route.name === 'approvals', }, { - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badges', text: i18n.ts.roles, to: '/admin/roles', active: currentPage.value?.route.name === 'roles', @@ -137,42 +137,42 @@ const menuDef = computed(() => [{ to: '/admin/emojis', active: currentPage.value?.route.name === 'emojis', }, { - icon: 'ph-sparkle ph-bold ph-lg', + icon: 'ti ti-sparkles', text: i18n.ts.avatarDecorations, to: '/admin/avatar-decorations', active: currentPage.value?.route.name === 'avatarDecorations', }, { - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', + icon: 'ti ti-whirl', text: i18n.ts.federation, to: '/admin/federation', active: currentPage.value?.route.name === 'federation', }, { - icon: 'ph-clock ph-bold ph-lg-play', + icon: 'ti ti-clock-play', text: i18n.ts.jobQueue, to: '/admin/queue', active: currentPage.value?.route.name === 'queue', }, { - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', text: i18n.ts.files, to: '/admin/files', active: currentPage.value?.route.name === 'files', }, { - icon: 'ph-megaphone ph-bold ph-lg', + icon: 'ti ti-speakerphone', text: i18n.ts.announcements, to: '/admin/announcements', active: currentPage.value?.route.name === 'announcements', }, { - icon: 'ph-flag ph-bold ph-lg', + icon: 'ti ti-ad', text: i18n.ts.ads, to: '/admin/ads', active: currentPage.value?.route.name === 'ads', }, { - icon: 'ph-warning-circle ph-bold ph-lg', + icon: 'ti ti-exclamation-circle', text: i18n.ts.abuseReports, to: '/admin/abuses', active: currentPage.value?.route.name === 'abuses', }, { - icon: 'ph-list ph-bold ph-lg-search', + icon: 'ti ti-list-search', text: i18n.ts.moderationLogs, to: '/admin/modlog', active: currentPage.value?.route.name === 'modlog', @@ -180,47 +180,47 @@ const menuDef = computed(() => [{ }, { title: i18n.ts.settings, items: [{ - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts.general, to: '/admin/settings', active: currentPage.value?.route.name === 'settings', }, { - icon: 'ph-paint-roller ph-bold ph-lg', + icon: 'ti ti-paint', text: i18n.ts.branding, to: '/admin/branding', active: currentPage.value?.route.name === 'branding', }, { - icon: 'ph-shield ph-bold ph-lg', + icon: 'ti ti-shield', text: i18n.ts.moderation, to: '/admin/moderation', active: currentPage.value?.route.name === 'moderation', }, { - icon: 'ph-envelope ph-bold ph-lg', + icon: 'ti ti-mail', text: i18n.ts.emailServer, to: '/admin/email-settings', active: currentPage.value?.route.name === 'email-settings', }, { - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', text: i18n.ts.objectStorage, to: '/admin/object-storage', active: currentPage.value?.route.name === 'object-storage', }, { - icon: 'ph-lock ph-bold ph-lg', + icon: 'ti ti-lock', text: i18n.ts.security, to: '/admin/security', active: currentPage.value?.route.name === 'security', }, { - icon: 'ph-planet ph-bold ph-lg', + icon: 'ti ti-planet', text: i18n.ts.relays, to: '/admin/relays', active: currentPage.value?.route.name === 'relays', }, { - icon: 'ph-prohibit ph-bold ph-lg', + icon: 'ti ti-ban', text: i18n.ts.instanceBlocking, to: '/admin/instance-block', active: currentPage.value?.route.name === 'instance-block', }, { - icon: 'ph-ghost ph-bold ph-lg', + icon: 'ti ti-ghost', text: i18n.ts.proxyAccount, to: '/admin/proxy-account', active: currentPage.value?.route.name === 'proxy-account', @@ -230,7 +230,12 @@ const menuDef = computed(() => [{ to: '/admin/external-services', active: currentPage.value?.route.name === 'external-services', }, { - icon: 'ph-faders ph-bold ph-lg', + icon: 'ti ti-webhook', + text: 'Webhook', + 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', @@ -238,32 +243,29 @@ const menuDef = computed(() => [{ }, { title: i18n.ts.info, items: [{ - icon: 'ph-database ph-bold ph-lg', + icon: 'ti ti-database', text: i18n.ts.database, to: '/admin/database', active: currentPage.value?.route.name === 'database', }], }]); -watch(narrow.value, () => { - if (currentPage.value?.route.name == null && !narrow.value) { - router.push('/admin/overview'); - } -}); - onMounted(() => { - ro.observe(el.value); - - narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; + if (el.value != null) { + ro.observe(el.value); + narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; + } if (currentPage.value?.route.name == null && !narrow.value) { - router.push('/admin/overview'); + router.replace('/admin/overview'); } }); onActivated(() => { - narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; + if (el.value != null) { + narrow.value = el.value.offsetWidth < NARROW_THRESHOLD; + } if (currentPage.value?.route.name == null && !narrow.value) { - router.push('/admin/overview'); + router.replace('/admin/overview'); } }); @@ -305,25 +307,25 @@ function invite() { function adminLookup(ev: MouseEvent) { os.popupMenu([{ text: i18n.ts.user, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', action: () => { lookupUser(); }, }, { text: `${i18n.ts.user} (${i18n.ts.email})`, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', action: () => { lookupUserByEmail(); }, }, { text: i18n.ts.file, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', action: () => { lookupFile(); }, }, { text: i18n.ts.lookup, - icon: 'ph-planet ph-bold ph-lg', + icon: 'ti ti-world-search', action: () => { lookup(); }, diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index fcb67633f62405cdf79a09a29b28ec93b27be110..e090616b26a2437fd37b4d1640113ffed7c40eeb 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -8,15 +8,23 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> - <MkTextarea v-if="tab === 'block'" v-model="blockedHosts"> - <span>{{ i18n.ts.blockedInstances }}</span> - <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> - </MkTextarea> - <MkTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock"> - <span>{{ i18n.ts.silencedInstances }}</span> - <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> - </MkTextarea> - <MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <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> @@ -36,18 +44,21 @@ 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); @@ -59,15 +70,15 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'block', title: i18n.ts.block, - icon: 'ph-prohibit ph-bold ph-lg', + icon: 'ti ti-ban', }, { key: 'silence', title: i18n.ts.silence, - icon: 'ph-eye-closed ph-bold ph-lg', + icon: 'ti ti-eye-off', }]); definePageMetadata(() => ({ title: i18n.ts.instanceBlocking, - icon: 'ph-prohibit ph-bold ph-lg', + icon: 'ti ti-ban', })); </script> diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue index 4dc8ded7a246a9b859c2f2e121a6c7129aa6120c..c4f2c292e07d22ac978678f0b062e418ddce87c7 100644 --- a/packages/frontend/src/pages/admin/invites.vue +++ b/packages/frontend/src/pages/admin/invites.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="800"> <div class="_gaps_m"> <MkFolder :expanded="false"> - <template #icon><i class="ph-plus ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-plus"></i></template> <template #label>{{ i18n.ts.createInviteCode }}</template> <div class="_gaps_m"> @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local"> <template #label>{{ i18n.ts.expirationDate }}</template> </MkInput> - <MkInput v-model="createCount" type="number"> + <MkInput v-model="createCount" type="number" min="1"> <template #label>{{ i18n.ts.createCount }}</template> </MkInput> <MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton> @@ -115,7 +115,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.invite, - icon: 'ph-user-plus ph-bold ph-lg', + icon: 'ti ti-user-plus', })); </script> diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 46ffb6bbc69f28f551c79d3f5f10946da8a9351a..6297b9a1828e470e13fb80c0532cc48cdee328c8 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -25,12 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only <FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink> <MkInput v-model="tosUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <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="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts.privacyPolicyUrl }}</template> </MkInput> @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkTextarea> <MkInput v-model="inquiryUrl" type="url"> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template> <template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template> </MkInput> @@ -70,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #footer> <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> </template> @@ -144,7 +144,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.moderation, - icon: 'ph-shield ph-bold ph-lg', + icon: 'ti ti-shield', })); </script> diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue index 03d5d6ece127c89cf1e2fdfa545dee658feea9aa..f6f276de53a54bfa85a35a2f40ce379b704b028c 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -8,9 +8,40 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label> <b :class="{ - [$style.logGreen]: ['createRole', 'addCustomEmoji', 'createGlobalAnnouncement', 'createUserAnnouncement', 'createAd', 'createInvitation', 'createAvatarDecoration'].includes(log.type), - [$style.logYellow]: ['markSensitiveDriveFile', 'resetPassword'].includes(log.type), - [$style.logRed]: ['suspend', 'approve', 'deleteRole', 'suspendRemoteInstance', 'deleteGlobalAnnouncement', 'deleteUserAnnouncement', 'deleteCustomEmoji', 'deleteNote', 'deleteDriveFile', 'deleteAd', 'deleteAvatarDecoration'].includes(log.type) + [$style.logGreen]: [ + 'createRole', + 'addCustomEmoji', + 'createGlobalAnnouncement', + 'createUserAnnouncement', + 'createAd', + 'createInvitation', + 'createAvatarDecoration', + 'createSystemWebhook', + 'createAbuseReportNotificationRecipient', + ].includes(log.type), + [$style.logYellow]: [ + 'markSensitiveDriveFile', + 'resetPassword', + 'suspendRemoteInstance', + ].includes(log.type), + [$style.logRed]: [ + 'suspend', + 'approve', + 'deleteRole', + 'deleteGlobalAnnouncement', + 'deleteUserAnnouncement', + 'deleteCustomEmoji', + 'deleteNote', + 'deleteDriveFile', + 'deleteAd', + 'deleteAvatarDecoration', + 'deleteSystemWebhook', + 'deleteAbuseReportNotificationRecipient', + 'deleteAccount', + 'deletePage', + 'deleteFlash', + 'deleteGalleryPost', + ].includes(log.type) }" >{{ i18n.ts._moderationLogTypes[log.type] }}</b> <span v-if="log.type === 'updateUserNote'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> @@ -18,8 +49,8 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="log.type === 'approve'">: @{{ 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="ph-arrow-right ph-bold ph-lg"></i> {{ log.info.roleName }}</span> - <span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ph-prohibit ph-bold ph-lg"></i> {{ log.info.roleName }}</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> + <span v-else-if="log.type === 'unassignRole'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }} <i class="ti ti-equal-not"></i> {{ log.info.roleName }}</span> <span v-else-if="log.type === 'createRole'">: {{ log.info.role.name }}</span> <span v-else-if="log.type === 'updateRole'">: {{ log.info.before.name }}</span> <span v-else-if="log.type === 'deleteRole'">: {{ log.info.role.name }}</span> @@ -41,6 +72,16 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="log.type === 'createAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span> <span v-else-if="log.type === 'updateAvatarDecoration'">: {{ log.info.before.name }}</span> <span v-else-if="log.type === 'deleteAvatarDecoration'">: {{ log.info.avatarDecoration.name }}</span> + <span v-else-if="log.type === 'createSystemWebhook'">: {{ log.info.webhook.name }}</span> + <span v-else-if="log.type === 'updateSystemWebhook'">: {{ log.info.before.name }}</span> + <span v-else-if="log.type === 'deleteSystemWebhook'">: {{ log.info.webhook.name }}</span> + <span v-else-if="log.type === 'createAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span> + <span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span> + <span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span> + <span v-else-if="log.type === 'deleteAccount'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span> + <span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span> + <span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span> + <span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span> </template> <template #icon> <MkAvatar :user="log.user" :class="$style.avatar"/> @@ -115,11 +156,20 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> <template v-else-if="log.type === 'updateRemoteInstanceNote'"> - <div>{{ i18n.ts.user }}: {{ log.info.userId }}</div> <div :class="$style.diff"> <CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/> </div> </template> + <template v-else-if="log.type === 'updateSystemWebhook'"> + <div :class="$style.diff"> + <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> + </div> + </template> + <template v-else-if="log.type === 'updateAbuseReportNotificationRecipient'"> + <div :class="$style.diff"> + <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> + </div> + </template> <details> <summary>raw</summary> diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue index 850d36063efcac95508a3facf113de67c33d7e32..4e0d0f941ea353822f886881ba95216ef27c610d 100644 --- a/packages/frontend/src/pages/admin/modlog.vue +++ b/packages/frontend/src/pages/admin/modlog.vue @@ -60,6 +60,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.moderationLogs, - icon: 'ph-list ph-bold ph-lg-search', + icon: 'ti ti-list-search', })); </script> diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue index 4f362e1814de09e507f902494dc39ad6d17a7bf1..5fddb715cd5c6ef90e69eacd9b3e5c8ecfe500a7 100644 --- a/packages/frontend/src/pages/admin/object-storage.vue +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -40,12 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSplit :minWidth="280"> <MkInput v-model="objectStorageAccessKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>Access key</template> </MkInput> <MkInput v-model="objectStorageSecretKey" type="password"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>Secret key</template> </MkInput> </FormSplit> @@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #footer> <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> </template> @@ -151,7 +151,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.objectStorage, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', })); </script> diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue index 20e0d6e5786189eee7a6dcfda6a4434e60ec53ec..a92034f2d7c601c091bb69255c56e41bd98d132e 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -99,7 +99,7 @@ function save() { const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-check ph-bold ph-lg', + icon: 'ti ti-check', text: i18n.ts.save, handler: save, }]); @@ -108,6 +108,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.other, - icon: 'ph-faders ph-bold ph-lg', + icon: 'ti ti-adjustments', })); </script> diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue index 3a3550c6c08706f6f1db678a7e6aae32e0cfdb9d..ea01d073eaa7eebc462b75211b96c189d33f7493 100644 --- a/packages/frontend/src/pages/admin/overview.federation.vue +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="!fetching" class="items"> <div class="item _panel sub"> - <div class="icon"><i class="ph-globe-hemisphere-west ph-bold ph-lg-download"></i></div> + <div class="icon"><i class="ti ti-world-download"></i></div> <div class="body"> <div class="value"> {{ number(federationSubActive) }} @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div class="item _panel pub"> - <div class="icon"><i class="ph-globe-hemisphere-west ph-bold ph-lg-upload"></i></div> + <div class="icon"><i class="ti ti-world-upload"></i></div> <div class="body"> <div class="value"> {{ number(federationPubActive) }} @@ -193,4 +193,3 @@ onMounted(async () => { } } </style> - diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index 27ae52c32c1fbdf83aca06dc6cb050ddc35e59a2..37399a5724c808f093a91f6a93aab19f80154c22 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="fetching"/> <div v-else :class="$style.root"> <div class="item _panel users"> - <div class="icon"><i class="ph-users ph-bold ph-lg"></i></div> + <div class="icon"><i class="ti ti-users"></i></div> <div class="body"> <div class="value"> <MkNumber :value="stats.originalUsersCount" style="margin-right: 0.5em;"/> @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div class="item _panel notes"> - <div class="icon"><i class="ph-pencil-simple ph-bold ph-lg"></i></div> + <div class="icon"><i class="ti ti-pencil"></i></div> <div class="body"> <div class="value"> <MkNumber :value="stats.originalNotesCount" style="margin-right: 0.5em;"/> @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div class="item _panel instances"> - <div class="icon"><i class="ph-planet ph-bold ph-lg"></i></div> + <div class="icon"><i class="ti ti-planet"></i></div> <div class="body"> <div class="value"> <MkNumber :value="stats.instances" style="margin-right: 0.5em;"/> @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div class="item _panel online"> - <div class="icon"><i class="ph-broadcast ph-bold ph-lg"></i></div> + <div class="icon"><i class="ti ti-access-point"></i></div> <div class="body"> <div class="value"> <MkNumber :value="onlineUsersCount" style="margin-right: 0.5em;"/> diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index bf766600e5e4afba9711c7ba6936d64850ff6b2b..1de4dc0dc88ff01a310e9570c8a6031aadc592df 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -186,7 +186,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.dashboard, - icon: 'ph-gauge ph-bold ph-lg', + icon: 'ti ti-dashboard', })); </script> diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue index 59fd5911d477b7b234769dd688248e81c39e8409..81db9f1da9c213a4b75ec7fff909c163892c50a8 100644 --- a/packages/frontend/src/pages/admin/proxy-account.vue +++ b/packages/frontend/src/pages/admin/proxy-account.vue @@ -66,6 +66,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.proxyAccount, - icon: 'ph-ghost ph-bold ph-lg', + 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 f7b4b27a68c356352d19501b7991c30f03c5159a..8d3fe35320e31917340bf530cd9b87e96e560484 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <MkFolder :defaultOpen="true" :max-height="250"> - <template #icon><i class="ph-warning ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-alert-triangle"></i></template> <template #label>Errored instances</template> <template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template> diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue index ba6911a943d85ef4cd68b03536cecf7271cc6e13..8d77d927d7bfc04310794648d1611f40af7f385e 100644 --- a/packages/frontend/src/pages/admin/queue.vue +++ b/packages/frontend/src/pages/admin/queue.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XQueue v-if="tab === 'deliver'" domain="deliver"/> <XQueue v-else-if="tab === 'inbox'" domain="inbox"/> <br> - <MkButton @click="promoteAllQueues"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.retryAllQueuesNow }}</MkButton> + <MkButton @click="promoteAllQueues"><i class="ti ti-reload"></i> {{ i18n.ts.retryAllQueuesNow }}</MkButton> </MkSpacer> </MkStickyContainer> </template> @@ -53,7 +53,7 @@ function promoteAllQueues() { const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-arrow-square-out ph-bold ph-lg', + icon: 'ti ti-external-link', text: i18n.ts.dashboard, handler: () => { window.open(config.url + '/queue', '_blank', 'noopener'); @@ -70,6 +70,6 @@ const headerTabs = computed(() => [{ definePageMetadata(() => ({ title: i18n.ts.jobQueue, - icon: 'ph-clock ph-bold ph-lg-play', + icon: 'ti ti-clock-play', })); </script> diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 6ff0d8bd225aebf033ee83ae33d13833a617aebf..04982eea1fabdd1944ecec2f241346379ea7f5da 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;"> <div>{{ relay.inbox }}</div> <div style="margin: 8px 0;"> - <i v-if="relay.status === 'accepted'" class="ph-check ph-bold ph-lg" :class="$style.icon" style="color: var(--success);"></i> - <i v-else-if="relay.status === 'rejected'" class="ph-prohibit ph-bold ph-lg" :class="$style.icon" style="color: var(--error);"></i> - <i v-else class="ph-clock ph-bold ph-lg" :class="$style.icon"></i> + <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i> + <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i> + <i v-else class="ti ti-clock" :class="$style.icon"></i> <span>{{ i18n.ts._relayStatus[relay.status] }}</span> </div> - <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton> + <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> </div> </div> </MkSpacer> @@ -77,7 +77,7 @@ refresh(); const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.addRelay, handler: addRelay, }]); @@ -86,7 +86,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.relays, - icon: 'ph-planet ph-bold ph-lg', + icon: 'ti ti-planet', })); </script> diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue index e6023d2f2ada9f8a90f71e33dd46f7ee2c4973d6..60f06d50badb2f284e1965dab9b39b8e87470a3c 100644 --- a/packages/frontend/src/pages/admin/roles.edit.vue +++ b/packages/frontend/src/pages/admin/roles.edit.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #footer> <div :class="$style.footer"> <MkSpacer :contentMax="600" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> </template> @@ -89,7 +89,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: role.value ? `${i18n.ts._role.edit}: ${role.value.name}` : i18n.ts._role.new, - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badge', })); </script> diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 99a31d5157418bb13e83553098b423f299ed6fe7..8200244cd79b915de4bbd15e0e03d821562766b8 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkSelect v-model="rolePermission" :readonly="readonly"> - <template #label><i class="ph-shield ph-bold ph-lg-lock"></i> {{ i18n.ts._role.permission }}</template> + <template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template> <template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template> <option value="normal">{{ i18n.ts.normalUser }}</option> <option value="moderator">{{ i18n.ts.moderator }}</option> @@ -39,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSelect> <MkSelect v-model="role.target" :readonly="readonly"> - <template #label><i class="ph-users ph-bold ph-lg"></i> {{ i18n.ts._role.assignTarget }}</template> + <template #label><i class="ti ti-users"></i> {{ i18n.ts._role.assignTarget }}</template> <template #caption><div v-html="i18n.ts._role.descriptionOfAssignTarget.replaceAll('\n', '<br>')"></div></template> <option value="manual">{{ i18n.ts._role.manual }}</option> <option value="conditional">{{ i18n.ts._role.conditional }}</option> @@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label><i class="ph-scroll ph-bold ph-lg"></i> {{ i18n.ts._role.policies }}</template> <div class="_gaps_s"> <MkInput v-model="q" type="search"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> </MkInput> <MkFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])"> @@ -418,6 +418,26 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])"> + <template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template> + <template #suffix> + <span v-if="role.policies.canUpdateBioMedia.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.canUpdateBioMedia.value ? i18n.ts.yes : i18n.ts.no }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateBioMedia)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkSwitch v-model="role.policies.canUpdateBioMedia.value" :disabled="role.policies.canUpdateBioMedia.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + <MkRange v-model="role.policies.canUpdateBioMedia.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.pinMax, 'pinLimit'])"> <template #label>{{ i18n.ts._role._options.pinMax }}</template> <template #suffix> @@ -665,9 +685,9 @@ const rolePermission = computed({ const q = ref(''); function getPriorityIcon(option) { - if (option.priority === 2) return 'ph-arrow-up ph-bold ph-lg'; - if (option.priority === 1) return 'ph-arrow-up ph-bold ph-lg'; - return 'ph-circle ph-bold ph-lg'; + if (option.priority === 2) return 'ti ti-arrows-up'; + if (option.priority === 1) return 'ti ti-arrow-narrow-up'; + return 'ti ti-point'; } function matchQuery(keywords: string[]): boolean { diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 593183ea07a5f61b47fab330297e8513f3e8a8f2..6bce60cfb955b171aa46c0a3f160a838cf6bfed1 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="700"> <div class="_gaps"> <div class="_buttons"> - <MkButton primary rounded @click="edit"><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts.edit }}</MkButton> - <MkButton danger rounded @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton primary rounded @click="edit"><i class="ti ti-pencil"></i> {{ i18n.ts.edit }}</MkButton> + <MkButton danger rounded @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> <MkFolder> - <template #icon><i class="ph-info ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-info-circle"></i></template> <template #label>{{ i18n.ts.info }}</template> <XEditor :modelValue="role" readonly/> </MkFolder> <MkFolder v-if="role.target === 'manual'" defaultOpen> - <template #icon><i class="ph-users ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-users"></i></template> <template #label>{{ i18n.ts.users }}</template> <template #suffix>{{ role.usersCount }}</template> <div class="_gaps"> - <MkButton primary rounded @click="assign"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton> + <MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <MkPagination :pagination="usersPagination" :displayLimit="50"> <template #empty> @@ -40,8 +40,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA :class="$style.userItemMainBody" :to="`/admin/user/${item.user.id}`"> <MkUserCardMini :user="item.user"/> </MkA> - <button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button> - <button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.userToggle" @click="toggleItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button> </div> <div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub"> <div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div> @@ -149,7 +149,7 @@ async function assign() { async function unassign(user, ev) { os.popupMenu([{ text: i18n.ts.unassign, - icon: 'ph-x ph-bold ph-lg', + icon: 'ti ti-x', danger: true, action: async () => { await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id }); @@ -172,7 +172,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: `${i18n.ts.role}: ${role.name}`, - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badge', })); </script> diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index f104f9a00d08f2ec92cef4c0f47128606acfe15b..0a8bd0e898e5ebc3819f7137ff4a8258d0f13062 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts._role.baseRole }}</template> <div class="_gaps_s"> <MkInput v-model="baseRoleQ" type="search"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> </MkInput> <MkFolder v-if="matchQuery([i18n.ts._role._options.rateLimitFactor, 'rateLimitFactor'])"> @@ -153,6 +153,14 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBioMedia, 'canUpdateBioMedia'])"> + <template #label>{{ i18n.ts._role._options.canUpdateBioMedia }}</template> + <template #suffix>{{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }}</template> + <MkSwitch v-model="policies.canUpdateBioMedia"> + <template #label>{{ i18n.ts.enable }}</template> + </MkSwitch> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])"> <template #label>{{ i18n.ts._role._options.pinMax }}</template> <template #suffix>{{ policies.pinLimit }}</template> @@ -228,7 +236,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton> </div> </MkFolder> - <MkButton primary rounded @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._role.new }}</MkButton> + <MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton> <div class="_gaps_s"> <MkFoldableSection> <template #header>{{ i18n.ts._role.manualRoles }}</template> @@ -263,7 +271,7 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { instance } from '@/instance.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'; @@ -287,6 +295,7 @@ async function updateBaseRole() { await os.apiWithDialog('admin/roles/update-default-policies', { policies, }); + fetchInstance(true); } function create() { @@ -299,7 +308,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.roles, - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badges', })); </script> diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 887d3c14469a7e70ea40d9e3726f503bfcd7ab22..2e80622d7a146f4b324b5ca2cf4d3c23f6b30033 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSuspense :p="init"> <div class="_gaps_m"> <MkFolder> - <template #icon><i class="ph-shield ph-bold ph-lg"></i></template> + <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> @@ -35,21 +35,21 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>Use Verifymail.io API</template> </MkSwitch> <MkInput v-model="verifymailAuthKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <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="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>TrueMail API Instance</template> </MkInput> <MkInput v-model="truemailAuthKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>TrueMail API Auth Key</template> </MkInput> - <MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </MkFolder> @@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTextarea v-model="bannedEmailDomains"> <template #label>Banned Email Domains List</template> </MkTextarea> - <MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </MkFolder> @@ -149,6 +149,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.security, - icon: 'ph-lock ph-bold ph-lg', + icon: 'ti ti-lock', })); </script> diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue index db2bb56eac413eef5d65636a462c03c89d8fbe40..4788c16830a4fda8911d966767f31b4c85dbcc2b 100644 --- a/packages/frontend/src/pages/admin/server-rules.vue +++ b/packages/frontend/src/pages/admin/server-rules.vue @@ -23,16 +23,16 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.item"> <div :class="$style.itemHeader"> <div :class="$style.itemNumber" v-text="String(index + 1)"/> - <span :class="$style.itemHandle"><i class="ph-list ph-bold ph-lg"/></span> - <button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ph-x ph-bold ph-lg"></i></button> + <span :class="$style.itemHandle"><i class="ti ti-menu"/></span> + <button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ti ti-x"></i></button> </div> <MkInput v-model="serverRules[index]"/> </div> </template> </Sortable> <div :class="$style.commands"> - <MkButton rounded @click="serverRules.push('')"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> </MkSpacer> @@ -69,7 +69,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.serverRules, - icon: 'ph-check ph-bold ph-lg', + icon: 'ti ti-checkbox', })); </script> diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index ebbe252cb76a814260b7b9632943bdfdb2d0a569..90ac259c5328a048b6f98728d833df016791325f 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkInput v-model="maintainerEmail" type="email"> - <template #prefix><i class="ph-envelope ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-mail"></i></template> <template #label>{{ i18n.ts.maintainerEmail }}</template> </MkInput> </FormSplit> <MkInput v-model="repositoryUrl" type="url"> <template #label>{{ i18n.ts.repositoryUrl }}</template> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #caption>{{ i18n.ts.repositoryUrlDescription }}</template> </MkInput> @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="impressumUrl" type="url"> <template #label>{{ i18n.ts.impressumUrl }}</template> - <template #prefix><i class="ph-link ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-link"></i></template> <template #caption>{{ i18n.ts.impressumDescription }}</template> </MkInput> @@ -89,12 +89,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="enableServiceWorker"> <MkInput v-model="swPublicKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>Public key</template> </MkInput> <MkInput v-model="swPrivateKey"> - <template #prefix><i class="ph-key ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-key"></i></template> <template #label>Private key</template> </MkInput> </template> @@ -201,7 +201,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #footer> <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> - <MkButton primary rounded @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </MkSpacer> </div> </template> @@ -325,7 +325,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.general, - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', })); </script> diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue new file mode 100644 index 0000000000000000000000000000000000000000..0c07122af33a4032f9f5bef0a4a3ae0f0cfbb477 --- /dev/null +++ b/packages/frontend/src/pages/admin/system-webhook.item.vue @@ -0,0 +1,117 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :class="$style.main"> + <span :class="$style.icon"> + <i v-if="!entity.isActive" class="ti ti-player-pause"/> + <i v-else-if="entity.latestStatus === null" class="ti ti-circle"/> + <i + v-else-if="[200, 201, 204].includes(entity.latestStatus)" + class="ti ti-check" + :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"> + <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> +</template> + +<script lang="ts" setup> +import { entities } from 'misskey-js'; +import { toRefs } from 'vue'; + +const emit = defineEmits<{ + (ev: 'edit', value: entities.SystemWebhook): void; + (ev: 'delete', value: entities.SystemWebhook): void; +}>(); + +const props = defineProps<{ + entity: entities.SystemWebhook; +}>(); + +const { entity } = toRefs(props); + +function onEditClick() { + emit('edit', entity.value); +} + +function onDeleteClick() { + emit('delete', entity.value); +} + +</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 new file mode 100644 index 0000000000000000000000000000000000000000..7a40eec94494402105317836c6323f6b472b5148 --- /dev/null +++ b/packages/frontend/src/pages/admin/system-webhook.vue @@ -0,0 +1,96 @@ +<!-- +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="900"> + <div class="_gaps_m"> + <MkButton :class="$style.linkButton" full @click="onCreateWebhookClicked"> + {{ i18n.ts._webhookSettings.createWebhook }} + </MkButton> + + <FormSection> + <div class="_gaps"> + <XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/> + </div> + </FormSection> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, onMounted, ref } from 'vue'; +import { entities } from 'misskey-js'; +import XItem from './system-webhook.item.vue'; +import FormSection from '@/components/form/section.vue'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { i18n } from '@/i18n.js'; +import XHeader from '@/pages/admin/_header_.vue'; +import MkButton from '@/components/MkButton.vue'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js'; +import * as os from '@/os.js'; + +const webhooks = ref<entities.SystemWebhook[]>([]); + +const headerActions = computed(() => []); +const headerTabs = computed(() => []); + +async function onCreateWebhookClicked() { + await showSystemWebhookEditorDialog({ + mode: 'create', + }); + + await fetchWebhooks(); +} + +async function onEditButtonClicked(webhook: entities.SystemWebhook) { + await showSystemWebhookEditorDialog({ + mode: 'edit', + id: webhook.id, + }); + + await fetchWebhooks(); +} + +async function onDeleteButtonClicked(webhook: entities.SystemWebhook) { + const result = await os.confirm({ + type: 'warning', + title: i18n.ts._webhookSettings.deleteConfirm, + }); + if (!result.canceled) { + await misskeyApi('admin/system-webhook/delete', { + id: webhook.id, + }); + await fetchWebhooks(); + } +} + +async function fetchWebhooks() { + const result = await misskeyApi('admin/system-webhook/list', {}); + webhooks.value = result.sort((a, b) => a.id.localeCompare(b.id)); +} + +onMounted(async () => { + await fetchWebhooks(); +}); + +definePageMetadata(() => ({ + title: 'SystemWebhook', + icon: 'ti ti-webhook', +})); +</script> + +<style module lang="scss"> +.linkButton { + text-align: left; + padding: 10px 18px; +} +</style> diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index 80696c8ceaef162e549a5d0d3af946a7bb7c818b..e99dcfa489da4908b8e4ee955184ba0b3bda59e9 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -34,11 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSelect> </div> <div :class="$style.inputs"> - <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false" @update:modelValue="$refs.users.reload()"> + <MkInput v-model="searchUsername" style="flex: 1;" type="text" :spellcheck="false"> <template #prefix>@</template> <template #label>{{ i18n.ts.username }}</template> </MkInput> - <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> + <MkInput v-model="searchHost" style="flex: 1;" type="text" :spellcheck="false" :disabled="pagination.params.origin === 'local'"> <template #prefix>@</template> <template #label>{{ i18n.ts.host }}</template> </MkInput> @@ -121,17 +121,17 @@ function show(user) { } const headerActions = computed(() => [{ - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', text: i18n.ts.search, handler: searchUser, }, { asFullButton: true, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.addUser, handler: addUser, }, { asFullButton: true, - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', text: i18n.ts.lookup, handler: lookupUser, }]); @@ -140,7 +140,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.users, - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', })); </script> diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue index c6373e8d6048fe5b8dd152f97c8d12274e41f9e9..b31807f9f5dee8e4a3c36254166efc24be4f0c1d 100644 --- a/packages/frontend/src/pages/ads.vue +++ b/packages/frontend/src/pages/ads.vue @@ -22,7 +22,7 @@ import { instance } from '@/instance.js'; definePageMetadata(() => ({ title: i18n.ts.ads, - icon: 'ph-flag ph-bold ph-lg', + icon: 'ti ti-ad', })); </script> diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue index fc4ec19b4ee2a3ba774ef3b5c322b7a374d85420..802a6bf399cd6bfa598874e4cdc5e7b536178656 100644 --- a/packages/frontend/src/pages/announcement.vue +++ b/packages/frontend/src/pages/announcement.vue @@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only mode="out-in" > <div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement"> - <div v-if="announcement.forYou" :class="$style.forYou"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.forYou }}</div> + <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div> <div :class="$style.header"> <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> <span style="margin-right: 0.5em;"> - <i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i> - <i v-else-if="announcement.icon === 'warning'" class="ph-warning-circle ph-bold ph-lg" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ph-seal-warning ph-bold ph-lg" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ph-check-circle ph-bold ph-lg" style="color: var(--success);"></i> + <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> </span> <Mfm :text="announcement.title"/> </div> @@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer"> - <MkButton primary @click="read(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton> + <MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> </div> </div> <MkError v-else-if="error" @retry="fetch()"/> @@ -104,11 +104,20 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements, - icon: 'ph-megaphone ph-bold ph-lg', + icon: 'ti ti-speakerphone', })); </script> <style lang="scss" module> +.fadeEnterActive, +.fadeLeaveActive { + transition: opacity 0.125s ease; +} +.fadeEnterFrom, +.fadeLeaveTo { + opacity: 0; +} + .announcement { padding: 16px; } diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index 44dd2e90d844eb5c0cce0dcfda08878d1c70e3da..a9bcfee80351f0031636ee18ee4e446d82c8f080 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo> <MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps"> <section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement"> - <div v-if="announcement.forYou" :class="$style.forYou"><i class="ph-push-pin ph-bold ph-lg"></i> {{ i18n.ts.forYou }}</div> + <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div> <div :class="$style.header"> <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span> <span style="margin-right: 0.5em;"> - <i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i> - <i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i> + <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> </span> <MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA> </div> @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkA> </div> <div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer"> - <MkButton primary @click="read(announcement)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.gotIt }}</MkButton> + <MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> </div> </section> </MkPagination> @@ -104,16 +104,16 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'current', title: i18n.ts.currentAnnouncements, - icon: 'ph-fire ph-bold ph-lg', + icon: 'ti ti-flare', }, { key: 'past', title: i18n.ts.pastAnnouncements, - icon: 'ph-circle ph-bold ph-lg', + icon: 'ti ti-point', }]); definePageMetadata(() => ({ title: i18n.ts.announcements, - icon: 'ph-megaphone ph-bold ph-lg', + icon: 'ti ti-speakerphone', })); </script> diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 3e8deff71183ebc807349c7811abd7a8c90e14d4..e947ec9ba5dbe7206b3ce6cf13bfdbe92ee857bd 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="800"> - <div ref="rootEl" v-hotkey.global="keymap"> + <div ref="rootEl"> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div :class="$style.tl"> <MkTimeline @@ -44,9 +44,6 @@ const antenna = ref<Misskey.entities.Antenna | null>(null); const queue = ref(0); const rootEl = shallowRef<HTMLElement>(); const tlEl = shallowRef<InstanceType<typeof MkTimeline>>(); -const keymap = computed(() => ({ - 't': focus, -})); function queueUpdated(q) { queue.value = q; @@ -80,11 +77,11 @@ watch(() => props.antennaId, async () => { }, { immediate: true }); const headerActions = computed(() => antenna.value ? [{ - icon: 'ph-calendar ph-bold ph-lg', + icon: 'ti ti-calendar-time', text: i18n.ts.jumpToSpecifiedDate, handler: timetravel, }, { - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts.settings, handler: settings, }] : []); @@ -93,7 +90,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: antenna.value ? antenna.value.name : i18n.ts.antennas, - icon: 'ph-flying-saucer ph-bold ph-lg', + icon: 'ti ti-antenna', })); </script> diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue index 4d0cb2897f261a37a16ea23fb729e9a7851f228c..30f12a8fb39bd143888e7955ea9af7301200e88d 100644 --- a/packages/frontend/src/pages/api-console.vue +++ b/packages/frontend/src/pages/api-console.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> <MkButton primary :disabled="sending" @click="send"> <template v-if="sending"><MkEllipsis/></template> - <template v-else><i class="ph-paper-plane-tilt ph-bold ph-lg"></i> Send</template> + <template v-else><i class="ti ti-send"></i> Send</template> </MkButton> </div> <div v-if="res"> @@ -89,6 +89,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: 'API console', - icon: 'ph-terminal-window ph-bold ph-lg-2', + icon: 'ti ti-terminal-2', })); </script> diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue index cb735a26d803ed7723475d1e890581fadef73bd4..b9f45e219c110598fccf92a9728d9e36c1df3a34 100644 --- a/packages/frontend/src/pages/auth.vue +++ b/packages/frontend/src/pages/auth.vue @@ -120,7 +120,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts._auth.shareAccessTitle, - icon: 'ph-squares-four ph-bold ph-lg', + icon: 'ti ti-apps', })); </script> diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue index 41c87d362560eff87d8321705077035e0cbacc50..ad9ec3c4eecec5f4c20b2b2fd3be48793a950e88 100644 --- a/packages/frontend/src/pages/avatar-decorations.vue +++ b/packages/frontend/src/pages/avatar-decorations.vue @@ -23,8 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.imageUrl }}</template> </MkInput> <div class="buttons _buttons"> - <MkButton class="button" inline primary @click="save(avatarDecoration)"><i class="ph-floppy ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="avatarDecoration.id != null" class="button" inline danger @click="del(avatarDecoration)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <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> </div> </MkFolder> @@ -87,7 +87,7 @@ load(); const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.add, handler: add, }]); @@ -96,6 +96,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.avatarDecorations, - icon: 'ph-sparkle ph-bold ph-lg', + icon: 'ti ti-sparkles', })); </script> diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index ea4374dd3c5351a395edae1b993fa590cf1a8862..d3f4a65b893256e5e3b9a26ceddf8d723c33a85c 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -29,10 +29,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> <div> - <MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._channel.setBanner }}</MkButton> + <MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton> <div v-else-if="bannerUrl"> <img :src="bannerUrl" style="width: 100%;"/> - <MkButton @click="removeBannerImage()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts._channel.removeBanner }}</MkButton> + <MkButton @click="removeBannerImage()"><i class="ti ti-trash"></i> {{ i18n.ts._channel.removeBanner }}</MkButton> </div> </div> @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.pinnedNotes }}</template> <div class="_gaps"> - <MkButton primary rounded @click="addPinnedNote()"><i class="ph-plus ph-bold ph-lg"></i></MkButton> + <MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton> <Sortable v-model="pinnedNotes" @@ -50,9 +50,9 @@ SPDX-License-Identifier: AGPL-3.0-only > <template #item="{element,index}"> <div :class="$style.pinnedNote"> - <button class="_button" :class="$style.pinnedNoteHandle"><i class="ph-list ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.pinnedNoteHandle"><i class="ti ti-menu"></i></button> {{ element.id }} - <button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ti ti-x"></i></button> </div> </template> </Sortable> @@ -60,8 +60,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <div class="_buttons"> - <MkButton primary @click="save()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton> - <MkButton v-if="channelId" danger @click="archive()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.archive }}</MkButton> + <MkButton primary @click="save()"><i class="ti ti-device-floppy"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton> + <MkButton v-if="channelId" danger @click="archive()"><i class="ti ti-trash"></i> {{ i18n.ts.archive }}</MkButton> </div> </div> </MkSpacer> @@ -204,7 +204,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create, - icon: 'ph-television ph-bold ph-lg', + icon: 'ti ti-device-tv', })); </script> diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 7f49c984a86429aee61a70fbc86bd8ea70d8f060..e922599642846699bf0825f9a9be63199afa1695 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="channel && tab === 'overview'" key="overview" class="_gaps"> <div class="_panel" :class="$style.bannerContainer"> <XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/> - <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton> - <MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ph-star ph-bold ph-lg"></i></MkButton> + <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton> + <MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton> <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner"> <div :class="$style.bannerStatus"> - <div><i class="ph-users ph-bold ph-lg"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> - <div><i class="ph-pencil-simple ph-bold ph-lg"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> + <div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div> + <div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div> </div> <div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div> <div :class="$style.bannerFade"></div> @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkFoldableSection> - <template #header><i class="ph-push-pin ph-bold ph-lg" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template> + <template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template> <div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps"> <MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/> </div> @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <div> <MkInput v-model="searchQuery" @enter="search()"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> </MkInput> <MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton> </div> @@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.footer"> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16"> <div class="_buttonsCenter"> - <MkButton inline rounded primary gradate @click="openPostForm()"><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts.postToTheChannel }}</MkButton> + <MkButton inline rounded primary gradate @click="openPostForm()"><i class="ti ti-pencil"></i> {{ i18n.ts.postToTheChannel }}</MkButton> </div> </MkSpacer> </div> @@ -93,7 +93,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import { PageHeaderItem } from '@/types/page-header.js'; import { isSupportShare } from '@/scripts/navigator.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { miLocalStorage } from '@/local-storage.js'; import { useRouter } from '@/router/supplier.js'; import { deepMerge } from '@/scripts/merge.js'; @@ -229,7 +229,7 @@ const headerActions = computed(() => { }]; headerItems.push({ - icon: 'ph-share-network ph-bold ph-lg', + icon: 'ti ti-link', text: i18n.ts.copyUrl, handler: async (): Promise<void> => { if (!channel.value) { @@ -243,7 +243,7 @@ const headerActions = computed(() => { if (isSupportShare()) { headerItems.push({ - icon: 'ph-share-network ph-bold ph-lg', + icon: 'ti ti-share', text: i18n.ts.share, handler: async (): Promise<void> => { if (!channel.value) { @@ -262,7 +262,7 @@ const headerActions = computed(() => { if (($i && $i.id === channel.value.userId) || iAmModerator) { headerItems.push({ - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts.edit, handler: edit, }); @@ -277,24 +277,24 @@ const headerActions = computed(() => { const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', }, { key: 'timeline', title: i18n.ts.timeline, - icon: 'ph-house ph-bold ph-lg', + icon: 'ti ti-home', }, { key: 'featured', title: i18n.ts.featured, - icon: 'ph-lightning ph-bold ph-lg', + icon: 'ti ti-bolt', }, { key: 'search', title: i18n.ts.search, - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', }]); definePageMetadata(() => ({ title: channel.value ? channel.value.name : i18n.ts.channel, - icon: 'ph-television ph-bold ph-lg', + icon: 'ti ti-device-tv', })); </script> diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index 253b272d2a481c525872763e897565ed465abb21..bde1650754abc95d12181aa6c5fddad6988abfc4 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="tab === 'search'" key="search"> <div class="_gaps"> <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> </MkInput> <MkRadios v-model="searchType" @update:modelValue="search()"> <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option> @@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkPagination> </div> <div v-else-if="tab === 'owned'" key="owned"> - <MkButton class="new" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton> + <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkPagination v-slot="{items}" :pagination="ownedPagination"> <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/> </MkPagination> @@ -125,7 +125,7 @@ function create() { } const headerActions = computed(() => [{ - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.create, handler: create, }]); @@ -133,27 +133,27 @@ const headerActions = computed(() => [{ const headerTabs = computed(() => [{ key: 'search', title: i18n.ts.search, - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', }, { key: 'featured', title: i18n.ts._channel.featured, - icon: 'ph-shooting-star ph-bold ph-lg', + icon: 'ti ti-comet', }, { key: 'favorites', title: i18n.ts.favorites, - icon: 'ph-star ph-bold ph-lg', + icon: 'ti ti-star', }, { key: 'following', title: i18n.ts._channel.following, - icon: 'ph-eye ph-bold ph-lg', + icon: 'ti ti-eye', }, { key: 'owned', title: i18n.ts._channel.owned, - icon: 'ph-pencil-simple-line ph-bold ph-lg', + icon: 'ti ti-edit', }]); definePageMetadata(() => ({ title: i18n.ts.channel, - icon: 'ph-television ph-bold ph-lg', + icon: 'ti ti-device-tv', })); </script> diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue index 679fb67d257f0ac5a2535b3a4a553a53dbe99411..9e9b5e8688a293dbf19e9b62796cbd90848b39f9 100644 --- a/packages/frontend/src/pages/clicker.vue +++ b/packages/frontend/src/pages/clicker.vue @@ -18,7 +18,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; definePageMetadata(() => ({ title: 'ðŸªðŸ‘ˆ', - icon: 'ph-cookie ph-bold ph-lg', + icon: 'ti ti-cookie', })); </script> diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 9c42d2d3b8abb17a393c50bda0c96d79f2987c67..506d906683f5e654b9b9b2fac6327cbbd4390605 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else>({{ i18n.ts.noDescription }})</div> <div> - <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ph-heart ph-bold pd-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> - <MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ph-heart ph-bold pd-lg"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> + <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts.favorite" asLike rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton> </div> </div> <div :class="$style.user"> @@ -43,7 +43,7 @@ import { url } from '@/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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ clipId: string, @@ -94,7 +94,7 @@ async function unfavorite() { } const headerActions = computed(() => clip.value && isOwned.value ? [{ - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.edit, handler: async (): Promise<void> => { const { canceled, result } = await os.form(clip.value.name, { @@ -127,14 +127,14 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ clipsCache.delete(); }, }, ...(clip.value.isPublic ? [{ - icon: 'ph-share-network ph-bold ph-lg', + 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: 'ph-share-network ph-bold ph-lg', + icon: 'ti ti-share', text: i18n.ts.share, handler: async (): Promise<void> => { navigator.share({ @@ -144,7 +144,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ }); }, }] : []), { - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', text: i18n.ts.delete, danger: true, handler: async (): Promise<void> => { @@ -164,7 +164,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ definePageMetadata(() => ({ title: clip.value ? clip.value.name : i18n.ts.clip, - icon: 'ph-paperclip ph-bold ph-lg', + icon: 'ti ti-paperclip', })); </script> diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue index b6cfebf2296def417fca2709b2b4ae4bf5254d27..1f2bee5a778a006049ef940cd97c2c61c6316400 100644 --- a/packages/frontend/src/pages/contact.vue +++ b/packages/frontend/src/pages/contact.vue @@ -7,18 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only <MkStickyContainer> <template #header><MkPageHeader/></template> <MkSpacer :contentMax="600" :marginMin="20"> - <div class="_gaps"> - <MkKeyValue> - <template #key>{{ i18n.ts.inquiry }}</template> + <div class="_gaps_m"> + <MkKeyValue :copy="instance.maintainerName"> + <template #key>{{ i18n.ts.administrator }}</template> <template #value> - <MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink> + <template v-if="instance.maintainerName">{{ instance.maintainerName }}</template> + <span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span> </template> </MkKeyValue> - - <MkKeyValue> - <template #key>{{ i18n.ts.email }}</template> + <MkKeyValue :copy="instance.maintainerEmail"> + <template #key>{{ i18n.ts.contact }}</template> + <template #value> + <template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template> + <span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span> + </template> + </MkKeyValue> + <MkKeyValue :copy="instance.inquiryUrl"> + <template #key>{{ i18n.ts.inquiry }}</template> <template #value> - <div>{{ instance.maintainerEmail }}</div> + <MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink> + <span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span> </template> </MkKeyValue> </div> @@ -28,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { i18n } from '@/i18n.js'; -import { definePageMetadata } from '@/scripts/page-metadata.js'; import { instance } from '@/instance.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkLink from '@/components/MkLink.vue'; definePageMetadata(() => ({ title: i18n.ts.inquiry, - icon: 'ph-question ph-bold ph-lg', + icon: 'ti ti-help-circle', })); </script> diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 9357735c82a4320336a7d6b18bf6e40d11ae01dd..8904096875370440b57796f63996eaa74e061dec 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="ogwlenmc"> <div v-if="tab === 'local'" class="local"> <MkInput v-model="query" :debounce="true" type="search" autocapitalize="off"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.search }}</template> </MkInput> <MkSwitch v-model="selectMode" style="margin: 8px 0;"> @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'remote'" class="remote"> <FormSplit> <MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-search"></i></template> <template #label>{{ i18n.ts.search }}</template> </MkInput> <MkInput v-model="host" :debounce="true"> @@ -132,18 +132,19 @@ const toggleSelect = (emoji) => { }; const add = async (ev: MouseEvent) => { - os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { }, { done: result => { if (result.created) { emojisPaginationComponent.value.prepend(result.created); } }, - }, 'closed'); + closed: () => dispose(), + }); }; const edit = (emoji) => { - os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { emoji: emoji, }, { done: result => { @@ -156,7 +157,8 @@ const edit = (emoji) => { emojisPaginationComponent.value.removeItem(emoji.id); } }, - }, 'closed'); + closed: () => dispose(), + }); }; const importEmoji = (emoji) => { @@ -172,7 +174,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => { }, { text: i18n.ts.import, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', action: () => { importEmoji(emoji); }, }, { @@ -188,7 +190,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => { const menu = (ev: MouseEvent) => { os.popupMenu([{ - icon: 'ph-download ph-bold ph-lg', + icon: 'ti ti-download', text: i18n.ts.export, action: async () => { misskeyApi('export-custom-emojis', { @@ -206,7 +208,7 @@ const menu = (ev: MouseEvent) => { }); }, }, { - icon: 'ph-upload ph-bold ph-lg', + icon: 'ti ti-upload', text: i18n.ts.import, action: async () => { const file = await selectFile(ev.currentTarget ?? ev.target); @@ -302,11 +304,11 @@ const delBulk = async () => { const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.addEmoji, handler: add, }, { - icon: 'ph-dots-three ph-bold ph-lg', + icon: 'ti ti-dots', handler: menu, }]); diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 872dd4d5cfe4b07677ca6bdb849aec0ec438e5df..4ed2a676788f251ddf7015bd4ee23ab3077f68f2 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -14,34 +14,40 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.fileQuickActionsRoot"> <button class="_button" :class="$style.fileNameEditBtn" @click="rename()"> <h2 class="_nowrap" :class="$style.fileName">{{ file.name }}</h2> - <i class="ph-pencil-simple ph-bold ph-lg" :class="$style.fileNameEditIcon"></i> + <i class="ti ti-pencil" :class="$style.fileNameEditIcon"></i> </button> <div :class="$style.fileQuickActionsOthers"> <button v-tooltip="i18n.ts.createNoteFromTheFile" class="_button" :class="$style.fileQuickActionsOthersButton" @click="postThis()"> - <i class="ph-pencil-simple ph-bold ph-lg"></i> + <i class="ti ti-pencil"></i> </button> <button v-if="isImage" v-tooltip="i18n.ts.cropImage" class="_button" :class="$style.fileQuickActionsOthersButton" @click="crop()"> - <i class="ph-crop ph-bold ph-lg"></i> + <i class="ti ti-crop"></i> </button> <button v-if="file.isSensitive" v-tooltip="i18n.ts.unmarkAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()"> - <i class="ph-eye ph-bold ph-lg"></i> + <i class="ti ti-eye"></i> </button> <button v-else v-tooltip="i18n.ts.markAsSensitive" class="_button" :class="$style.fileQuickActionsOthersButton" @click="toggleSensitive()"> - <i class="ph-eye-slash ph-bold ph-lg"></i> + <i class="ti ti-eye-exclamation"></i> </button> <a v-tooltip="i18n.ts.download" :href="file.url" :download="file.name" class="_button" :class="$style.fileQuickActionsOthersButton"> - <i class="ph-download ph-bold ph-lg"></i> + <i class="ti ti-download"></i> </a> <button v-tooltip="i18n.ts.delete" class="_button" :class="[$style.fileQuickActionsOthersButton, $style.danger]" @click="deleteFile()"> - <i class="ph-trash ph-bold ph-lg"></i> + <i class="ti ti-trash"></i> </button> </div> </div> - <div> - <button class="_button" :class="$style.fileAltEditBtn" @click="describe()"> + <div class="_gaps_s"> + <button class="_button" :class="$style.kvEditBtn" @click="move()"> + <MkKeyValue> + <template #key>{{ i18n.ts.folder }}</template> + <template #value>{{ folderHierarchy.join(' > ') }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template> + </MkKeyValue> + </button> + <button class="_button" :class="$style.kvEditBtn" @click="describe()"> <MkKeyValue> <template #key>{{ i18n.ts.description }}</template> - <template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ph-pencil-simple ph-bold ph-lg" :class="$style.fileAltEditIcon"></i></template> + <template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template> </MkKeyValue> </button> <MkKeyValue :class="$style.fileMetaDataChildren"> @@ -90,6 +96,18 @@ const props = defineProps<{ const fetching = ref(true); 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; +}); const isImage = computed(() => file.value?.type.startsWith('image/')); async function fetch() { @@ -122,6 +140,19 @@ function crop() { }); } +function move() { + if (!file.value) return; + + os.selectDriveFolder(false).then(folder => { + misskeyApi('drive/files/update', { + fileId: file.value.id, + folderId: folder[0] ? folder[0].id : null, + }).then(async () => { + await fetch(); + }); + }); +} + function toggleSensitive() { if (!file.value) return; @@ -160,7 +191,7 @@ function rename() { function describe() { if (!file.value) return; - os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.value.comment ?? '', file: file.value, }, { @@ -172,7 +203,8 @@ function describe() { await fetch(); }); }, - }, 'closed'); + closed: () => dispose(), + }); } async function deleteFile() { @@ -233,6 +265,7 @@ onMounted(async () => { background-color: var(--accentedBg); color: var(--accent); text-decoration: none; + outline: none; } &.danger { @@ -280,14 +313,14 @@ onMounted(async () => { padding: .5rem 1rem; } -.fileAltEditBtn { +.kvEditBtn { text-align: start; display: block; width: 100%; padding: .5rem 1rem; border-radius: var(--radius); - .fileAltEditIcon { + .kvEditIcon { display: inline-block; color: transparent; visibility: hidden; @@ -298,7 +331,7 @@ onMounted(async () => { color: var(--accent); background-color: var(--accentedBg); - .fileAltEditIcon { + .kvEditIcon { color: var(--accent); visibility: visible; } diff --git a/packages/frontend/src/pages/drive.file.vue b/packages/frontend/src/pages/drive.file.vue index 7cb2976e76628d0dd585a1cb4a9457f27c542942..5711ec8b3a0f9bf92446f8bdd6d7610b4bea67e2 100644 --- a/packages/frontend/src/pages/drive.file.vue +++ b/packages/frontend/src/pages/drive.file.vue @@ -41,15 +41,15 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'info', title: i18n.ts.info, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', }, { key: 'notes', title: i18n.ts._fileViewer.attachedNotes, - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', }]); definePageMetadata(() => ({ title: i18n.ts._fileViewer.title, - icon: 'ph-file-text ph-bold ph-lg', + icon: 'ti ti-file', })); </script> diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue index 7403986061b77a1ecd1eefa52336336c0ab25b86..25e140f67fdc0024b923af9b0cd627312d705fc0 100644 --- a/packages/frontend/src/pages/drive.vue +++ b/packages/frontend/src/pages/drive.vue @@ -24,7 +24,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: folder.value ? folder.value.name : i18n.ts.drive, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', hideHeader: true, })); </script> diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index f1fa580c36004f41105a63b022aa2490a9657490..0f0b7e1ea8e67eba6767f643648f9e586c22c571 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> </div> </div> - <div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.replaying }}</span></div> + <div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div> </div> <div v-if="replaying" class="_woodenFrame"> @@ -116,9 +116,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_woodenFrameInner"> <div class="_buttonsCenter"> - <MkButton @click="endReplay"><i class="ph-stop ph-bold ph-lg"></i> {{ i18n.ts.endReplay }}</MkButton> - <MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ph-skip-forward ph-bold ph-lg"></i> x4</MkButton> - <MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ph-skip-forward ph-bold ph-lg"></i> x16</MkButton> + <MkButton @click="endReplay"><i class="ti ti-player-stop"></i> {{ i18n.ts.endReplay }}</MkButton> + <MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton> + <MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton> </div> </div> </div> @@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_woodenFrame" style="margin-left: auto;"> <div class="_woodenFrameInner" style="text-align: center;"> - <div @click="showConfig = !showConfig"><i class="ph-gear ph-bold ph-lg"></i></div> + <div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div> </div> </div> </div> @@ -173,7 +173,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;"> <img :src="getTextureImageUrl(mono)" style="width: 32px; vertical-align: bottom;"/> - <div v-if="i < game.monoDefinitions.length - 1" style="display: inline-block; margin-left: 4px; vertical-align: bottom;"><i class="ph-arrow-fat-right ph-bold ph-lg"></i></div> + <div v-if="i < game.monoDefinitions.length - 1" style="display: inline-block; margin-left: 4px; vertical-align: bottom;"><i class="ti ti-arrow-big-right"></i></div> </div> </div> </div> @@ -210,7 +210,7 @@ import { apiUrl } from '@/config.js'; import { $i } from '@/account.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; type FrontendMonoDefinition = { id: string; @@ -1008,8 +1008,18 @@ function attachGameEvents() { const domX = rect.left + (x * viewScale); const domY = rect.top + (y * viewScale); const scoreUnit = getScoreUnit(props.gameMode); - os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end'); - os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (scoreUnit === 'pt' ? '' : scoreUnit) }, {}, 'end'); + + { + const { dispose } = os.popup(MkRippleEffect, { x: domX, y: domY }, { + end: () => dispose(), + }); + } + + { + const { dispose } = os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (scoreUnit === 'pt' ? '' : scoreUnit) }, { + end: () => dispose(), + }); + } if (nextMono) { const def = monoDefinitions.value.find(x => x.id === nextMono.id)!; @@ -1220,7 +1230,7 @@ onDeactivated(() => { definePageMetadata(() => ({ title: i18n.ts.bubbleGame, - icon: 'ph-orange-slice ph-bold ph-lg', + icon: 'ti ti-apple', })); </script> diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 3ece281468f4742d3004517daea6fabfa8ea209c..54352c9b0d71df8e318103d8840eff97fa7a5e35 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_woodenFrameInner"> <div class="_gaps" style="padding: 16px;"> - <div style="font-size: 90%;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ i18n.ts.soundWillBePlayed }}</div> + <div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div> <MkSwitch v-model="mute"> <template #label>{{ i18n.ts.mute }}</template> </MkSwitch> @@ -123,7 +123,7 @@ function onGameEnd() { definePageMetadata(() => ({ title: i18n.ts.bubbleGame, - icon: 'ph-game-controller ph-bold ph-lg', + icon: 'ti ti-device-gamepad', })); </script> diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index d03d3d912865ac610bc7fce49a2c70561119e7da..c5f0dde8784bfb7362e360dfca17829f61a8bb4f 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="emoji" #header>:{{ emoji.name }}:</template> <template v-else #header>New emoji</template> - <div> - <MkSpacer :marginMin="20" :marginMax="28"> + <div style="display: flex; flex-direction: column; min-height: 100%;"> + <MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;"> <div class="_gaps_m"> <div v-if="imgUrl != null" :class="$style.imgs"> <div style="background: #000;" :class="$style.imgContainer"> @@ -54,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #suffix>{{ rolesThatCanBeUsedThisEmojiAsReaction.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisEmojiAsReaction.length }}</template> <div class="_gaps"> - <MkButton rounded @click="addRole"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> + <MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <div v-for="role in rolesThatCanBeUsedThisEmojiAsReaction" :key="role.id" :class="$style.roleItem"> <MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/> - <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ph-x ph-bold ph-lg"></i></button> - <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ph-prohibit ph-bold ph-lg"></i></button> + <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button> + <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> </div> <MkInfo>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription }}</MkInfo> @@ -68,11 +68,11 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkSwitch v-model="isSensitive">isSensitive</MkSwitch> <MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch> - <MkButton v-if="emoji" danger @click="del()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-if="emoji" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </MkSpacer> <div :class="$style.footer"> - <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ph-check ph-bold ph-lg"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton> + <MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton> </div> </div> </MkWindow> @@ -239,10 +239,12 @@ async function del() { .footer { position: sticky; + z-index: 10000; bottom: 0; left: 0; padding: 12px; border-top: solid 0.5px var(--divider); + background: var(--acrylicBg); -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); } diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index c9805af51b77d1654bdbb69974e2b668dbbce6b0..03a3b8f1c0e4826dbd5465a7133cfcaaeab76fb3 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import * as os from '@/os.js'; import * as Misskey from 'misskey-js'; +import * as os from '@/os.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; @@ -31,21 +31,21 @@ function menu(ev) { text: ':' + props.emoji.name + ':', }, { text: i18n.ts.copy, - icon: 'ph-copy ph-bold ph-lg', + icon: 'ti ti-copy', action: () => { copyToClipboard(`:${props.emoji.name}:`); os.success(); }, }, { text: i18n.ts.info, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', action: async () => { - os.popup(MkCustomEmojiDetailedDialog, { + const { dispose } = os.popup(MkCustomEmojiDetailedDialog, { emoji: await misskeyApiGet('emoji', { name: props.emoji.name, - }) + }), }, { - anchor: ev.target, + closed: () => dispose(), }); }, }], ev.currentTarget ?? ev.target); diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index c9ab5443b69bbe2292092dd3952a9f135d2a1dd1..805d8269463a960cae27fae562e0d23891f3bcce 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -12,11 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="origin === 'local'"> <template v-if="tag == null"> <MkFoldableSection class="_margin" persistKey="explore-pinned-users"> - <template #header><i class="ph-bookmark ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template> + <template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template> <MkUserList :pagination="pinnedUsers"/> </MkFoldableSection> <MkFoldableSection class="_margin" persistKey="explore-popular-users"> - <template #header><i class="ph-chart-line ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> + <template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> <MkUserList :pagination="popularUsers"/> </MkFoldableSection> <MkFoldableSection class="_margin" persistKey="explore-recently-updated-users"> @@ -24,14 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserList :pagination="recentlyUpdatedUsers"/> </MkFoldableSection> <MkFoldableSection class="_margin" persistKey="explore-recently-registered-users"> - <template #header><i class="ph-plus ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template> + <template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template> <MkUserList :pagination="recentlyRegisteredUsers"/> </MkFoldableSection> </template> </div> <div v-else> <MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin"> - <template #header><i class="ph-hash ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template> + <template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template> <div> <MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA> @@ -40,13 +40,13 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFoldableSection> <MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin"> - <template #header><i class="ph-hash ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> + <template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> <MkUserList :pagination="tagUsers"/> </MkFoldableSection> <template v-if="tag == null"> <MkFoldableSection class="_margin"> - <template #header><i class="ph-chart-line ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> + <template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> <MkUserList :pagination="popularUsersF"/> </MkFoldableSection> <MkFoldableSection class="_margin"> @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserList :pagination="recentlyUpdatedUsersF"/> </MkFoldableSection> <MkFoldableSection class="_margin"> - <template #header><i class="ph-rocket-launch ph-bold ph-lg ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template> + <template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template> <MkUserList :pagination="recentlyRegisteredUsersF"/> </MkFoldableSection> </template> diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index c599a290f77699e8ef225aeece624ee3ba760e34..b1a8183d9b8bec3158395be8cb1c2d7882c0dc97 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -48,20 +48,20 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'featured', - icon: 'ph-lightning ph-bold ph-lg', + icon: 'ti ti-bolt', title: i18n.ts.featured, }, { key: 'users', - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', title: i18n.ts.users, }, { key: 'roles', - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badges', title: i18n.ts.roles, }]); definePageMetadata(() => ({ title: i18n.ts.explore, - icon: 'ph-hash ph-bold ph-lg', + icon: 'ti ti-hash', })); </script> diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue index 5835f0c093b3883a119f1f39487723fd451f4720..cb1feef0166462bb83dc0772fb4c5bd3e4569975 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -16,14 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items }"> - <MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" - v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> + <MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> <MkNote :key="item.id" :note="item.note" :class="$style.note"/> </MkDateSeparatedList> - <MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'sharkey'" - v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> - <SkNote :key="item.id" :note="item.note" :class="$style.note"/> - </MkDateSeparatedList> </template> </MkPagination> </MkSpacer> @@ -32,14 +27,19 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkPagination from '@/components/MkPagination.vue'; -import MkNote from '@/components/MkNote.vue'; -import SkNote from '@/components/SkNote.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; +import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); + const pagination = { endpoint: 'i/favorites' as const, limit: 10, @@ -47,7 +47,7 @@ const pagination = { definePageMetadata(() => ({ title: i18n.ts.favorites, - icon: 'ph-star ph-bold ph-lg', + icon: 'ti ti-star', })); </script> diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 1a832dfdecab30ab3d7cbaa4f72bed495711825e..d282ed48107af9be3a3813d47116897913b4eb64 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true"> <template #label>{{ i18n.ts._play.summary }}</template> </MkTextarea> - <MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ph-caret-down ph-bold ph-lg"></i></MkButton> + <MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ti ti-chevron-down"></i></MkButton> <MkCodeEditor v-model="script" lang="is"> <template #label>{{ i18n.ts._play.script }}</template> </MkCodeEditor> @@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only <option :key="'private'" :value="'private'">{{ i18n.ts.private }}</option> </MkSelect> <div class="_buttons"> - <MkButton primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton @click="show"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.show }}</MkButton> - <MkButton v-if="flash" danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <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> @@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; +import { AISCRIPT_VERSION } from '@syuilo/aiscript'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -48,7 +49,7 @@ import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; import { useRouter } from '@/router/supplier.js'; -const PRESET_DEFAULT = `/// @ 0.18.0 +const PRESET_DEFAULT = `/// @ ${AISCRIPT_VERSION} var name = "" @@ -66,7 +67,7 @@ Ui:render([ ]) `; -const PRESET_OMIKUJI = `/// @ 0.18.0 +const PRESET_OMIKUJI = `/// @ ${AISCRIPT_VERSION} // ユーザーã”ã¨ã«æ—¥æ›¿ã‚ã‚Šã®ãŠã¿ãã˜ã®ãƒ—リセット // é¸æŠžè‚¢ @@ -109,7 +110,7 @@ Ui:render([ ]) `; -const PRESET_SHUFFLE = `/// @ 0.18.0 +const PRESET_SHUFFLE = `/// @ ${AISCRIPT_VERSION} // å·»ã戻ã—å¯èƒ½ãªæ–‡å—シャッフルã®ãƒ—リセット let string = "ペペãƒãƒ³ãƒãƒ¼ãƒŽ" @@ -188,7 +189,7 @@ var cursor = 0 do() `; -const PRESET_QUIZ = `/// @ 0.18.0 +const PRESET_QUIZ = `/// @ ${AISCRIPT_VERSION} let title = '地ç†ã‚¯ã‚¤ã‚º' let qas = [{ @@ -301,7 +302,7 @@ qaEls.push(Ui:C:container({ Ui:render(qaEls) `; -const PRESET_TIMELINE = `/// @ 0.18.0 +const PRESET_TIMELINE = `/// @ ${AISCRIPT_VERSION} // APIリクエストを行ã„ãƒãƒ¼ã‚«ãƒ«ã‚¿ã‚¤ãƒ ラインを表示ã™ã‚‹ãƒ—リセット @fetch() { @@ -368,7 +369,6 @@ const props = defineProps<{ }>(); const flash = ref<Misskey.entities.Flash | null>(null); -const visibility = ref<'private' | 'public'>('public'); if (props.id) { flash.value = await misskeyApi('flash/show', { @@ -379,6 +379,7 @@ if (props.id) { const title = ref(flash.value?.title ?? 'New Play'); const summary = ref(flash.value?.summary ?? ''); const permissions = ref(flash.value?.permissions ?? []); +const visibility = ref<'private' | 'public'>(flash.value?.visibility ?? 'public'); const script = ref(flash.value?.script ?? PRESET_DEFAULT); function selectPreset(ev: MouseEvent) { diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index 7e56d3f51b22e953ff7db8a5400ba0d502771ee3..f63a799365de1bdcb46e37f26ac75bc539d0091f 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="tab === 'my'" key="my"> <div class="_gaps"> - <MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton> + <MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkPagination v-slot="{items}" :pagination="myFlashsPagination"> <div class="_gaps_s"> <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/> @@ -71,7 +71,7 @@ function create() { } const headerActions = computed(() => [{ - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.create, handler: create, }]); @@ -79,19 +79,19 @@ const headerActions = computed(() => [{ const headerTabs = computed(() => [{ key: 'featured', title: i18n.ts._play.featured, - icon: 'ph-fire ph-bold ph-lg', + icon: 'ti ti-flare', }, { key: 'my', title: i18n.ts._play.my, - icon: 'ph-pencil-simple-line ph-bold ph-lg', + icon: 'ti ti-edit', }, { key: 'liked', title: i18n.ts._play.liked, - icon: 'ph-heart ph-bold ph-lg', + icon: 'ti ti-heart', }]); definePageMetadata(() => ({ title: 'Play', - icon: 'ph-play ph-bold ph-lg', + icon: 'ti ti-player-play', })); </script> diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index febfdbaaf75060cab858d6bfd17f82b8a26fd89d..1b277c936a04b8dda4b49fbe30dfe7fe790123a6 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -16,13 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="actions _panel"> <div class="items"> - <MkButton v-tooltip="i18n.ts.reload" class="button" rounded @click="reset"><i class="ph-arrows-clockwise ph-bold ph-lg"></i></MkButton> + <MkButton v-tooltip="i18n.ts.reload" class="button" rounded @click="reset"><i class="ti ti-reload"></i></MkButton> </div> <div class="items"> - <MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> - <MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> - <MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ph-link ph-bold ph-lg ti-fw"></i></MkButton> - <MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></MkButton> + <MkButton v-if="flash.isLiked" v-tooltip="i18n.ts.unlike" asLike class="button" rounded primary @click="unlike()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton> + <MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton> + <MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton> + <MkButton v-if="$i && $i.id !== flash.user.id" class="button" rounded @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></MkButton> </div> </div> </div> @@ -32,13 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="summary"><Mfm :text="flash.summary" :isBlock="true"/></div> <MkButton class="start" gradate rounded large @click="start">Play</MkButton> <div class="info"> - <span v-tooltip="i18n.ts.numberOfLikes"><i class="ph-heart ph-bold ph-lg"></i> {{ flash.likedCount }}</span> + <span v-tooltip="i18n.ts.numberOfLikes"><i class="ti ti-heart"></i> {{ flash.likedCount }}</span> </div> </div> </div> </Transition> <MkFolder :defaultOpen="false" :max-height="280" class="_margin"> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts._play.viewSource }}</template> <MkCode :code="flash.script" lang="is" class="_monospace"/> @@ -46,8 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.footer"> <Mfm :text="`By @${flash.user.username}`"/> <div class="date"> - <div v-if="flash.createdAt != flash.updatedAt"><i class="ph-clock ph-bold ph-lg"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="flash.updatedAt" mode="detail"/></div> - <div><i class="ph-clock ph-bold ph-lg"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div> + <div v-if="flash.createdAt != flash.updatedAt"><i class="ti ti-clock"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="flash.updatedAt" mode="detail"/></div> + <div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div> </div> </div> <MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA> @@ -61,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef } from 'vue'; +import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; import { Interpreter, Parser, values } from '@syuilo/aiscript'; import MkButton from '@/components/MkButton.vue'; @@ -78,7 +79,9 @@ import MkCode from '@/components/MkCode.vue'; 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { MenuItem } from '@/types/menu'; +import { pleaseLogin } from '@/scripts/please-login.js'; const props = defineProps<{ id: string; @@ -109,11 +112,11 @@ function share(ev: MouseEvent) { }, ...(isSupportShare() ? [{ text: i18n.ts.share, - icon: 'ph-share-network ph-bold ph-lg ti-fw', + icon: 'ti ti-share', action: shareWithNavigator, }] : []), ], ev.currentTarget ?? ev.target); - } +} function copyLink() { if (!flash.value) return; @@ -143,6 +146,7 @@ function shareWithNote() { function like() { if (!flash.value) return; + pleaseLogin(); os.apiWithDialog('flash/like', { flashId: flash.value.id, @@ -154,6 +158,7 @@ function like() { async function unlike() { if (!flash.value) return; + pleaseLogin(); const confirm = await os.confirm({ type: 'warning', @@ -226,6 +231,53 @@ async function run() { } } +function reportAbuse() { + if (!flash.value) return; + + const pageUrl = `${url}/play/${flash.value.id}`; + + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + user: flash.value.user, + initialComment: `Play: ${pageUrl}\n-----\n`, + }, { + closed: () => dispose(), + }); +} + +function showMenu(ev: MouseEvent) { + if (!flash.value) return; + + const menu: MenuItem[] = [ + ...($i && $i.id !== flash.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 || !flash.value) return; + + os.apiWithDialog('flash/delete', { flashId: flash.value.id }); + }), + }, + ] : []), + ] : []), + ]; + + os.popupMenu(menu, ev.currentTarget ?? ev.target); +} + function reset() { if (aiscript.value) aiscript.value.abort(); started.value = false; diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 4cdfe28916c457b58e930eb00bbf6031ed69b2d5..d50887b2e9f1df7f67044c112b10adc289dcd4dc 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only <p class="acct">@{{ acct(req.follower) }}</p> </div> <div class="commands"> - <MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ph-check ph-bold ph-lg"/> {{ i18n.ts.accept }}</MkButton> - <MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ph-x ph-bold ph-lg"/> {{ i18n.ts.reject }}</MkButton> + <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> </div> </div> </div> @@ -71,7 +71,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.followRequests, - icon: 'ph-user-plus ph-bold ph-lg', + icon: 'ti ti-user-plus', })); </script> diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue deleted file mode 100644 index 247b0ac639a0a797dfc4d239af344ad4bda26a8b..0000000000000000000000000000000000000000 --- a/packages/frontend/src/pages/follow.vue +++ /dev/null @@ -1,71 +0,0 @@ -<!-- -SPDX-FileCopyrightText: syuilo and misskey-project -SPDX-License-Identifier: AGPL-3.0-only ---> - -<template> -<div> -</div> -</template> - -<script lang="ts" setup> -import { } from 'vue'; -import * as Misskey from 'misskey-js'; -import * as os from '@/os.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { i18n } from '@/i18n.js'; -import { defaultStore } from '@/store.js'; -import { mainRouter } from '@/router/main.js'; - -async function follow(user): Promise<void> { - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.tsx.followConfirm({ name: user.name || user.username }), - }); - - if (canceled) { - window.close(); - return; - } - - os.apiWithDialog('following/create', { - userId: user.id, - withReplies: defaultStore.state.defaultWithReplies, - }); - user.withReplies = defaultStore.state.defaultWithReplies; -} - -const acct = new URL(location.href).searchParams.get('acct'); -if (acct == null) { - throw new Error('acct required'); -} - -let promise; - -if (acct.startsWith('https://')) { - promise = misskeyApi('ap/show', { - uri: acct, - }); - promise.then(res => { - if (res.type === 'User') { - follow(res.object); - } else if (res.type === 'Note') { - mainRouter.push(`/notes/${res.object.id}`); - } else { - os.alert({ - type: 'error', - text: 'Not a user', - }).then(() => { - window.close(); - }); - } - }); -} else { - promise = misskeyApi('users/show', Misskey.acct.parse(acct)); - promise.then(user => { - follow(user); - }); -} - -os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); -</script> diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index d2fe271b0f6ab08c4c4d63df74679486658eb510..a68a7e5c41425ddfe3494b3af9a33b67224d4d06 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -19,18 +19,18 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_s"> <div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }"> <div class="name">{{ file.name }}</div> - <button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ph-x ph-bold ph-lg"></i></button> + <button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button> </div> - <MkButton primary @click="selectFile"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.attachFile }}</MkButton> + <MkButton primary @click="selectFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton> </div> <MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch> <div class="_buttons"> - <MkButton v-if="postId" primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-else primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.publish }}</MkButton> + <MkButton v-if="postId" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-else primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.publish }}</MkButton> - <MkButton v-if="postId" danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-if="postId" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </FormSuspense> </MkSpacer> @@ -124,7 +124,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: props.postId ? i18n.ts.edit : i18n.ts.postToGallery, - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', })); </script> diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 96979250bd40c4c06990fa268c30db6475b57ffb..e0b137ed28f318050be972bfaa4f390307b81413 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <div v-if="tab === 'explore'" key="explore"> <MkFoldableSection class="_margin"> - <template #header><i class="ph-clock ph-bold ph-lg"></i>{{ i18n.ts.recentPosts }}</template> + <template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template> <MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true"> <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkPagination> </MkFoldableSection> <MkFoldableSection class="_margin"> - <template #header><i class="ph-shooting-star ph-bold ph-lg"></i>{{ i18n.ts.popularPosts }}</template> + <template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template> <MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disableAutoLoad="true"> <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkPagination> </div> <div v-else-if="tab === 'my'" key="my"> - <MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.postToGallery }}</MkA> + <MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA> <MkPagination v-slot="{items}" :pagination="myPostsPagination"> <div :class="$style.items"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> @@ -98,7 +98,7 @@ watch(() => props.tag, () => { }); const headerActions = computed(() => [{ - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.create, handler: () => { router.push('/gallery/new'); @@ -112,11 +112,11 @@ const headerTabs = computed(() => [{ }, { key: 'liked', title: i18n.ts._gallery.liked, - icon: 'ph-heart ph-bold ph-lg', + icon: 'ti ti-heart', }, { key: 'my', title: i18n.ts._gallery.my, - icon: 'ph-pencil-simple-line ph-bold ph-lg', + icon: 'ti ti-edit', }]); definePageMetadata(() => ({ diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 99bd29f8023cfb87f2e2037b648a147effb14904..913758ba7e969d53d72f32ceeee2425bd599dd5d 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -19,18 +19,19 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="title">{{ post.title }}</div> <div class="description"><Mfm :text="post.description" :isBlock="true"/></div> <div class="info"> - <i class="ph-clock ph-bold ph-lg"></i> <MkTime :time="post.createdAt" mode="detail"/> + <i class="ti ti-clock"></i> <MkTime :time="post.createdAt" mode="detail"/> </div> <div class="actions"> <div class="like"> - <MkButton v-if="post.isLiked" v-tooltip="i18n.ts._gallery.unlike" class="button" primary @click="unlike()"><i class="ph-heart-break ph-bold ph-lg"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton> - <MkButton v-else v-tooltip="i18n.ts._gallery.like" class="button" @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton> + <MkButton v-if="post.isLiked" v-tooltip="i18n.ts._gallery.unlike" class="button" primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts._gallery.like" class="button" @click="like()"><i class="ti ti-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton> </div> <div class="other"> - <button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-simple ph-bold ph-lg ti-fw"></i></button> - <button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat ph-bold ph-lg ti-fw"></i></button> - <button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> - <button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> + <button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ti ti-pencil ti-fw"></i></button> + <button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ti ti-repeat ti-fw"></i></button> + <button v-tooltip="i18n.ts.copyLink" v-click-anime class="_button" @click="copyLink"><i class="ti ti-link ti-fw"></i></button> + <button v-if="isSupportShare()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ti ti-share ti-fw"></i></button> + <button v-if="$i && $i.id !== post.user.id" v-click-anime class="_button" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button> </div> </div> <div class="user"> @@ -44,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkAd :prefer="['horizontal', 'horizontal-big']"/> <MkContainer :max-height="300" :foldable="true" class="other"> - <template #icon><i class="ph-clock ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-clock"></i></template> <template #header>{{ i18n.ts.recentPosts }}</template> <MkPagination v-slot="{items}" :pagination="otherPostsPagination"> <div class="sdrarzaf"> @@ -62,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, watch, ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -77,8 +78,9 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; +import { MenuItem } from '@/types/menu'; const router = useRouter(); @@ -153,13 +155,56 @@ function edit() { router.push(`/gallery/${post.value.id}/edit`); } +function reportAbuse() { + if (!post.value) return; + + const pageUrl = `${url}/gallery/${post.value.id}`; + + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + user: post.value.user, + initialComment: `Post: ${pageUrl}\n-----\n`, + }, { + closed: () => dispose(), + }); +} + +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); +} + watch(() => props.postId, fetchPost, { immediate: true }); -const headerActions = computed(() => [{ - icon: 'ph-pencil-simple ph-bold ph-lg', - text: i18n.ts.edit, - handler: edit, -}]); +const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue index 2822fbde89d6d44444bea78872f4f9f449ab3036..b52f4decaaad915a639aeb725409aa062b52a674 100644 --- a/packages/frontend/src/pages/games.vue +++ b/packages/frontend/src/pages/games.vue @@ -8,12 +8,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader/></template> <MkSpacer :contentMax="800"> <div class="_gaps"> - <div class="_panel"> + <div class="_panel" :class="$style.link"> <MkA to="/bubble-game"> <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> </MkA> </div> - <div class="_panel"> + <div class="_panel" :class="$style.link"> <MkA to="/reversi"> <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> </MkA> @@ -29,6 +29,13 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; definePageMetadata(() => ({ title: 'Misskey Games', - icon: 'ph-game-controller ph-bold ph-lg', + icon: 'ti ti-device-gamepad', })); </script> + +<style module> +.link:focus-within { + outline: 2px solid var(--focus); + outline-offset: -2px; +} +</style> diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue index 32f6fbd1853b6d2a2680d61ab65f6de5e0cdee4f..4bee437f65820c9188b830ee01b03aaa1e94689c 100644 --- a/packages/frontend/src/pages/install-extensions.vue +++ b/packages/frontend/src/pages/install-extensions.vue @@ -10,9 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="uiPhase === 'fetching'"/> <div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot"> <div :class="$style.extInstallerIconWrapper"> - <i v-if="data.type === 'plugin'" class="ph-plug ph-bold ph-lg"></i> - <i v-else-if="data.type === 'theme'" class="ph-palette ph-bold ph-lg"></i> - <i v-else class="ph-download ph-bold ph-lg"></i> + <i v-if="data.type === 'plugin'" class="ti ti-plug"></i> + <i v-else-if="data.type === 'theme'" class="ti ti-palette"></i> + <i v-else class="ti ti-download"></i> </div> <h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2> <div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div> @@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #value>{{ i18n.ts[data.meta.base] }}</template> </MkKeyValue> <MkFolder> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts._plugin.viewSource }}</template> <MkCode :code="data.raw ?? ''"/> @@ -69,18 +69,18 @@ SPDX-License-Identifier: AGPL-3.0-only <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template> <template #value> <!--ã“ã®ç”»é¢ãŒå‡ºã¦ã„る時点ã§ãƒãƒƒã‚·ãƒ¥ã®æ¤œè¨¼ã«ã¯æˆåŠŸã—ã¦ã„ã‚‹--> - <i class="ph-check ph-bold ph-lg" style="color: var(--accent)"></i> + <i class="ti ti-check" style="color: var(--accent)"></i> </template> </MkKeyValue> </div> </FormSection> <div class="_buttonsCenter"> - <MkButton primary @click="install()"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton> + <MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> </div> </div> <div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]"> <div :class="$style.extInstallerIconWrapper"> - <i class="ph-x-circle ph-bold ph-lg"></i> + <i class="ti ti-circle-x"></i> </div> <h2 :class="$style.extInstallerTitle">{{ errorKV?.title }}</h2> <div :class="$style.extInstallerNormDesc">{{ errorKV?.description }}</div> @@ -314,7 +314,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts._externalResourceInstaller.title, - icon: 'ph-download ph-bold ph-lg', + icon: 'ti ti-download', })); </script> diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 0cc4c1a48a1d3ad26f24e2d14122d7562bffe136..4ff26197d8427d7ccfe06d763a795bb4fb384e38 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -48,7 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only <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> - <MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton> + <MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @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> </MkTextarea> @@ -169,6 +170,7 @@ const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'au const isBlocked = ref(false); const isSilenced = ref(false); const isNSFW = ref(false); +const isMediaSilenced = ref(false); const faviconUrl = ref<string | null>(null); const moderationNote = ref(''); @@ -198,8 +200,9 @@ async function fetch(): Promise<void> { isBlocked.value = instance.value?.isBlocked ?? false; isSilenced.value = instance.value?.isSilenced ?? false; isNSFW.value = instance.value?.isNSFW ?? false; + isMediaSilenced.value = instance.value?.isMediaSilenced ?? false; faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); - moderationNote.value = instance.value?.moderationNote; + moderationNote.value = instance.value?.moderationNote ?? ''; } async function toggleBlock(): Promise<void> { @@ -221,6 +224,16 @@ async function toggleSilenced(): Promise<void> { }); } +async function toggleMediaSilenced(): Promise<void> { + if (!meta.value) throw new Error('No meta?'); + if (!instance.value) throw new Error('No instance?'); + const { host } = instance.value; + const mediaSilencedHosts = meta.value.mediaSilencedHosts ?? []; + await misskeyApi('admin/update-meta', { + mediaSilencedHosts: isMediaSilenced.value ? mediaSilencedHosts.concat([host]) : mediaSilencedHosts.filter(x => x !== host), + }); +} + async function stopDelivery(): Promise<void> { if (!instance.value) throw new Error('No instance?'); suspensionState.value = 'manuallySuspended'; @@ -261,7 +274,7 @@ fetch(); const headerActions = computed(() => [{ text: `https://${props.host}`, - icon: 'ph-arrow-square-out ph-bold ph-lg', + icon: 'ti ti-external-link', handler: () => { window.open(`https://${props.host}`, '_blank', 'noopener'); }, @@ -270,24 +283,24 @@ const headerActions = computed(() => [{ const headerTabs = computed(() => [{ key: 'overview', title: i18n.ts.overview, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', }, { key: 'chart', title: i18n.ts.charts, - icon: 'ph-chart-line ph-bold ph-lg', + icon: 'ti ti-chart-line', }, { key: 'users', title: i18n.ts.users, - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', }, { key: 'raw', title: 'Raw', - icon: 'ph-code ph-bold ph-lg', + icon: 'ti ti-code', }]); definePageMetadata(() => ({ title: props.host, - icon: 'ph-hard-drives ph-bold ph-lg', + icon: 'ti ti-server', })); </script> diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue index b8c006eb77839e57c56ee2967a9c1c058a835c80..ef485a94467f4de4b45211723f1ec240b806ae42 100644 --- a/packages/frontend/src/pages/invite.vue +++ b/packages/frontend/src/pages/invite.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <div :class="$style.text"> - <i class="ph-warning ph-bold ph-lg"></i> + <i class="ti ti-alert-triangle"></i> {{ i18n.ts.nothing }} </div> </div> @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer v-else :contentMax="800"> <div class="_gaps_m" style="text-align: center;"> <div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div> - <MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ph-user-plus ph-bold ph-lg"></i> {{ i18n.ts.createInviteCode }}</MkButton> + <MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton> <div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div> <MkPagination ref="pagingComponent" :pagination="pagination"> @@ -95,7 +95,7 @@ update(); definePageMetadata(() => ({ title: i18n.ts.invite, - icon: 'ph-user-plus ph-bold ph-lg', + icon: 'ti ti-user-plus', })); </script> diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index 87070d9167cc51d47f382b4559ce2752a767cf07..03ab804d0502db8f3f36fd3b41a3ab739d8fcd20 100644 --- a/packages/frontend/src/pages/list.vue +++ b/packages/frontend/src/pages/list.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"> - <i class="ph-warning ph-bold ph-lg"></i> + <i class="ti ti-alert-triangle"></i> {{ i18n.ts.nothing }} </p> </div> @@ -26,9 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> </div> - <MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ph-heart-break ph-bold ph-lg"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton> - <MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton> - <MkButton inline @click="create()"><i class="ph-download ph-bold ph-lg" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton> + <MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton> + <MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton> + <MkButton inline @click="create()"><i class="ti ti-download" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton> </MkSpacer> </MkStickyContainer> </template> @@ -103,7 +103,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: list.value ? list.value.name : i18n.ts.lists, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', })); </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue new file mode 100644 index 0000000000000000000000000000000000000000..3233953942392d1782458cdd1268ddac4e7cb7c6 --- /dev/null +++ b/packages/frontend/src/pages/lookup.vue @@ -0,0 +1,97 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :contentMax="800"> + <div v-if="state === 'done'" class="_buttonsCenter"> + <MkButton @click="close">{{ i18n.ts.close }}</MkButton> + <MkButton @click="goToMisskey">{{ i18n.ts.goToMisskey }}</MkButton> + </div> + <div v-else class="_fullInfo"> + <MkLoading/> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { mainRouter } from '@/router/main.js'; +import MkButton from '@/components/MkButton.vue'; + +const state = ref<'fetching' | 'done'>('fetching'); + +function fetch() { + const params = new URL(location.href).searchParams; + + // acctã®ã»ã†ã¯deprecated + let uri = params.get('uri') ?? params.get('acct'); + if (uri == null) { + state.value = 'done'; + return; + } + + let promise: Promise<any>; + + if (uri.startsWith('https://')) { + promise = misskeyApi('ap/show', { + uri, + }); + promise.then(res => { + if (res.type === 'User') { + mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`); + } else if (res.type === 'Note') { + mainRouter.replace(`/notes/${res.object.id}`); + } else { + os.alert({ + type: 'error', + text: 'Not a user', + }); + } + }); + } else { + if (uri.startsWith('acct:')) { + uri = uri.slice(5); + } + promise = misskeyApi('users/show', Misskey.acct.parse(uri)); + promise.then(user => { + mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`); + }); + } + + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); +} + +function close(): void { + window.close(); + + // é–‰ã˜ãªã‘ã‚Œã°100ms後タイムライン㫠+ window.setTimeout(() => { + location.href = '/'; + }, 100); +} + +function goToMisskey(): void { + location.href = '/'; +} + +fetch(); + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata({ + title: i18n.ts.lookup, + icon: 'ti ti-world-search', +}); +</script> diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue index 4812bfe70faa820d768d41c0d8741805e84e24dc..7e70f91710ff536441c2609f94c5d9fd5e45338c 100644 --- a/packages/frontend/src/pages/miauth.vue +++ b/packages/frontend/src/pages/miauth.vue @@ -95,7 +95,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: 'MiAuth', - icon: 'ph-squares-four ph-bold ph-lg', + icon: 'ti ti-apps', })); </script> diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index ee2330a4f7604041429d27b1b317ff700c4c5121..2b8518747f2cf8694985ef0d390978c9f4e5bfb7 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -4,43 +4,33 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div> - <XAntenna :antenna="draft" @created="onAntennaCreated"/> -</div> +<MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + + <MkAntennaEditor @created="onAntennaCreated"/> +</MkStickyContainer> </template> <script lang="ts" setup> -import { ref } from 'vue'; -import XAntenna from './editor.vue'; +import { computed } from 'vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache } from '@/cache.js'; import { useRouter } from '@/router/supplier.js'; +import MkAntennaEditor from '@/components/MkAntennaEditor.vue'; const router = useRouter(); -const draft = ref({ - name: '', - src: 'all', - userListId: null, - users: [], - keywords: [], - excludeKeywords: [], - excludeBots: false, - withReplies: false, - caseSensitive: false, - localOnly: false, - withFile: false, - notify: false, -}); - function onAntennaCreated() { antennasCache.delete(); router.push('/my/antennas'); } +const headerActions = computed(() => []); +const headerTabs = computed(() => []); + definePageMetadata(() => ({ - title: i18n.ts.manageAntennas, - icon: 'ph-flying-saucer ph-bold ph-lg', + title: i18n.ts.createAntenna, + icon: 'ti ti-antenna', })); </script> diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index a262e932f306ef085cf5b18c9598fdd3ecce1605..9f927cd1a07058bc838a10e92fa526c499e7572a 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -4,15 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class=""> - <XAntenna v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/> -</div> +<MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + + <MkAntennaEditor v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/> +</MkStickyContainer> </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; -import XAntenna from './editor.vue'; +import MkAntennaEditor from '@/components/MkAntennaEditor.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; @@ -36,8 +38,11 @@ misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaRespons antenna.value = antennaResponse; }); +const headerActions = computed(() => []); +const headerTabs = computed(() => []); + definePageMetadata(() => ({ - title: i18n.ts.manageAntennas, - icon: 'ph-flying-saucer ph-bold ph-lg', + title: i18n.ts.editAntenna, + icon: 'ti ti-antenna', })); </script> diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue index a312672f747c3f2bcb7f3db91a7badff4c544b26..9233695900006c34279d1291917439d712718824 100644 --- a/packages/frontend/src/pages/my-antennas/index.vue +++ b/packages/frontend/src/pages/my-antennas/index.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> + <MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <div v-if="antennas.length > 0" class="_gaps"> <MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`"> @@ -45,7 +45,7 @@ fetch(); const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', + icon: 'ti ti-refresh', text: i18n.ts.reload, handler: () => { antennasCache.delete(); @@ -57,7 +57,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.manageAntennas, - icon: 'ph-flying-saucer ph-bold ph-lg', + icon: 'ti ti-antenna', })); onActivated(() => { diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index 1d5c134b27fa8f117432bcd5f2bd0c84d08c45e1..ece998a7a552f7dccb3594064af902014cdfb567 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="700"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <div v-if="tab === 'my'" key="my" class="_gaps"> - <MkButton primary rounded class="add" @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> + <MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps"> - <MkClipPreview v-for="item in items" :key="item.id" :clip="item"/> + <MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/> </MkPagination> </div> <div v-else-if="tab === 'favorites'" key="favorites" class="_gaps"> @@ -93,16 +93,16 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'my', title: i18n.ts.myClips, - icon: 'ph-paperclip ph-bold ph-lg', + icon: 'ti ti-paperclip', }, { key: 'favorites', title: i18n.ts.favorites, - icon: 'ph-heart ph-bold ph-lg', + icon: 'ti ti-heart', }]); definePageMetadata(() => ({ title: i18n.ts.clip, - icon: 'ph-paperclip ph-bold ph-lg', + icon: 'ti ti-paperclip', })); </script> diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index f2469be8dea402187e8b10642e49853912e8b357..cd68be4ac3cbfb648b195251326057bb8af0639a 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.createList }}</MkButton> + <MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton> <div v-if="items.length > 0" class="_gaps"> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`"> @@ -61,7 +61,7 @@ async function create() { const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', + icon: 'ti ti-refresh', text: i18n.ts.reload, handler: () => { userListsCache.delete(); @@ -73,7 +73,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.manageLists, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', })); onActivated(() => { diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index 1b7aa3f93839387cbeede1ec93a5f3d787d157d2..a2ceb222feeac22490ca9f03ecabc28189ef7688 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -38,8 +38,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA :class="$style.userItemBody" :to="`${userPage(item.user)}`"> <MkUserCardMini :user="item.user"/> </MkA> - <button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ph-dots-three ph-bold ph-lg"></i></button> - <button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.menu" @click="showMembershipMenu(item, $event)"><i class="ti ti-dots"></i></button> + <button class="_button" :class="$style.remove" @click="removeUser(item, $event)"><i class="ti ti-x"></i></button> </div> </div> </div> @@ -118,7 +118,7 @@ function addUser() { async function removeUser(item, ev) { os.popupMenu([{ text: i18n.ts.remove, - icon: 'ph-x ph-bold ph-lg', + icon: 'ti ti-x', danger: true, action: async () => { if (!list.value) return; @@ -133,22 +133,25 @@ async function removeUser(item, ev) { } async function showMembershipMenu(item, ev) { + const withRepliesRef = ref(item.withReplies); os.popupMenu([{ - text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline, - icon: item.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg', - action: async () => { - misskeyApi('users/lists/update-membership', { - listId: list.value.id, - userId: item.userId, - withReplies: !item.withReplies, - }).then(() => { - paginationEl.value.updateItem(item.id, (old) => ({ - ...old, - withReplies: !item.withReplies, - })); - }); - }, + 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, + userId: item.userId, + withReplies, + }).then(() => { + paginationEl.value!.updateItem(item.id, (old) => ({ + ...old, + withReplies, + })); + }); + }); } async function deleteList() { @@ -188,7 +191,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: list.value ? list.value.name : i18n.ts.lists, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', })); </script> diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue index 6f69f9285dfa12201a2931b09f5253c5b073799b..93a792c42f8cd41c12cae6b54fcc964f97e077a2 100644 --- a/packages/frontend/src/pages/not-found.vue +++ b/packages/frontend/src/pages/not-found.vue @@ -33,6 +33,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.notFound, - icon: 'ph-warning ph-bold ph-lg', + icon: 'ti ti-alert-triangle', })); </script> diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 418b33754b3695a69c9618c4328c7deab3009373..d0bbaeddd96bd4fe961301eaf6bd4a14b24c0d08 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -16,17 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_margin"> <div v-if="!showNext" class="_buttons" :class="$style.loadNext"> - <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ph-caret-up ph-bold ph-lg"></i> <i class="ph-television-simple ph-bold ph-lg"></i></MkButton> - <MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ph-caret-up ph-bold ph-lg"></i> <i class="ph-user ph-bold ph-lg"></i></MkButton> + <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton> + <MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton> </div> - <div v-if="defaultStore.state.noteDesign === 'misskey'" class="_margin _gaps_s"> + <div class="_margin _gaps_s"> <MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/> <MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/> </div> - <div v-else-if="defaultStore.state.noteDesign === 'sharkey'" class="_margin _gaps_s"> - <MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/> - <SkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/> - </div> <div v-if="clips && clips.length > 0" class="_margin"> <div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div> <div class="_gaps"> @@ -34,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-if="!showPrev" class="_buttons" :class="$style.loadPrev"> - <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ph-caret-down ph-bold ph-lg"></i> <i class="ph-television-simple ph-bold ph-lg"></i></MkButton> - <MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ph-caret-down ph-bold ph-lg"></i> <i class="ph-user ph-bold ph-lg"></i></MkButton> + <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ti ti-chevron-down"></i> <i class="ti ti-device-tv"></i></MkButton> + <MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ti ti-chevron-down"></i> <i class="ti ti-user"></i></MkButton> </div> </div> @@ -52,12 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { defineAsyncComponent, computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import type { Paging } from '@/components/MkPagination.vue'; -import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; import MkNotes from '@/components/MkNotes.vue'; -import SkNoteDetailed from '@/components/SkNoteDetailed.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkButton from '@/components/MkButton.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -67,6 +61,12 @@ import { dateString } from '@/filters/date.js'; import MkClipPreview from '@/components/MkClipPreview.vue'; import { defaultStore } from '@/store.js'; +const MkNoteDetailed = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNoteDetailed.vue') : + null +); + const props = defineProps<{ noteId: string; initialTab?: string; diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 9cbb6323a889fb398e5bd004abf0df73153ea48a..28f583829653684e0053c8757ded31b24652dba4 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -58,7 +58,7 @@ function setFilter(ev) { }, })); const items = includeTypes.value != null ? [{ - icon: 'ph-x ph-bold ph-lg', + icon: 'ti ti-x', text: i18n.ts.clear, action: () => { includeTypes.value = null; @@ -69,12 +69,12 @@ function setFilter(ev) { const headerActions = computed(() => [tab.value === 'all' ? { text: i18n.ts.filter, - icon: 'ph-funnel ph-bold ph-lg', + icon: 'ti ti-filter', highlighted: includeTypes.value != null, handler: setFilter, } : undefined, tab.value === 'all' ? { text: i18n.ts.markAllAsRead, - icon: 'ph-check ph-bold ph-lg', + icon: 'ti ti-check', handler: () => { os.apiWithDialog('notifications/mark-all-as-read'); }, @@ -83,20 +83,20 @@ const headerActions = computed(() => [tab.value === 'all' ? { const headerTabs = computed(() => [{ key: 'all', title: i18n.ts.all, - icon: 'ph-circle ph-bold ph-lg', + icon: 'ti ti-point', }, { key: 'mentions', title: i18n.ts.mentions, - icon: 'ph-at ph-bold ph-lg', + icon: 'ti ti-at', }, { key: 'directNotes', title: i18n.ts.directNotes, - icon: 'ph-envelope ph-bold ph-lg', + icon: 'ti ti-mail', }]); definePageMetadata(() => ({ title: i18n.ts.notifications, - icon: 'ph-bell ph-bold ph-lg', + icon: 'ti ti-bell', })); </script> diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue index 80b6a237ea8e79939d0582fe73d28d4da8ec71c5..733e34eb2cd68cb77f3c6cf8485ea1ef7b45f302 100644 --- a/packages/frontend/src/pages/oauth.vue +++ b/packages/frontend/src/pages/oauth.vue @@ -53,7 +53,7 @@ function onLogin(res): void { definePageMetadata(() => ({ title: 'OAuth', - icon: 'ph-squares-four ph-bold ph-lg', + icon: 'ti ti-apps', })); </script> diff --git a/packages/frontend/src/pages/page-editor/common.ts b/packages/frontend/src/pages/page-editor/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..420c8fc967eb4ee2fe509ac355ee607f5a4e4ea6 --- /dev/null +++ b/packages/frontend/src/pages/page-editor/common.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { i18n } from '@/i18n.js'; + +export function getPageBlockList() { + return [ + { value: 'section', text: i18n.ts._pages.blocks.section }, + { value: 'text', text: i18n.ts._pages.blocks.text }, + { value: 'image', text: i18n.ts._pages.blocks.image }, + { value: 'note', text: i18n.ts._pages.blocks.note }, + ]; +} diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue index 2a55c083d1870700d1a5adfdf709de6a175be994..1cfe7a6d2d67c2fbff31e1ebf9acc1759ad86e0c 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> <XContainer :draggable="true" @remove="() => $emit('remove')"> - <template #header><i class="ph-image-square ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.image }}</template> + <template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template> <template #func> <button @click="choose()"> - <i class="ph-folder ph-bold ph-lg"></i> + <i class="ti ti-folder"></i> </button> </template> diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue index 0bbe256cbf27822724263ad21b14cf46535a7c72..0a28386986dc58ed9ca304d37c5feefce2dbf3aa 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> <XContainer :draggable="true" @remove="() => $emit('remove')"> - <template #header><i class="ph-note ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.note }}</template> + <template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template> <section style="padding: 16px;" class="_gaps_s"> <MkInput v-model="id"> diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue index 2e4402085c7375255af32198d58f0d14d157dc98..0f8dc33143eaff37400bcdd92ad69bc0cfbb95c9 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue @@ -6,16 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> <XContainer :draggable="true" @remove="() => $emit('remove')"> - <template #header><i class="ph-note ph-bold ph-lg"></i> {{ props.modelValue.title }}</template> + <template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template> <template #func> <button class="_button" @click="rename()"> - <i class="ph-pencil-simple ph-bold ph-lg"></i> + <i class="ti ti-pencil"></i> </button> </template> <section class="ilrvjyvi"> <XBlocks v-model="children" class="children"/> - <MkButton rounded class="add" @click="add()"><i class="ph-plus ph-bold ph-lg"></i></MkButton> + <MkButton rounded class="add" @click="add()"><i class="ti ti-plus"></i></MkButton> </section> </XContainer> </template> @@ -29,6 +29,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { deepClone } from '@/scripts/clone.js'; import MkButton from '@/components/MkButton.vue'; +import { getPageBlockList } from '@/pages/page-editor/common.js'; const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue')); @@ -53,11 +54,9 @@ watch(children, () => { deep: true, }); -const getPageBlockList = inject<(any) => any>('getPageBlockList'); - async function rename() { const { canceled, result: title } = await os.inputText({ - title: 'Enter title', + title: i18n.ts._pages.enterSectionTitle, default: props.modelValue.title, }); if (canceled) return; diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue index e83ad058b446b03cdc5424501a3fa6c3d04b6a43..14c3e6845e9a3f2dc19ee1fed8afb24df84d766a 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <!-- eslint-disable vue/no-mutating-props --> <XContainer :draggable="true" @remove="() => $emit('remove')"> - <template #header><i class="ph-text-align-left ph-bold ph-lg"></i> {{ i18n.ts._pages.blocks.text }}</template> + <template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template> <section> <textarea ref="inputEl" v-model="text" :class="$style.textarea"></textarea> diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue index 7ef66c1c0c9c2685de0dab9c74775527ec9f11c4..2531de0e627210574514dd406d6d3f26501c3109 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.container.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue @@ -10,14 +10,14 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="buttons"> <slot name="func"></slot> <button v-if="removable" class="_button" @click="remove()"> - <i class="ph-trash ph-bold ph-lg"></i> + <i class="ti ti-trash"></i> </button> <button v-if="draggable" class="drag-handle _button"> - <i class="ph-list ph-bold ph-lg-2"></i> + <i class="ti ti-menu-2"></i> </button> <button class="_button" @click="toggleContent(!showBody)"> - <template v-if="showBody"><i class="ph-caret-up ph-bold ph-lg"></i></template> - <template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template> + <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> + <template v-else><i class="ti ti-chevron-down"></i></template> </button> </div> </header> diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 92de9d57a5ebdb322154cb7a5caf4253a07170ad..6bc3c0908a4998a10474cc65f8d8fbea618ebf94 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700"> <div class="jqqmcavi"> - <MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="ph-arrow-square-out ph-bold ph-lg"></i> {{ i18n.ts._pages.viewPage }}</MkButton> - <MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.duplicate }}</MkButton> - <MkButton v-if="pageId && !readonly" inline class="button" danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="ti ti-external-link"></i> {{ i18n.ts._pages.viewPage }}</MkButton> + <MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="ti ti-copy"></i> {{ i18n.ts.duplicate }}</MkButton> + <MkButton v-if="pageId && !readonly" inline class="button" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> <div v-if="tab === 'settings'"> @@ -40,10 +40,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="hideTitleWhenPinned">{{ i18n.ts._pages.hideTitleWhenPinned }}</MkSwitch> <div class="eyeCatch"> - <MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts._pages.eyeCatchingImageSet }}</MkButton> + <MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="ti ti-plus"></i> {{ i18n.ts._pages.eyeCatchingImageSet }}</MkButton> <div v-else-if="eyeCatchingImage"> <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/> - <MkButton v-if="!readonly" @click="removeEyeCatchingImage()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts._pages.eyeCatchingImageRemove }}</MkButton> + <MkButton v-if="!readonly" @click="removeEyeCatchingImage()"><i class="ti ti-trash"></i> {{ i18n.ts._pages.eyeCatchingImageRemove }}</MkButton> </div> </div> </div> @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.contents"> <XBlocks v-model="content" class="content"/> - <MkButton v-if="!readonly" rounded class="add" @click="add()"><i class="ph-plus ph-bold ph-lg"></i></MkButton> + <MkButton v-if="!readonly" rounded class="add" @click="add()"><i class="ti ti-plus"></i></MkButton> </div> </div> </MkSpacer> @@ -76,6 +76,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { $i } from '@/account.js'; import { mainRouter } from '@/router/main.js'; +import { getPageBlockList } from '@/pages/page-editor/common.js'; const props = defineProps<{ initPageId?: string; @@ -100,7 +101,6 @@ const alignCenter = ref(false); const hideTitleWhenPinned = ref(false); provide('readonly', readonly.value); -provide('getPageBlockList', getPageBlockList); watch(eyeCatchingImageId, async () => { if (eyeCatchingImageId.value == null) { @@ -215,15 +215,6 @@ async function add() { content.value.push({ id, type }); } -function getPageBlockList() { - return [ - { value: 'section', text: i18n.ts._pages.blocks.section }, - { value: 'text', text: i18n.ts._pages.blocks.text }, - { value: 'image', text: i18n.ts._pages.blocks.image }, - { value: 'note', text: i18n.ts._pages.blocks.note }, - ]; -} - function setEyeCatchingImage(img) { selectFile(img.currentTarget ?? img.target, null).then(file => { eyeCatchingImageId.value = file.id; @@ -276,18 +267,18 @@ const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'settings', title: i18n.ts._pages.pageSetting, - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', }, { key: 'contents', title: i18n.ts._pages.contents, - icon: 'ph-note ph-bold ph-lg', + icon: 'ti ti-note', }]); definePageMetadata(() => ({ title: props.initPageId ? i18n.ts._pages.editPage : props.initPageName && props.initUser ? i18n.ts._pages.readPage : i18n.ts._pages.newPage, - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', })); </script> diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 99b8b4143f5bccbb0633d7cf0a2e8d1e158eb81f..fc8627a772c10ad3d6e9c50e01962848b9d1bf86 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -47,8 +47,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar :user="page.user" :class="$style.avatar" indicator link preview/> <MkA :to="`/@${username}`"><MkUserName :user="page.user" :nowrap="false"/></MkA> </div> <div :class="$style.pageBannerTitleSubActions"> - <MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkA> - <button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> + <MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA> + <button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button> </div> </div> </div> @@ -58,12 +58,14 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.pageActions"> <div> - <MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" asLike primary @click="unlike()"><i class="ph-heart-break ph-bold ph-lg"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> - <MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ph-heart ph-bold ph-lg"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> + <MkButton v-if="page.isLiked" v-tooltip="i18n.ts._pages.unlike" class="button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> + <MkButton v-else v-tooltip="i18n.ts._pages.like" class="button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="page.likedCount > 0" class="count">{{ page.likedCount }}</span></MkButton> </div> <div :class="$style.other"> - <button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ph-link ph-bold ph-lg ti-fw"></i></button> - <button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ph-share-network ph-bold ph-lg ti-fw"></i></button> + <MkA v-if="page.userId === $i?.id" v-tooltip="i18n.ts._pages.editThisPage" :to="`/pages/edit/${page.id}`" class="_button" :class="$style.generalActionButton"><i class="ti ti-pencil ti-fw"></i></MkA> + <button v-tooltip="i18n.ts.copyLink" class="_button" :class="$style.generalActionButton" @click="copyLink"><i class="ti ti-link ti-fw"></i></button> + <button v-tooltip="i18n.ts.share" class="_button" :class="$style.generalActionButton" @click="share"><i class="ti ti-share ti-fw"></i></button> + <button v-if="$i" v-click-anime class="_button" :class="$style.generalActionButton" @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></button> </div> </div> <div :class="$style.pageUser"> @@ -75,21 +77,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user!" :inline="true" :transparent="false" :full="true" :class="$style.follow"/> </div> <div :class="$style.pageDate"> - <div><i class="ph ph-clock ph-bold ph-lg"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div> - <div v-if="page.createdAt != page.updatedAt"><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div> - </div> - <div :class="$style.pageLinks"> - <MkA v-if="!$i || $i.id !== page.userId" :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ i18n.ts._pages.viewSource }}</MkA> - <template v-if="$i && $i.id === page.userId"> - <MkA :to="`/pages/edit/${page.id}`" class="link">{{ i18n.ts._pages.editThisPage }}</MkA> - <button v-if="$i.pinnedPageId === page.id" class="link _textButton" @click="pin(false)">{{ i18n.ts.unpin }}</button> - <button v-else class="link _textButton" @click="pin(true)">{{ i18n.ts.pin }}</button> - </template> + <div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div> + <div v-if="page.createdAt != page.updatedAt"><i class="ti ti-clock-edit"></i> {{ i18n.ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div> </div> </div> <MkAd :prefer="['horizontal', 'horizontal-big']"/> <MkContainer :max-height="300" :foldable="true" class="other"> - <template #icon><i class="ph-clock ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-clock"></i></template> <template #header>{{ i18n.ts.recentPosts }}</template> <MkPagination v-slot="{items}" :pagination="otherPostsPagination" :class="$style.relatedPagesRoot" class="_gaps"> <MkPagePreview v-for="page in items" :key="page.id" :page="page" :class="$style.relatedPagesItem"/> @@ -104,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { computed, watch, ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; import XPage from '@/components/page/page.vue'; import MkButton from '@/components/MkButton.vue'; @@ -125,7 +119,11 @@ import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { instance } from '@/instance.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { useRouter } from '@/router/supplier.js'; +import { MenuItem } from '@/types/menu'; + +const router = useRouter(); const props = defineProps<{ pageName: string; @@ -170,12 +168,12 @@ function share(ev: MouseEvent) { os.popupMenu([ { text: i18n.ts.shareWithNote, - icon: 'ph-pencil-simple', + icon: 'ti ti-pencil', action: shareWithNote, }, ...(isSupportShare() ? [{ text: i18n.ts.share, - icon: 'ph-share-network', + icon: 'ti ti-share', action: shareWithNavigator, }] : []), ], ev.currentTarget ?? ev.target); @@ -242,6 +240,69 @@ function pin(pin) { }); } +function reportAbuse() { + if (!page.value) return; + + const pageUrl = `${url}/@${props.username}/pages/${props.pageName}`; + + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + user: page.value.user, + initialComment: `Page: ${pageUrl}\n-----\n`, + }, { + closed: () => dispose(), + }); +} + +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 ? [{ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => pin(false), + }] : [{ + 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); +} + watch(() => path.value, fetchPage, { immediate: true }); const headerActions = computed(() => []); @@ -286,6 +347,7 @@ definePageMetadata(() => ({ background-color: var(--accentedBg); color: var(--accent); text-decoration: none; + outline: none; } } diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index 7b4dd83068caf4b24a9572f4046c5ae772a35e62..4ef9d3b0917f018e9c16111be60d3c59e73561a0 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-else-if="tab === 'my'" key="my" class="_gaps"> - <MkButton class="new" @click="create()"><i class="ph-plus ph-bold ph-lg"></i></MkButton> + <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton> <MkPagination v-slot="{items}" :pagination="myPagesPagination"> <div class="_gaps"> <MkPagePreview v-for="page in items" :key="page.id" :page="page"/> @@ -69,7 +69,7 @@ function create() { } const headerActions = computed(() => [{ - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.create, handler: create, }]); @@ -77,19 +77,19 @@ const headerActions = computed(() => [{ const headerTabs = computed(() => [{ key: 'featured', title: i18n.ts._pages.featured, - icon: 'ph-fire ph-bold ph-lg', + icon: 'ti ti-flare', }, { key: 'my', title: i18n.ts._pages.my, - icon: 'ph-pencil-simple-line ph-bold ph-lg', + icon: 'ti ti-edit', }, { key: 'liked', title: i18n.ts._pages.liked, - icon: 'ph-heart ph-bold ph-lg', + icon: 'ti ti-heart', }]); definePageMetadata(() => ({ title: i18n.ts.pages, - icon: 'ph-note ph-bold ph-lg', + icon: 'ti ti-note', })); </script> diff --git a/packages/frontend/src/pages/preview.vue b/packages/frontend/src/pages/preview.vue new file mode 100644 index 0000000000000000000000000000000000000000..8e07b190aa6c16e1b064b37c133920ab14b940f1 --- /dev/null +++ b/packages/frontend/src/pages/preview.vue @@ -0,0 +1,26 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div> + <MkSample/> +</div> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import MkSample from '@/components/MkPreview.vue'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata(computed(() => ({ + title: i18n.ts.preview, + icon: 'ti ti-eye', +}))); +</script> diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index 350c4fea1d62a7387526719b98cc1248ecb98d6e..bac1d2bb700752e7bfc2f142d9c2266acec87412 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -98,6 +98,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.registry, - icon: 'ph-faders ph-bold ph-lg', + icon: 'ti ti-adjustments', })); </script> diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index 61bf5f4545ef809eff3c98d64aeeb25eb3e7b85a..c40d13f664d6c2d1502917be71607ad21b5435c0 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -30,14 +30,14 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.value }} (JSON)</template> </MkCodeEditor> - <MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkKeyValue> <template #key>{{ i18n.ts.updatedAt }}</template> <template #value><MkTime :time="value.updatedAt" mode="detail"/></template> </MkKeyValue> - <MkButton danger @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </template> </div> </MkSpacer> @@ -125,6 +125,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.registry, - icon: 'ph-faders ph-bold ph-lg', + icon: 'ti ti-adjustments', })); </script> diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index de0c8981879e94ff44758d8cc6e0e021f7acc839..c641874b17a316cee1695981cffad09eb1017729 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -75,6 +75,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.registry, - icon: 'ph-faders ph-bold ph-lg', + icon: 'ti ti-adjustments', })); </script> diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue index 8b0b4baa677354ff9014e1f4d2199b6e4656c8fe..6d240295352ddf9d44f086a54dbdd6be9d78d5c1 100644 --- a/packages/frontend/src/pages/reset-password.vue +++ b/packages/frontend/src/pages/reset-password.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer v-if="token" :contentMax="700" :marginMin="16" :marginMax="32"> <div class="_gaps_m"> <MkInput v-model="password" type="password"> - <template #prefix><i class="ph-lock ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-lock"></i></template> <template #label>{{ i18n.ts.newPassword }}</template> </MkInput> @@ -44,7 +44,9 @@ async function save() { onMounted(() => { if (props.token == null) { - os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {}, 'closed'); + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { + closed: () => dispose(), + }); mainRouter.push('/'); } }); @@ -55,6 +57,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.resetPassword, - icon: 'ph-lock ph-bold ph-lg', + icon: 'ti ti-lock', })); </script> diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index af0309b699118f3fab9c7466698e09ce04d05b84..7d9cefa5c9f44e7d2b91806967ac4289c0b910f3 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -89,12 +89,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;"> <div>{{ logPos }} / {{ game.logs.length }}</div> <div v-if="!autoplaying" class="_buttonsCenter"> - <MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ph-caret-left ph-bold ph-lg"></i></MkButton> - <MkButton :disabled="logPos === 0" @click="logPos--"><i class="ph-caret-left ph-bold ph-lg"></i></MkButton> - <MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ph-caret-right ph-bold ph-lg"></i></MkButton> - <MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ph-caret-right ph-bold ph-lg"></i></MkButton> + <MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton> + <MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton> + <MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton> + <MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ti ti-chevrons-right"></i></MkButton> </div> - <MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ph-play ph-bold ph-lg"></i></MkButton> + <MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton> </div> <div class="_panel" style="padding: 16px;"> @@ -169,7 +169,7 @@ const props = defineProps<{ const showBoardLabels = ref<boolean>(false); const useAvatarAsStone = ref<boolean>(true); const autoplaying = ref<boolean>(false); -// eslint-disable-next-line vue/no-setup-props-destructure +// eslint-disable-next-line vue/no-setup-props-reactivity-loss const game = ref<Misskey.entities.ReversiGameDetailed & { logs: Reversi.Serializer.SerializedLog[] }>(deepClone(props.game)); const logPos = ref<number>(game.value.logs.length); const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({ diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index 93b0972e9cede2255430459eb8af572c794768e9..31c0003130ca9a6c8c26e238d889e0e0f2880cc5 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -23,10 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div style="padding: 16px;"> - <div v-if="game.map == null"><i class="ph-dice-five ph-bold ph-lg"></i></div> + <div v-if="game.map == null"><i class="ti ti-dice"></i></div> <div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> <div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)"> - <i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ph-circle-half ph-bold ph-lg' : 'ph-circle ph-bold ph-lg'"></i> + <i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i> </div> </div> </div> diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 21b7797240dfb3af7cccea5fec9294dfe491b5c3..97a793753d8b18598e30513b90798fd6ecd29535 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -20,6 +20,7 @@ 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 { i18n } from '@/i18n.js'; import { useInterval } from '@/scripts/use-interval.js'; @@ -44,7 +45,7 @@ function start(_game: Misskey.entities.ReversiGameDetailed) { if (shareWhenStart.value) { misskeyApi('notes/create', { - text: i18n.ts._reversi.iStartedAGame + '\n' + location.href, + text: `${i18n.ts._reversi.iStartedAGame}\n${url}/reversi/g/${props.gameId}`, visibility: 'home', }); } @@ -115,6 +116,6 @@ onUnmounted(() => { definePageMetadata(() => ({ title: 'Reversi', - icon: 'ph-game-controller ph-bold ph-lg', + icon: 'ti ti-device-gamepad', })); </script> diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue index c863b918344b27a53cdbe8b19161846d712cb652..51a03e441801df034807441aca575e3780ed9432 100644 --- a/packages/frontend/src/pages/reversi/index.vue +++ b/packages/frontend/src/pages/reversi/index.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary gradate rounded @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton> <MkButton primary gradate rounded @click="matchUser">{{ i18n.ts.invite }}</MkButton> </div> - <div style="font-size: 90%; opacity: 0.7; text-align: center;"><i class="ph-music-notes ph-bold ph-lg"></i> {{ i18n.ts.soundWillBePlayed }}</div> + <div style="font-size: 90%; opacity: 0.7; text-align: center;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div> </div> <MkFolder v-if="invitations.length > 0" :defaultOpen="true"> @@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> <div :class="$style.gamePreviewPlayers"> - <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span> - <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span> + <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> + <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/> <span style="margin: 0 1em;">vs</span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> - <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span> - <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span> + <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> + <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> </div> <div :class="$style.gamePreviewFooter"> <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span> @@ -63,13 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.gamePreviews"> <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`"> <div :class="$style.gamePreviewPlayers"> - <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span> - <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span> + <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> + <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/> <span style="margin: 0 1em;">vs</span> <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/> - <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ph-x ph-bold ph-lg"></i></span> - <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ph-trophy ph-bold ph-lg"></i></span> + <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span> + <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span> </div> <div :class="$style.gamePreviewFooter"> <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span> @@ -263,7 +263,7 @@ onUnmounted(() => { definePageMetadata(() => ({ title: 'Reversi', - icon: 'ph-game-controller ph-bold ph-lg', + icon: 'ti ti-device-gamepad', })); </script> diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index 8621b61eeb417e9dd7d856e925829764fcf88fa3..45edbc5da284b91f8a12c9235134b0f7826fdb66 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/> <p :class="$style.text"> - <i class="ph-warning ph-bold ph-lg"></i> + <i class="ti ti-alert-triangle"></i> {{ error }} </p> </div> @@ -85,17 +85,17 @@ const users = computed(() => ({ const headerTabs = computed(() => [{ key: 'users', - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', title: i18n.ts.users, }, { key: 'timeline', - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', title: i18n.ts.timeline, }]); definePageMetadata(() => ({ title: role.value ? role.value.name : i18n.ts.role, - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badge', })); </script> @@ -118,4 +118,3 @@ definePageMetadata(() => ({ border-radius: var(--radius-md); } </style> - diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index fb3657cdc9fcd9ef0ea0ccdb79f71fd04284e86a..9aaa8ff9c633cc589ec93d663a76b4ec23404ea5 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.editor" class="_panel"> <MkCodeEditor v-model="code" lang="aiscript"/> </div> - <MkButton primary @click="run()"><i class="ph-play ph-bold ph-lg"></i></MkButton> + <MkButton primary @click="run()"><i class="ti ti-player-play"></i></MkButton> </div> <MkContainer v-if="root && components.length > 1" :key="uiKey" :foldable="true"> @@ -154,7 +154,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.scratchpad, - icon: 'ph-terminal-window ph-bold ph-lg-2', + icon: 'ti ti-terminal-2', })); </script> diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue index 525e71cf00c59ff25309cd3f700c3810f1c1ae4e..66c5c92480785baf77e02068bd45b9c914cbe360 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -6,14 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <div class="_gaps"> - <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search"> + <template #prefix><i class="ti ti-search"></i></template> </MkInput> - <MkFolder> - <template #label>{{ i18n.ts.options }}</template> + <MkFoldableSection :expanded="true"> + <template #header>{{ i18n.ts.options }}</template> <div class="_gaps_m"> - <MkSwitch v-model="isLocalOnly">{{ i18n.ts.localOnly }}</MkSwitch> + <MkRadios v-model="hostSelect"> + <template #label>{{ i18n.ts.host }}</template> + <option value="all" default>{{ i18n.ts.all }}</option> + <option value="local">{{ i18n.ts.local }}</option> + <option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option> + </MkRadios> + <MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search"> + <template #prefix><i class="ti ti-server"></i></template> + </MkInput> <MkSwitch v-model="order">Sort by newest to oldest</MkSwitch> <MkSelect v-model="filetype" small> <template #label>File Type</template> @@ -25,18 +33,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder :defaultOpen="true"> <template #label>{{ i18n.ts.specifyUser }}</template> - <template v-if="user" #suffix>@{{ user.username }}</template> + <template v-if="user" #suffix>@{{ user.username }}{{ user.host ? `@${user.host}` : "" }}</template> - <div style="text-align: center;" class="_gaps"> - <div v-if="user">@{{ user.username }}</div> - <div> - <MkButton v-if="user == null" primary rounded inline @click="selectUser">{{ i18n.ts.selectUser }}</MkButton> - <MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton> + <div class="_gaps"> + <div :class="$style.userItem"> + <MkUserCardMini v-if="user" :class="$style.userCard" :user="user" :withChart="false"/> + <MkButton v-if="user == null && $i != null" transparent :class="$style.addMeButton" @click="selectSelf"><div :class="$style.addUserButtonInner"><span><i class="ti ti-plus"></i><i class="ti ti-user"></i></span><span>{{ i18n.ts.selectSelf }}</span></div></MkButton> + <MkButton v-if="user == null" transparent :class="$style.addUserButton" @click="selectUser"><div :class="$style.addUserButtonInner"><i class="ti ti-plus"></i><span>{{ i18n.ts.selectUser }}</span></div></MkButton> + <button class="_button" :class="$style.remove" :disabled="user == null" @click="removeUser"><i class="ti ti-x"></i></button> </div> </div> </MkFolder> </div> - </MkFolder> + </MkFoldableSection> <div> <MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton> </div> @@ -50,7 +59,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { computed, ref, toRef, watch } from 'vue'; +import type { UserDetailed } from 'misskey-js/entities.js'; +import type { Paging } from '@/components/MkPagination.vue'; import MkNotes from '@/components/MkNotes.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; @@ -62,45 +73,135 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFolder from '@/components/MkFolder.vue'; import { useRouter } from '@/router/supplier.js'; +import MkUserCardMini from '@/components/MkUserCardMini.vue'; +import MkRadios from '@/components/MkRadios.vue'; +import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; -const router = useRouter(); +const props = withDefaults(defineProps<{ + query?: string; + userId?: string; + username?: string; + host?: string | null; +}>(), { + query: '', + userId: undefined, + username: undefined, + host: '', +}); +const router = useRouter(); const key = ref(0); -const searchQuery = ref(''); -const searchOrigin = ref('combined'); -const notePagination = ref(); -const user = ref<any>(null); -const isLocalOnly = ref(false); +const searchQuery = ref(toRef(props, 'query').value); +const notePagination = ref<Paging>(); +const user = ref<UserDetailed | null>(null); +const hostInput = ref(toRef(props, 'host').value); const order = ref(false); const filetype = ref(null); +const noteSearchableScope = instance.noteSearchableScope ?? 'local'; + +const hostSelect = ref<'all' | 'local' | 'specified'>('all'); + +const setHostSelectWithInput = (after:string|undefined|null, before:string|undefined|null) => { + if (before === after) return; + if (after === '') hostSelect.value = 'all'; + else hostSelect.value = 'specified'; +}; + +setHostSelectWithInput(hostInput.value, undefined); + +watch(hostInput, setHostSelectWithInput); + +const searchHost = computed(() => { + if (hostSelect.value === 'local') return '.'; + if (hostSelect.value === 'specified') return hostInput.value; + return null; +}); + +if (props.userId != null) { + misskeyApi('users/show', { userId: props.userId }).then(_user => { + user.value = _user; + }); +} else if (props.username != null) { + misskeyApi('users/show', { + username: props.username, + ...(props.host != null && props.host !== '') ? { host: props.host } : {}, + }).then(_user => { + user.value = _user; + }); +} + function selectUser() { - os.selectUser({ includeSelf: true }).then(_user => { + os.selectUser({ includeSelf: true, localOnly: instance.noteSearchableScope === 'local' }).then(_user => { user.value = _user; + hostInput.value = _user.host ?? ''; }); } +function selectSelf() { + user.value = $i as UserDetailed | null; + hostInput.value = null; +} + +function removeUser() { + user.value = null; + hostInput.value = ''; +} + async function search() { const query = searchQuery.value.toString().trim(); if (query == null || query === '') return; - if (query.startsWith('http://') || query.startsWith('https://')) { - const promise = misskeyApi('ap/show', { - uri: query, + //#region AP lookup + if (query.startsWith('http://') || query.startsWith('https://') && !query.includes(' ')) { + const confirm = await os.confirm({ + type: 'info', + text: i18n.ts.lookupConfirm, }); + if (!confirm.canceled) { + const promise = misskeyApi('ap/show', { + uri: query, + }); + + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); + + const res = await promise; - os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); + if (res.type === 'User') { + router.push(`/@${res.object.username}@${res.object.host}`); + } else if (res.type === 'Note') { + router.push(`/notes/${res.object.id}`); + } - const res = await promise; + return; + } + } + //#endregion - if (res.type === 'User') { - router.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - router.push(`/notes/${res.object.id}`); + if (query.length > 1 && !query.includes(' ')) { + if (query.startsWith('@')) { + const confirm = await os.confirm({ + type: 'info', + text: i18n.ts.lookupConfirm, + }); + if (!confirm.canceled) { + router.push(`/${query}`); + return; + } } - return; + if (query.startsWith('#')) { + const confirm = await os.confirm({ + type: 'info', + text: i18n.ts.openTagPageConfirm, + }); + if (!confirm.canceled) { + router.push(`/tags/${encodeURIComponent(query.substring(1))}`); + return; + } + } } notePagination.value = { @@ -109,13 +210,51 @@ async function search() { params: { query: searchQuery.value, userId: user.value ? user.value.id : null, + ...(searchHost.value ? { host: searchHost.value } : {}), order: order.value ? 'desc' : 'asc', filetype: filetype.value, }, }; - if (isLocalOnly.value) notePagination.value.params.host = '.'; - key.value++; } </script> +<style lang="scss" module> +.userItem { + display: flex; + justify-content: center; +} +.addMeButton { + border: 2px dashed var(--fgTransparent); + padding: 12px; + margin-right: 16px; +} +.addUserButton { + border: 2px dashed var(--fgTransparent); + padding: 12px; + flex-grow: 1; +} +.addUserButtonInner { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + min-height: 38px; +} +.userCard { + flex-grow: 1; +} +.remove { + width: 32px; + height: 32px; + align-self: center; + + & > i:before { + color: #ff2a2a; + } + + &:disabled { + opacity: 0; + } +} +</style> diff --git a/packages/frontend/src/pages/search.stories.impl.ts b/packages/frontend/src/pages/search.stories.impl.ts new file mode 100644 index 0000000000000000000000000000000000000000..0110a7ab8ed1718829b66658988623d682009357 --- /dev/null +++ b/packages/frontend/src/pages/search.stories.impl.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import search_ from './search.vue'; +import { userDetailed } from '@/../.storybook/fakes.js'; +import { commonHandlers } from '@/../.storybook/mocks.js'; + +const localUser = userDetailed('someuserid', 'miskist', null, 'Local Misskey User'); + +export const Default = { + render(args) { + return { + components: { + search_, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '<search_ v-bind="props" />', + }; + }, + args: { + ignoreNotesSearchAvailable: true, + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/users/show', () => { + return HttpResponse.json(userDetailed()); + }), + http.post('/api/users/search', () => { + return HttpResponse.json([userDetailed(), localUser]); + }), + ], + }, + }, +} satisfies StoryObj<typeof search_>; + +export const NoteSearchDisabled = { + ...Default, + args: {}, +} satisfies StoryObj<typeof search_>; + +export const WithUsernameLocal = { + ...Default, + + args: { + ...Default.args, + username: localUser.username, + host: localUser.host, + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/users/show', () => { + return HttpResponse.json(localUser); + }), + http.post('/api/users/search', () => { + return HttpResponse.json([userDetailed(), localUser]); + }), + ], + }, + }, +} satisfies StoryObj<typeof search_>; + +export const WithUserType = { + ...Default, + args: { + type: 'user', + }, +} satisfies StoryObj<typeof search_>; diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index 8dda3b5b027f448230cab2f9c0f84227db3dbace..a355c0eeaa703b2e0e5a980545bca678ce8e6e87 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps"> <div class="_gaps"> - <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> - <template #prefix><i class="ph-magnifying-glass ph-bold ph-lg"></i></template> + <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search"> + <template #prefix><i class="ti ti-search"></i></template> </MkInput> <MkRadios v-model="searchOrigin" @update:modelValue="search()"> <option value="combined">{{ i18n.ts.all }}</option> @@ -25,7 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, toRef } from 'vue'; +import type { Endpoints } from 'misskey-js'; +import type { Paging } from '@/components/MkPagination.vue'; import MkUserList from '@/components/MkUserList.vue'; import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; @@ -36,34 +38,74 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useRouter } from '@/router/supplier.js'; +const props = withDefaults(defineProps<{ + query?: string, + origin?: Endpoints['users/search']['req']['origin'], +}>(), { + query: '', + origin: 'combined', +}); + const router = useRouter(); const key = ref(''); -const searchQuery = ref(''); -const searchOrigin = ref('combined'); -const userPagination = ref(); +const searchQuery = ref(toRef(props, 'query').value); +const searchOrigin = ref(toRef(props, 'origin').value); +const userPagination = ref<Paging>(); async function search() { const query = searchQuery.value.toString().trim(); if (query == null || query === '') return; - if (query.startsWith('http://') || query.startsWith('https://')) { - const promise = misskeyApi('ap/show', { - uri: query, + //#region AP lookup + if (query.startsWith('http://') || query.startsWith('https://') && !query.includes(' ')) { + const confirm = await os.confirm({ + type: 'info', + text: i18n.ts.lookupConfirm, }); + if (!confirm.canceled) { + const promise = misskeyApi('ap/show', { + uri: query, + }); + + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); - os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); + const res = await promise; - const res = await promise; + if (res.type === 'User') { + router.push(`/@${res.object.username}@${res.object.host}`); + } else if (res.type === 'Note') { + router.push(`/notes/${res.object.id}`); + } - if (res.type === 'User') { - router.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - router.push(`/notes/${res.object.id}`); + return; } + } + //#endregion - return; + if (query.length > 1 && !query.includes(' ')) { + if (query.startsWith('@')) { + const confirm = await os.confirm({ + type: 'info', + text: i18n.ts.lookupConfirm, + }); + if (!confirm.canceled) { + router.push(`/${query}`); + return; + } + } + + if (query.startsWith('#')) { + const confirm = await os.confirm({ + type: 'info', + text: i18n.ts.openTagPageConfirm, + }); + if (!confirm.canceled) { + router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`); + return; + } + } } if (query.match(/^@[a-z0-9_.-]+@[a-z0-9_.-]+$/i)) { diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index fe56297c65eb5323884f42c93f0dd8520ab6dd2e..38d7548fa8460beb00052f5a18a0f2fc5ff4a3e8 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <MkSpacer v-if="tab === 'note'" key="note" :contentMax="800"> - <div v-if="notesSearchAvailable"> - <XNote/> + <div v-if="notesSearchAvailable || ignoreNotesSearchAvailable"> + <XNote v-bind="props"/> </div> <div v-else> <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> @@ -18,42 +18,58 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800"> - <XUser/> + <XUser v-bind="props"/> </MkSpacer> </MkHorizontalSwipe> </MkStickyContainer> </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, ref } from 'vue'; +import { computed, defineAsyncComponent, ref, toRef } from 'vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; +import { notesSearchAvailable } from '@/scripts/check-permissions.js'; import MkInfo from '@/components/MkInfo.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; +const props = withDefaults(defineProps<{ + query?: string, + userId?: string, + username?: string, + host?: string | null, + type?: 'note' | 'user', + origin?: 'combined' | 'local' | 'remote', + // For storybook only + ignoreNotesSearchAvailable?: boolean, +}>(), { + query: '', + userId: undefined, + username: undefined, + host: undefined, + type: 'note', + origin: 'combined', + ignoreNotesSearchAvailable: false, +}); + const XNote = defineAsyncComponent(() => import('./search.note.vue')); const XUser = defineAsyncComponent(() => import('./search.user.vue')); -const tab = ref('note'); - -const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes)); +const tab = ref(toRef(props, 'type').value); const headerActions = computed(() => []); const headerTabs = computed(() => [{ key: 'note', title: i18n.ts.notes, - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', }, { key: 'user', title: i18n.ts.users, - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', }]); definePageMetadata(() => ({ title: i18n.ts.search, - icon: 'ph-magnifying-glass ph-bold ph-lg', + icon: 'ti ti-search', })); </script> diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue index 857e28e1a15d755e69ddb57fbd5a6c006d91c9d7..2244047b31659dbef7fa8aeee974b48d23ee0103 100644 --- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue +++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_buttonsCenter" style="margin-top: 16px;"> <MkButton rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton> - <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </MkSpacer> </div> @@ -62,8 +62,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div>{{ i18n.ts._2fa.step3 }}</div> </div> <div class="_buttonsCenter" style="margin-top: 16px;"> - <MkButton rounded @click="page--"><i class="ph-arrow-left ph-bold ph-lg"></i> {{ i18n.ts.goBack }}</MkButton> - <MkButton primary rounded gradate @click="tokenDone">{{ i18n.ts.continue }} <i class="ph-arrow-right ph-bold ph-lg"></i></MkButton> + <MkButton rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton> + <MkButton primary rounded gradate @click="tokenDone">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton> </div> </MkSpacer> </div> @@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="text-align: center; font-weight: bold;">{{ i18n.ts._2fa.checkBackupCodesBeforeCloseThisWizard }}</div> <MkFolder :defaultOpen="true"> - <template #icon><i class="ph-key ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts._2fa.backupCodes }}</template> <div class="_gaps"> @@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> </div> - <MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.download }}</MkButton> + <MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton> </div> </MkFolder> </div> diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index 0396a3b0855874be607ba47e97cc69ca80a0d857..6a9a1e16e20666c158ebf9684c2e18ad1edce027 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -16,10 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInfo> <MkFolder :defaultOpen="true"> - <template #icon><i class="ph-shield ph-bold ph-lg-lock"></i></template> + <template #icon><i class="ti ti-shield-lock"></i></template> <template #label>{{ i18n.ts.totp }}</template> <template #caption>{{ i18n.ts.totpDescription }}</template> - <template #suffix><i v-if="$i.twoFactorEnabled" class="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template> + <template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--success)"></i></template> <div v-if="$i.twoFactorEnabled" class="_gaps_s"> <div v-text="i18n.ts._2fa.alreadyRegistered"/> @@ -32,12 +32,12 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-else-if="!$i.twoFactorEnabled" class="_gaps_s"> <MkButton primary gradate @click="registerTOTP">{{ i18n.ts._2fa.registerTOTP }}</MkButton> - <MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ph-question ph-bold ph-lg"></i> {{ i18n.ts.learnMore }}</MkLink> + <MkLink url="https://misskey-hub.net/docs/for-users/stepped-guides/how-to-enable-2fa/" target="_blank"><i class="ti ti-help-circle"></i> {{ i18n.ts.learnMore }}</MkLink> </div> </MkFolder> <MkFolder> - <template #icon><i class="ph-key ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-key"></i></template> <template #label>{{ i18n.ts.securityKeyAndPasskey }}</template> <div class="_gaps_s"> <MkInfo> @@ -58,8 +58,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ key.name }}</template> <template #suffix><I18n :src="i18n.ts.lastUsedAt"><template #t><MkTime :time="key.lastUsed"/></template></I18n></template> <div class="_buttons"> - <MkButton @click="renameKey(key)"><i class="ph-textbox ph-bold ph-lg"></i> {{ i18n.ts.rename }}</MkButton> - <MkButton danger @click="unregisterKey(key)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.unregister }}</MkButton> + <MkButton @click="renameKey(key)"><i class="ti ti-forms"></i> {{ i18n.ts.rename }}</MkButton> + <MkButton danger @click="unregisterKey(key)"><i class="ti ti-trash"></i> {{ i18n.ts.unregister }}</MkButton> </div> </MkFolder> </template> @@ -108,9 +108,11 @@ async function registerTOTP(): Promise<void> { token: auth.result.token, }); - os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), { twoFactorData, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } async function unregisterTOTP(): Promise<void> { diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index f5effbd68b1ca72f845b692ace386ef27d7a76e1..08c9261dcf09ec783e890aace71aead11b4c12f1 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSuspense :p="init"> <div class="_gaps"> <div class="_buttons"> - <MkButton primary @click="addAccount"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.addAccount }}</MkButton> - <MkButton @click="init"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.reloadAccountsList }}</MkButton> + <MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton> + <MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton> </div> <MkUserCardMini v-for="user in accounts" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/> @@ -48,11 +48,11 @@ const init = async () => { function menu(account, ev) { os.popupMenu([{ text: i18n.ts.switch, - icon: 'ph-arrows-left-right ph-bold ph-lg', + icon: 'ti ti-switch-horizontal', action: () => switchAccount(account), }, { text: i18n.ts.logout, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: () => removeAccount(account), }], ev.currentTarget ?? ev.target); @@ -74,22 +74,24 @@ async function removeAccount(account) { } function addExistingAccount() { - os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { done: async res => { await addAccounts(res.id, res.i); os.success(); init(); }, - }, 'closed'); + closed: () => dispose(), + }); } function createAccount() { - os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { done: async res => { await addAccounts(res.id, res.i); switchAccountWithToken(res.i); }, - }, 'closed'); + closed: () => dispose(), + }); } async function switchAccount(account: any) { @@ -108,7 +110,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.accounts, - icon: 'ph-users ph-bold ph-lg', + icon: 'ti ti-users', })); </script> diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue index f8f340d602a1cd42b61d8208fd4ac22110ee2c00..b35d406a98e21a3cde397ca0abfa7149e082a276 100644 --- a/packages/frontend/src/pages/settings/api.vue +++ b/packages/frontend/src/pages/settings/api.vue @@ -23,7 +23,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const isDesktop = ref(window.innerWidth >= 1100); function generateToken() { - os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, { done: async result => { const { name, permissions } = result; const { token } = await misskeyApi('miauth/gen-token', { @@ -38,7 +38,8 @@ function generateToken() { text: token, }); }, - }, 'closed'); + closed: () => dispose(), + }); } const headerActions = computed(() => []); @@ -47,6 +48,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: 'API', - icon: 'ph-webhooks-logo ph-bold ph-lg', + icon: 'ti ti-api', })); </script> diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue index abdb5d1cdd24abe4522a26071344898b1672c956..c58ea5d3785024b3149df67e78dced184c4fc913 100644 --- a/packages/frontend/src/pages/settings/apps.vue +++ b/packages/frontend/src/pages/settings/apps.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only </ul> </details> <div> - <MkButton inline danger @click="revoke(token)"><i class="ph-trash ph-bold ph-lg"></i></MkButton> + <MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton> </div> </div> </div> @@ -77,7 +77,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.installedApps, - icon: 'ph-plug ph-bold ph-lg', + icon: 'ti ti-plug', })); </script> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue index 1b731ff6243e3f2b246741f5ade57791a3f18a4d..0767fa7864c6b5493a7bf7a00a0bf08d0ce882f1 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <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/> - <i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ph-lock ph-bold ph-lg"></i> + <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> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue index 327e0ef723c07a4617f175a2a0a4f9ecc5582bbe..ce1d4e48d829801d93968870ee1e87060113ac31 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -36,9 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSpacer> <div :class="$style.footer" class="_buttonsCenter"> - <MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.update }}</MkButton> - <MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ph-x ph-bold ph-lg"></i> {{ i18n.ts.detach }}</MkButton> - <MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.attach }}</MkButton> + <MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton> + <MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton> + <MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton> </div> </div> </MkModalWindow> diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue index a60d7209cfc7d1115c6ca26eb5c172f89b6be70b..77229d3349d04c5fa331f67076083877411ff40f 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -67,7 +67,7 @@ misskeyApi('get-avatar-decorations').then(_avatarDecorations => { }); function openDecoration(avatarDecoration, index?: number) { - os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), { decoration: avatarDecoration, usingIndex: index, }, { @@ -108,7 +108,8 @@ function openDecoration(avatarDecoration, index?: number) { }); $i.avatarDecorations = update; }, - }, 'closed'); + closed: () => dispose(), + }); } function detachAllDecorations() { @@ -130,7 +131,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.avatarDecorations, - icon: 'ph-sparkle ph-bold ph-lg', + icon: 'ti ti-sparkles', })); </script> diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue index 59733e896f28251eb297219b5705b9ed248aea6c..cf05e75acc5577c2cb456e4c2511d9494acf725b 100644 --- a/packages/frontend/src/pages/settings/custom-css.vue +++ b/packages/frontend/src/pages/settings/custom-css.vue @@ -47,6 +47,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.customCss, - icon: 'ph-code ph-bold ph-lg', + icon: 'ti ti-code', })); </script> diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index 81ae9bc2f7127482a63f5a57e3aac2f8655bafda..e574ec7dc06fb3eca44830ec74b2835479775902 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -38,6 +38,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.deck, - icon: 'ph-text-columns ph-bold ph-lg', + icon: 'ti ti-columns', })); </script> diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index fef12fee06a1a6df7969d779aac2fd8865f931f0..3f7db1b779dd44832a13973d5ecb8dfdf9998d8a 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script setup lang="ts"> -import { computed, ref, watch } from 'vue'; +import { computed, ref, watch, type StyleValue } from 'vue'; import tinycolor from 'tinycolor2'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -102,10 +102,10 @@ function fetchDriveInfo(): void { }); } -function genUsageBar(fsize: number): object { +function genUsageBar(fsize: number): StyleValue { return { width: `${fsize / usage.value * 100}%`, - background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }), + background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }).toHslString(), }; } @@ -119,7 +119,7 @@ function onContextMenu(ev: MouseEvent, file): void { definePageMetadata(() => ({ title: i18n.ts.drivecleaner, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', })); </script> diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 06de4163b6b16628ad7ff2c3bb00a6aa5c97fa12..fa0963784432c432233b04c4e576954faa43bf0a 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormLink to="" @click="chooseUploadFolder()"> {{ i18n.ts.uploadFolder }} <template #suffix>{{ uploadFolder ? uploadFolder.name : '-' }}</template> - <template #suffixIcon><i class="ph-folder ph-bold ph-lg"></i></template> + <template #suffixIcon><i class="ti ti-folder"></i></template> </FormLink> <FormLink to="/settings/drive/cleaner"> {{ i18n.ts.drivecleaner }} @@ -90,7 +90,7 @@ const meterStyle = computed(() => { h: 180 - (usage.value / capacity.value * 180), s: 0.7, l: 0.5, - }), + }).toHslString(), }; }); @@ -144,7 +144,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.drive, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', })); </script> diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue index 938abb0651acc8ce7614f78f90649c589125c860..f226647569e7ea3ee7949a0f6692707187eb4d1d 100644 --- a/packages/frontend/src/pages/settings/email.vue +++ b/packages/frontend/src/pages/settings/email.vue @@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection first> <template #label>{{ i18n.ts.emailAddress }}</template> <MkInput v-model="emailAddress" type="email" manualSave> - <template #prefix><i class="ph-envelope ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-mail"></i></template> <template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template> - <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ph-check ph-bold ph-lg" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template> + <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template> </MkInput> </FormSection> @@ -115,6 +115,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.email, - icon: 'ph-envelope ph-bold ph-lg', + icon: 'ti ti-mail', })); </script> diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue index e9936ca5f28aa61c877335bc44f8f275397dcb48..1df723c850366ddaeb3d0241d3bd01d8ea5e7c6a 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> <MkFolder :defaultOpen="true"> - <template #icon><i class="ph-pin ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-pin"></i></template> <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template> <template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template> @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #footer> <button class="_button" :class="$style.emojisAdd" @click="chooseReaction"> - <i class="ph-plus ph-bold ph-lg"></i> + <i class="ti ti-plus"></i> </button> </template> </Sortable> @@ -38,15 +38,15 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_buttons"> - <MkButton inline @click="previewReaction"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton> - <MkButton inline danger @click="setDefaultReaction"><i class="ph-arrow-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton> - <MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton> + <MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton> </div> </div> </MkFolder> <MkFolder> - <template #icon><i class="ph-pin ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-pin"></i></template> <template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template> <template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template> @@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #footer> <button class="_button" :class="$style.emojisAdd" @click="chooseEmoji"> - <i class="ph-plus ph-bold ph-lg"></i> + <i class="ti ti-plus"></i> </button> </template> </Sortable> @@ -78,9 +78,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_buttons"> - <MkButton inline @click="previewEmoji"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton> - <MkButton inline danger @click="setDefaultEmoji"><i class="ph-arrow-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton> - <MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton> + <MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton> </div> </div> </MkFolder> @@ -278,7 +278,7 @@ watch(pinnedEmojis, () => { definePageMetadata(() => ({ title: i18n.ts.emojiPicker, - icon: 'ph-smiley ph-bold ph-lg', + icon: 'ti ti-mood-happy', })); </script> diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index eef69f694ae19f331a6e02fb37c6c21b8748e665..75afe67be70306f1886139adb05af1722f97ce94 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -27,9 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="overridedDeviceKind"> <template #label>{{ i18n.ts.overridedDeviceKind }}</template> <option :value="null">{{ i18n.ts.auto }}</option> - <option value="smartphone"><i class="ph-device-mobile ph-bold ph-lg"/> {{ i18n.ts.smartphone }}</option> - <option value="tablet"><i class="ph-device-tablet ph-bold ph-lg"/> {{ i18n.ts.tablet }}</option> - <option value="desktop"><i class="ph-desktop ph-bold ph-lg"/> {{ i18n.ts.desktop }}</option> + <option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option> + <option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option> + <option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option> </MkRadios> <FormSection> @@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.pinnedList }}</template> <!-- 複数ピンæ¢ã‚管ç†ã§ãるよã†ã«ã—ãŸã„ã‘ã©ã‚ã‚“ã©ã„ã®ã§ä¸€æ—¦ã²ã¨ã¤ã®ã¿ --> <MkButton v-if="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton> - <MkButton v-else danger @click="removePinnedList()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }}</MkButton> + <MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> </MkFolder> </div> </FormSection> @@ -98,8 +98,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRadios> <MkRadios v-model="noteDesign"> <template #label>Note Design</template> - <option value="sharkey"><i class="sk-icons sk-shark ph-bold" style="top: 2px;position: relative;"></i> Sharkey</option> - <option value="misskey"><i class="sk-icons sk-misskey ph-bold" style="top: 2px;position: relative;"></i> Misskey</option> + <option value="sharkey"><i class="sk-icons sk-shark sk-icons-lg" style="top: 2px;position: relative;"></i> Sharkey</option> + <option value="misskey"><i class="sk-icons sk-misskey sk-icons-lg" style="top: 2px;position: relative;"></i> Misskey</option> </MkRadios> <MkSwitch v-model="limitWidthOfReaction">{{ i18n.ts.limitWidthOfReaction }}</MkSwitch> </div> @@ -157,16 +157,16 @@ SPDX-License-Identifier: AGPL-3.0-only <!-- <p class="caption">Testing Testing</p> --> <MkRadios v-model="notificationPosition"> <template #label>{{ i18n.ts.position }}</template> - <option value="leftTop"><i class="ph-arrow-up-left ph-bold ph-lg"></i> {{ i18n.ts.leftTop }}</option> - <option value="rightTop"><i class="ph-arrow-up-right ph-bold ph-lg"></i> {{ i18n.ts.rightTop }}</option> - <option value="leftBottom"><i class="ph-arrow-down-left ph-bold ph-lg"></i> {{ i18n.ts.leftBottom }}</option> - <option value="rightBottom"><i class="ph-arrow-down-right ph-bold ph-lg"></i> {{ i18n.ts.rightBottom }}</option> + <option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option> + <option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option> + <option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option> + <option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option> </MkRadios> <MkRadios v-model="notificationStackAxis"> <template #label>{{ i18n.ts.stackAxis }}</template> - <option value="vertical"><i class="ph-split-vertical ph-bold ph-lg"></i> {{ i18n.ts.vertical }}</option> - <option value="horizontal"><i class="ph-split-horizontal ph-bold ph-lg"></i> {{ i18n.ts.horizontal }}</option> + <option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option> + <option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option> </MkRadios> <MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton> @@ -213,8 +213,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkRadios v-model="cornerRadius"> <template #label>{{ i18n.ts.cornerRadius }}</template> - <option :value="null"><i class="sk-icons sk-shark ph-bold" style="top: 2px;position: relative;"></i> Sharkey</option> - <option value="misskey"><i class="sk-icons sk-misskey ph-bold" style="top: 2px;position: relative;"></i> Misskey</option> + <option :value="null"><i class="sk-icons sk-shark sk-icons-lg" style="top: 2px;position: relative;"></i> Sharkey</option> + <option value="misskey"><i class="sk-icons sk-misskey sk-icons-lg" style="top: 2px;position: relative;"></i> Misskey</option> </MkRadios> </div> </FormSection> @@ -234,6 +234,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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> </div> <MkSelect v-model="serverDisconnectedBehavior"> <template #label>{{ i18n.ts.whenServerDisconnected }}</template> @@ -241,6 +242,12 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option> <option value="disabled">{{ i18n.ts._serverDisconnectedBehavior.disabled }}</option> </MkSelect> + <MkSelect v-model="contextMenu"> + <template #label>{{ i18n.ts._contextMenu.title }}</template> + <option value="app">{{ i18n.ts._contextMenu.app }}</option> + <option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option> + <option value="native">{{ i18n.ts._contextMenu.native }}</option> + </MkSelect> <MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing> <template #label>{{ i18n.ts.numberOfPageCache }}</template> <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template> @@ -303,13 +310,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.additionalEmojiDictionary }}</template> <div class="_buttons"> <template v-for="lang in emojiIndexLangs" :key="lang"> - <MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton> - <MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ph-download ph-bold ph-lg"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton> + <MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton> + <MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton> </template> </div> </MkFolder> <FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink> - <FormLink to="/settings/custom-css"><template #icon><i class="ph-code ph-bold ph-lg"></i></template>{{ i18n.ts.customCss }}</FormLink> + <FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink> </div> </FormSection> </div> @@ -426,6 +433,8 @@ const visibilityOnBoost = computed(defaultStore.makeGetterSetter('visibilityOnBo const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe')); const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer')); const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow')); +const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia')); +const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -485,6 +494,8 @@ watch([ showVisibilitySelectorOnBoost, visibilityOnBoost, alwaysConfirmFollow, + confirmWhenRevealingSensitiveMedia, + contextMenu, ], async () => { await reloadAsk(); }); @@ -579,7 +590,7 @@ function testNotification(): void { async function testNotificationDot() { const success = await worksOnInstance(); - + if (success) { os.toast(i18n.ts.notificationDotWorking); } else { @@ -615,7 +626,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.general, - icon: 'ph-faders ph-bold ph-lg', + icon: 'ti ti-adjustments', })); const useCustomSearchEngine = computed(() => !Object.keys(searchEngineMap).includes(searchEngine.value)); diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index 87bde70fc2d914681261cb70134f5af7113391f6..9c02b604c07c493af2d21ad21c7aa75ab423ccbb 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> <FormSection first> - <template #label><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.allNotes }}</template> + <template #label><i class="ti ti-pencil"></i> {{ i18n.ts._exportOrImport.allNotes }}</template> <div class="_gaps_s"> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <template #icon><i class="ti ti-download"></i></template> + <MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> <MkFolder v-if="$i && $i.policies.canImportNotes"> <template #label>{{ i18n.ts.import }}</template> @@ -29,27 +29,27 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </FormSection> <FormSection> - <template #label><i class="ph-star ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template> + <template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <template #icon><i class="ti ti-download"></i></template> + <MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> </FormSection> <FormSection> <template #label><i class="ph-paperclip ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.clips }}</template> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <template #icon><i class="ti ti-download"></i></template> + <MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> </FormSection> <FormSection> - <template #label><i class="ph-users ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.followingList }}</template> + <template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template> <div class="_gaps_s"> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-download"></i></template> <div class="_gaps_s"> <MkSwitch v-model="excludeMutingUsers"> {{ i18n.ts._exportOrImport.excludeMutingUsers }} @@ -57,76 +57,76 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="excludeInactiveUsers"> {{ i18n.ts._exportOrImport.excludeInactiveUsers }} </MkSwitch> - <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <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"> <template #label>{{ i18n.ts.import }}</template> - <template #icon><i class="ph-upload ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-upload"></i></template> <MkSwitch v-model="withReplies"> {{ i18n.ts._exportOrImport.withReplies }} </MkSwitch> - <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton> + <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> </MkFolder> </div> </FormSection> <FormSection> - <template #label><i class="ph-users ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.userLists }}</template> + <template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.userLists }}</template> <div class="_gaps_s"> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <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"> <template #label>{{ i18n.ts.import }}</template> - <template #icon><i class="ph-upload ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton> + <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> </MkFolder> </div> </FormSection> <FormSection> - <template #label><i class="ph-user-minus ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.muteList }}</template> + <template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.muteList }}</template> <div class="_gaps_s"> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <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"> <template #label>{{ i18n.ts.import }}</template> - <template #icon><i class="ph-upload ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton> + <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> </MkFolder> </div> </FormSection> <FormSection> - <template #label><i class="ph-user-minus ph-bold ph-lg"></i> {{ i18n.ts._exportOrImport.blockingList }}</template> + <template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.blockingList }}</template> <div class="_gaps_s"> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <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"> <template #label>{{ i18n.ts.import }}</template> - <template #icon><i class="ph-upload ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton> + <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> </MkFolder> </div> </FormSection> <FormSection> - <template #label><i class="ph-flying-saucer ph-bold ph-lg"></i> {{ i18n.ts.antennas }}</template> + <template #label><i class="ti ti-antenna"></i> {{ i18n.ts.antennas }}</template> <div class="_gaps_s"> <MkFolder> <template #label>{{ i18n.ts.export }}</template> - <template #icon><i class="ph-download ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="exportAntennas()"><i class="ph-download ph-bold ph-lg"></i> {{ i18n.ts.export }}</MkButton> + <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"> <template #label>{{ i18n.ts.import }}</template> - <template #icon><i class="ph-upload ph-bold ph-lg"></i></template> - <MkButton primary :class="$style.button" inline @click="importAntennas($event)"><i class="ph-upload ph-bold ph-lg"></i> {{ i18n.ts.import }}</MkButton> + <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> </MkFolder> </div> </FormSection> @@ -252,7 +252,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.importAndExport, - icon: 'ph-package ph-bold ph-lg', + icon: 'ti ti-package', })); </script> diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 35fb1a03f49fdcfff9c00363de001efd90f1cd7f..f6f2ffc553d1bd26aaf49c248ff28a00207fb5f3 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -40,7 +40,7 @@ import { useRouter } from '@/router/supplier.js'; const indexInfo = { title: i18n.ts.settings, - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', hideHeader: true, }; const INFO = ref(indexInfo); @@ -62,37 +62,37 @@ const ro = new ResizeObserver((entries, observer) => { const menuDef = computed(() => [{ title: i18n.ts.basicSettings, items: [{ - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', text: i18n.ts.profile, to: '/settings/profile', active: currentPage.value?.route.name === 'profile', }, { - icon: 'ph-lock ph-bold ph-lg-open', + icon: 'ti ti-lock-open', text: i18n.ts.privacy, to: '/settings/privacy', active: currentPage.value?.route.name === 'privacy', }, { - icon: 'ph-smiley ph-bold ph-lg', + icon: 'ti ti-mood-happy', text: i18n.ts.emojiPicker, to: '/settings/emoji-picker', active: currentPage.value?.route.name === 'emojiPicker', }, { - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', text: i18n.ts.drive, to: '/settings/drive', active: currentPage.value?.route.name === 'drive', }, { - icon: 'ph-bell ph-bold ph-lg', + icon: 'ti ti-bell', text: i18n.ts.notifications, to: '/settings/notifications', active: currentPage.value?.route.name === 'notifications', }, { - icon: 'ph-envelope ph-bold ph-lg', + icon: 'ti ti-mail', text: i18n.ts.email, to: '/settings/email', active: currentPage.value?.route.name === 'email', }, { - icon: 'ph-lock ph-bold ph-lg', + icon: 'ti ti-lock', text: i18n.ts.security, to: '/settings/security', active: currentPage.value?.route.name === 'security', @@ -100,32 +100,32 @@ const menuDef = computed(() => [{ }, { title: i18n.ts.clientSettings, items: [{ - icon: 'ph-faders ph-bold ph-lg', + icon: 'ti ti-adjustments', text: i18n.ts.general, to: '/settings/general', active: currentPage.value?.route.name === 'general', }, { - icon: 'ph-palette ph-bold ph-lg', + icon: 'ti ti-palette', text: i18n.ts.theme, to: '/settings/theme', active: currentPage.value?.route.name === 'theme', }, { - icon: 'ph-list ph-bold ph-lg-2', + icon: 'ti ti-menu-2', text: i18n.ts.navbar, to: '/settings/navbar', active: currentPage.value?.route.name === 'navbar', }, { - icon: 'ph-equals ph-bold ph-lg', + icon: 'ti ti-equal-double', text: i18n.ts.statusbar, to: '/settings/statusbar', active: currentPage.value?.route.name === 'statusbar', }, { - icon: 'ph-music-notes ph-bold ph-lg', + icon: 'ti ti-music', text: i18n.ts.sounds, to: '/settings/sounds', active: currentPage.value?.route.name === 'sounds', }, { - icon: 'ph-plug ph-bold ph-lg', + icon: 'ti ti-plug', text: i18n.ts.plugins, to: '/settings/plugin', active: currentPage.value?.route.name === 'plugin', @@ -133,57 +133,57 @@ const menuDef = computed(() => [{ }, { title: i18n.ts.otherSettings, items: [{ - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badges', text: i18n.ts.roles, to: '/settings/roles', active: currentPage.value?.route.name === 'roles', }, { - icon: 'ph-prohibit ph-bold ph-lg', + icon: 'ti ti-ban', text: i18n.ts.muteAndBlock, to: '/settings/mute-block', active: currentPage.value?.route.name === 'mute-block', }, { - icon: 'ph-key ph-bold ph-lg', + icon: 'ti ti-api', text: 'API', to: '/settings/api', active: currentPage.value?.route.name === 'api', }, { - icon: 'ph-webhooks-logo ph-bold ph-lg', + icon: 'ti ti-webhook', text: 'Webhook', to: '/settings/webhook', active: currentPage.value?.route.name === 'webhook', }, { - icon: 'ph-package ph-bold ph-lg', + icon: 'ti ti-package', text: i18n.ts.importAndExport, to: '/settings/import-export', active: currentPage.value?.route.name === 'import-export', }, { - icon: 'ph-airplane ph-bold ph-lg', + icon: 'ti ti-plane', text: `${i18n.ts.accountMigration}`, to: '/settings/migration', active: currentPage.value?.route.name === 'migration', }, { - icon: 'ph-dots-three ph-bold ph-lg', + icon: 'ti ti-dots', text: i18n.ts.other, to: '/settings/other', active: currentPage.value?.route.name === 'other', }], }, { items: [{ - icon: 'ph-floppy-disk ph-bold ph-lg', + icon: 'ti ti-device-floppy', text: i18n.ts.preferencesBackups, to: '/settings/preferences-backups', active: currentPage.value?.route.name === 'preferences-backups', }, { type: 'button', - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', text: i18n.ts.clearCache, action: async () => { await clearCache(); }, }, { type: 'button', - icon: 'ph-power ph-bold ph-lg', + icon: 'ti ti-power', text: i18n.ts.logout, action: async () => { const { canceled } = await os.confirm({ @@ -197,9 +197,6 @@ const menuDef = computed(() => [{ }], }]); -watch(narrow, () => { -}); - onMounted(() => { ro.observe(el.value); diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 12f29e2ff8d871b5d7769dfe15d30b4cc0e767c5..ddc23945dd58fb4ba6c646e274329d95772d37d8 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> <MkFolder :defaultOpen="true"> - <template #icon><i class="ph-airplane-landing ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-plane-arrival"></i></template> <template #label>{{ i18n.ts._accountMigration.moveFrom }}</template> <template #caption>{{ i18n.ts._accountMigration.moveFromSub }}</template> @@ -15,12 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._accountMigration.moveFromDescription }} </FormInfo> <div> - <MkButton :disabled="accountAliases.length >= 10" inline style="margin-right: 8px;" @click="add"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> - <MkButton inline primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton :disabled="accountAliases.length >= 10" inline style="margin-right: 8px;" @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> <div class="_gaps"> <MkInput v-for="(_, i) in accountAliases" v-model="accountAliases[i]"> - <template #prefix><i class="ph-airplane-landing ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-plane-arrival"></i></template> <template #label>{{ i18n.tsx._accountMigration.moveFromLabel({ n: i + 1 }) }}</template> </MkInput> </div> @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder :defaultOpen="!!$i.movedTo"> - <template #icon><i class="ph-airplane-takeoff ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-plane-departure"></i></template> <template #label>{{ i18n.ts._accountMigration.moveTo }}</template> <div class="_gaps_m"> @@ -39,11 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only <FormInfo warn>{{ i18n.ts._accountMigration.moveCannotBeUndone }}</FormInfo> <MkInput v-model="moveToAccount"> - <template #prefix><i class="ph-airplane-takeoff ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-plane-departure"></i></template> <template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template> </MkInput> <MkButton inline danger :disabled="!moveToAccount" @click="move"> - <i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._accountMigration.startMigration }} + <i class="ti ti-check"></i> {{ i18n.ts._accountMigration.startMigration }} </MkButton> </template> <template v-else-if="$i"> @@ -123,7 +123,7 @@ init(); definePageMetadata(() => ({ title: i18n.ts.accountMigration, - icon: 'ph-airplane ph-bold ph-lg', + icon: 'ti ti-plane', })); </script> diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue index 3b3376a9a7313349a837e25398145dae77234b2f..d1fde2fc1ca9e1568667d0d5e2a6d957053983d9 100644 --- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts._instanceMute.heading }}</template> <template #caption>{{ i18n.ts._instanceMute.instanceMuteDescription }}<br>{{ i18n.ts._instanceMute.instanceMuteDescription2 }}</template> </MkTextarea> - <MkButton primary :disabled="!changed" @click="save()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index 588184826d1c9a4d3be3a3c060a5d92206402712..a5c381200f1254613cf75a77674a89e148309631 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -20,14 +20,14 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-globe-simple ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-planet-off"></i></template> <template #label>{{ i18n.ts.instanceMute }}</template> <XInstanceMute/> </MkFolder> <MkFolder> - <template #icon><i class="ph-repeat ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-repeat-off"></i></template> <template #label>{{ i18n.ts.mutedUsers }} ({{ i18n.ts.renote }})</template> <MkPagination :pagination="renoteMutingPagination"> @@ -45,8 +45,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)"> <MkUserCardMini :user="item.mutee"/> </MkA> - <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button> - <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.userToggle" @click="toggleRenoteMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unrenoteMute(item.mutee, $event)"><i class="ti ti-x"></i></button> </div> <div v-if="expandedRenoteMuteItems.includes(item.id)" :class="$style.userItemSub"> <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> @@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-eye-slash ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-eye-off"></i></template> <template #label>{{ i18n.ts.mutedUsers }}</template> <MkPagination :pagination="mutingPagination"> @@ -76,8 +76,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA :class="$style.userItemMainBody" :to="userPage(item.mutee)"> <MkUserCardMini :user="item.mutee"/> </MkA> - <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button> - <button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.userToggle" @click="toggleMuteItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unmute(item.mutee, $event)"><i class="ti ti-x"></i></button> </div> <div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub"> <div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div> @@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-prohibit ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-ban"></i></template> <template #label>{{ i18n.ts.blockedUsers }}</template> <MkPagination :pagination="blockingPagination"> @@ -109,8 +109,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)"> <MkUserCardMini :user="item.blockee"/> </MkA> - <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ph-caret-down ph-bold ph-lg"></i></button> - <button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button> + <button class="_button" :class="$style.remove" @click="unblock(item.blockee, $event)"><i class="ti ti-x"></i></button> </div> <div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub"> <div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div> @@ -163,7 +163,7 @@ const expandedBlockItems = ref([]); async function unrenoteMute(user, ev) { os.popupMenu([{ text: i18n.ts.renoteUnmute, - icon: 'ph-x ph-bold ph-lg', + icon: 'ti ti-x', action: async () => { await os.apiWithDialog('renote-mute/delete', { userId: user.id }); //role.users = role.users.filter(u => u.id !== user.id); @@ -174,7 +174,7 @@ async function unrenoteMute(user, ev) { async function unmute(user, ev) { os.popupMenu([{ text: i18n.ts.unmute, - icon: 'ph-x ph-bold ph-lg', + icon: 'ti ti-x', action: async () => { await os.apiWithDialog('mute/delete', { userId: user.id }); //role.users = role.users.filter(u => u.id !== user.id); @@ -185,7 +185,7 @@ async function unmute(user, ev) { async function unblock(user, ev) { os.popupMenu([{ text: i18n.ts.unblock, - icon: 'ph-x ph-bold ph-lg', + icon: 'ti ti-x', action: async () => { await os.apiWithDialog('blocking/delete', { userId: user.id }); //role.users = role.users.filter(u => u.id !== user.id); @@ -231,7 +231,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.muteAndBlock, - icon: 'ph-prohibit ph-bold ph-lg', + icon: 'ti ti-ban', })); </script> diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue index faf16ca368c4fa8f2d7c0c158d5e43d3c7d5c6bc..f5837abe984532c654fa5dc615cc9ad28faba635 100644 --- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue +++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts._wordMute.muteWordsDescription }}<br>{{ i18n.ts._wordMute.muteWordsDescription2 }}</template> </MkTextarea> </div> - <MkButton primary inline :disabled="!changed" @click="save()"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton primary inline :disabled="!changed" @click="save()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> </template> diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue index ae5f081e1c1f65f1e36a632878db0602bbf0d06c..7f8460e316409cf33cbc998369885ba96a36286f 100644 --- a/packages/frontend/src/pages/settings/navbar.vue +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -21,18 +21,18 @@ SPDX-License-Identifier: AGPL-3.0-only v-if="element.type === '-' || navbarItemDef[element.type]" :class="$style.item" > - <button class="_button" :class="$style.itemHandle"><i class="ph-list ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button> <i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span> - <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button> </div> </template> </Sortable> </MkContainer> </FormSlot> <div class="_buttons"> - <MkButton @click="addItem"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.addItem }}</MkButton> - <MkButton danger @click="reset"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton> - <MkButton primary class="save" @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton @click="addItem"><i class="ti ti-plus"></i> {{ i18n.ts.addItem }}</MkButton> + <MkButton danger @click="reset"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> + <MkButton primary class="save" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> <MkRadios v-model="menuDisplay"> @@ -120,7 +120,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.navbar, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', })); </script> diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue index 8d78ce7031179efa1691ac06ec2adadfd8fb4f16..1a945aa3ad7534894860fcd360b1222dd01dbaa1 100644 --- a/packages/frontend/src/pages/settings/notifications.notification-config.vue +++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSelect> <div class="_buttons"> - <MkButton inline primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> </template> diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 36fe7df03e0e0fa2d20afc3b1cfd37434a4c1086..a861f6ee0d227116267ac3bcfa007e6f066487f8 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -133,6 +133,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.notifications, - icon: 'ph-bell ph-bold ph-lg', + icon: 'ti ti-bell', })); </script> diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 683e5f0e30da153d623763cc0fb1378f034b4ed1..86bbae431d93c2f7e1c5ddab6e5b49860b66d292 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection first> <div class="_gaps_s"> <MkFolder> - <template #icon><i class="ph-info ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-info-circle"></i></template> <template #label>{{ i18n.ts.accountInfo }}</template> <div class="_gaps_m"> @@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-warning ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-alert-triangle"></i></template> <template #label>{{ i18n.ts.closeAccount }}</template> <div class="_gaps_m"> @@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-flask ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-flask"></i></template> <template #label>{{ i18n.ts.experimentalFeatures }}</template> <div class="_gaps_m"> @@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts.developer }}</template> <div class="_gaps_m"> @@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSection> <FormSection> - <FormLink to="/registry"><template #icon><i class="ph-faders ph-bold ph-lg"></i></template>{{ i18n.ts.registry }}</FormLink> + <FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink> </FormSection> <FormSection> @@ -189,6 +189,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.other, - icon: 'ph-dots-three ph-bold ph-lg', + icon: 'ti ti-dots', })); </script> diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue index f3dd862bd11b08bce0452800b6d60fd2bec7a9c1..3ab26e80d96f29c1d5ed9f77a8d49393dadd4929 100644 --- a/packages/frontend/src/pages/settings/plugin.install.vue +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkCodeEditor> <div> - <MkButton :disabled="code == null" primary inline @click="install"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton> + <MkButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> </div> </div> </template> @@ -55,6 +55,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts._plugin.install, - icon: 'ph-download ph-bold ph-lg', + icon: 'ti ti-download', })); </script> diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue index 349015dd28b0cf24d2ccdfa10d8a5a8bcea10837..3c3dcfe41e57458d35b934ac525f247bd79d2121 100644 --- a/packages/frontend/src/pages/settings/plugin.vue +++ b/packages/frontend/src/pages/settings/plugin.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="_gaps_m"> - <FormLink to="/settings/plugin/install"><template #icon><i class="ph-download ph-bold ph-lg"></i></template>{{ i18n.ts._plugin.install }}</FormLink> + <FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink> <FormSection> <template #label>{{ i18n.ts.manage }}</template> @@ -37,17 +37,17 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="_buttons"> - <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ph-gear ph-bold ph-lg"></i> {{ i18n.ts.settings }}</MkButton> - <MkButton inline danger @click="uninstall(plugin)"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.uninstall }}</MkButton> + <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton> + <MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton> </div> <MkFolder> - <template #icon><i class="ph-terminal-window ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-terminal-2"></i></template> <template #label>{{ i18n.ts._plugin.viewLog }}</template> <div class="_gaps_s"> <div class="_buttons"> - <MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton> + <MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> </div> <MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/> @@ -55,12 +55,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts._plugin.viewSource }}</template> <div class="_gaps_s"> <div class="_buttons"> - <MkButton inline @click="copy(plugin.src)"><i class="ph-copy ph-bold ph-lg"></i> {{ i18n.ts.copy }}</MkButton> + <MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton> </div> <MkCode :code="plugin.src ?? ''" lang="is"/> @@ -82,7 +82,7 @@ import MkCode from '@/components/MkCode.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import * as os from '@/os.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { ColdDeviceStorage } from '@/store.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; @@ -141,6 +141,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.plugins, - icon: 'ph-plug ph-bold ph-lg', + icon: 'ti ti-plug', })); </script> diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index b99e1e03e4795c219e664ee4191155ca405eac20..f9fd494ce95f272ac12a9d973c2a7c1ea3c4beb9 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -118,8 +118,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'sound_note', 'sound_noteMy', 'sound_notification', - 'sound_antenna', - 'sound_channel', ]; const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ 'lightTheme', @@ -425,25 +423,25 @@ function menu(ev: MouseEvent, profileId: string) { return os.popupMenu([{ text: ts._preferencesBackups.apply, - icon: 'ph-check ph-bold ph-lg', + icon: 'ti ti-check', action: () => applyProfile(profileId), }, { type: 'a', text: ts.download, - icon: 'ph-download ph-bold ph-lg', + 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, - icon: 'ph-textbox ph-bold ph-lg', + icon: 'ti ti-forms', action: () => rename(profileId), }, { text: ts._preferencesBackups.save, - icon: 'ph-floppy-disk ph-bold ph-lg', + icon: 'ti ti-device-floppy', action: () => save(profileId), }, { type: 'divider' }, { text: ts.delete, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', action: () => deleteProfile(profileId), danger: true, }], (ev.currentTarget ?? ev.target ?? undefined) as unknown as HTMLElement | undefined); @@ -465,7 +463,7 @@ onUnmounted(() => { definePageMetadata(() => ({ title: ts.preferencesBackups, - icon: 'ph-floppy-disk ph-bold ph-lg', + icon: 'ti ti-device-floppy', })); </script> diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 86cf5ab241778ff078bb15c1881ba009d0e3683f..b155d6e316774b8a0b09fd7101bf6459e8c43d92 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -120,6 +120,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.privacy, - icon: 'ph-lock ph-bold ph-lg-open', + icon: 'ti ti-lock-open', })); </script> diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 408cf4ed6792e6d744774ea91ab0abd60a3129c8..6cc19db127b01f45a1293c3db5b83c13ae61e6d6 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration @click="changeAvatar"/> <div class="_buttonsCenter"> <MkButton primary rounded @click="changeAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton> - <MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ph-sparkle ph-bold ph-lg"></i></MkButton> + <MkButton primary rounded link to="/settings/avatar-decoration">{{ i18n.ts.decorate }} <i class="ti ti-sparkles"></i></MkButton> </div> </div> </div> @@ -30,12 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInput v-model="profile.location" manualSave> <template #label>{{ i18n.ts.location }}</template> - <template #prefix><i class="ph-map-pin ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-map-pin"></i></template> </MkInput> <MkInput v-model="profile.birthday" :max="setMaxBirthDate()" type="date" manualSave> <template #label>{{ i18n.ts.birthday }}</template> - <template #prefix><i class="ph-cake ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-cake"></i></template> </MkInput> <MkInput v-model="profile.listenbrainz" manualSave> @@ -50,15 +50,15 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSlot> <MkFolder> - <template #icon><i class="ph-list ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-list"></i></template> <template #label>{{ i18n.ts._profile.metadataEdit }}</template> <div :class="$style.metadataRoot"> <div :class="$style.metadataMargin"> - <MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.add }}</MkButton> - <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> - <MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ph-arrows-down-up ph-bold ph-lg"></i> {{ i18n.ts.rearrange }}</MkButton> - <MkButton inline primary @click="saveFields"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton> + <MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> <Sortable @@ -72,8 +72,8 @@ SPDX-License-Identifier: AGPL-3.0-only > <template #item="{element, index}"> <div :class="$style.fieldDragItem"> - <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ph-list ph-bold ph-lg"></i></button> - <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ph-x ph-bold ph-lg"></i></button> + <button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button> + <button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button> <div :class="$style.dragItemForm"> <FormSplit :minWidth="200"> <MkInput v-model="element.name" small> @@ -396,7 +396,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.profile, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', })); </script> @@ -481,6 +481,7 @@ definePageMetadata(() => ({ &:hover, &:focus { opacity: .7; } + &:active { cursor: pointer; } diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue index 273cf013f08733b7374251d8d74ea6187e80bf16..5346a58a7970eaea6b9d215396bba5979addc944 100644 --- a/packages/frontend/src/pages/settings/roles.vue +++ b/packages/frontend/src/pages/settings/roles.vue @@ -39,7 +39,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.roles, - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badges', })); </script> diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue index 43e5104d71190e9312d352044c92ed561d4d2037..de0f63630edd2eae7344bc6c0a3e42a17300a5fe 100644 --- a/packages/frontend/src/pages/settings/security.vue +++ b/packages/frontend/src/pages/settings/security.vue @@ -19,8 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <div v-for="item in items" :key="item.id" v-panel class="timnmucd"> <header> - <i v-if="item.success" class="ph-check ph-bold ph-lg icon succ"></i> - <i v-else class="ph-x-circle ph-bold ph-lg icon fail"></i> + <i v-if="item.success" class="ti ti-check icon succ"></i> + <i v-else class="ti ti-circle-x icon fail"></i> <code class="ip _monospace">{{ item.ip }}</code> <MkTime :time="item.createdAt" class="time"/> </header> @@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <FormSlot> - <MkButton danger @click="regenerateToken"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton> + <MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton> <template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template> </FormSlot> </FormSection> @@ -105,7 +105,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.security, - icon: 'ph-lock ph-bold ph-lg', + icon: 'ti ti-lock', })); </script> diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue index 307c5eaae462721fdd7767849c9aaabe4356c035..81478fede58f49094dbeb926087dc6748b2dd992 100644 --- a/packages/frontend/src/pages/settings/sounds.sound.vue +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -9,7 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts.sound }}</template> <option v-for="x in soundsTypes" :key="x ?? 'null'" :value="x">{{ getSoundTypeName(x) }}</option> </MkSelect> - <div v-if="type === '_driveFile_'" :class="$style.fileSelectorRoot"> + <div v-if="type === '_driveFile_' && driveFileError === true" :class="$style.fileSelectorRoot"> + <MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton> + <div :class="$style.fileErrorRoot"> + <MkCondensedLine>{{ i18n.ts._soundSettings.driveFileError }}</MkCondensedLine> + </div> + </div> + <div v-else-if="type === '_driveFile_'" :class="$style.fileSelectorRoot"> <MkButton :class="$style.fileSelectorButton" inline rounded primary @click="selectSound">{{ i18n.ts.selectFile }}</MkButton> <div :class="['_nowrap', !fileUrl && $style.fileNotSelected]">{{ friendlyFileName }}</div> </div> @@ -18,14 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only </MkRange> <div class="_buttons"> - <MkButton inline @click="listen"><i class="ph-play ph-bold ph-lg"></i> {{ i18n.ts.listen }}</MkButton> - <MkButton inline primary @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> + <MkButton inline @click="listen"><i class="ti ti-player-play"></i> {{ i18n.ts.listen }}</MkButton> + <MkButton inline primary :disabled="!hasChanged || driveFileError" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import type { SoundType } from '@/scripts/sound.js'; import MkSelect from '@/components/MkSelect.vue'; import MkButton from '@/components/MkButton.vue'; @@ -51,13 +57,18 @@ const type = ref<SoundType>(props.type); const fileId = ref(props.fileId); const fileUrl = ref(props.fileUrl); const fileName = ref<string>(''); +const driveFileError = ref(false); +const hasChanged = ref(false); const volume = ref(props.volume); if (type.value === '_driveFile_' && fileId.value) { - const apiRes = await misskeyApi('drive/files/show', { + await misskeyApi('drive/files/show', { fileId: fileId.value, + }).then((res) => { + fileName.value = res.name; + }).catch((res) => { + driveFileError.value = true; }); - fileName.value = apiRes.name; } function getSoundTypeName(f: SoundType): string { @@ -107,9 +118,21 @@ function selectSound(ev) { fileUrl.value = file.url; fileName.value = file.name; fileId.value = file.id; + driveFileError.value = false; + hasChanged.value = true; }); } +watch([type, volume], ([typeTo, volumeTo], [typeFrom, volumeFrom]) => { + if (typeFrom !== typeTo && typeTo !== '_driveFile_') { + fileUrl.value = undefined; + fileName.value = ''; + fileId.value = undefined; + driveFileError.value = false; + } + hasChanged.value = true; +}); + function listen() { if (type.value === '_driveFile_' && (!fileUrl.value || !fileId.value)) { os.alert({ @@ -131,6 +154,10 @@ function listen() { } function save() { + if (hasChanged.value === false || driveFileError.value === true) { + return; + } + if (type.value === '_driveFile_' && !fileUrl.value) { os.alert({ type: 'warning', @@ -163,6 +190,13 @@ function save() { gap: 8px; } +.fileErrorRoot { + flex-grow: 1; + min-width: 0; + font-weight: 700; + color: var(--error); +} + .fileSelectorButton { flex-shrink: 0; } diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index bf398ac30304ea0d0a099231318c1c023843e23b..9fcf564e552340e6317c7d8e7158b40a6b8b03d2 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -21,13 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkFolder v-for="type in operationTypes" :key="type"> <template #label>{{ i18n.ts._sfx[type] }}</template> <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template> - - <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/> + <Suspense> + <template #default> + <XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/> + </template> + <template #fallback> + <MkLoading/> + </template> + </Suspense> </MkFolder> </div> </FormSection> - <MkButton danger @click="reset()"><i class="ph-arrow-clockwise ph-bold ph-lg"></i> {{ i18n.ts.default }}</MkButton> + <MkButton danger @click="reset()"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton> </div> </template> @@ -54,8 +60,6 @@ const sounds = ref<Record<OperationType, Ref<SoundStore>>>({ note: defaultStore.reactiveState.sound_note, noteMy: defaultStore.reactiveState.sound_noteMy, notification: defaultStore.reactiveState.sound_notification, - antenna: defaultStore.reactiveState.sound_antenna, - channel: defaultStore.reactiveState.sound_channel, reaction: defaultStore.reactiveState.sound_reaction, }); @@ -96,6 +100,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.sounds, - icon: 'ph-music-notes ph-bold ph-lg', + icon: 'ti ti-music', })); </script> diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue index 92e389a288f00e03ee93a74c915fb12de3b940b2..67943524ef4450f2b8885a9e5476f245dd7c3d2e 100644 --- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="statusbar.props.shuffle"> <template #label>{{ i18n.ts.shuffle }}</template> </MkSwitch> - <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number"> + <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1"> <template #label>{{ i18n.ts.refreshInterval }}</template> </MkInput> <MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1"> @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> </template> <template v-else-if="statusbar.type === 'federation'"> - <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number"> + <MkInput v-model="statusbar.props.refreshIntervalSec" manualSave type="number" min="1"> <template #label>{{ i18n.ts.refreshInterval }}</template> </MkInput> <MkRange v-model="statusbar.props.marqueeDuration" :min="5" :max="150" :step="1"> diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue index fa924d13f0b44d8d49a8a7b99629784235827435..1ae3de7994c792e778f4effebba821dbbc1851f8 100644 --- a/packages/frontend/src/pages/settings/statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -52,6 +52,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.statusbar, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', })); </script> diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue index 01ae5286b7e98966e2562507af514ce4f07db472..4f05d3784cc9db99a4c51acc138355a5fb277520 100644 --- a/packages/frontend/src/pages/settings/theme.install.vue +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkCodeEditor> <div class="_buttons"> - <MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ph-eye ph-bold ph-lg"></i> {{ i18n.ts.preview }}</MkButton> - <MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.install }}</MkButton> + <MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> </div> </div> </template> @@ -61,6 +61,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts._theme.install, - icon: 'ph-download ph-bold ph-lg', + icon: 'ti ti-download', })); </script> diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue index 43d76951c06f2718baf3bac0eca97b7cc8086052..579ca6b20be4effb15c171abc56d16ca478532ee 100644 --- a/packages/frontend/src/pages/settings/theme.manage.vue +++ b/packages/frontend/src/pages/settings/theme.manage.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #label>{{ i18n.ts._theme.code }}</template> <template #caption><button class="_textButton" @click="copyThemeCode()">{{ i18n.ts.copy }}</button></template> </MkTextarea> - <MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.uninstall }}</MkButton> + <MkButton v-if="!builtinThemes.some(t => t.id == selectedTheme.id)" danger @click="uninstall()"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton> </template> </div> </template> @@ -38,7 +38,7 @@ import MkSelect from '@/components/MkSelect.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import { Theme, getBuiltinThemesRef } from '@/scripts/theme.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; import { getThemes, removeTheme } from '@/theme-store.js'; import { i18n } from '@/i18n.js'; @@ -78,6 +78,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts._theme.manage, - icon: 'ph-wrench ph-bold ph-lg', + icon: 'ti ti-tool', })); </script> diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index 9b493f5ffeeadcfc2e7c96f467aa8ff638d0e8b2..ad07a6b5392a841dfdfb54a7dabcfbda89bf6699 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="selects"> <MkSelect v-model="lightThemeId" large class="select"> <template #label>{{ i18n.ts.themeForLightMode }}</template> - <template #prefix><i class="ph-sun ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-sun"></i></template> <option v-if="instanceLightTheme" :key="'instance:' + instanceLightTheme.id" :value="instanceLightTheme.id">{{ instanceLightTheme.name }}</option> <optgroup v-if="installedLightThemes.length > 0" :label="i18n.ts._theme.installedThemes"> <option v-for="x in installedLightThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option> @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSelect> <MkSelect v-model="darkThemeId" large class="select"> <template #label>{{ i18n.ts.themeForDarkMode }}</template> - <template #prefix><i class="ph-moon ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-moon"></i></template> <option v-if="instanceDarkTheme" :key="'instance:' + instanceDarkTheme.id" :value="instanceDarkTheme.id">{{ instanceDarkTheme.name }}</option> <optgroup v-if="installedDarkThemes.length > 0" :label="i18n.ts._theme.installedThemes"> <option v-for="x in installedDarkThemes" :key="'installed:' + x.id" :value="x.id">{{ x.name }}</option> @@ -58,10 +58,10 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <div class="_formLinksGrid"> - <FormLink to="/settings/theme/manage"><template #icon><i class="ph-wrench ph-bold ph-lg"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink> - <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></template>{{ i18n.ts._theme.explore }}</FormLink> - <FormLink to="/settings/theme/install"><template #icon><i class="ph-download ph-bold ph-lg"></i></template>{{ i18n.ts._theme.install }}</FormLink> - <FormLink to="/theme-editor"><template #icon><i class="ph-paint-roller ph-bold ph-lg"></i></template>{{ i18n.ts._theme.make }}</FormLink> + <FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink> + <FormLink to="https://assets.misskey.io/theme/list" external><template #icon><i class="ti ti-world"></i></template>{{ i18n.ts._theme.explore }}</FormLink> + <FormLink to="/settings/theme/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._theme.install }}</FormLink> + <FormLink to="/theme-editor"><template #icon><i class="ti ti-paint"></i></template>{{ i18n.ts._theme.make }}</FormLink> </div> </FormSection> @@ -179,7 +179,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.theme, - icon: 'ph-palette ph-bold ph-lg', + icon: 'ti ti-palette', })); </script> @@ -213,12 +213,18 @@ definePageMetadata(() => ({ } } + .dn:focus-visible ~ .toggle { + outline: 2px solid var(--focus); + outline-offset: 2px; + } + .toggle { cursor: pointer; display: inline-block; position: relative; width: 90px; height: 50px; + margin: 4px; // focus用ã®ã‚¢ã‚¦ãƒˆãƒ©ã‚¤ãƒ³ background-color: #83D8FF; border-radius: 90px - 6; transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index 99326c867167269d982437c7da1bf8f72ebfdfeb..058ef69c35e3745243dbd5d15dcc63794bd1b9ab 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -14,12 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkInput v-model="secret"> - <template #prefix><i class="ph-lock ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-lock"></i></template> <template #label>{{ i18n.ts._webhookSettings.secret }}</template> </MkInput> <FormSection> - <template #label>{{ i18n.ts._webhookSettings.events }}</template> + <template #label>{{ i18n.ts._webhookSettings.trigger }}</template> <div class="_gaps_s"> <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> @@ -35,8 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSwitch v-model="active">{{ i18n.ts._webhookSettings.active }}</MkSwitch> <div class="_buttons"> - <MkButton primary inline @click="save"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton> - <MkButton danger inline @click="del"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.delete }}</MkButton> + <MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton danger inline @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> </template> @@ -116,6 +116,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: 'Edit webhook', - icon: 'ph-webhooks-logo ph-bold ph-lg', + icon: 'ti ti-webhook', })); </script> diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index 299386338ab728a80154b657fc85deec353299d3..d62357caaff859a6d23a2198120da3f3669f5130 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -14,12 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkInput v-model="secret"> - <template #prefix><i class="ph-lock ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-lock"></i></template> <template #label>{{ i18n.ts._webhookSettings.secret }}</template> </MkInput> <FormSection> - <template #label>{{ i18n.ts._webhookSettings.events }}</template> + <template #label>{{ i18n.ts._webhookSettings.trigger }}</template> <div class="_gaps_s"> <MkSwitch v-model="event_follow">{{ i18n.ts._webhookSettings._events.follow }}</MkSwitch> @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only </FormSection> <div class="_buttons"> - <MkButton primary inline @click="create"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.create }}</MkButton> + <MkButton primary inline @click="create"><i class="ti ti-check"></i> {{ i18n.ts.create }}</MkButton> </div> </div> </template> @@ -84,6 +84,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: 'Create new webhook', - icon: 'ph-webhooks-logo ph-bold ph-lg', + icon: 'ti ti-webhook', })); </script> diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue index 3717abb13ebac633074fc6f998e24509b3324f95..0d11b00c97d0510f639e99feb90f468285101027 100644 --- a/packages/frontend/src/pages/settings/webhook.vue +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -15,10 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <FormLink v-for="webhook in items" :key="webhook.id" :to="`/settings/webhook/edit/${webhook.id}`"> <template #icon> - <i v-if="webhook.active === false" class="ph-pause ph-bold ph-lg"></i> - <i v-else-if="webhook.latestStatus === null" class="ph-circle ph-bold ph-lg"></i> - <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ph-check ph-bold ph-lg" :style="{ color: 'var(--success)' }"></i> - <i v-else class="ph-warning ph-bold ph-lg" :style="{ color: 'var(--error)' }"></i> + <i v-if="webhook.active === false" class="ti ti-player-pause"></i> + <i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i> + <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i> + <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i> </template> {{ webhook.name || webhook.url }} <template #suffix> @@ -52,6 +52,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: 'Webhook', - icon: 'ph-webhooks-logo ph-bold ph-lg', + icon: 'ti ti-webhook', })); </script> diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue index 8d974b17fb31535351b6451ac54f476efb5bf73f..37f6558d6432bc7a5c61cf5ded83ac6bc8977d3d 100644 --- a/packages/frontend/src/pages/share.vue +++ b/packages/frontend/src/pages/share.vue @@ -201,6 +201,6 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.share, - icon: 'ph-share-network ph-bold ph-lg', + icon: 'ti ti-share', })); </script> diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue index b08a304cfd6ca5afac56dfbef0b604e34d1e92ff..226f08bf8e4120daffde635db6343bf8dc8986d0 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.formContainer"> <form :class="$style.form" class="_panel" @submit.prevent="submit()"> <div :class="$style.banner"> - <i class="ph-check ph-bold ph-lg"></i> + <i class="ti ti-user-check"></i> </div> <div class="_gaps_m" style="padding: 32px;"> <div>{{ i18n.tsx.clickToFinishEmailVerification({ ok: i18n.ts.gotIt }) }}</div> diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index d9c94569a72e7a5a4286b3eb46da2acb38bcdc78..9b77392872fbfee9ee137ee83002ae335cba9c74 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-if="$i" #footer> <div :class="$style.footer"> <MkSpacer :contentMax="800" :marginMin="16" :marginMax="16"> - <MkButton rounded primary :class="$style.button" @click="post()"><i class="ph-pencil-simple ph-bold ph-lg"></i>{{ i18n.ts.postToHashtag }}</MkButton> + <MkButton rounded primary :class="$style.button" @click="post()"><i class="ti ti-pencil"></i>{{ i18n.ts.postToHashtag }}</MkButton> </MkSpacer> </div> </template> @@ -57,7 +57,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: props.tag, - icon: 'ph-hash ph-bold ph-lg', + icon: 'ti ti-hash', })); </script> diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index d020320b44dabb2a9a5db691f5a7c880e08c00df..1cdccdc244b7ca35ff68db9833db1247f707ac1b 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkFolder> <MkFolder :defaultOpen="false"> - <template #icon><i class="ph-code ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts.editCode }}</template> <div class="_gaps_m"> @@ -212,7 +212,7 @@ watch(theme, apply, { deep: true }); const headerActions = computed(() => [{ asFullButton: true, - icon: 'ph-check ph-bold ph-lg', + icon: 'ti ti-check', text: i18n.ts.saveAs, handler: saveAs, }]); @@ -221,7 +221,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.themeEditor, - icon: 'ph-palette ph-bold ph-lg', + icon: 'ti ti-palette', })); </script> diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 1e5a244dd4ab230e543f8c83821fd11edd66a3e1..20d8abccf6e2772e63c209456e0d2f64f0035629 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> <MkSpacer :contentMax="800"> <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> - <div :key="src" ref="rootEl" v-hotkey.global="keymap"> - <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> + <div :key="src" ref="rootEl"> + <MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> {{ i18n.ts._timelineDescription[src] }} </MkInfo> <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> @@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, provide, shallowRef, ref } from 'vue'; +import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue'; import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -46,7 +46,6 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; import { $i } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; @@ -54,22 +53,19 @@ import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; import { 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'; provide('shouldOmitHeaderTitle', true); -const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); -const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); -const isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable); -const keymap = { - 't': focus, -}; - const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>(); const rootEl = shallowRef<HTMLElement>(); +type TimelinePageSrc = BasicTimelineType | `list:${string}`; + const queue = ref(0); -const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global'); -const src = computed<'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`>({ +const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global'); +const src = computed<TimelinePageSrc>({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x), }); @@ -79,7 +75,11 @@ const withRenotes = computed<boolean>({ }); // computed内ã§ã®ç„¡é™ãƒ«ãƒ¼ãƒ—を防ããŸã‚ã®ãƒ•ãƒ©ã‚° -const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies'); +const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>( + defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' : + defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' : + false, +); const withReplies = computed<boolean>({ get: () => { @@ -151,7 +151,7 @@ async function chooseList(ev: MouseEvent): Promise<void> { (lists.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.createNew, to: '/my/lists', }, @@ -171,7 +171,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> { (antennas.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.createNew, to: '/my/antennas', }, @@ -196,7 +196,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { (channels.length === 0 ? undefined : { type: 'divider' }), { type: 'link' as const, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.createNew, to: '/channels', }, @@ -204,7 +204,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { os.popupMenu(items, ev.currentTarget ?? ev.target); } -function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`): void { +function saveSrc(newSrc: TimelinePageSrc): void { const out = deepMerge({ src: newSrc }, defaultStore.state.tl); if (newSrc.startsWith('userList:')) { @@ -239,23 +239,36 @@ function focus(): void { } function closeTutorial(): void { - if (!['home', 'local', 'social', 'global'].includes(src.value)) return; + if (!isBasicTimeline(src.value)) return; const before = defaultStore.state.timelineTutorials; before[src.value] = true; defaultStore.set('timelineTutorials', before); } +function switchTlIfNeeded() { + if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) { + src.value = availableBasicTimelines()[0]; + } +} + +onMounted(() => { + switchTlIfNeeded(); +}); +onActivated(() => { + switchTlIfNeeded(); +}); + const headerActions = computed(() => { const tmp = [ { - icon: 'ph-dots-three ph-bold ph-lg', + icon: 'ti ti-dots', text: i18n.ts.options, handler: (ev) => { os.popupMenu([{ type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, - }, src.value === 'local' || src.value === 'social' ? { + }, isBasicTimeline(src.value) && hasWithReplies(src.value) ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, @@ -268,14 +281,14 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, - disabled: src.value === 'local' || src.value === 'social' ? withReplies : false, + disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false, }], ev.currentTarget ?? ev.target); }, }, ]; if (deviceKind === 'desktop') { tmp.unshift({ - icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', + icon: 'ti ti-refresh', text: i18n.ts.reload, handler: (ev: Event) => { tlComponent.value?.reloadTimeline(); @@ -288,68 +301,40 @@ const headerActions = computed(() => { const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ key: 'list:' + l.id, title: l.name, - icon: 'ph-star ph-bold ph-lg', - iconOnly: true, -}))), { - key: 'home', - title: i18n.ts._timelines.home, - icon: 'ph-house ph-bold ph-lg', + icon: 'ti ti-star', iconOnly: true, -}, ...(isLocalTimelineAvailable ? [{ - key: 'local', - title: i18n.ts._timelines.local, - icon: 'ph-planet ph-bold ph-lg', - iconOnly: true, -}, { - key: 'social', - title: i18n.ts._timelines.social, - icon: 'ph-rocket-launch ph-bold ph-lg', +}))), ...availableBasicTimelines().map(tl => ({ + key: tl, + title: i18n.ts._timelines[tl], + icon: basicTimelineIconClass(tl), iconOnly: true, -}] : []), ...(isBubbleTimelineAvailable ? [{ - key: 'bubble', - title: 'Bubble', - icon: 'ph-drop ph-bold ph-lg', - iconOnly: true, -}] : []), ...(isGlobalTimelineAvailable ? [{ - key: 'global', - title: i18n.ts._timelines.global, - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', - iconOnly: true, -}] : []), { - icon: 'ph-list ph-bold ph-lg', +})), { + icon: 'ti ti-list', title: i18n.ts.lists, iconOnly: true, onClick: chooseList, }, { - icon: 'ph-flying-saucer ph-bold ph-lg', + icon: 'ti ti-antenna', title: i18n.ts.antennas, iconOnly: true, onClick: chooseAntenna, }, { - icon: 'ph-television ph-bold ph-lg', + icon: 'ti ti-device-tv', title: i18n.ts.channel, iconOnly: true, onClick: chooseChannel, }] as Tab[]); -const headerTabsWhenNotLogin = computed(() => [ - ...(isLocalTimelineAvailable ? [{ - key: 'local', - title: i18n.ts._timelines.local, - icon: 'ph-planet ph-bold ph-lg', - iconOnly: true, - }] : []), - ...(isGlobalTimelineAvailable ? [{ - key: 'global', - title: i18n.ts._timelines.global, - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', - iconOnly: true, - }] : []), -] as Tab[]); +const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(tl => ({ + key: tl, + title: i18n.ts._timelines[tl], + icon: basicTimelineIconClass(tl), + iconOnly: true, +}))] as Tab[]); definePageMetadata(() => ({ title: i18n.ts.timeline, - icon: src.value === 'local' ? 'ph-planet ph-bold ph-lg' : src.value === 'social' ? 'ph-rocket-launch ph-bold ph-lg' : src.value === 'global' ? 'ph-globe-hemisphere-west ph-bold ph-lg' : src.value === 'bubble' ? 'ph-drop ph-bold ph-lg' : 'ph-house ph-bold ph-lg', + icon: isBasicTimeline(src.value) ? basicTimelineIconClass(src.value) : 'ti ti-home', })); </script> diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue index c60e3c0f2953b441c7b6f3e0ac609108211dc324..6566263c010f6fc834ba67d372e5e5802078570c 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -98,7 +98,7 @@ const headerActions = computed(() => list.value ? [{ }], ev.currentTarget ?? ev.target); }, }, { - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts.settings, handler: settings, }] : []); @@ -107,7 +107,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: list.value ? list.value.name : i18n.ts.lists, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', })); </script> diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue index b6ebfb8abcd69c046c20cfb7d1d1574c3173441c..a77493fe47d6e82796164a0c6d0d87ab4dc1c726 100644 --- a/packages/frontend/src/pages/user-tag.vue +++ b/packages/frontend/src/pages/user-tag.vue @@ -36,7 +36,7 @@ const tagUsers = computed(() => ({ definePageMetadata(() => ({ title: props.tag, - icon: 'ph-user-circle ph-bold ph-lg', + icon: 'ti ti-user-search', })); </script> diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue index 271631e8d12c8acf163d42651b4ed0eea9534caa..994bd5270511f388fb9724dc80442e81882e3cbb 100644 --- a/packages/frontend/src/pages/user/activity.vue +++ b/packages/frontend/src/pages/user/activity.vue @@ -7,19 +7,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="700"> <div class="_gaps"> <MkFoldableSection class="item"> - <template #header><i class="ph-pulse ph-bold ph-lg"></i> Heatmap</template> + <template #header><i class="ti ti-activity"></i> Heatmap</template> <MkHeatmap :user="user" :src="'notes'"/> </MkFoldableSection> <MkFoldableSection class="item"> - <template #header><i class="ph-pencil-simple ph-bold ph-lg"></i> Notes</template> + <template #header><i class="ti ti-pencil"></i> Notes</template> <XNotes :user="user"/> </MkFoldableSection> <MkFoldableSection class="item"> - <template #header><i class="ph-users ph-bold ph-lg"></i> Following</template> + <template #header><i class="ti ti-users"></i> Following</template> <XFollowing :user="user"/> </MkFoldableSection> <MkFoldableSection class="item"> - <template #header><i class="ph-eye ph-bold ph-lg"></i> PV</template> + <template #header><i class="ti ti-eye"></i> PV</template> <XPv :user="user"/> </MkFoldableSection> </div> diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue index e8addf88b701487ea0174bb4b3af7152fc7f9588..70883242e5d5b8c77cdf581687ba4af1bfd97346 100644 --- a/packages/frontend/src/pages/user/followers.vue +++ b/packages/frontend/src/pages/user/followers.vue @@ -54,7 +54,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.user, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', ...user.value ? { title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`, subtitle: i18n.ts.followers, diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue index 8e4da403839db64b6d0cbb27b794e3deab810d0f..37b25f694f9930462f1a887fe328f0ec4c9640aa 100644 --- a/packages/frontend/src/pages/user/following.vue +++ b/packages/frontend/src/pages/user/following.vue @@ -54,7 +54,7 @@ const headerTabs = computed(() => []); definePageMetadata(() => ({ title: i18n.ts.user, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', ...user.value ? { title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`, subtitle: i18n.ts.following, diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 095645636da59cd9fb90f921ef781eb03e3abb5a..b997fe1c3f1ac34512f252f055605581d6ee43a1 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -22,18 +22,18 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserName class="name" :user="user" :nowrap="true"/> <div class="bottom"> <span class="username"><MkAcct :user="user" :detail="true"/></span> - <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ph-shield ph-bold ph-lg"></i></span> - <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ph-lock ph-bold ph-lg"></i></span> - <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ph-robot ph-bold ph-lg"></i></span> + <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span> + <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> + <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> - <i class="ph-pencil-simple-line ph-bold ph-lg"/> {{ i18n.ts.addMemo }} + <i class="ti ti-edit"/> {{ i18n.ts.addMemo }} </button> </div> </div> <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span> - <div v-if="$i" class="actions"> - <button class="menu _button" @click="menu"><i class="ph-dots-three ph-bold ph-lg"></i></button> - <MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> + <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"/> </div> </div> <MkAvatar class="avatar" :user="user" indicator/> @@ -41,9 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkUserName :user="user" :nowrap="false" class="name"/> <div class="bottom"> <span class="username"><MkAcct :user="user" :detail="true"/></span> - <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ph-shield ph-bold ph-lg"></i></span> - <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ph-lock ph-bold ph-lg"></i></span> - <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ph-robot ph-bold ph-lg"></i></span> + <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span> + <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> + <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> </div> </div> <div v-if="user.roles.length > 0" class="roles"> @@ -79,26 +79,26 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="fields system"> <dl v-if="user.location" class="field"> - <dt class="name"><i class="ph-map-pin ph-bold ph-lg ti-fw"></i> {{ i18n.ts.location }}</dt> + <dt class="name"><i class="ti ti-map-pin ti-fw"></i> {{ i18n.ts.location }}</dt> <dd class="value">{{ user.location }}</dd> </dl> <dl v-if="user.birthday" class="field"> - <dt class="name"><i class="ph-cake ph-bold ph-lg ti-fw"></i> {{ i18n.ts.birthday }}</dt> + <dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt> <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd> </dl> <dl class="field"> - <dt class="name"><i class="ph-calendar ph-bold ph-lg ti-fw"></i> {{ i18n.ts.registeredDate }}</dt> + <dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt> <dd class="value">{{ dateString(user.createdAt) }} (<MkTime :time="user.createdAt"/>)</dd> </dl> </div> <div v-if="user.fields.length > 0" class="fields"> <dl v-for="(field, i) in user.fields" :key="i" class="field"> <dt class="name"> - <Mfm :text="field.name" :plain="true" :colored="false"/> + <Mfm :text="field.name" :author="user" :plain="true" :colored="false"/> </dt> <dd class="value"> <Mfm :text="field.value" :author="user" :colored="false"/> - <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ph-seal-check ph-bold ph-lg" :class="$style.verifiedLink"></i> + <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i> </dd> </dl> </div> @@ -120,12 +120,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="contents _gaps"> - <div v-if="user.pinnedNotes.length > 0 && defaultStore.state.noteDesign === 'misskey'" class="_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> - <div v-else-if="user.pinnedNotes.length > 0 && defaultStore.state.noteDesign === 'sharkey'" class="_gaps"> - <SkNote 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> <template v-if="narrow"> <MkLazy> @@ -171,9 +168,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkTab from '@/components/MkTab.vue'; -import MkNote from '@/components/MkNote.vue'; import MkNotes from '@/components/MkNotes.vue'; -import SkNote from '@/components/SkNote.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import MkAccountMoved from '@/components/MkAccountMoved.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; @@ -186,13 +181,20 @@ import number from '@/filters/number.js'; import { userPage } from '@/filters/user.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import { defaultStore } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; import { dateString } from '@/filters/date.js'; import { confetti } from '@/scripts/confetti.js'; -import { defaultStore } from '@/store.js'; 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'; + +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); function calcAge(birthdate: string): number { const date = new Date(birthdate); @@ -258,9 +260,15 @@ if (props.user.listenbrainz) { const background = computed(() => { if (props.user.backgroundUrl == null) return {}; - return { - '--backgroundImageStatic': `url('${props.user.backgroundUrl}')` - }; + if (defaultStore.state.disableShowingAnimatedImages) { + return { + '--backgroundImageStatic': `url('${getStaticImageUrl(props.user.backgroundUrl)}')` + }; + } else { + return { + '--backgroundImageStatic': `url('${props.user.backgroundUrl}')` + }; + } }); watch(moderationNote, async () => { @@ -289,9 +297,15 @@ const AllPagination = { const style = computed(() => { if (props.user.bannerUrl == null) return {}; - return { - backgroundImage: `url(${ props.user.bannerUrl })`, - }; + if (defaultStore.state.disableShowingAnimatedImages) { + return { + backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`, + }; + } else { + return { + backgroundImage: `url(${ props.user.bannerUrl })`, + }; + } }); const age = computed(() => { @@ -478,11 +492,12 @@ onUnmounted(() => { > .name { display: block; - margin: 0; + margin: -10px; + padding: 10px; line-height: 32px; font-weight: bold; font-size: 1.8em; - text-shadow: 0 0 8px #000; + filter: drop-shadow(0 0 4px #000); } > .bottom { diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue index 857cf996ff2414c31e1a0c119fe8b4465bc7a096..45bc35067b2f7021192ada44c997d54397f0c1f0 100644 --- a/packages/frontend/src/pages/user/index.activity.vue +++ b/packages/frontend/src/pages/user/index.activity.vue @@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer> - <template #icon><i class="ph-chart-line ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-chart-line"></i></template> <template #header>{{ i18n.ts.activity }}</template> <template #func="{ buttonStyleClass }"> <button class="_button" :class="buttonStyleClass" @click="showMenu"> - <i class="ph-dots-three ph-bold ph-lg"></i> + <i class="ti ti-dots"></i> </button> </template> diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index be58cec24aaf3056090285669c7da3c43735961a..1430ae1296f02c04c8cbcc183035c9f3528711e2 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :max-height="300" :foldable="true"> - <template #icon><i class="ph-image-square ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-photo"></i></template> <template #header>{{ i18n.ts.files }}</template> <div :class="$style.root"> <MkLoading v-if="fetching"/> @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/> <div :class="$style.sensitive"> <div> - <div><i class="ph-eye-slash ph-bold ph-lg"></i> {{ i18n.ts.sensitive }}</div> + <div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div> <div>{{ i18n.ts.clickToShow }}</div> </div> </div> diff --git a/packages/frontend/src/pages/user/index.listenbrainz.vue b/packages/frontend/src/pages/user/index.listenbrainz.vue index 965a029fac740dd5dbe98775d4954c81c1199b7c..18092d9d875a0c4691cd12b814d42f460ccb4f39 100644 --- a/packages/frontend/src/pages/user/index.listenbrainz.vue +++ b/packages/frontend/src/pages/user/index.listenbrainz.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only </a> <a :href="listenbrainz.listenbrainzurl"> <div class="playicon"> - <i class="ph-play ph-bold ph-lg-filled"></i> + <i class="ph-play ph-bold ph-lg"></i> </div> </a> </div> diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index ebe4176196dc90545b161d8bda1de4c282df8f8b..87c8bb28665057d9323055634a58af20630868b1 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -83,39 +83,39 @@ const headerActions = computed(() => []); const headerTabs = computed(() => user.value ? [{ key: 'home', title: i18n.ts.overview, - icon: 'ph-house ph-bold ph-lg', + icon: 'ti ti-home', }, { key: 'notes', title: i18n.ts.notes, - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', }, { key: 'activity', title: i18n.ts.activity, - icon: 'ph-chart-line ph-bold ph-lg', + icon: 'ti ti-chart-line', }, ...(user.value.host == null ? [{ key: 'achievements', title: i18n.ts.achievements, - icon: 'ph-trophy ph-bold ph-lg', + icon: 'ti ti-medal', }] : []), ...($i && ($i.id === user.value.id || $i.isAdmin || $i.isModerator)) || user.value.publicReactions ? [{ key: 'reactions', title: i18n.ts.reaction, - icon: 'ph-smiley ph-bold ph-lg', + icon: 'ti ti-mood-happy', }] : [], { key: 'clips', title: i18n.ts.clips, - icon: 'ph-paperclip ph-bold ph-lg', + icon: 'ti ti-paperclip', }, { key: 'lists', title: i18n.ts.lists, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', }, { key: 'pages', title: i18n.ts.pages, - icon: 'ph-newspaper ph-bold ph-lg', + icon: 'ti ti-news', }, { key: 'flashs', title: 'Play', - icon: 'ph-play ph-bold ph-lg', + icon: 'ti ti-player-play', }, { key: 'gallery', title: i18n.ts.gallery, @@ -123,12 +123,12 @@ const headerTabs = computed(() => user.value ? [{ }, { key: 'raw', title: 'Raw', - icon: 'ph-code ph-bold ph-lg', + icon: 'ti ti-code', }] : []); definePageMetadata(() => ({ title: i18n.ts.user, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', ...user.value ? { title: user.value.name ? ` (@${user.value.username})` : `@${user.value.username}`, subtitle: `@${getAcct(user.value)}`, diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 7d5861d2aea19edff6dc8613128161f08e0bcb60..31911649ac8170238da4825462624bd9761c9f13 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkInput v-model="password" type="password" data-cy-admin-password> <template #label>{{ i18n.ts.password }}</template> - <template #prefix><i class="ph-lock ph-bold ph-lg"></i></template> + <template #prefix><i class="ti ti-lock"></i></template> </MkInput> <div> <MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;"> diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue new file mode 100644 index 0000000000000000000000000000000000000000..252b1a2955c49f029b8f9c944556653d08ef5def --- /dev/null +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -0,0 +1,109 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div :key="note.id" :class="$style.note"> + <div class="_panel _gaps_s" :class="$style.content"> + <div v-if="note.cw != null" :class="$style.richcontent"> + <div><Mfm :text="note.cw" :author="note.user"/></div> + <MkCwButton v-model="showContent" :text="note.text" :renote="note.renote" :files="note.files" :poll="note.poll" style="margin: 4px 0;"/> + <div v-if="showContent"> + <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> + <Mfm v-if="note.text" :text="note.text" :isBlock="true" :author="note.user"/> + <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> + </div> + </div> + <div v-else ref="noteTextEl" :class="[$style.text, { [$style.collapsed]: shouldCollapse }]"> + <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> + <Mfm v-if="note.text" :text="note.text" :isBlock="true" :author="note.user"/> + <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> + </div> + <div v-if="note.files && note.files.length > 0" :class="$style.richcontent"> + <MkMediaList :mediaList="note.files.slice(0, 4)"/> + </div> + <div v-if="note.poll"> + <MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/> + </div> + <div v-if="note.reactionCount > 0" :class="$style.reactions"> + <MkReactionsViewer :note="note" :maxNumber="16"/> + </div> + </div> +</div> +</template> + +<script lang="ts" setup> +import { ref, shallowRef, onUpdated, onMounted } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; +import MkMediaList from '@/components/MkMediaList.vue'; +import MkPoll from '@/components/MkPoll.vue'; +import MkCwButton from '@/components/MkCwButton.vue'; + +defineProps<{ + note: Misskey.entities.Note; +}>(); + +const noteTextEl = shallowRef<HTMLDivElement>(); +const shouldCollapse = ref(false); +const showContent = ref(false); + +function calcCollapse() { + if (noteTextEl.value) { + const height = noteTextEl.value.scrollHeight; + if (height > 200) { + shouldCollapse.value = true; + } + } +} + +onMounted(() => { + calcCollapse(); +}); + +onUpdated(() => { + calcCollapse(); +}); +</script> + +<style lang="scss" module> +.note { + margin-left: auto; +} + +.text { + position: relative; + max-height: 200px; + overflow: hidden; + + &.collapsed::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 64px; + background: linear-gradient(0deg, var(--panel), var(--X15)); + } +} + +.content { + padding: 16px; + margin: 0 0 0 auto; + max-width: max-content; + border-radius: var(--radius-md); +} + +.reactions { + box-sizing: border-box; + margin: 8px -16px -8px; + padding: 8px 16px 0; + width: calc(100% + 32px); + border-top: 1px solid var(--divider); +} + +.richcontent { + min-width: 250px; +} +</style> diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 4768e31d1dab77e7a9e2af9b2353ed5d7084c441..045f424cdaedbec86ab3cfa9c9c6c8a9c13d777e 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -4,24 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.root"> - <div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]"> - <div v-for="note in notes" :key="note.id" :class="$style.note"> - <div class="_panel" :class="$style.content"> - <div> - <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :isBlock="true" :author="note.user"/> - <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> - </div> - <div v-if="note.files.length > 0" :class="$style.richcontent"> - <MkMediaList :mediaList="note.files"/> - </div> - <div v-if="note.poll"> - <MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/> - </div> - </div> - <MkReactionsViewer ref="reactionsViewer" :note="note"/> - </div> +<div :class="$style.root" class="_gaps"> + <div + ref="notesMainContainerEl" + class="_gaps" + :class="[$style.scrollBoxMain, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]" + @animationend="changeScrollState" + > + <XNote v-for="note in notes" :key="`${note.id}_1`" :class="$style.note" :note="note"/> + </div> + <div v-if="isScrolling" class="_gaps" :class="[$style.scrollBoxSub, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]"> + <XNote v-for="note in notes" :key="`${note.id}_2`" :class="$style.note" :note="note"/> </div> </div> </template> @@ -29,43 +22,54 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import * as Misskey from 'misskey-js'; import { onUpdated, ref, shallowRef } from 'vue'; -import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; -import MkMediaList from '@/components/MkMediaList.vue'; -import MkPoll from '@/components/MkPoll.vue'; +import XNote from '@/pages/welcome.timeline.note.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { getScrollContainer } from '@/scripts/scroll.js'; const notes = ref<Misskey.entities.Note[]>([]); const isScrolling = ref(false); -const scrollEl = shallowRef<HTMLElement>(); +const scrollState = ref<null | 'intro' | 'loop'>(null); +const notesMainContainerEl = shallowRef<HTMLElement>(); misskeyApiGet('notes/featured').then(_notes => { notes.value = _notes.filter(n => n.cw == null); }); +function changeScrollState() { + if (scrollState.value !== 'loop') { + scrollState.value = 'loop'; + } +} + onUpdated(() => { - if (!scrollEl.value) return; - const container = getScrollContainer(scrollEl.value); + if (!notesMainContainerEl.value) return; + const container = getScrollContainer(notesMainContainerEl.value); const containerHeight = container ? container.clientHeight : window.innerHeight; - if (scrollEl.value.offsetHeight > containerHeight) { + if (notesMainContainerEl.value.offsetHeight > containerHeight) { + if (scrollState.value === null) { + scrollState.value = 'intro'; + } isScrolling.value = true; } }); </script> <style lang="scss" module> -@keyframes scroll { +@keyframes scrollIntro { 0% { transform: translate3d(0, 0, 0); } - 5% { - transform: translate3d(0, 0, 0); + 100% { + transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0); } - 75% { - transform: translate3d(0, calc(-100% + 90vh), 0); +} + +@keyframes scrollConstant { + 0% { + transform: translate3d(0, -128px, 0); } - 90% { - transform: translate3d(0, calc(-100% + 90vh), 0); + 100% { + transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0); } } @@ -73,24 +77,26 @@ onUpdated(() => { text-align: right; } -.scrollbox { - &.scroll { - animation: scroll 45s linear infinite; +.scrollBoxMain { + &.scrollIntro { + animation: scrollIntro 30s linear forwards; + } + &.scrollLoop { + animation: scrollConstant 30s linear infinite; } } -.note { - margin: 16px 0 16px auto; -} - -.content { - padding: 16px; - margin: 0 0 0 auto; - max-width: max-content; - border-radius: var(--radius-md); +.scrollBoxSub { + &.scrollIntro { + animation: scrollIntro 30s linear forwards; + } + &.scrollLoop { + animation: scrollConstant 30s linear infinite; + } } -.richcontent { - min-width: 250px; +.root:has(.note:hover) .scrollBoxMain, +.root:has(.note:hover) .scrollBoxSub { + animation-play-state: paused; } </style> diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index a0a624ace73507735931b3b056e11276eb4eadf8..81233a5a5e451447948ad9bc7c636165b7477086 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -28,31 +28,10 @@ export async function install(plugin: Plugin): Promise<void> { }, log: (): void => { }, - /* dakkar 2024-06-20 - - passing an `err` triggers an unwanted side-effect inside the - AiScript Interpreter: - - - the plugin code throws an exception of any kind (in the - specific case that made us look, it was `note.text.split(...)` - on a note with no text) - - - the Interpreter's `handleError` calls `this.abort()` before - calling our `err` - - - from that point on, every evaluation of that Interpreter object - returns null - - - which, at least inside a noteViewInterruptor, causes all notes - to be replaced with a null - - I'm reporting this problem upstream, in the meantime we'll have - to do without error logs - */ - // err: (err): void => { - // pluginLogs.value.get(plugin.id).push(`${err}`); - // throw err; // install時ã®try-catchã«åå¿œã•ã›ã‚‹ - // }, + err: (err): void => { + pluginLogs.value.get(plugin.id).push(`${err}`); + throw err; // install時ã®try-catchã«åå¿œã•ã›ã‚‹ + }, }); initPlugin({ plugin, aiscript }); diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index f18dac2a44e2884d29c09f73f96d4414394074a8..14110d1f9bd0cbb9494d5692fa9e548d2818f1b3 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -232,13 +232,26 @@ const routes: RouteDef[] = [{ component: page(() => import('@/pages/search.vue')), query: { q: 'query', + userId: 'userId', + username: 'username', + host: 'host', channel: 'channel', type: 'type', origin: 'origin', }, }, { + // Legacy Compatibility path: '/authorize-follow', - component: page(() => import('@/pages/follow.vue')), + redirect: '/lookup', + loginRequired: true, +}, { + // Mastodon Compatibility + path: '/authorize_interaction', + redirect: '/lookup', + loginRequired: true, +}, { + path: '/lookup', + component: page(() => import('@/pages/lookup.vue')), loginRequired: true, }, { path: '/share', @@ -251,6 +264,9 @@ const routes: RouteDef[] = [{ }, { path: '/scratchpad', component: page(() => import('@/pages/scratchpad.vue')), +}, { + path: '/preview', + component: page(() => import('@/pages/preview.vue')), }, { path: '/auth/:token', component: page(() => import('@/pages/auth.vue')), @@ -475,6 +491,14 @@ const routes: RouteDef[] = [{ path: '/approvals', name: 'approvals', component: page(() => import('@/pages/admin/approvals.vue')), + }, { + path: '/abuse-report-notification-recipient', + name: 'abuse-report-notification-recipient', + component: page(() => import('@/pages/admin/abuse-report/notification-recipient.vue')), + }, { + path: '/system-webhook', + name: 'system-webhook', + component: page(() => import('@/pages/admin/system-webhook.vue')), }, { path: '/', component: page(() => import('@/pages/_empty_.vue')), diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts index b3d76e149f4357e228b8811a4fbb1b4838619598..f2feb29dfc57101f55316c8b4f20aba8bca10ee0 100644 --- a/packages/frontend/src/scripts/array.ts +++ b/packages/frontend/src/scripts/array.ts @@ -77,44 +77,6 @@ export function maximum(xs: number[]): number { return Math.max(...xs); } -/** - * Splits an array based on the equivalence relation. - * The concatenation of the result is equal to the argument. - */ -export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { - const groups = [] as T[][]; - for (const x of xs) { - const lastGroup = groups.at(-1); - if (lastGroup !== undefined && f(lastGroup[0], x)) { - lastGroup.push(x); - } else { - groups.push([x]); - } - } - return groups; -} - -/** - * Splits an array based on the equivalence relation induced by the function. - * The concatenation of the result is equal to the argument. - */ -export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] { - return groupBy((a, b) => f(a) === f(b), xs); -} - -export function groupByX<T>(collections: T[], keySelector: (x: T) => string) { - return collections.reduce((obj: Record<string, T[]>, item: T) => { - const key = keySelector(item); - if (typeof obj[key] === 'undefined') { - obj[key] = []; - } - - obj[key].push(item); - - return obj; - }, {}); -} - /** * Compare two arrays by lexicographical order */ diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed86529d5b8929ad5dd7675db621c18c207d32e0 --- /dev/null +++ b/packages/frontend/src/scripts/check-permissions.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { instance } from '@/instance.js'; +import { $i } from '@/account.js'; + +export const notesSearchAvailable = ( + // FIXME: instance.policies would be null in Vitest + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ($i == null && instance.policies != null && instance.policies.canSearchNotes) || + ($i != null && $i.policies.canSearchNotes) || + false +) as boolean; + +export const canSearchNonLocalNotes = ( + instance.noteSearchableScope === 'global' +); diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts index 216c0464b3d5a0d300debef9bd70020853418ca9..08f5b52daec397aeb016798d94289ea43cc39879 100644 --- a/packages/frontend/src/scripts/copy-to-clipboard.ts +++ b/packages/frontend/src/scripts/copy-to-clipboard.ts @@ -6,33 +6,6 @@ /** * Clipboardã«å€¤ã‚’コピー(TODO: æ–‡å—列以外も対応) */ -export default val => { - // 空div ç”Ÿæˆ - const tmp = document.createElement('div'); - // é¸æŠžç”¨ã®ã‚¿ã‚°ç”Ÿæˆ - const pre = document.createElement('pre'); - - // 親è¦ç´ ã®CSS㧠user-select: none ã ã¨ã‚³ãƒ”ーã§ããªã„ã®ã§æ›¸ãæ›ãˆã‚‹ - pre.style.webkitUserSelect = 'auto'; - pre.style.userSelect = 'auto'; - - tmp.appendChild(pre).textContent = val; - - // è¦ç´ ã‚’ç”»é¢å¤–㸠- const s = tmp.style; - s.position = 'fixed'; - s.right = '200%'; - - // body ã«è¿½åŠ - document.body.appendChild(tmp); - // è¦ç´ ã‚’é¸æŠž - document.getSelection().selectAllChildren(tmp); - - // クリップボードã«ã‚³ãƒ”ー - const result = document.execCommand('copy'); - - // è¦ç´ 削除 - document.body.removeChild(tmp); - - return result; -}; +export function copyToClipboard(input: string | null) { + if (input) navigator.clipboard.writeText(input); +} diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb7caea830650f6ee672973addfae6d1b7677121 --- /dev/null +++ b/packages/frontend/src/scripts/focus-trap.ts @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; + +const focusTrapElements = new Set<HTMLElement>(); +const ignoreElements = [ + 'script', + 'style', +]; + +function containsFocusTrappedElements(el: HTMLElement): boolean { + return Array.from(focusTrapElements).some((focusTrapElement) => { + return el.contains(focusTrapElement); + }); +} + +function getZIndex(el: HTMLElement): number { + const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10); + if (isNaN(zIndex)) { + return 0; + } + return zIndex; +} + +function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null { + let highestZIndexElement: HTMLElement | null = null; + let highestZIndex = -Infinity; + + focusTrapElements.forEach((el) => { + const zIndex = getZIndex(el); + if (zIndex > highestZIndex) { + highestZIndex = zIndex; + highestZIndexElement = el; + } + }); + + return highestZIndexElement == null ? null : { + el: highestZIndexElement, + zIndex: highestZIndex, + }; +} + +function releaseFocusTrap(el: HTMLElement): void { + focusTrapElements.delete(el); + if (el.inert === true) { + el.inert = false; + } + + const highestZIndexElement = getHighestZIndexElement(); + + if (el.parentElement != null && el !== document.body) { + el.parentElement.childNodes.forEach((siblingNode) => { + const siblingEl = getHTMLElementOrNull(siblingNode); + if (!siblingEl) return; + if ( + siblingEl !== el && + ( + highestZIndexElement == null || + siblingEl === highestZIndexElement.el || + siblingEl.contains(highestZIndexElement.el) + ) + ) { + siblingEl.inert = false; + } else if ( + highestZIndexElement != null && + siblingEl !== highestZIndexElement.el && + !siblingEl.contains(highestZIndexElement.el) && + !ignoreElements.includes(siblingEl.tagName.toLowerCase()) + ) { + siblingEl.inert = true; + } else { + siblingEl.inert = false; + } + }); + releaseFocusTrap(el.parentElement); + } +} + +export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void; +export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; }; +export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void { + const highestZIndexElement = getHighestZIndexElement(); + + const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex; + const zIndex = getZIndex(el); + + // If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead + // Focus trapping for this element will be done in the release function + if (!parent && zIndex < highestZIndex) { + focusTrapElements.add(el); + if (highestZIndexElement) { + focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls); + } + return { + release: () => { + releaseFocusTrap(el); + }, + }; + } + + if (el.inert === true) { + el.inert = false; + } + + if (el.parentElement != null && el !== document.body) { + el.parentElement.childNodes.forEach((siblingNode) => { + const siblingEl = getHTMLElementOrNull(siblingNode); + if (!siblingEl) return; + if ( + siblingEl !== el && + ( + hasInteractionWithOtherFocusTrappedEls === false || + (!focusTrapElements.has(siblingEl) && !containsFocusTrappedElements(siblingEl)) + ) && + !ignoreElements.includes(siblingEl.tagName.toLowerCase()) + ) { + siblingEl.inert = true; + } + }); + focusTrap(el.parentElement, hasInteractionWithOtherFocusTrappedEls, true); + } + + if (!parent) { + focusTrapElements.add(el); + + return { + release: () => { + releaseFocusTrap(el); + }, + }; + } +} diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index ea6ee61c8838196abe8fed8f1323392bef5da0b2..eb2da5ad8665c47ebe6c7d55b7c8f9684a79c407 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -3,30 +3,78 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export function focusPrev(el: Element | null, self = false, scroll = true) { - if (el == null) return; - if (!self) el = el.previousElementSibling; - if (el) { - if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus({ - preventScroll: !scroll, - }); - } else { - focusPrev(el.previousElementSibling, true); - } +import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js'; +import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; + +type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement; + +export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => { + if (input == null || !(input instanceof HTMLElement)) return false; + + if (input.tabIndex < 0) return false; + if ('disabled' in input && input.disabled === true) return false; + if ('readonly' in input && input.readonly === true) return false; + + if (!input.ownerDocument.contains(input)) return false; + + const style = window.getComputedStyle(input); + if (style.display === 'none') return false; + if (style.visibility === 'hidden') return false; + if (style.opacity === '0') return false; + if (style.pointerEvents === 'none') return false; + + return true; +}; + +export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { + const element = self ? input : getElementOrNull(input)?.previousElementSibling; + if (element == null) return; + if (isFocusable(element)) { + focusOrScroll(element, scroll); + } else { + focusPrev(element, false, scroll); } -} - -export function focusNext(el: Element | null, self = false, scroll = true) { - if (el == null) return; - if (!self) el = el.nextElementSibling; - if (el) { - if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus({ - preventScroll: !scroll, - }); - } else { - focusPrev(el.nextElementSibling, true); +}; + +export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { + const element = self ? input : getElementOrNull(input)?.nextElementSibling; + if (element == null) return; + if (isFocusable(element)) { + focusOrScroll(element, scroll); + } else { + focusNext(element, false, scroll); + } +}; + +export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { + const element = self ? input : getNodeOrNull(input)?.parentElement; + if (element == null) return; + if (isFocusable(element)) { + focusOrScroll(element, scroll); + } else { + focusParent(element, false, scroll); + } +}; + +const focusOrScroll = (element: HTMLElement, scroll: boolean) => { + if (scroll) { + const scrollContainer = getScrollContainer(element) ?? document.documentElement; + const scrollContainerTop = getScrollPosition(scrollContainer); + const stickyTop = getStickyTop(element, scrollContainer); + const stickyBottom = getStickyBottom(element, scrollContainer); + const top = element.getBoundingClientRect().top; + const bottom = element.getBoundingClientRect().bottom; + + let scrollTo = scrollContainerTop; + if (top < stickyTop) { + scrollTo += top - stickyTop; + } else if (bottom > window.innerHeight - stickyBottom) { + scrollTo += bottom - window.innerHeight + stickyBottom; } + scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' }); + } + + if (document.activeElement !== element) { + element.focus({ preventScroll: true }); } -} +}; diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/scripts/get-appear-note.ts new file mode 100644 index 0000000000000000000000000000000000000000..40ce80eac9fc42c221e7ac1edcf0f2e5053d86f9 --- /dev/null +++ b/packages/frontend/src/scripts/get-appear-note.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; + +export function getAppearNote(note: Misskey.entities.Note) { + return Misskey.note.isPureRenote(note) ? note.renote : note; +} diff --git a/packages/frontend/src/scripts/get-dom-node-or-null.ts b/packages/frontend/src/scripts/get-dom-node-or-null.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbf54675fda2c2fe6e836a70d5107463887ed294 --- /dev/null +++ b/packages/frontend/src/scripts/get-dom-node-or-null.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const getNodeOrNull = (input: unknown): Node | null => { + if (input instanceof Node) return input; + return null; +}; + +export const getElementOrNull = (input: unknown): Element | null => { + if (input instanceof Element) return input; + return null; +}; + +export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => { + if (input instanceof HTMLElement) return input; + return null; +}; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index a883404307120561cc02162508241047fb8f2c30..108648d640ad0defe1a53f80987715042a3a0ff5 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -6,7 +6,7 @@ import * as Misskey from 'misskey-js'; import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; -import copyToClipboard from '@/scripts/copy-to-clipboard.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'; @@ -27,7 +27,7 @@ function rename(file: Misskey.entities.DriveFile) { } function describe(file: Misskey.entities.DriveFile) { - os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.comment ?? '', file: file, }, { @@ -37,7 +37,17 @@ function describe(file: Misskey.entities.DriveFile) { comment: caption.length === 0 ? null : caption, }); }, - }, 'closed'); + closed: () => dispose(), + }); +} + +function move(file: Misskey.entities.DriveFile) { + os.selectDriveFolder(false).then(folder => { + misskeyApi('drive/files/update', { + fileId: file.id, + folderId: folder[0] ? folder[0].id : null, + }); + }); } function toggleSensitive(file: Misskey.entities.DriveFile) { @@ -82,53 +92,57 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss type: 'link', to: `/my/drive/file/${file.id}`, text: i18n.ts._fileViewer.title, - icon: 'ph-file-text ph-bold ph-lg', + icon: 'ti ti-info-circle', }, { type: 'divider' }, { text: i18n.ts.rename, - icon: 'ph-textbox ph-bold ph-lg', + icon: 'ti ti-forms', action: () => rename(file), + }, { + text: i18n.ts.move, + icon: 'ti ti-folder-symlink', + action: () => move(file), }, { text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: file.isSensitive ? 'ph-eye ph-bold ph-lg' : 'ph-eye-closed ph-bold ph-lg', + icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', action: () => toggleSensitive(file), }, { text: i18n.ts.describeFile, - icon: 'ph-text-indent ph-bold ph-lg', + icon: 'ti ti-text-caption', action: () => describe(file), }, ...isImage ? [{ text: i18n.ts.cropImage, - icon: 'ph-crop ph-bold ph-lg', + icon: 'ti ti-crop', action: () => os.cropImage(file, { aspectRatio: NaN, uploadFolder: folder ? folder.id : folder, }), }] : [], { type: 'divider' }, { text: i18n.ts.createNoteFromTheFile, - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', action: () => os.post({ initialFiles: [file], }), }, { text: i18n.ts.copyUrl, - icon: 'ph-link ph-bold ph-lg', + icon: 'ti ti-link', action: () => copyUrl(file), }, { type: 'a', href: file.url, target: '_blank', text: i18n.ts.download, - icon: 'ph-download ph-bold ph-lg', + icon: 'ti ti-download', download: file.name, }, { type: 'divider' }, { text: i18n.ts.delete, - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', danger: true, action: () => deleteFile(file), }]; if (defaultStore.state.devMode) { menu = menu.concat([{ type: 'divider' }, { - icon: 'ph-identification-card ph-bold ph-lg', + icon: 'ti ti-id', text: i18n.ts.copyFileId, action: () => { copyToClipboard(file.id); diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 23c98c18810c5b9e5ce4b196ef13e7e6d3775fce..a7ec4ce6d7735acd81a7c5183de681ad522d830b 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -11,7 +11,7 @@ import { i18n } from '@/i18n.js'; 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -20,6 +20,7 @@ import { clipsCache, favoritedChannelsCache } from '@/cache.js'; import { 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'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -34,14 +35,7 @@ export async function getNoteClipMenu(props: { } } - const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds.length === 0 && - props.note.poll == null - ); - - const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; + const appearNote = getAppearNote(props.note); const clips = await clipsCache.fetch(); const menu: MenuItem[] = [...clips.map(clip => ({ @@ -93,7 +87,7 @@ export async function getNoteClipMenu(props: { }); }, })), { type: 'divider' }, { - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', text: i18n.ts.createNew, action: async () => { const { canceled, result } = await os.form(i18n.ts.createNewClip, { @@ -129,24 +123,26 @@ export async function getNoteClipMenu(props: { export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem { return { - icon: 'ph-warning-circle ph-bold ph-lg', + icon: 'ti ti-exclamation-circle', text, action: (): void => { const localUrl = `${url}/notes/${note.id}`; let noteInfo = ''; if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`; noteInfo += `Local Note: ${localUrl}\n`; - os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: note.user, initialComment: `${noteInfo}-----\n`, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }, }; } export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem { return { - icon: 'ph-link ph-bold ph-lg', + icon: 'ti ti-link', text, action: (): void => { copyToClipboard(`${url}/notes/${note.id}`); @@ -173,14 +169,7 @@ export function getNoteMenu(props: { isDeleted: Ref<boolean>; currentClip?: Misskey.entities.Clip; }) { - const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds.length === 0 && - props.note.poll == null - ); - - const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; + const appearNote = getAppearNote(props.note); const cleanups = [] as (() => void)[]; @@ -268,6 +257,7 @@ export function getNoteMenu(props: { } async function unclip(): Promise<void> { + if (!props.currentClip) return; os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id }); props.isDeleted.value = true; } @@ -287,8 +277,8 @@ export function getNoteMenu(props: { function share(): void { navigator.share({ - title: i18n.tsx.noteOf({ user: appearNote.user.name }), - text: appearNote.text, + title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }), + text: appearNote.text ?? '', url: `${url}/notes/${appearNote.id}`, }); } @@ -317,17 +307,17 @@ export function getNoteMenu(props: { menu = [ ...( props.currentClip?.userId === $i.id ? [{ - icon: 'ph-backspace ph-bold ph-lg', + icon: 'ti ti-backspace', text: i18n.ts.unclip, danger: true, action: unclip, }, { type: 'divider' }] : [] ), { - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', text: i18n.ts.details, action: openDetail, }, { - icon: 'ph-copy ph-bold ph-lg', + icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) @@ -335,59 +325,59 @@ export function getNoteMenu(props: { getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') : undefined, (appearNote.url || appearNote.uri) ? { - icon: 'ph-arrow-square-out ph-bold ph-lg', + icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, } : undefined, ...(isSupportShare() ? [{ - icon: 'ph-share-network ph-bold ph-lg', + icon: 'ti ti-share', text: i18n.ts.share, action: share, }] : []), $i && $i.policies.canUseTranslator && instance.translatorAvailable ? { - icon: 'ph-translate ph-bold ph-lg', + icon: 'ti ti-language-hiragana', text: i18n.ts.translate, action: translate, } : undefined, { type: 'divider' }, statePromise.then(state => state.isFavorited ? { - icon: 'ph-star-half ph-bold ph-lg', + icon: 'ti ti-star-off', text: i18n.ts.unfavorite, action: () => toggleFavorite(false), } : { - icon: 'ph-star ph-bold ph-lg', + icon: 'ti ti-star', text: i18n.ts.favorite, action: () => toggleFavorite(true), }), { type: 'parent' as const, - icon: 'ph-paperclip ph-bold ph-lg', + icon: 'ti ti-paperclip', text: i18n.ts.clip, children: () => getNoteClipMenu(props), }, statePromise.then(state => state.isMutedThread ? { - icon: 'ph-bell-slash ph-bold ph-lg', + icon: 'ti ti-message-off', text: i18n.ts.unmuteThread, action: () => toggleThreadMute(false), } : { - icon: 'ph-bell-slash ph-bold ph-lg', + icon: 'ti ti-message-off', text: i18n.ts.muteThread, action: () => toggleThreadMute(true), }), appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? { - icon: 'ph-push-pin-slash ph-bold ph-lg', + icon: 'ti ti-pinned-off', text: i18n.ts.unpin, action: () => togglePin(false), } : { - icon: 'ph-push-pin ph-bold ph-lg', + icon: 'ti ti-pin', text: i18n.ts.pin, action: () => togglePin(true), } : undefined, { type: 'parent' as const, - icon: 'ph-user ph-bold ph-lg', + icon: 'ti ti-user', text: i18n.ts.user, children: async () => { const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); @@ -400,7 +390,7 @@ export function getNoteMenu(props: { ...($i.isModerator || $i.isAdmin ? [ { type: 'divider' }, { - icon: 'ph-megaphone ph-bold ph-lg', + icon: 'ti ti-speakerphone', text: i18n.ts.promote, action: promote }] @@ -416,7 +406,7 @@ export function getNoteMenu(props: { { type: 'divider' }, { type: 'parent' as const, - icon: 'ph-television ph-bold ph-lg', + icon: 'ti ti-device-tv', text: i18n.ts.channel, children: async () => { const channelChildMenu = [] as MenuItem[]; @@ -425,7 +415,7 @@ export function getNoteMenu(props: { if (channel.pinnedNoteIds.includes(appearNote.id)) { channelChildMenu.push({ - icon: 'ph-push-pin-slash ph-bold ph-lg', + icon: 'ti ti-pinned-off', text: i18n.ts.unpin, action: () => os.apiWithDialog('channels/update', { channelId: appearNote.channel!.id, @@ -434,7 +424,7 @@ export function getNoteMenu(props: { }); } else { channelChildMenu.push({ - icon: 'ph-push-pin ph-bold ph-lg', + icon: 'ti ti-pin', text: i18n.ts.pin, action: () => os.apiWithDialog('channels/update', { channelId: appearNote.channel!.id, @@ -456,13 +446,13 @@ export function getNoteMenu(props: { action: edit, } : undefined, { - icon: 'ph-pencil-simple-line ph-bold ph-lg', + icon: 'ti ti-edit', text: i18n.ts.deleteAndEdit, danger: true, action: delEdit, }, { - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', text: i18n.ts.delete, danger: true, action: del, @@ -472,11 +462,11 @@ export function getNoteMenu(props: { .filter(x => x !== undefined); } else { menu = [{ - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', text: i18n.ts.details, action: openDetail, }, { - icon: 'ph-copy ph-bold ph-lg', + icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) @@ -484,7 +474,7 @@ export function getNoteMenu(props: { getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') : undefined, (appearNote.url || appearNote.uri) ? { - icon: 'ph-arrow-square-out ph-bold ph-lg', + icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); @@ -495,7 +485,7 @@ export function getNoteMenu(props: { if (noteActions.length > 0) { menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({ - icon: 'ph-plug ph-bold ph-lg', + icon: 'ti ti-plug', text: action.title, action: () => { action.handler(appearNote); @@ -505,7 +495,7 @@ export function getNoteMenu(props: { if (defaultStore.state.devMode) { menu = menu.concat([{ type: "divider" }, { - icon: 'ph-identification-card ph-bold ph-lg', + icon: 'ti ti-id', text: i18n.ts.copyNoteId, action: () => { copyToClipboard(appearNote.id); @@ -541,14 +531,7 @@ export function getRenoteMenu(props: { renoteButton: ShallowRef<HTMLElement | undefined>; mock?: boolean; }) { - const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds.length === 0 && - props.note.poll == null - ); - - const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; + const appearNote = getAppearNote(props.note); const channelRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = []; @@ -557,14 +540,16 @@ export function getRenoteMenu(props: { if (appearNote.channel) { channelRenoteItems.push(...[{ text: i18n.ts.inChannelRenote, - icon: 'ph ph-repeat', + icon: 'ti ti-repeat', action: () => { const el = props.renoteButton.value; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } if (!props.mock) { @@ -578,7 +563,7 @@ export function getRenoteMenu(props: { }, }, { text: i18n.ts.inChannelQuote, - icon: 'ph ph-quotes', + icon: 'ti ti-quote', action: () => { if (!props.mock) { os.post({ @@ -593,14 +578,16 @@ export function getRenoteMenu(props: { if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) { normalRenoteItems.push(...[{ text: i18n.ts.renote, - icon: 'ph ph-repeat', + icon: 'ti ti-repeat', action: () => { const el = props.renoteButton.value; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; @@ -624,7 +611,7 @@ export function getRenoteMenu(props: { }, }, (props.mock) ? undefined : { text: i18n.ts.quote, - icon: 'ph ph-quotes', + icon: 'ti ti-quote', action: () => { os.post({ renote: appearNote, @@ -634,7 +621,7 @@ export function getRenoteMenu(props: { normalExternalChannelRenoteItems.push({ type: 'parent', - icon: 'ph ph-repeat', + icon: 'ti ti-repeat', text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel, children: async () => { const channels = await favoritedChannelsCache.fetch(); @@ -649,7 +636,9 @@ export function getRenoteMenu(props: { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } if (!props.mock) { diff --git a/packages/frontend/src/scripts/get-note-versions-menu.ts b/packages/frontend/src/scripts/get-note-versions-menu.ts index 9108191d7332e610a125e4601189d592b7c7e016..345cec9018e23c94dff57ed212bd3e285fd30ef4 100644 --- a/packages/frontend/src/scripts/get-note-versions-menu.ts +++ b/packages/frontend/src/scripts/get-note-versions-menu.ts @@ -27,12 +27,13 @@ export async function getNoteVersionsMenu(props: { const cleanups = [] as (() => void)[]; function openVersion(info): void { - os.popup(defineAsyncComponent(() => import('@/components/SkOldNoteWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/SkOldNoteWindow.vue')), { note: appearNote, oldText: info.text, updatedAt: info.oldDate ? info.oldDate : info.updatedAt, }, { - }, 'closed'); + closed: () => dispose(), + }); } const menu: MenuItem[] = []; diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 3329a34ff3d6cd483f9332496b2eccd3f30edb94..33f16a68aa618d4e8c6c8ae9b3b03b0324d1b20c 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -7,15 +7,17 @@ import { toUnicode } from 'punycode'; 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 { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { host, url } from '@/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; +import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js'; import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; +import { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; @@ -81,15 +83,6 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - async function toggleWithReplies() { - os.apiWithDialog('following/update', { - userId: user.id, - withReplies: !user.withReplies, - }).then(() => { - user.withReplies = !user.withReplies; - }); - } - async function toggleNotify() { os.apiWithDialog('following/update', { userId: user.id, @@ -100,9 +93,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter } function reportAbuse() { - os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: user, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } async function getConfirmed(text: string): Promise<boolean> { @@ -152,54 +147,61 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - let menu = [{ - icon: 'ph-at ph-bold ph-lg', + let menu: MenuItem[] = [{ + icon: 'ti ti-at', text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host ?? host}`); }, - }, ...(iAmModerator ? [{ - icon: 'ph-warning-circle ph-bold ph-lg', + }, ...( 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}`); }, }] : []), { - icon: 'ph-rss ph-bold ph-lg', + icon: 'ti ti-rss', text: i18n.ts.copyRSS, action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, }, ...(user.host != null && user.url != null ? [{ - icon: 'ph-share ph-bold ph-lg', + icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { if (user.url == null) return; window.open(user.url, '_blank', 'noopener'); }, }] : []), { - icon: 'ph-share-network ph-bold ph-lg', + icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }, { - icon: 'ph-envelope ph-bold ph-lg', + }, ...($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: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.editMemo, action: () => { editMemo(); }, }, { type: 'parent', - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', text: i18n.ts.addToList, children: async () => { const lists = await userListsCache.fetch(); @@ -232,7 +234,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }, }, { type: 'parent', - icon: 'ph-flying-saucer ph-bold ph-lg', + icon: 'ti ti-antenna', text: i18n.ts.addToAntenna, children: async () => { const antennas = await antennasCache.fetch(); @@ -257,13 +259,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }, })); }, - }] as any; + }] : [])] as any; if ($i && meId !== user.id) { if (iAmModerator) { menu = menu.concat([{ type: 'parent', - icon: 'ph-seal-check ph-bold ph-lg', + icon: 'ti ti-badges', text: i18n.ts.roles, children: async () => { const roles = await rolesCache.fetch(); @@ -304,41 +306,51 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter // フォãƒãƒ¼ã—ãŸã¨ã—ã¦ã‚‚ user.isFollowing ã¯ãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ æ›´æ–°ã•ã‚Œãªã„ã®ã§ä¸ä¾¿ãªãŸã‚ //if (user.isFollowing) { + const withRepliesRef = ref(user.withReplies); menu = menu.concat([{ - icon: user.withReplies ? 'ph-envelope-open ph-bold ph-lg' : 'ph-envelope ph-bold ph-lg-off', - text: user.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline, - action: toggleWithReplies, + type: 'switch', + icon: 'ti ti-messages', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withRepliesRef, }, { - icon: user.notify === 'none' ? 'ph-bell ph-bold ph-lg' : 'ph-bell ph-bold ph-lg-off', + 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, + withReplies, + }).then(() => { + user.withReplies = withReplies; + }); + }); //} menu = menu.concat([{ type: 'divider' }, { - icon: user.isMuted ? 'ph-eye ph-bold ph-lg' : 'ph-eye-slash ph-bold ph-lg', + icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, }, { - icon: user.isRenoteMuted ? 'ph-repeat ph-bold ph-lg' : 'ph-repeat ph-bold ph-lg-off', + icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off', text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute, action: toggleRenoteMute, }, { - icon: 'ph-prohibit ph-bold ph-lg', + icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, }]); if (user.isFollowed) { menu = menu.concat([{ - icon: 'ph-link ph-bold ph-lg-off', + icon: 'ti ti-link-off', text: i18n.ts.breakFollow, action: invalidateFollow, }]); } menu = menu.concat([{ type: 'divider' }, { - icon: 'ph-warning-circle ph-bold ph-lg', + icon: 'ti ti-exclamation-circle', text: i18n.ts.reportAbuse, action: reportAbuse, }]); @@ -346,7 +358,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if (user.host !== null) { menu = menu.concat([{ type: 'divider' }, { - icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', + icon: 'ti ti-refresh', text: i18n.ts.updateRemoteUser, action: userInfoUpdate, }]); @@ -354,7 +366,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if (defaultStore.state.devMode) { menu = menu.concat([{ type: 'divider' }, { - icon: 'ph-identification-card ph-bold ph-lg', + icon: 'ti ti-id', text: i18n.ts.copyUserId, action: () => { copyToClipboard(user.id); @@ -364,7 +376,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if ($i && meId === user.id) { menu = menu.concat([{ type: 'divider' }, { - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); @@ -374,7 +386,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if (userActions.length > 0) { menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({ - icon: 'ph-plug ph-bold ph-lg', + icon: 'ti ti-plug', text: action.title, action: () => { action.handler(user); diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts index 0600bff893b3c42dedbba55070eeb8e0e4a93e85..04fb2356945699e66fb7171c3a279b8119c2f1ab 100644 --- a/packages/frontend/src/scripts/hotkey.ts +++ b/packages/frontend/src/scripts/hotkey.ts @@ -2,94 +2,171 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js"; -import keyCode from './keycode.js'; +//#region types +export type Keymap = Record<string, CallbackFunction | CallbackObject>; -type Callback = (ev: KeyboardEvent) => void; +type CallbackFunction = (ev: KeyboardEvent) => unknown; -type Keymap = Record<string, Callback>; +type CallbackObject = { + callback: CallbackFunction; + allowRepeat?: boolean; +}; type Pattern = { which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; + ctrl: boolean; + alt: boolean; + shift: boolean; }; type Action = { patterns: Pattern[]; - callback: Callback; - allowRepeat: boolean; + callback: CallbackFunction; + options: Required<Omit<CallbackObject, 'callback'>>; +}; +//#endregion + +//#region consts +const KEY_ALIASES = { + 'esc': 'Escape', + 'enter': 'Enter', + 'space': ' ', + 'up': 'ArrowUp', + 'down': 'ArrowDown', + 'left': 'ArrowLeft', + 'right': 'ArrowRight', + 'plus': ['+', ';'], }; -const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { - const result = { - patterns: [], - callback, - allowRepeat: true, - } as Action; +const MODIFIER_KEYS = ['ctrl', 'alt', 'shift']; - if (patterns.match(/^\(.*\)$/) !== null) { - result.allowRepeat = false; - patterns = patterns.slice(1, -1); - } +const IGNORE_ELEMENTS = ['input', 'textarea']; +//#endregion + +//#region store +let latestHotkey: Pattern & { callback: CallbackFunction } | null = null; +//#endregion - result.patterns = patterns.split('|').map(part => { - const pattern = { - which: [], - ctrl: false, - alt: false, - shift: false, - } as Pattern; - - const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); - for (const key of keys) { - switch (key) { - case 'ctrl': pattern.ctrl = true; break; - case 'alt': pattern.alt = true; break; - case 'shift': pattern.shift = true; break; - default: pattern.which = keyCode(key).map(k => k.toLowerCase()); +//#region impl +export const makeHotkey = (keymap: Keymap) => { + const actions = parseKeymap(keymap); + return (ev: KeyboardEvent) => { + if ('pswp' in window && window.pswp != null) return; + if (document.activeElement != null) { + if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return; + if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return; + } + for (const action of actions) { + if (matchPatterns(ev, action)) { + ev.preventDefault(); + ev.stopPropagation(); + action.callback(ev); + storePattern(ev, action.callback); } } + }; +}; - return pattern; +const parseKeymap = (keymap: Keymap) => { + return Object.entries(keymap).map(([rawPatterns, rawCallback]) => { + const patterns = parsePatterns(rawPatterns); + const callback = parseCallback(rawCallback); + const options = parseOptions(rawCallback); + return { patterns, callback, options } as const satisfies Action; }); +}; - return result; -}); - -const ignoreElements = ['input', 'textarea']; +const parsePatterns = (rawPatterns: keyof Keymap) => { + return rawPatterns.split('|').map(part => { + const keys = part.split('+').map(trimLower); + const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x))); + const ctrl = keys.includes('ctrl'); + const alt = keys.includes('alt'); + const shift = keys.includes('shift'); + return { which, ctrl, alt, shift } as const satisfies Pattern; + }); +}; -function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { - const key = ev.key.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === ev.ctrlKey && - pattern.shift === ev.shiftKey && - pattern.alt === ev.altKey && - !ev.metaKey, - ); -} +const parseCallback = (rawCallback: Keymap[keyof Keymap]) => { + if (typeof rawCallback === 'object') { + return rawCallback.callback; + } + return rawCallback; +}; -export const makeHotkey = (keymap: Keymap) => { - const actions = parseKeymap(keymap); +const parseOptions = (rawCallback: Keymap[keyof Keymap]) => { + const defaultOptions = { + allowRepeat: false, + } as const satisfies Action['options']; + if (typeof rawCallback === 'object') { + const { callback, ...rawOptions } = rawCallback; + const options = { ...defaultOptions, ...rawOptions }; + return { ...options } as const satisfies Action['options']; + } + return { ...defaultOptions } as const satisfies Action['options']; +}; - return (ev: KeyboardEvent) => { - if (document.activeElement) { - if (ignoreElements.some(el => document.activeElement!.matches(el))) return; - if (document.activeElement.attributes['contenteditable']) return; +const matchPatterns = (ev: KeyboardEvent, action: Action) => { + const { patterns, options, callback } = action; + if (ev.repeat && !options.allowRepeat) return false; + const key = ev.key.toLowerCase(); + return patterns.some(({ which, ctrl, shift, alt }) => { + if ( + options.allowRepeat === false && + latestHotkey != null && + latestHotkey.which.includes(key) && + latestHotkey.ctrl === ctrl && + latestHotkey.alt === alt && + latestHotkey.shift === shift && + latestHotkey.callback === callback + ) { + return false; } + if (!which.includes(key)) return false; + if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false; + if (alt !== ev.altKey) return false; + if (shift !== ev.shiftKey) return false; + return true; + }); +}; - for (const action of actions) { - const matched = match(ev, action.patterns); +let lastHotKeyStoreTimer: number | null = null; - if (matched) { - if (!action.allowRepeat && ev.repeat) return; +const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => { + if (lastHotKeyStoreTimer != null) { + clearTimeout(lastHotKeyStoreTimer); + } - ev.preventDefault(); - ev.stopPropagation(); - action.callback(ev); - break; - } - } + latestHotkey = { + which: [ev.key.toLowerCase()], + ctrl: ev.ctrlKey || ev.metaKey, + alt: ev.altKey, + shift: ev.shiftKey, + callback, }; + + lastHotKeyStoreTimer = window.setTimeout(() => { + latestHotkey = null; + }, 500); }; + +const parseKeyCode = (input?: string | null) => { + if (input == null) return []; + const raw = getValueByKey(KEY_ALIASES, input); + if (raw == null) return [input]; + if (typeof raw === 'string') return [trimLower(raw)]; + return raw.map(trimLower); +}; + +const getValueByKey = < + T extends Record<keyof any, unknown>, + K extends keyof T | keyof any, + R extends K extends keyof T ? T[K] : T[keyof T] | undefined, +>(obj: T, key: K) => { + return obj[key] as R; +}; + +const trimLower = (str: string) => str.trim().toLowerCase(); +//#endregion diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts index 15b0cedc79feddb1e646ea6e30ac6caa6e274726..72ff8bd5ffa5596ceb8ffaabbada053f342d92c1 100644 --- a/packages/frontend/src/scripts/install-plugin.ts +++ b/packages/frontend/src/scripts/install-plugin.ts @@ -107,7 +107,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { } const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => { - os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { title: i18n.ts.tokenRequested, information: i18n.ts.pluginTokenRequestedDescription, initialName: realMeta.name, @@ -122,7 +122,8 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { }); res(token); }, - }, 'closed'); + closed: () => dispose(), + }); }); savePlugin({ diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index 406404c4622c940bdd0670df2962f6f872659bbb..e28e5725bc915539837a0590ae36e3988abfb05b 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js'; import { $i } from '@/account.js'; export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && $i.id === user.id) return true; + if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; if (user.followingVisibility === 'private') return false; if (user.followingVisibility === 'followers' && !user.isFollowing) return false; @@ -15,7 +15,7 @@ export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): bo return true; } export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && $i.id === user.id) return true; + if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; if (user.followersVisibility === 'private') return false; if (user.followersVisibility === 'followers' && !user.isFollowing) return false; diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts deleted file mode 100644 index 7ffceafadae19bbca1b702d9aae6009a203eae11..0000000000000000000000000000000000000000 --- a/packages/frontend/src/scripts/keycode.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export default (input: string): string[] => { - if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) { - const codes = aliases[input]; - return Array.isArray(codes) ? codes : [codes]; - } else { - return [input]; - } -}; - -export const aliases = { - 'esc': 'Escape', - 'enter': ['Enter', 'NumpadEnter'], - 'space': [' ', 'Spacebar'], - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'plus': ['NumpadAdd', 'Semicolon'], -}; diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index db3a96b15cffa11c3d770d1d1a407c08ef41d512..e20b23f166fcfbf5df31693cde6c229a64aba463 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -16,7 +16,7 @@ export async function lookup(router?: Router) { title: i18n.ts.lookup, }); const query = temp ? temp.trim() : ''; - if (canceled) return; + if (canceled || query.length <= 1) return; if (query.startsWith('@') && !query.includes(' ')) { _router.push(`/${query}`); diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 4e39a0fa06ec8b11fc10634eb32fd4e1a02c3801..9794a300da02cf0d43d837243dfbb6989c87d167 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -6,7 +6,7 @@ import { deepClone } from './clone.js'; import type { Cloneable } from './clone.js'; -type DeepPartial<T> = { +export type DeepPartial<T> = { [P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P]; }; diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 36de146c27c355eba787fb9357dd21c9663f4fae..63acf9d3debcec849f0bbd0280b0746891d44e4a 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -7,29 +7,24 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { MFM_TAGS } from '@/const.js'; +import type { MenuItem } from '@/types/menu.js'; /** * MFMã®è£…飾ã®ãƒªã‚¹ãƒˆã‚’表示ã™ã‚‹ */ -export function mfmFunctionPicker(src: any, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) { - return new Promise((res, rej) => { - os.popupMenu([{ - text: i18n.ts.addMfmFunction, - type: 'label', - }, ...getFunctionList(textArea, textRef)], src); - }); +export function mfmFunctionPicker(src: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) { + os.popupMenu([{ + text: i18n.ts.addMfmFunction, + type: 'label', + }, ...getFunctionList(textArea, textRef)], src); } -function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) : object[] { - const ret: object[] = []; - MFM_TAGS.forEach(tag => { - ret.push({ - text: tag, - icon: 'ph-brackets-curly ph-bold ph-lg', - action: () => add(textArea, textRef, tag), - }); - }); - return ret; +function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>): MenuItem[] { + return MFM_TAGS.map(tag => ({ + text: tag, + icon: 'ph-brackets-curly ph-bold ph-lg', + action: () => add(textArea, textRef, tag), + })); } function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, type: string) { diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts new file mode 100644 index 0000000000000000000000000000000000000000..53b2a9e44151857adfcd262a7e8065ebbf3b2303 --- /dev/null +++ b/packages/frontend/src/scripts/player-url-transform.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { hostname } from '@/config.js'; + +export function transformPlayerUrl(url: string): string { + const urlObj = new URL(url); + if (!['https:', 'http:'].includes(urlObj.protocol)) throw new Error('Invalid protocol'); + + const urlParams = new URLSearchParams(urlObj.search); + + if (urlObj.hostname === 'player.twitch.tv') { + // Twitchã¯CSPã®åˆ¶ç´„ã‚ã‚Š + // https://dev.twitch.tv/docs/embed/video-and-clips/ + urlParams.set('parent', hostname); + urlParams.set('allowfullscreen', ''); + urlParams.set('autoplay', 'true'); + } else { + urlParams.set('autoplay', '1'); + urlParams.set('auto_play', '1'); + } + urlObj.search = urlParams.toString(); + + return urlObj.toString(); +} diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts index 9e512727912bad0eeea90aba3a0b5febe5315172..18f05bc7f42855bbbd87a6afab3b4f71870814a1 100644 --- a/packages/frontend/src/scripts/please-login.ts +++ b/packages/frontend/src/scripts/please-login.ts @@ -8,19 +8,57 @@ import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { popup } from '@/os.js'; -export function pleaseLogin(path?: string) { +export type OpenOnRemoteOptions = { + /** + * 外部ã®Misskey Webã§ç‰¹å®šã®ãƒ‘スを開ã + */ + type: 'web'; + + /** + * 内部パス(例: `/settings`) + */ + path: string; +} | { + /** + * 外部ã®Misskey Webã§ç…§ä¼šã™ã‚‹ + */ + type: 'lookup'; + + /** + * 照会ã—ãŸã„エンティティã®URL + * + * (例: `https://misskey.example.com/notes/abcdexxxxyz`) + */ + url: string; +} | { + /** + * 外部ã®Misskeyã§ãƒŽãƒ¼ãƒˆã™ã‚‹ + */ + type: 'share'; + + /** + * `/share` ページã«æ¸¡ã™ã‚¯ã‚¨ãƒªã‚¹ãƒˆãƒªãƒ³ã‚° + * + * @see https://go.misskey-hub.net/spec/share/ + */ + params: Record<string, string>; +}; + +export function pleaseLogin(path?: string, openOnRemote?: OpenOnRemoteOptions) { if ($i) return; - popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { autoSet: true, - message: i18n.ts.signinRequired, + message: openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired, + openOnRemote, }, { cancelled: () => { if (path) { window.location.href = path; } }, - }, 'closed'); + closed: () => dispose(), + }); throw new Error('signin required'); } diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts index 8edb6fca05391389ebb2a45dc0bbf8e3fdc640a9..f0274034b5b3d027824c9041236386d240b577f3 100644 --- a/packages/frontend/src/scripts/scroll.ts +++ b/packages/frontend/src/scripts/scroll.ts @@ -23,6 +23,14 @@ export function getStickyTop(el: HTMLElement, container: HTMLElement | null = nu return getStickyTop(el.parentElement, container, newTop); } +export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) { + if (!el.parentElement) return bottom; + const data = el.dataset.stickyContainerFooterHeight; + const newBottom = data ? Number(data) + bottom : bottom; + if (el === container) return newBottom; + return getStickyBottom(el.parentElement, container, newBottom); +} + export function getScrollPosition(el: HTMLElement | null): number { const container = getScrollContainer(el); return container == null ? window.scrollY : container.scrollTop; diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index fd7cfc697b9910cc4cbf3225d159340eadbc4c74..9aa38178b20e9838884c713700bb100554f98f01 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -93,15 +93,15 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss ref: keepOriginal, }, { text: i18n.ts.upload, - icon: 'ph-upload ph-bold ph-lg', + icon: 'ti ti-upload', action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)), }, { text: i18n.ts.fromDrive, - icon: 'ph-cloud ph-bold ph-lg', + icon: 'ti ti-cloud', action: () => chooseFileFromDrive(multiple).then(files => res(files)), }, { text: i18n.ts.fromUrl, - icon: 'ph-link ph-bold ph-lg', + icon: 'ti ti-link', action: () => chooseFileFromUrl().then(file => res([file])), }], src); }); diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index fcd59510df1836c08e53245bcce0ee60c1edf8ee..05f82fce7d273335969af6de90bf6de4654b81f3 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -74,8 +74,6 @@ export const soundsTypes = [ export const operationTypes = [ 'noteMy', 'note', - 'antenna', - 'channel', 'notification', 'reaction', ] as const; @@ -126,10 +124,33 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) */ export function playMisskeySfx(operationType: OperationType) { const sound = defaultStore.state[`sound_${operationType}`]; - if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return; + playMisskeySfxFile(sound).then((succeed) => { + if (!succeed && sound.type === '_driveFile_') { + // ドライブファイルãŒå˜åœ¨ã—ãªã„å ´åˆã¯ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã®ã‚µã‚¦ãƒ³ãƒ‰ã‚’å†ç”Ÿã™ã‚‹ + const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>; + if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`); + playMisskeySfxFileInternal({ + type: soundName, + volume: sound.volume, + }); + } + }); +} + +/** + * サウンドè¨å®šå½¢å¼ã§æŒ‡å®šã•ã‚ŒãŸéŸ³å£°ã‚’å†ç”Ÿã™ã‚‹ + * @param soundStore サウンドè¨å®š + */ +export async function playMisskeySfxFile(soundStore: SoundStore): Promise<boolean> { + // 連続ã—ã¦å†ç”Ÿã—ãªã„ + if (!canPlay) return false; + // ユーザーアクティベーションãŒå¿…è¦ãªå ´åˆã¯ãã‚ŒãŒãªã„å ´åˆã¯å†ç”Ÿã—ãªã„ + if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false; + // サウンドãŒãªã„å ´åˆã¯å†ç”Ÿã—ãªã„ + if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false; canPlay = false; - playMisskeySfxFile(sound).finally(() => { + return await playMisskeySfxFileInternal(soundStore).finally(() => { // ã”ãçŸæ™‚é–“ã«éŸ³ãŒé‡è¤‡ã—ãªã„よã†ã« setTimeout(() => { canPlay = true; @@ -137,23 +158,22 @@ export function playMisskeySfx(operationType: OperationType) { }); } -/** - * サウンドè¨å®šå½¢å¼ã§æŒ‡å®šã•ã‚ŒãŸéŸ³å£°ã‚’å†ç”Ÿã™ã‚‹ - * @param soundStore サウンドè¨å®š - */ -export async function playMisskeySfxFile(soundStore: SoundStore) { +async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boolean> { if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { - return; + return false; } const masterVolume = defaultStore.state.sound_masterVolume; if (isMute() || masterVolume === 0 || soundStore.volume === 0) { - return; + return true; // ミュート時ã¯æˆåŠŸã¨ã—ã¦æ‰±ã† } const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; - const buffer = await loadAudio(url); - if (!buffer) return; + const buffer = await loadAudio(url).catch(() => { + return undefined; + }); + if (!buffer) return false; const volume = soundStore.volume * masterVolume; createSourceNode(buffer, { volume }).soundSource.start(); + return true; } export async function playUrl(url: string, opts: { diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts index e3072b3b7d2b62df354623ed78e7ec075f8d9225..5a8265af9e1050d0c7e6ac260828d26f92b2f499 100644 --- a/packages/frontend/src/scripts/url.ts +++ b/packages/frontend/src/scripts/url.ts @@ -21,3 +21,8 @@ export function query(obj: Record<string, any>): string { export function appendQuery(url: string, query: string): string { return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; } + +export function extractDomain(url: string) { + const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im); + return match ? match[1] : null; +} diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts index bed221a6225504e84e6e113e094f07905969c487..bba64fc6ee0982f9ce8669fc1bbed4cb404b4cdf 100644 --- a/packages/frontend/src/scripts/use-chart-tooltip.ts +++ b/packages/frontend/src/scripts/use-chart-tooltip.ts @@ -17,20 +17,16 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio borderColor: string; text: string; }[] | null>(null); - let disposeTooltipComponent; - - os.popup(MkChartTooltip, { + const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, { showing: tooltipShowing, x: tooltipX, y: tooltipY, title: tooltipTitle, series: tooltipSeries, - }, {}).then(({ dispose }) => { - disposeTooltipComponent = dispose; - }); + }, {}); onUnmounted(() => { - if (disposeTooltipComponent) disposeTooltipComponent(); + disposeTooltipComponent(); }); onDeactivated(() => { diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 13f544e58858c196af8f0360430323825494074a..dda320dbacc7188e6fbc8481acbb7c76c94fe2d3 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -520,6 +520,14 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + confirmWhenRevealingSensitiveMedia: { + where: 'device', + default: false, + }, + contextMenu: { + where: 'device', + default: 'app' as 'app' | 'appWithShift' | 'native', + }, sound_masterVolume: { where: 'device', @@ -545,14 +553,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore, }, - sound_antenna: { - where: 'device', - default: { type: 'syuilo/triple', volume: 1 } as SoundStore, - }, - sound_channel: { - where: 'device', - default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore, - }, sound_reaction: { where: 'device', default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 37d1a3c557b37946391ed35e5563ab5bebff2a69..62ba7a08d55e45bce8e6f4d5b54cbd3f2875b8c4 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -143,6 +143,10 @@ a { -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; + &:focus-visible { + outline-offset: 2px; + } + &:hover { text-decoration: underline; } @@ -173,12 +177,21 @@ rt { white-space: initial; } +:focus-visible { + outline: var(--focus) solid 2px; + outline-offset: -2px; + + &:hover { + text-decoration: none; + } +} + .ph-bold { width: 1.28em; vertical-align: -12%; line-height: 1em; - &:before { + &::before { font-size: 128%; } } @@ -264,10 +277,6 @@ rt { text-decoration: none; } - &:focus-visible { - outline: none; - } - &:disabled { opacity: 0.5; cursor: default; @@ -304,13 +313,17 @@ rt { ._help { color: var(--accent); - cursor: help + cursor: help; } ._textButton { @extend ._button; color: var(--accent); + &:focus-visible { + outline-offset: 2px; + } + &:not(:disabled):hover { text-decoration: underline; } diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts new file mode 100644 index 0000000000000000000000000000000000000000..5080ef4b9654ff101d8663cf465d01959373fb65 --- /dev/null +++ b/packages/frontend/src/timelines.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; + +export const basicTimelineTypes = [ + 'home', + 'local', + 'social', + 'bubble', + 'global', +] as const; + +export type BasicTimelineType = typeof basicTimelineTypes[number]; + +export function isBasicTimeline(timeline: string): timeline is BasicTimelineType { + return basicTimelineTypes.includes(timeline as BasicTimelineType); +} + +export function basicTimelineIconClass(timeline: BasicTimelineType): string { + switch (timeline) { + case 'home': + return 'ti ti-home'; + case 'local': + return 'ti ti-planet'; + case 'social': + return 'ti ti-universe'; + case 'bubble': + return 'ph-drop ph-bold ph-lg'; + case 'global': + return 'ti ti-whirl'; + } +} + +export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined | null): boolean { + switch (timeline) { + case 'home': + return $i != null; + case 'local': + return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); + case 'social': + return $i != null && $i.policies.ltlAvailable; + case 'bubble': + return ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable); + case 'global': + return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); + default: + return false; + } +} + +export function availableBasicTimelines(): BasicTimelineType[] { + return basicTimelineTypes.filter(isAvailableBasicTimeline); +} + +export function hasWithReplies(timeline: BasicTimelineType | undefined | null): boolean { + return timeline === 'local' || timeline === 'social'; +} diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue index 37d89f682fe44c3796791fcda6f33ba3edc285ab..374bc20b545024b8658d0dae8be088df8c092939 100644 --- a/packages/frontend/src/ui/_common_/announcements.vue +++ b/packages/frontend/src/ui/_common_/announcements.vue @@ -12,10 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only :to="`/announcements/${announcement.id}`" > <span :class="$style.icon"> - <i v-if="announcement.icon === 'info'" class="ph-info ph-bold ph-lg"></i> - <i v-else-if="announcement.icon === 'warning'" class="ph-warning ph-bold ph-lg" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ph-x-circle ph-bold ph-lg" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ph-check ph-bold ph-lg" style="color: var(--success);"></i> + <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> </span> <span :class="$style.title">{{ announcement.title }}</span> <span :class="$style.body">{{ announcement.text }}</span> diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index f1c23860b6bf13b80d4811e1506a92d12125dbe0..17079b3ddc705e9aa08127e1b8e9a9155f6d730c 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -16,17 +16,17 @@ function toolsMenuItems(): MenuItem[] { type: 'link', to: '/scratchpad', text: i18n.ts.scratchpad, - icon: 'ph-terminal-window ph-bold ph-lg-2', + icon: 'ti ti-terminal-2', }, { type: 'link', to: '/api-console', text: 'API Console', - icon: 'ph-terminal-window ph-bold ph-lg-2', + icon: 'ti ti-terminal-2', }, { type: 'link', to: '/clicker', text: 'ðŸªðŸ‘ˆ', - icon: 'ph-cookie ph-bold ph-lg', + icon: 'ti ti-cookie', }, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? { type: 'link', to: '/custom-emojis-manager', @@ -36,7 +36,7 @@ function toolsMenuItems(): MenuItem[] { type: 'link', to: '/avatar-decorations', text: i18n.ts.manageAvatarDecorations, - icon: 'ph-sparkle ph-bold ph-lg', + icon: 'ti ti-sparkles', } : undefined]; } @@ -47,7 +47,7 @@ export function openInstanceMenu(ev: MouseEvent) { }, { type: 'link', text: i18n.ts.instanceInfo, - icon: 'ph-info ph-bold ph-lg', + icon: 'ti ti-info-circle', to: '/about', }, { type: 'link', @@ -57,73 +57,75 @@ export function openInstanceMenu(ev: MouseEvent) { }, { type: 'link', text: i18n.ts.federation, - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', + icon: 'ti ti-whirl', to: '/about#federation', }, { type: 'link', text: i18n.ts.charts, - icon: 'ph-chart-line ph-bold ph-lg', + icon: 'ti ti-chart-line', to: '/about#charts', }, { type: 'divider' }, { type: 'link', text: i18n.ts.ads, - icon: 'ph-flag ph-bold ph-lg', + icon: 'ti ti-ad', to: '/ads', }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? { type: 'link', to: '/invite', text: i18n.ts.invite, - icon: 'ph-user-plus ph-bold ph-lg', + icon: 'ti ti-user-plus', } : undefined, { type: 'parent', text: i18n.ts.tools, - icon: 'ph-toolbox ph-bold ph-lg', + icon: 'ti ti-tool', children: toolsMenuItems(), }, { type: 'divider' }, { type: 'link', text: i18n.ts.inquiry, - icon: 'ph-question ph-bold ph-lg', + icon: 'ti ti-help-circle', to: '/contact', }, (instance.impressumUrl) ? { + type: 'a', text: i18n.ts.impressum, - icon: 'ph-newspaper-clipping ph-bold ph-lg', - action: () => { - window.open(instance.impressumUrl, '_blank', 'noopener'); - }, + icon: 'ti ti-file-invoice', + href: instance.impressumUrl, + target: '_blank', } : undefined, (instance.tosUrl) ? { + type: 'a', text: i18n.ts.termsOfService, - icon: 'ph-notebook ph-bold ph-lg', - action: () => { - window.open(instance.tosUrl, '_blank', 'noopener'); - }, + icon: 'ti ti-notebook', + href: instance.tosUrl, + target: '_blank', } : undefined, (instance.privacyPolicyUrl) ? { + type: 'a', text: i18n.ts.privacyPolicy, - icon: 'ph-shield ph-bold ph-lg', - action: () => { - window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); - }, + 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', - action: () => { - window.open(instance.donationUrl, '_blank', 'noopener'); - }, + href: instance.donationUrl, + target: '_blank', } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) ? undefined : { type: 'divider' }, { + type: 'a', text: i18n.ts.document, - icon: 'ph-libghtbulb ph-bold ph-lg', - action: () => { - window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener'); - }, + icon: 'ti ti-bulb', + href: 'https://misskey-hub.net/docs/for-users/', + target: '_blank', }, ($i) ? { text: i18n.ts._initialTutorial.launchTutorial, - icon: 'ph-presentation ph-bold ph-lg', + icon: 'ti ti-presentation', action: () => { - os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed'); + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { + closed: () => dispose(), + }); }, } : undefined, { type: 'link', text: i18n.ts.aboutMisskey, - icon: 'sk-icons sk-shark ph-bold', + icon: 'sk-icons sk-shark sk-icons-lg', to: '/about-sharkey', }], 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 65b27a0cc266bf3f23dff2c1b5bac4c58e89d3db..442b6479dd29c6af2a50789cab1023516a29b8ac 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -232,7 +232,7 @@ if ($i) { right: 15px; pointer-events: none; - &:before { + &::before { content: ""; display: block; width: 18px; diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index 85340fa2b781b92945450fa41994151d3f10267f..a3f9d6cf2cc5a6f58a6206f43024aeba5c461c96 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.middle"> <MkA :class="$style.item" :activeClass="$style.active" to="/" exact> - <i :class="$style.itemIcon" class="ph-house ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> + <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" :class="$style.divider"></div> @@ -27,19 +27,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div :class="$style.divider"></div> <MkA v-if="$i.isAdmin || $i.isModerator" :class="$style.item" :activeClass="$style.active" to="/admin"> - <i :class="$style.itemIcon" class="ph-gauge ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> </MkA> <button :class="$style.item" class="_button" @click="more"> - <i :class="$style.itemIcon" class="ph-dots-nine ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> + <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> </button> <MkA :class="$style.item" :activeClass="$style.active" to="/settings"> - <i :class="$style.itemIcon" class="ph-gear ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> + <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> </MkA> </div> <div :class="$style.bottom"> <button class="_button" :class="$style.post" data-cy-open-post-form @click="os.post"> - <i :class="$style.postIcon" class="ph-pencil-simple ph-bold ph-lg ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span> + <i :class="$style.postIcon" class="ti ti-pencil ti-fw"></i><span style="position: relative;">{{ i18n.ts.note }}</span> </button> <button class="_button" :class="$style.account" @click="openAccountMenu"> <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" class="_nowrap" :user="$i"/> @@ -74,8 +74,9 @@ function openAccountMenu(ev: MouseEvent) { } function more() { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, { - }, 'closed'); + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, { + closed: () => dispose(), + }); } </script> @@ -138,7 +139,7 @@ function more() { font-weight: bold; text-align: left; - &:before { + &::before { content: ""; display: block; width: calc(100% - 38px); @@ -154,7 +155,7 @@ function more() { } &:hover, &.active { - &:before { + &::before { background: var(--accentLighten); } } @@ -225,7 +226,7 @@ function more() { } &:hover, &.active { - &:before { + &::before { content: ""; display: block; width: calc(100% - 24px); diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index 65763bcfa8f4687ae968722f3227a8276eb0d293..1f1fd37def98a6019635b38d2e7b87b232936f1f 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.middle"> <MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact> - <i :class="$style.itemIcon" class="ph-house ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> + <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" :class="$style.divider"></div> @@ -37,19 +37,19 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div :class="$style.divider"></div> <MkA v-if="$i.isAdmin || $i.isModerator" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin"> - <i :class="$style.itemIcon" class="ph-gauge ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span> </MkA> <button class="_button" :class="$style.item" @click="more"> - <i :class="$style.itemIcon" class="ph-dots-nine ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> + <i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span> <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span> </button> <MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings"> - <i :class="$style.itemIcon" class="ph-gear ph-bold ph-lg ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> + <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span> </MkA> </div> <div :class="$style.bottom"> <button v-tooltip.noDelay.right="i18n.ts.note" class="_button" :class="[$style.post]" data-cy-open-post-form @click="os.post"> - <i class="ph-pencil-simple ph-bold ph-lg ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> + <i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span> </button> <button v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu"> <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/> @@ -99,10 +99,11 @@ function openAccountMenu(ev: MouseEvent) { } function more(ev: MouseEvent) { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { src: ev.currentTarget ?? ev.target, }, { - }, 'closed'); + closed: () => dispose(), + }); } </script> @@ -165,6 +166,15 @@ function more(ev: MouseEvent) { display: block; text-align: center; width: 100%; + + &:focus-visible { + outline: none; + + > .instanceIcon { + outline: 2px solid var(--focus); + outline-offset: 2px; + } + } } .instanceIcon { @@ -191,7 +201,7 @@ function more(ev: MouseEvent) { font-weight: bold; text-align: left; - &:before { + &::before { content: ""; display: block; width: calc(100% - 38px); @@ -206,8 +216,17 @@ function more(ev: MouseEvent) { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); } + &:focus-visible { + outline: none; + + &::before { + outline: 2px solid var(--fgOnAccent); + outline-offset: -4px; + } + } + &:hover, &.active { - &:before { + &::before { background: var(--accentLighten); } } @@ -233,6 +252,14 @@ function more(ev: MouseEvent) { text-align: left; box-sizing: border-box; overflow: clip; + + &:focus-visible { + outline: none; + + > .avatar { + box-shadow: 0 0 0 4px var(--focus); + } + } } .avatar { @@ -281,10 +308,19 @@ function more(ev: MouseEvent) { color: var(--navActive); } - &:hover, &.active { + &:focus-visible { + outline: none; + + &::before { + outline: 2px solid var(--focus); + outline-offset: -2px; + } + } + + &:hover, &.active, &:focus { color: var(--accent); - &:before { + &::before { content: ""; display: block; width: calc(100% - 34px); @@ -351,6 +387,15 @@ function more(ev: MouseEvent) { display: block; text-align: center; width: 100%; + + &:focus-visible { + outline: none; + + > .instanceIcon { + outline: 2px solid var(--focus); + outline-offset: 2px; + } + } } .instanceIcon { @@ -375,7 +420,7 @@ function more(ev: MouseEvent) { height: 52px; text-align: center; - &:before { + &::before { content: ""; display: block; position: absolute; @@ -390,8 +435,17 @@ function more(ev: MouseEvent) { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); } + &:focus-visible { + outline: none; + + &::before { + outline: 2px solid var(--fgOnAccent); + outline-offset: -4px; + } + } + &:hover, &.active { - &:before { + &::before { background: var(--accentLighten); } } @@ -412,6 +466,14 @@ function more(ev: MouseEvent) { padding: 20px 0; width: 100%; overflow: clip; + + &:focus-visible { + outline: none; + + > .avatar { + box-shadow: 0 0 0 4px var(--focus); + } + } } .avatar { @@ -441,11 +503,20 @@ function more(ev: MouseEvent) { width: 100%; text-align: center; - &:hover, &.active { + &:focus-visible { + outline: none; + + &::before { + outline: 2px solid var(--focus); + outline-offset: -2px; + } + } + + &:hover, &.active, &:focus { text-decoration: none; color: var(--accent); - &:before { + &::before { content: ""; display: block; height: 100%; diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index 968c3969bb15ab03f53cc3a2890fb403dabb445a..ad93b7e61c556b183cab4c161dd8467a2e236c97 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected"> - <div><i class="ph-warning ph-bold ph-lg"></i> {{ i18n.ts.disconnectedFromServer }}</div> + <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div> <div :class="$style.command" class="_buttons"> <MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton> <MkButton small>{{ i18n.ts.doNothing }}</MkButton> diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue index 527670e103a947a0c364d155f60ce409aa53bbdc..c03afd6cd62aa73422174015571cef6b2a67d3ac 100644 --- a/packages/frontend/src/ui/classic.header.vue +++ b/packages/frontend/src/ui/classic.header.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/> </button> <MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact> - <i class="ph-house ph-bold ph-lg ti-fw"></i> + <i class="ti ti-home ti-fw"></i> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" class="divider"></div> @@ -22,23 +22,23 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div class="divider"></div> <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> - <i class="ph-gauge ph-bold ph-lg ti-fw"></i> + <i class="ti ti-dashboard ti-fw"></i> </MkA> <button v-click-anime class="item _button" @click="more"> - <i class="ph-dots-three ph-bold ph-lg ti-fw"></i> + <i class="ti ti-dots ti-fw"></i> <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> </button> </div> <div class="right"> <MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> - <i class="ph-gear ph-bold ph-lg ti-fw"></i> + <i class="ti ti-settings ti-fw"></i> </MkA> <button v-click-anime class="item _button account" @click="openAccountMenu"> <MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/> </button> <div class="post" @click="os.post()"> <MkButton class="button" gradate full rounded> - <i class="ph-pencil-simple ph-bold ph-lg ti-fw"></i> + <i class="ti ti-pencil ti-fw"></i> </MkButton> </div> </div> @@ -71,11 +71,12 @@ const otherNavItemIndicated = computed<boolean>(() => { }); function more(ev: MouseEvent) { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { src: ev.currentTarget ?? ev.target, anchor: { x: 'center', y: 'bottom' }, }, { - }, 'closed'); + closed: () => dispose(), + }); } function openAccountMenu(ev: MouseEvent) { diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index 25b9095574bedf585a34fde17afd31caf6c1e565..d49df8e8ac651eb44b94988d4d86e56933e7356e 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only </button> <div class="post" data-cy-open-post-form @click="os.post"> <MkButton class="button" gradate full rounded> - <i class="ph-pencil-simple ph-bold ph-lg ti-fw"></i><span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span> + <i class="ti ti-pencil ti-fw"></i><span v-if="!iconOnly" class="text">{{ i18n.ts.note }}</span> </MkButton> </div> <div class="divider"></div> <MkA v-click-anime class="item index" activeClass="active" to="/" exact> - <i class="ph-house ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> + <i class="ti ti-home ti-fw"></i><span class="text">{{ i18n.ts.timeline }}</span> </MkA> <template v-for="item in menu"> <div v-if="item === '-'" class="divider"></div> @@ -29,14 +29,14 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <div class="divider"></div> <MkA v-if="$i.isAdmin || $i.isModerator" v-click-anime class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> - <i class="ph-gauge ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> + <i class="ti ti-dashboard ti-fw"></i><span class="text">{{ i18n.ts.controlPanel }}</span> </MkA> <button v-click-anime class="item _button" @click="more"> - <i class="ph-dots-three ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> + <i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span> <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span> </button> <MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> - <i class="ph-gear ph-bold ph-lg ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> + <i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span> </MkA> <div class="divider"></div> <div class="about"> @@ -86,9 +86,11 @@ function calcViewState() { } function more(ev: MouseEvent) { - os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { src: ev.currentTarget ?? ev.target, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } function openAccountMenu(ev: MouseEvent) { diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index ea9ea56b906de3125024436b76747ebfe02dcd35..ce5a22a61d4665f235f8fe75e9dafaca1be0ca1f 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -118,13 +118,13 @@ function onContextmenu(ev: MouseEvent) { type: 'label', text: path, }, { - icon: fullView.value ? 'ph-arrows-in-simple ph-bold ph-lg' : 'ph-frame-corners ph-bold ph-lg', + icon: fullView.value ? 'ti ti-minimize' : 'ti ti-maximize', text: fullView.value ? i18n.ts.quitFullView : i18n.ts.fullView, action: () => { fullView.value = !fullView.value; }, }, { - icon: 'ph-frame-corners ph-bold ph-lg', + icon: 'ti ti-window-maximize', text: i18n.ts.openInWindow, action: () => { os.pageWindow(path); diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 68c7f0fcd23e3bf989eaa8d2ee2ff619b9336c92..44f1af5f8fdde0fa8c486f50cb9f206c3ca36b4a 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only :ref="id" :key="id" :class="$style.column" - :column="columns.find(c => c.id === id)" + :column="columns.find(c => c.id === id)!" :isStacked="ids.length > 1" @headerWheel="onWheel" /> @@ -36,29 +36,29 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div :class="$style.sideMenu"> <div :class="$style.sideMenuTop"> - <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ph-caret-down ph-bold ph-lg"></i></button> - <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ph-trash ph-bold ph-lg"></i></button> + <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button> + <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button> </div> <div :class="$style.sideMenuMiddle"> - <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ph-plus ph-bold ph-lg"></i></button> + <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button> </div> <div :class="$style.sideMenuBottom"> - <button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ph-gear ph-bold ph-lg"></i></button> + <button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings"></i></button> </div> </div> </div> </div> <div v-if="isMobile" :class="$style.nav"> - <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ph-list ph-bold ph-lg-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> - <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ph-house ph-bold ph-lg"></i></button> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> + <button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> - <i :class="$style.navButtonIcon" class="ph-bell ph-bold ph-lg"></i> + <i :class="$style.navButtonIcon" class="ti ti-bell"></i> <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"> <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> </span> </button> - <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ph-pencil-simple ph-bold ph-lg"></i></button> + <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> </div> <Transition @@ -95,7 +95,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; -import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; +import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; +import type { ColumnType } from './deck/deck-store.js'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; @@ -152,10 +153,12 @@ window.addEventListener('resize', () => { const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet'; const drawerMenuShowing = ref(false); +/* const route = 'TODO'; watch(route, () => { drawerMenuShowing.value = false; }); +*/ const columns = deckStore.reactiveState.columns; const layout = deckStore.reactiveState.layout; @@ -174,32 +177,20 @@ function showSettings() { const columnsEl = shallowRef<HTMLElement>(); const addColumn = async (ev) => { - const columns = [ - 'main', - 'widgets', - 'notifications', - 'tl', - 'antenna', - 'list', - 'channel', - 'mentions', - 'direct', - 'roleTimeline', - ]; - const { canceled, result: column } = await os.select({ title: i18n.ts._deck.addColumn, - items: columns.map(column => ({ + items: columnTypes.map(column => ({ value: column, text: i18n.ts._deck._columns[column], })), }); - if (canceled) return; + if (canceled || column == null) return; addColumnToStore({ type: column, id: uuid(), name: i18n.ts._deck._columns[column], width: 330, + soundSetting: { type: null, volume: 1 }, }); }; @@ -211,7 +202,7 @@ const onContextmenu = (ev) => { }; function onWheel(ev: WheelEvent) { - if (ev.deltaX === 0) { + if (ev.deltaX === 0 && columnsEl.value != null) { columnsEl.value.scrollLeft += ev.deltaY; } } @@ -236,13 +227,13 @@ function changeProfile(ev: MouseEvent) { }, }))), { type: 'divider' as const }, { text: i18n.ts._deck.newProfile, - icon: 'ph-plus ph-bold ph-lg', + icon: 'ti ti-plus', action: async () => { const { canceled, result: name } = await os.inputText({ title: i18n.ts._deck.profile, minLength: 1, }); - if (canceled) return; + if (canceled || name == null) return; deckStore.set('profile', name); unisonReload(); diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index ca7d7102a3974797fd7a09938041d25601b1d427..987bd4db557e4a7888ec38719f634bb359ac89f6 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -4,9 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ph-flying-saucer ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> <MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @note="onNote"/> @@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, ref, shallowRef, watch } from 'vue'; +import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue'; +import type { entities as MisskeyEntities } from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -22,6 +23,7 @@ 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 { antennasCache } from '@/cache.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; @@ -46,14 +48,36 @@ watch(soundSetting, v => { async function setAntenna() { const antennas = await misskeyApi('antennas/list'); - const { canceled, result: antenna } = await os.select({ + const { canceled, result: antenna } = await os.select<MisskeyEntities.Antenna | '_CREATE_'>({ title: i18n.ts.selectAntenna, - items: antennas.map(x => ({ - value: x, text: x.name, - })), + items: [ + { value: '_CREATE_', text: i18n.ts.createNew }, + (antennas.length > 0 ? { + sectionTitle: i18n.ts.createdAntennas, + items: antennas.map(x => ({ + value: x, text: x.name, + })), + } : undefined), + ], default: props.column.antennaId, }); - if (canceled) return; + if (canceled || antenna == null) return; + + if (antenna === '_CREATE_') { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAntennaEditorDialog.vue')), {}, { + created: (newAntenna: MisskeyEntities.Antenna) => { + antennasCache.delete(); + updateColumn(props.column.id, { + antennaId: newAntenna.id, + }); + }, + closed: () => { + dispose(); + }, + }); + return; + } + updateColumn(props.column.id, { antennaId: antenna.id, }); @@ -69,17 +93,17 @@ function onNote() { const menu: MenuItem[] = [ { - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.selectAntenna, action: setAntenna, }, { - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts.editAntenna, action: editAntenna, }, { - icon: 'ph-bell-ringing ph-bold ph-lg', + icon: 'ti ti-bell', text: i18n.ts._deck.newNoteNotificationSettings, action: () => soundSettingsButton(soundSetting), }, diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 10b3d5a1eca3c72f26937b66ea6e1e2b462a5b7e..518df7a6fcd530c9764ccf435b6f0a12b33708b6 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -4,14 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ph-television ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> <template v-if="column.channelId"> <div style="padding: 8px; text-align: center;"> - <MkButton primary gradate rounded inline small @click="post"><i class="ph-pencil-simple ph-bold ph-lg"></i></MkButton> + <MkButton primary gradate rounded inline small @click="post"><i class="ti ti-pencil"></i></MkButton> </div> <MkTimeline ref="timeline" :key="column.channelId + column.withRenotes + column.onlyFiles" src="channel" :channel="column.channelId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @note="onNote"/> </template> @@ -83,6 +83,7 @@ async function setChannel() { } async function post() { + if (props.column.channelId == null) return; if (!channel.value || channel.value.id !== props.column.channelId) { channel.value = await misskeyApi('channels/show', { channelId: props.column.channelId, @@ -99,7 +100,7 @@ function onNote() { } const menu: MenuItem[] = [{ - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.selectChannel, action: setChannel, }, { @@ -111,7 +112,7 @@ const menu: MenuItem[] = [{ text: i18n.ts.fileAttachedOnly, ref: onlyFiles, }, { - icon: 'ph-bell-ringing ph-bold ph-lg', + icon: 'ti ti-bell', text: i18n.ts._deck.newNoteNotificationSettings, action: () => soundSettingsButton(soundSetting), }]; diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index f9efb9d88ceabcccfe06ff5252e71d53c51eca0f..5fed70fc9055763e45531d682e39eab195fa5310 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only </svg> <div :class="$style.color"></div> <button v-if="isStacked && !isMainColumn" :class="$style.toggleActive" class="_button" @click="toggleActive"> - <template v-if="active"><i class="ph-caret-up ph-bold ph-lg"></i></template> - <template v-else><i class="ph-caret-down ph-bold ph-lg"></i></template> + <template v-if="active"><i class="ti ti-chevron-up"></i></template> + <template v-else><i class="ti ti-chevron-down"></i></template> </button> <span :class="$style.title"><slot name="header"></slot></span> <svg viewBox="0 0 16 16" version="1.1" :class="$style.grabber"> <path fill="currentColor" d="M10 13a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm0-4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm-4 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm5-9a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path> </svg> - <button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button> + <button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button> </header> <div v-if="active" ref="body" :class="$style.body"> <slot></slot> @@ -105,7 +105,7 @@ function toggleActive() { function getMenu() { let items: MenuItem[] = [{ - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts._deck.configureColumn, action: async () => { const { canceled, result } = await os.form(props.column.name, { @@ -132,46 +132,46 @@ function getMenu() { }, { type: 'parent', text: i18n.ts.move + '...', - icon: 'ph-arrows-out-cardinal ph-bold ph-lg', + icon: 'ti ti-arrows-move', children: [{ - icon: 'ph-arrow-left ph-bold ph-lg', + icon: 'ti ti-arrow-left', text: i18n.ts._deck.swapLeft, action: () => { swapLeftColumn(props.column.id); }, }, { - icon: 'ph-arrow-right ph-bold ph-lg', + icon: 'ti ti-arrow-right', text: i18n.ts._deck.swapRight, action: () => { swapRightColumn(props.column.id); }, }, props.isStacked ? { - icon: 'ph-arrow-up ph-bold ph-lg', + icon: 'ti ti-arrow-up', text: i18n.ts._deck.swapUp, action: () => { swapUpColumn(props.column.id); }, } : undefined, props.isStacked ? { - icon: 'ph-arrow-down ph-bold ph-lg', + icon: 'ti ti-arrow-down', text: i18n.ts._deck.swapDown, action: () => { swapDownColumn(props.column.id); }, } : undefined], }, { - icon: 'ph-stack ph-bold ph-lg', + icon: 'ti ti-stack-2', text: i18n.ts._deck.stackLeft, action: () => { stackLeftColumn(props.column.id); }, }, props.isStacked ? { - icon: 'ph-frame-corners ph-bold ph-lg', + icon: 'ti ti-window-maximize', text: i18n.ts._deck.popRight, action: () => { popRightColumn(props.column.id); }, } : undefined, { type: 'divider' }, { - icon: 'ph-trash ph-bold ph-lg', + icon: 'ti ti-trash', text: i18n.ts.remove, danger: true, action: () => { @@ -186,7 +186,7 @@ function getMenu() { if (props.refresher) { items = [{ - icon: 'ph-arrows-counter-clockwise ph-bold ph-lg', + icon: 'ti ti-refresh', text: i18n.ts.reload, action: () => { if (props.refresher) { @@ -271,7 +271,7 @@ function onDrop(ev) { border-radius: var(--radius); &.draghover { - &:after { + &::after { content: ""; display: block; position: absolute; @@ -285,7 +285,7 @@ function onDrop(ev) { } &.dragging { - &:after { + &::after { content: ""; display: block; position: absolute; diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 1a4f7c5e17528c15c59c9476e1bf01edd008d67e..eb587554b97ceb592a3475ce40d54eee83a2bd6d 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -6,6 +6,7 @@ import { throttle } from 'throttle-debounce'; import { markRaw } from 'vue'; import { notificationTypes } from 'misskey-js'; +import type { BasicTimelineType } from '@/timelines.js'; import { Storage } from '@/pizzax.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { deepClone } from '@/scripts/clone.js'; @@ -17,9 +18,24 @@ type ColumnWidget = { data: Record<string, any>; }; +export const columnTypes = [ + 'main', + 'widgets', + 'notifications', + 'tl', + 'antenna', + 'list', + 'channel', + 'mentions', + 'direct', + 'roleTimeline', +] as const; + +export type ColumnType = typeof columnTypes[number]; + export type Column = { id: string; - type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'channel' | 'list' | 'mentions' | 'direct'; + type: ColumnType; name: string | null; width: number; widgets?: ColumnWidget[]; @@ -30,7 +46,7 @@ export type Column = { channelId?: string; roleId?: string; excludeTypes?: typeof notificationTypes[number][]; - tl?: 'home' | 'local' | 'social' | 'global' | 'bubble'; + tl?: BasicTimelineType; withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; @@ -265,7 +281,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { const columns = deepClone(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; + if (column == null || column.widgets == null) return; column.widgets = column.widgets.filter(w => w.id !== widget.id); columns[columnIndex] = column; deckStore.set('columns', columns); @@ -287,7 +303,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat const columns = deepClone(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; + if (column == null || column.widgets == null) return; column.widgets = column.widgets.map(w => w.id === widgetId ? { ...w, data: widgetData, diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index 09412ce386748325159a3f19836f96b9c619725c..d12a18f760e4e856a6a80fba8903ca1a1bb705e2 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> - <template #header><i class="ph-envelope ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template> <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> @@ -34,7 +34,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>(); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.value.pagingComponent?.reload().then(() => { + tlComponent.value?.pagingComponent?.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index eb8e657e98c0fbbe9c473538e3cd9156e3d43ce7..a0e318f7eb7c629065970673dea86cdb5d345241 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -4,9 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ph-list ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> <MkTimeline v-if="column.listId" ref="timeline" :key="column.listId + column.withRenotes + column.onlyFiles" src="list" :list="column.listId" :withRenotes="withRenotes" :onlyFiles="onlyFiles" @note="onNote"/> @@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { watch, shallowRef, ref } from 'vue'; +import type { entities as MisskeyEntities } from 'misskey-js'; import XColumn from './column.vue'; import { updateColumn, Column } from './deck-store.js'; import MkTimeline from '@/components/MkTimeline.vue'; @@ -23,6 +24,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; import { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; +import { userListsCache } from '@/cache.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; @@ -58,17 +60,38 @@ watch(soundSetting, v => { async function setList() { const lists = await misskeyApi('users/lists/list'); - const { canceled, result: list } = await os.select({ + const { canceled, result: list } = await os.select<MisskeyEntities.UserList | '_CREATE_'>({ title: i18n.ts.selectList, - items: lists.map(x => ({ - value: x, text: x.name, - })), + items: [ + { value: '_CREATE_', text: i18n.ts.createNew }, + (lists.length > 0 ? { + sectionTitle: i18n.ts.createdLists, + items: lists.map(x => ({ + value: x, text: x.name, + })), + } : undefined), + ], default: props.column.listId, }); - if (canceled) return; - updateColumn(props.column.id, { - listId: list.id, - }); + if (canceled || list == null) return; + + if (list === '_CREATE_') { + const { canceled, result: name } = await os.inputText({ + title: i18n.ts.enterListName, + }); + if (canceled || name == null || name === '') return; + + const res = await os.apiWithDialog('users/lists/create', { name: name }); + userListsCache.delete(); + + updateColumn(props.column.id, { + listId: res.id, + }); + } else { + updateColumn(props.column.id, { + listId: list.id, + }); + } } function editList() { @@ -81,12 +104,12 @@ function onNote() { const menu: MenuItem[] = [ { - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.selectList, action: setList, }, { - icon: 'ph-gear ph-bold ph-lg', + icon: 'ti ti-settings', text: i18n.ts.editList, action: editList, }, @@ -101,7 +124,7 @@ const menu: MenuItem[] = [ ref: onlyFiles, }, { - icon: 'ph-bell-ringing ph-bold ph-lg', + icon: 'ti ti-bell', text: i18n.ts._deck.newNoteNotificationSettings, action: () => soundSettingsButton(soundSetting), }, diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 847dcf247a6bf30fa80c35c69290d3f6166615d6..79c967191709a6e7fb635c4a1d31cbe490a79f37 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -66,7 +66,7 @@ function onContextmenu(ev: MouseEvent) { type: 'label', text: path, }, { - icon: 'ph-frame-corners ph-bold ph-lg', + icon: 'ti ti-window-maximize', text: i18n.ts.openInWindow, action: () => { os.pageWindow(path); diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index 70ec98119a9d22361aaa5c261342555488a45185..7b25a55ec339009aa693fd82e19b3bf0a4af7833 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> - <template #header><i class="ph-at ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template> <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> @@ -26,7 +26,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>(); function reloadTimeline() { return new Promise<void>((res) => { - tlComponent.value.pagingComponent?.reload().then(() => { + tlComponent.value?.pagingComponent?.reload().then(() => { res(); }); }); diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index 837953b1e8971a1f883ba259053332148e0aea18..19ccfc1f7c6ddc8ec17dc5135984ad9c3f7297fe 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()"> - <template #header><i class="ph-bell ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template> +<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="async () => { await notificationsComponent?.reload() }"> + <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template> <XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/> </XColumn> @@ -27,7 +27,7 @@ const props = defineProps<{ const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>(); function func() { - os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { excludeTypes: props.column.excludeTypes, }, { done: async (res) => { @@ -36,11 +36,12 @@ function func() { excludeTypes: excludeTypes, }); }, - }, 'closed'); + closed: () => dispose(), + }); } const menu = [{ - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.notificationSetting, action: func, }]; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index 09a6438afd197ad4b019be60a2373cdd544e588a..a375e9c574f82276f5fac5d8f77fe93d49df84f1 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -4,9 +4,9 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i class="ph-seal-check ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span> + <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> <MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @note="onNote"/> @@ -53,7 +53,7 @@ async function setRole() { })), default: props.column.roleId, }); - if (canceled) return; + if (canceled || role == null) return; updateColumn(props.column.id, { roleId: role.id, }); @@ -64,11 +64,11 @@ function onNote() { } const menu: MenuItem[] = [{ - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.role, action: setRole, }, { - icon: 'ph-bell-ringing ph-bold ph-lg', + icon: 'ti ti-bell', text: i18n.ts._deck.newNoteNotificationSettings, action: () => soundSettingsButton(soundSetting), }]; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 0c0a43540331429e71c2e5510c6c01c5354bbc1b..17afa12551cbbda7e1d4136cbfe3c6e3e049831e 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -4,19 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i v-if="column.tl === 'home'" class="ph-house ph-bold ph-lg"></i> - <i v-else-if="column.tl === 'local'" class="ph-planet ph-bold ph-lg"></i> - <i v-else-if="column.tl === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i> - <i v-else-if="column.tl === 'bubble'" class="ph-thumb-up ph-bold ph-lg"></i> - <i v-else-if="column.tl === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></i> + <i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/> <span style="margin-left: 8px;">{{ column.name }}</span> </template> - <div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'bubble' && !isBubbleTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled"> + <div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled"> <p :class="$style.disabledTitle"> - <i class="ph-minus-circle ph-bold ph-lg"></i> + <i class="ti ti-circle-minus"></i> {{ i18n.ts._disabledTimeline.title }} </p> <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> @@ -35,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, watch, ref, shallowRef } from 'vue'; +import { onMounted, watch, ref, shallowRef, computed } from 'vue'; import XColumn from './column.vue'; import { removeColumn, updateColumn, Column } from './deck-store.js'; +import type { MenuItem } from '@/types/menu.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; +import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import { instance } from '@/instance.js'; -import { 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'; @@ -53,12 +49,8 @@ const props = defineProps<{ isStacked: boolean; }>(); -const disabled = ref(false); const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); -const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); -const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); -const isBubbleTimelineAvailable = ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); const withRenotes = ref(props.column.withRenotes ?? true); const withReplies = ref(props.column.withReplies ?? false); @@ -89,11 +81,6 @@ watch(soundSetting, v => { onMounted(() => { if (props.column.tl == null) { setType(); - } else if ($i) { - disabled.value = ( - (!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) || - (!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)) || - (!((instance.policies.btlAvailable) || ($i.policies.btlAvailable)) && ['bubble'].includes(props.column.tl))); } }); @@ -107,7 +94,7 @@ async function setType() { }, { value: 'social' as const, text: i18n.ts._timelines.social, }, { - value: 'bubble' as const, text: 'Bubble', + value: 'bubble' as const, text: i18n.ts._timelines.bubble, }, { value: 'global' as const, text: i18n.ts._timelines.global, }], @@ -118,8 +105,9 @@ async function setType() { } return; } + if (src == null) return; updateColumn(props.column.id, { - tl: src, + tl: src ?? undefined, }); } @@ -127,19 +115,19 @@ function onNote() { sound.playMisskeySfxFile(soundSetting.value); } -const menu: MenuItem[] = [{ - icon: 'ph-pencil-simple ph-bold ph-lg', +const menu = computed<MenuItem[]>(() => [{ + icon: 'ti ti-pencil', text: i18n.ts.timeline, action: setType, }, { - icon: 'ph-bell-ringing ph-bold ph-lg', + icon: 'ti ti-bell', text: i18n.ts._deck.newNoteNotificationSettings, action: () => soundSettingsButton(soundSetting), }, { type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, -}, props.column.tl === 'local' || props.column.tl === 'social' ? { +}, hasWithReplies(props.column.tl) ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, @@ -148,8 +136,8 @@ const menu: MenuItem[] = [{ type: 'switch', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, - disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false, -}]; + disabled: hasWithReplies(props.column.tl) ? withReplies : false, +}]); </script> <style lang="scss" module> diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue index 92d1a673f60f371320fd46f51b991f9042c2ce57..9995996771d9675d3e8a47ec9d37e9cda74d636a 100644 --- a/packages/frontend/src/ui/deck/widgets-column.vue +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :naked="true" :column="column" :isStacked="isStacked"> - <template #header><i class="ph-stack ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template> + <template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template> <div :class="$style.root"> <div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div> @@ -49,7 +49,7 @@ function func() { } const menu = [{ - icon: 'ph-pencil-simple ph-bold ph-lg', + icon: 'ti ti-pencil', text: i18n.ts.editWidgets, action: func, }]; diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 3a48c5eab9f1d2ec890eb81e6f51c71d881017f5..d2b5d8cc42152de90bd4b9242a4e8dc2dfb9fc32 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -22,19 +22,19 @@ SPDX-License-Identifier: AGPL-3.0-only <XWidgets/> </div> - <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ph-stack ph-bold ph-lg"></i></button> + <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> - <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ph-list ph-bold ph-lg-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> - <button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ph-house ph-bold ph-lg"></i></button> + <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button> + <button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button> <button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"> - <i :class="$style.navButtonIcon" class="ph-bell ph-bold ph-lg"></i> + <i :class="$style.navButtonIcon" class="ti ti-bell"></i> <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"> <span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span> </span> </button> - <button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ph-stack ph-bold ph-lg"></i></button> - <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ph-pencil-simple ph-bold ph-lg"></i></button> + <button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button> + <button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button> </div> <Transition @@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only :leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''" > <div v-if="widgetsShowing" :class="$style.widgetsDrawer"> - <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ph-x ph-bold ph-lg"></i></button> + <button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button> <XWidgets/> </div> </Transition> @@ -209,7 +209,7 @@ const onContextmenu = (ev) => { type: 'label', text: path, }, { - icon: 'ph-frame-corners ph-bold ph-lg', + icon: 'ti ti-window-maximize', text: i18n.ts.openInWindow, action: () => { os.pageWindow(path); diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue index 7e413284030f6acfd783c77c1d47a47cb9398f6a..fc0a4475d23aa365a8f66443518ad5cc73766dce 100644 --- a/packages/frontend/src/ui/universal.widgets.vue +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/> - <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts.editWidgetsExit }}</button> - <button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ph-pencil-simple ph-bold ph-lg"></i> {{ i18n.ts.editWidgets }}</button> + <button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button> + <button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button> </div> </template> diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index 0c818ba21e1b6cbc4a3d12aa4c551d4f0c8ee2a1..ccc8d8fd973efeac84ebeed9ad26e6f7e8a36245 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="main"> <div v-if="!isRoot" class="header"> <div v-if="narrow === false" class="wide"> - <MkA to="/" class="link" activeClass="active"><i class="ph-house ph-bold ph-lg icon"></i> {{ i18n.ts.home }}</MkA> + <MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i> {{ i18n.ts.home }}</MkA> <MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ph-chat-text ph-bold ph-lg icon"></i> {{ i18n.ts.timeline }}</MkA> - <MkA to="/explore" class="link" activeClass="active"><i class="ph-hash ph-bold ph-lg icon"></i> {{ i18n.ts.explore }}</MkA> - <MkA to="/channels" class="link" activeClass="active"><i class="ph-television ph-bold ph-lg icon"></i> {{ i18n.ts.channel }}</MkA> + <MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i> {{ i18n.ts.explore }}</MkA> + <MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i> {{ i18n.ts.channel }}</MkA> </div> <div v-else-if="narrow === true" class="narrow"> <button class="menu _button" @click="showMenu = true"> - <i class="ph-list ph-bold ph-lg-2 icon"></i> + <i class="ti ti-menu-2 icon"></i> </button> </div> </div> @@ -47,14 +47,14 @@ SPDX-License-Identifier: AGPL-3.0-only <Transition :name="'tray'"> <div v-if="showMenu" class="menu"> - <MkA to="/" class="link" activeClass="active"><i class="ph-house ph-bold ph-lg icon"></i>{{ i18n.ts.home }}</MkA> + <MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i>{{ i18n.ts.home }}</MkA> <MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ph-chat-text ph-bold ph-lg icon"></i>{{ i18n.ts.timeline }}</MkA> - <MkA to="/explore" class="link" activeClass="active"><i class="ph-hash ph-bold ph-lg icon"></i>{{ i18n.ts.explore }}</MkA> - <MkA to="/announcements" class="link" activeClass="active"><i class="ph-megaphone ph-bold ph-lg icon"></i>{{ i18n.ts.announcements }}</MkA> - <MkA to="/channels" class="link" activeClass="active"><i class="ph-television ph-bold ph-lg icon"></i>{{ i18n.ts.channel }}</MkA> + <MkA to="/explore" class="link" activeClass="active"><i class="ti ti-hash icon"></i>{{ i18n.ts.explore }}</MkA> + <MkA to="/announcements" class="link" activeClass="active"><i class="ti ti-speakerphone icon"></i>{{ i18n.ts.announcements }}</MkA> + <MkA to="/channels" class="link" activeClass="active"><i class="ti ti-device-tv icon"></i>{{ i18n.ts.channel }}</MkA> <div class="divider"></div> - <MkA to="/pages" class="link" activeClass="active"><i class="ph-newspaper ph-bold ph-lg icon"></i>{{ i18n.ts.pages }}</MkA> - <MkA to="/play" class="link" activeClass="active"><i class="ph-play ph-bold ph-lg icon"></i>Play</MkA> + <MkA to="/pages" class="link" activeClass="active"><i class="ti ti-news icon"></i>{{ i18n.ts.pages }}</MkA> + <MkA to="/play" class="link" activeClass="active"><i class="ti ti-player-play icon"></i>Play</MkA> <MkA to="/gallery" class="link" activeClass="active"><i class="ph-images-square ph-bold ph-lgs icon"></i>{{ i18n.ts.gallery }}</MkA> <div class="action"> <button class="_buttonPrimary" @click="signup()">{{ i18n.ts.signup }}</button> @@ -124,15 +124,19 @@ const keymap = computed(() => { }); function signin() { - os.popup(XSigninDialog, { + const { dispose } = os.popup(XSigninDialog, { autoSet: true, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } function signup() { - os.popup(XSignupDialog, { + const { dispose } = os.popup(XSignupDialog, { autoSet: true, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); } onMounted(() => { diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue index 77193a34758796b51907ddc471c8a343b7143e59..47502b2af1feae601323484c5476743e86a49829 100644 --- a/packages/frontend/src/ui/zen.vue +++ b/packages/frontend/src/ui/zen.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only See https://github.com/misskey-dev/misskey/issues/10905 --> <div v-if="showBottom" :class="$style.bottom"> - <button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ph-house ph-bold ph-lg"></i></button> + <button v-tooltip="i18n.ts.goToMisskey" :class="['_button', '_shadow', $style.button]" @click="goToMisskey"><i class="ti ti-home"></i></button> </div> </template> diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue index 9b65ca5e4ad6576e9432a2ded9d1f4c50ea1eb38..0aaf18ddd1bbfc1a5206fe31de8a2f96aa6964a5 100644 --- a/packages/frontend/src/widgets/WidgetActivity.vue +++ b/packages/frontend/src/widgets/WidgetActivity.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent" data-cy-mkw-activity class="mkw-activity"> - <template #icon><i class="ph-chart-line ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-chart-line"></i></template> <template #header>{{ i18n.ts._widgets.activity }}</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ph-caret-up-down ph-bold ph-lg"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> <div> <MkLoading v-if="fetching"/> diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 70fac9ae55329c0cbc0c446e61b43793df725cb5..8e29254819e08c60baef8ae92ca9c27b62cc1693 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-aiscript class="mkw-aiscript"> - <template #icon><i class="ph-terminal-window ph-bold ph-lg-2"></i></template> + <template #icon><i class="ti ti-terminal-2"></i></template> <template #header>{{ i18n.ts._widgets.aiscript }}</template> <div class="uylguesu _monospace"> diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index e22697ca6831910482d266737179ead34f7b14ef..49fd103d37eb60f8ce8ed72ff2f7bcac741b6ab8 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" class="mkw-bdayfollowings"> - <template #icon><i class="ph-cake ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-cake"></i></template> <template #header>{{ i18n.ts._widgets.birthdayFollowings }}</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ph-arrows-clockwise ph-bold ph-lg"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="actualFetch()"><i class="ti ti-refresh"></i></button></template> <div :class="$style.bdayFRoot"> <MkLoading v-if="fetching"/> diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue index 06b71311c4e95e30174a13dc3b55ce6e49abeaca..19843f3949c582d5b9195173564fa5df270877eb 100644 --- a/packages/frontend/src/widgets/WidgetCalendar.vue +++ b/packages/frontend/src/widgets/WidgetCalendar.vue @@ -121,7 +121,7 @@ defineExpose<WidgetComponentExpose>({ .root { padding: 16px 0; - &:after { + &::after { content: ""; display: block; clear: both; diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue index 9d231ae7154e1a87b013f423fc29b552d689cf5c..5c978fdf72cf7b14532f79cf1e3f805dca36227e 100644 --- a/packages/frontend/src/widgets/WidgetClicker.vue +++ b/packages/frontend/src/widgets/WidgetClicker.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" class="mkw-clicker"> - <template #icon><i class="ph-cookie ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-cookie"></i></template> <template #header>Clicker</template> <MkClickerGame/> </MkContainer> diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index ae770f9816a0dd8382aa193107060e6804f7ba5f..2ea1253c2b42b354786ea9d93a6672da223b0bde 100644 --- a/packages/frontend/src/widgets/WidgetFederation.vue +++ b/packages/frontend/src/widgets/WidgetFederation.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" data-cy-mkw-federation class="mkw-federation"> - <template #icon><i class="ph-globe-hemisphere-west ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-whirl"></i></template> <template #header>{{ i18n.ts._widgets.federation }}</template> <div class="wbrkwalb"> diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue index 962521b25cc65e6bf43030a3bec5e2195b1feac0..ce8b0dd60254c275704989ea0be42960d76f09ae 100644 --- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue +++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue @@ -81,16 +81,19 @@ defineExpose<WidgetComponentExpose>({ .body { text-overflow: ellipsis; overflow: clip; + margin-left: -10px; + padding: 10px; } .name { color: #fff; - filter: drop-shadow(0 0 4px #000); + filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5)); font-weight: bold; } .host { color: #fff; - filter: drop-shadow(0 0 4px #000); + filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5)); + } </style> diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index b3e364a6d7720d968bb2c936dab64f79d53c4e8b..edf6622a137dc53a30d5c33876f502badab992ad 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div data-cy-mkw-jobQueue class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }"> <div class="inbox"> - <div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ph-warning ph-bold ph-lg icon"></i></div> + <div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ti ti-alert-triangle icon"></i></div> <div class="values"> <div> <div>Process</div> @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div class="deliver"> - <div class="label">Deliver queue<i v-if="current.deliver.waiting > 0" class="ph-warning ph-bold ph-lg icon"></i></div> + <div class="label">Deliver queue<i v-if="current.deliver.waiting > 0" class="ti ti-alert-triangle icon"></i></div> <div class="values"> <div> <div>Process</div> diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue index d9efe546232227810822a3aab275eccc76abd377..b69152b4feaa3be66ca57a2dfb46c68e0d8753b6 100644 --- a/packages/frontend/src/widgets/WidgetMemo.vue +++ b/packages/frontend/src/widgets/WidgetMemo.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-memo class="mkw-memo"> - <template #icon><i class="ph-note ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-note"></i></template> <template #header>{{ i18n.ts._widgets.memo }}</template> <div :class="$style.root"> diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue index d590e7768e89e0df2fa34f0d192b79d7a864fbf2..773c078b49e3d5a13be366308c2c14b0cf069caf 100644 --- a/packages/frontend/src/widgets/WidgetNotifications.vue +++ b/packages/frontend/src/widgets/WidgetNotifications.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :style="`height: ${widgetProps.height}px;`" :showHeader="widgetProps.showHeader" :scrollable="true" data-cy-mkw-notifications class="mkw-notifications"> - <template #icon><i class="ph-bell ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-bell"></i></template> <template #header>{{ i18n.ts.notifications }}</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ph-gear ph-bold ph-lg"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template> <div> <XNotifications :excludeTypes="widgetProps.excludeTypes"/> @@ -54,7 +54,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name, ); const configureNotification = () => { - os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { excludeTypes: widgetProps.excludeTypes, }, { done: async (res) => { @@ -62,7 +62,8 @@ const configureNotification = () => { widgetProps.excludeTypes = excludeTypes; save(); }, - }, 'closed'); + closed: () => dispose(), + }); }; defineExpose<WidgetComponentExpose>({ diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index e578ebe2c51c6573a226f85051e09481c5480275..86ece91de66b3235a2b3344e8655609d25a35bbd 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" data-cy-mkw-photos class="mkw-photos"> - <template #icon><i class="ph-camera ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-camera"></i></template> <template #header>{{ i18n.ts._widgets.photos }}</template> <div class=""> diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue index a5578d4de62a63fe882df9ae376592043225126c..ae39098305b3fab79f9fc698f269af0339113f0c 100644 --- a/packages/frontend/src/widgets/WidgetProfile.vue +++ b/packages/frontend/src/widgets/WidgetProfile.vue @@ -82,16 +82,19 @@ defineExpose<WidgetComponentExpose>({ .body { text-overflow: ellipsis; overflow: clip; + margin-left: -10px; + padding: 10px; } .name { color: #fff; - filter: drop-shadow(0 0 4px #000); + filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5)); font-weight: bold; } .username { color: #fff; - filter: drop-shadow(0 0 4px #000); + filter: drop-shadow(0 0 4px #000) drop-shadow(0 0 0.1px rgba(0, 0, 0, 0.5)); + font-weight: normal; } </style> diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue index aa4bf155de6eb0fd041bc3d07276fc7f559ea0c4..e5758662cc49479c8ef5b79ff5b8f8bbb7d1a2ce 100644 --- a/packages/frontend/src/widgets/WidgetRss.vue +++ b/packages/frontend/src/widgets/WidgetRss.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-rss class="mkw-rss"> - <template #icon><i class="ph-rss ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-rss"></i></template> <template #header>RSS</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ph-gear ph-bold ph-lg"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template> <div class="ekmkgxbj"> <MkLoading v-if="fetching"/> diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue index f1dde14bf6b8352204ff5d8d1f9e4aa5ee786ab7..16306ef5ba9b39ca8fed919115d266aba9a1f576 100644 --- a/packages/frontend/src/widgets/WidgetRssTicker.vue +++ b/packages/frontend/src/widgets/WidgetRssTicker.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :naked="widgetProps.transparent" :showHeader="widgetProps.showHeader" class="mkw-rss-ticker"> - <template #icon><i class="ph-rss ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-rss"></i></template> <template #header>RSS</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ph-gear ph-bold ph-lg"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template> <div :class="$style.feed"> <div v-if="fetching" :class="$style.loading"> diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue index aeb1bc6ee0b593ccc77c075896813ce602be9929..294c97e2932d0ae065c12409653025d5ccafd191 100644 --- a/packages/frontend/src/widgets/WidgetSearch.vue +++ b/packages/frontend/src/widgets/WidgetSearch.vue @@ -143,11 +143,12 @@ async function search() { key.value++; - os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/SkSearchResultWindow.vue')), { noteKey: key.value, notePagination: notePagination.value, }, { - }, 'closed'); + closed: () => dispose(), + }); } defineExpose<WidgetComponentExpose>({ diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index f6cf13290f192186df5ce698f676a307de35cf2e..d02f9b8e224abdba1a8005ae620b1b1f4ff1b193 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -6,24 +6,20 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" data-cy-mkw-timeline class="mkw-timeline"> <template #icon> - <i v-if="widgetProps.src === 'home'" class="ph-house ph-bold ph-lg"></i> - <i v-else-if="widgetProps.src === 'local'" class="ph-planet ph-bold ph-lg"></i> - <i v-else-if="widgetProps.src === 'social'" class="ph-rocket-launch ph-bold ph-lg"></i> - <i v-else-if="widgetProps.src === 'bubble'" class="ph-drop ph-bold ph-lg"></i> - <i v-else-if="widgetProps.src === 'global'" class="ph-globe-hemisphere-west ph-bold ph-lg"></i> - <i v-else-if="widgetProps.src === 'list'" class="ph-list ph-bold ph-lg"></i> - <i v-else-if="widgetProps.src === 'antenna'" class="ph-flying-saucer ph-bold ph-lg"></i> + <i v-if="isBasicTimeline(widgetProps.src)" :class="basicTimelineIconClass(widgetProps.src)"></i> + <i v-else-if="widgetProps.src === 'list'" class="ti ti-list"></i> + <i v-else-if="widgetProps.src === 'antenna'" class="ti ti-antenna"></i> </template> <template #header> <button class="_button" @click="choose"> <span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.ts._timelines[widgetProps.src] }}</span> - <i :class="menuOpened ? 'ph-caret-up ph-bold ph-lg' : 'ph-caret-down ph-bold ph-lg'" style="margin-left: 8px;"></i> + <i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i> </button> </template> - <div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'bubble' && !isBubbleTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled"> + <div v-if="isBasicTimeline(widgetProps.src) && !isAvailableBasicTimeline(widgetProps.src)" :class="$style.disabled"> <p :class="$style.disabledTitle"> - <i class="ph-minus ph-bold ph-lg"></i> + <i class="ti ti-minus"></i> {{ i18n.ts._disabledTimeline.title }} </p> <p :class="$style.disabledDescription">{{ i18n.ts._disabledTimeline.description }}</p> @@ -43,13 +39,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; +import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; const name = 'timeline'; -const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); -const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); -const isBubbleTimelineAvailable = (($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable)); const widgetPropsDef = { showHeader: { @@ -103,7 +95,7 @@ const choose = async (ev) => { ]); const antennaItems = antennas.map(antenna => ({ text: antenna.name, - icon: 'ph-flying-saucer ph-bold ph-lg', + icon: 'ti ti-antenna', action: () => { widgetProps.antenna = antenna; setSrc('antenna'); @@ -111,33 +103,17 @@ const choose = async (ev) => { })); const listItems = lists.map(list => ({ text: list.name, - icon: 'ph-list ph-bold ph-lg', + icon: 'ti ti-list', action: () => { widgetProps.list = list; setSrc('list'); }, })); - os.popupMenu([{ - text: i18n.ts._timelines.home, - icon: 'ph-house ph-bold ph-lg', - action: () => { setSrc('home'); }, - }, { - text: i18n.ts._timelines.local, - icon: 'ph-planet ph-bold ph-lg', - action: () => { setSrc('local'); }, - }, { - text: i18n.ts._timelines.social, - icon: 'ph-rocket-launch ph-bold ph-lg', - action: () => { setSrc('social'); }, - }, { - text: 'Bubble', - icon: 'ph-drop ph-bold ph-lg', - action: () => { setSrc('bubble'); }, - }, { - text: i18n.ts._timelines.global, - icon: 'ph-globe-hemisphere-west ph-bold ph-lg', - action: () => { setSrc('global'); }, - }, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { + os.popupMenu([...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(() => { menuOpened.value = false; }); }; diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue index 978a1a86f70562b1999a88eeff6f261588e8c2c4..4299181a273f93c23758e6cd3325bf12df0c1377 100644 --- a/packages/frontend/src/widgets/WidgetTrends.vue +++ b/packages/frontend/src/widgets/WidgetTrends.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" data-cy-mkw-trends class="mkw-trends"> - <template #icon><i class="ph-hash ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-hash"></i></template> <template #header>{{ i18n.ts._widgets.trends }}</template> <div class="wbrkwala"> diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue index 0e4fe2fbd38544132cb1896a914dbe16ab9f32fd..d9f4dc49ea87aeeb621e6e111470383f3c4d184c 100644 --- a/packages/frontend/src/widgets/WidgetUserList.vue +++ b/packages/frontend/src/widgets/WidgetUserList.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" class="mkw-userList"> - <template #icon><i class="ph-users ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-users"></i></template> <template #header>{{ list ? list.name : i18n.ts._widgets.userList }}</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure()"><i class="ph-gear ph-bold ph-lg"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure()"><i class="ti ti-settings"></i></button></template> <div :class="$style.root"> <div v-if="widgetProps.listId == null" class="init"> diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue index e00ef187f3a1023f8770ce8544b357f7000f65cf..ba98a926ff13b8a4c59a58bca3e5f9d9fb9b84d3 100644 --- a/packages/frontend/src/widgets/server-metric/cpu.vue +++ b/packages/frontend/src/widgets/server-metric/cpu.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="vrvdvrys"> <XPie class="pie" :value="usage"/> <div> - <p><i class="ph-cpu ph-bold ph-lg"></i>CPU</p> + <p><i class="ti ti-cpu"></i>CPU</p> <p>{{ meta.cpu.cores }} Logical cores</p> <p>{{ meta.cpu.model }}</p> </div> diff --git a/packages/frontend/src/widgets/server-metric/disk.vue b/packages/frontend/src/widgets/server-metric/disk.vue index e94a8b684844f72f379790b0b4f39990b6f68f8e..0602b8cd271a62aee16d5245c1d63c8cbd0b5268 100644 --- a/packages/frontend/src/widgets/server-metric/disk.vue +++ b/packages/frontend/src/widgets/server-metric/disk.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="zbwaqsat"> <XPie class="pie" :value="usage"/> <div> - <p><i class="ph-database ph-bold ph-lg"></i>Disk</p> + <p><i class="ti ti-database"></i>Disk</p> <p>Total: {{ bytes(total, 1) }}</p> <p>Free: {{ bytes(available, 1) }}</p> <p>Used: {{ bytes(used, 1) }}</p> diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index 1180a2a059fba4e61096b50bbe53332937a02ef2..86d84b4f336581153796c60c8dea4eaaebe54ae6 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -5,9 +5,9 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" :naked="widgetProps.transparent"> - <template #icon><i class="ph-hard-drives ph-bold ph-lg"></i></template> + <template #icon><i class="ti ti-server"></i></template> <template #header>{{ i18n.ts._widgets.serverMetric }}</template> - <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ph-caret-up-down ph-bold ph-lg"></i></button></template> + <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> <div v-if="meta" data-cy-mkw-serverMetric class="mkw-serverMetric"> <XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/> diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue index ba56d14211c64819430c512793d2a1191dcba36c..ff4c6a44b40dd5c747ebdf4e3a6ea4427fdd13a1 100644 --- a/packages/frontend/src/widgets/server-metric/mem.vue +++ b/packages/frontend/src/widgets/server-metric/mem.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="zlxnikvl"> <XPie class="pie" :value="usage"/> <div> - <p><i class="ph-selection-all ph-bold ph-lg"></i>RAM</p> + <p><i class="ti ti-section"></i>RAM</p> <p>Total: {{ bytes(total, 1) }}</p> <p>Used: {{ bytes(used, 1) }}</p> <p>Free: {{ bytes(free, 1) }}</p> diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index 819629a9cf8aafe01b2473a2b7a5c2cb72377141..fe4d20289492776252989f792ada957b8d70ce47 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -37,13 +37,13 @@ ], "lib": [ "esnext", - "dom" + "dom", + "dom.iterable" ], "jsx": "preserve" }, "compileOnSave": false, "include": [ - ".eslintrc.js", "./**/*.ts", "./**/*.vue" ], diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts index 850515b59e18a134f520a43432b2247ea360af86..07cf3b4a69e06c3e946b59ca31e8efc1298463bf 100644 --- a/packages/frontend/vite.config.local-dev.ts +++ b/packages/frontend/vite.config.local-dev.ts @@ -1,6 +1,8 @@ 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'; @@ -14,7 +16,15 @@ const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')) const httpUrl = `http://localhost:${port}/`; const websocketUrl = `ws://localhost:${port}/`; -const devConfig = { +// 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', @@ -53,17 +63,14 @@ const devConfig = { '/bios': httpUrl, '/cli': httpUrl, '/inbox': httpUrl, + '/emoji/': httpUrl, '/notes': { target: httpUrl, - headers: { - 'Accept': 'application/activity+json', - }, + bypass: varyHandler, }, '/users': { target: httpUrl, - headers: { - 'Accept': 'application/activity+json', - }, + bypass: varyHandler, }, '/.well-known': { target: httpUrl, diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 4d150aa3577e38d2ed59cf2ed1ddb778cf2ca8b6..674fdbf6801bc2ba826771f2b4c0190e393f1f5d 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -5,9 +5,10 @@ import { type UserConfig, defineConfig } from 'vite'; import locales from '../../locales/index.js'; import meta from '../../package.json'; -import packageInfo from './package.json' assert { type: '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'; const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm']; @@ -71,6 +72,7 @@ export function getConfig(): UserConfig { pluginVue(), pluginUnwindCssModuleClassName(), pluginJson5(), + ...pluginReplaceIcons(), ...process.env.NODE_ENV === 'production' ? [ pluginReplace({ diff --git a/packages/frontend/vite.replaceIcons.ts b/packages/frontend/vite.replaceIcons.ts new file mode 100644 index 0000000000000000000000000000000000000000..92ac568ef34e9d2db1b4cf82a2f4d79c8cc77efe --- /dev/null +++ b/packages/frontend/vite.replaceIcons.ts @@ -0,0 +1,376 @@ +import pluginReplace from '@rollup/plugin-replace'; +import type { RollupReplaceOptions } from '@rollup/plugin-replace'; + +function iconsReplace(opts: RollupReplaceOptions) { + return pluginReplace({ + ...opts, + preventAssignment: false, + // only replace these strings at the start of strings, and make + // sure they're followed by a word-boundary that's not a dash + delimiters: ['(?<=["\'`])', '\\b(?!-)'], + }); +} + +export function pluginReplaceIcons() { + return [ + iconsReplace({ + values: { + 'ti ti-alert-triangle': 'ph-warning ph-bold ph-lg', + }, + exclude: [ + '**/components/MkAnnouncementDialog.*', + '**/pages/announcement.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-alert-triangle': 'ph-warning-circle ph-bold ph-lg', + }, + include: [ + '**/components/MkAnnouncementDialog.*', + '**/pages/announcement.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-apps': 'ph-squares-four ph-bold ph-lg', + }, + include: [ + '**/pages/**', + ], + }), + iconsReplace({ + values: { + 'ti ti-apps': 'ph-stack ph-bold ph-lg', + }, + include: [ + '**/ui/**', + ], + }), + iconsReplace({ + values: { + 'ti ti-clock-play': 'ph-clock ph-bold ph-lg', + }, + exclude: [ + '**/components/MkMedia*', + ], + }), + iconsReplace({ + values: { + 'ti ti-clock-play': 'ph ph-gauge ph-bold ph-lg', + }, + include: [ + '**/components/MkMedia*', + ], + }), + iconsReplace({ + values: { + 'ti ti-photo': 'ph-image-square ph-bold ph-lg', + }, + exclude: [ + '**/pages/admin-user.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-photo': 'ph-image ph-bold ph-lg', + }, + include: [ + '**/pages/admin-user.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-reload': 'ph-arrow-clockwise ph-bold ph-lg', + }, + exclude: [ + '**/pages/settings/emoji-picker.*', + '**/pages/flash/flash.*', + '**/components/MkPageWindow.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-reload': 'ph-arrow-counter-clockwise ph-bold ph-lg', + }, + include: [ + '**/pages/settings/emoji-picker.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-reload': 'ph-arrows-clockwise ph-bold ph-lg', + }, + include: [ + '**/pages/flash/flash.*', + '**/components/MkPageWindow.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-repeat': 'ph-rocket-launch ph-bold ph-lg', + }, + exclude: [ + '**/components/MkMedia*', + '**/scripts/get-user-menu.*', + '**/pages/gallery/post.*', + ], + }), + iconsReplace({ + values: { + 'ti ti-repeat': 'ph ph-repeat ph-bold ph-lg', + }, + include: [ + '**/components/MkMedia*', + '**/scripts/get-user-menu.*', + '**/pages/gallery/post.*', + ], + }), + iconsReplace({ + values: { + 'icon ti ti-brand-youtube': 'icon ph-youtube-logo ph-bold ph-lg', + 'ti ti ti-folder-symlink': 'sk-icons sk-foldermove sk-icons-lg', + 'ti ti-123': 'ph-numpad ph-bold ph-lg', + 'ti ti-access-point': 'ph-broadcast ph-bold ph-lg', + 'ti ti-activity': 'ph-pulse ph-bold ph-lg', + 'ti ti-ad': 'ph-flag ph-bold ph-lg', + 'ti ti-adjustments': 'ph-faders ph-bold ph-lg', + 'ti ti-align-box-left-bottom': 'ph-arrow-down-left ph-bold ph-lg', + 'ti ti-align-box-left-top': 'ph-arrow-up-left ph-bold ph-lg', + 'ti ti-align-box-right-bottom': 'ph-arrow-down-right ph-bold ph-lg', + 'ti ti-align-box-right-top': 'ph-arrow-up-right ph-bold ph-lg', + 'ti ti-align-left': 'ph-text-align-left ph-bold ph-lg', + 'ti ti-antenna': 'ph-flying-saucer ph-bold ph-lg', + '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-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', + 'ti ti-arrow-down': 'ph-arrow-down ph-bold ph-lg', + 'ti ti-arrow-left': 'ph-arrow-left ph-bold ph-lg', + 'ti ti-arrow-narrow-up': 'ph-arrow-up ph-bold ph-lg', + 'ti ti-arrow-right': 'ph-arrow-right ph-bold ph-lg', + 'ti ti-arrow-up': 'ph-arrow-up ph-bold ph-lg', + 'ti ti-arrows-maximize': 'ph-arrows-out ph-bold ph-lg', + 'ti ti-arrows-minimize': 'ph-arrows-in ph-bold ph-lg', + 'ti ti-arrows-move': 'ph-arrows-out-cardinal ph-bold ph-lg', + 'ti ti-arrows-sort': 'ph-arrows-down-up ph-bold ph-lg', + 'ti ti-arrows-up': 'ph-arrow-line-up ph-bold ph-lg', + 'ti ti-asterisk': 'ph-asterisk ph-bold ph-lg', + 'ti ti-at': 'ph-at ph-bold ph-lg', + 'ti ti-backspace': 'ph-backspace ph-bold ph-lg', + 'ti ti-badge': 'ph-seal-check ph-bold ph-lg', + 'ti ti-badges': 'ph-seal-check ph-bold ph-lg', + 'ti ti-ban': 'ph-prohibit ph-bold ph-lg', + 'ti ti-bell': 'ph-bell ph-bold ph-lg', + 'ti ti-bell-off': 'ph-bell ph-bold ph-lg', + 'ti ti-bell-plus': 'ph-bell-ringing ph-bold ph-lg', + 'ti ti-bell-ringing-2': 'ph-bell-ringing ph-bold ph-lg', + 'ti ti-bolt': 'ph-lightning ph-bold ph-lg', + 'ti ti-bookmark': 'ph-bookmark ph-bold ph-lg', + 'ti ti-brand-x': 'ph-twitter-logo ph-bold ph-lg', + 'ti ti-bulb': 'ph-lightbulb ph-bold ph-lg', + 'ti ti-cake': 'ph-cake ph-bold ph-lg', + 'ti ti-calendar': 'ph-calendar ph-bold ph-lg', + 'ti ti-calendar-time': 'ph-calendar ph-bold ph-lg', + 'ti ti-camera': 'ph-camera ph-bold ph-lg', + 'ti ti-carousel-horizontal': 'ph-split-horizontal ph-bold ph-lg', + 'ti ti-carousel-vertical': 'ph-split-vertical ph-bold ph-lg', + 'ti ti-chart-arrows': 'ph-chart-bar-horizontal ph-bold ph-lg', + 'ti ti-chart-line': 'ph-chart-line ph-bold ph-lg', + 'ti ti-check': 'ph-check ph-bold ph-lg', + 'ti ti-checkbox': 'ph-check ph-bold ph-lg', + 'ti ti-checklist': 'ph-list-checks ph-bold ph-lg', + 'ti ti-checkup-list': 'ph-list-checks ph-bold ph-lg', + 'ti ti-chevron-double-right': 'ph-caret-double-right ph-bold ph-lg', + 'ti ti-chevron-left': 'ph-caret-left ph-bold ph-lg', + 'ti ti-chevron-right': 'ph-caret-right ph-bold ph-lg', + 'ti ti-chevron-up': 'ph-caret-up ph-bold ph-lg', + 'ti ti-chevrons-left': 'ph-caret-dobule-left ph-bold ph-lg', + 'ti ti-chevrons-right': 'ph-caret-right ph-bold ph-lg', + 'ti ti-circle': 'ph-circle ph-bold ph-lg', + 'ti ti-circle-check': 'ph-seal-check ph-bold ph-lg', + 'ti ti-circle-filled': 'ph-circle-half ph-bold ph-lg', + 'ti ti-circle-minus': 'ph-minus-circle ph-bold ph-lg', + 'ti ti-circle-x': 'ph-x-circle ph-bold ph-lg', + 'ti ti-clock': 'ph-clock ph-bold ph-lg', + 'ti ti-clock-edit': 'ph-pencil-simple ph-bold ph-lg', + 'ti ti-cloud': 'ph-cloud ph-bold ph-lg', + 'ti ti-code': 'ph-code ph-bold ph-lg', + 'ti ti-columns': 'ph-text-columns ph-bold ph-lg', + 'ti ti-comet': 'ph-shooting-star ph-bold ph-lg', + 'ti ti-confetti': 'ph-confetti ph-bold ph-lg', + 'ti ti-cookie': 'ph-cookie ph-bold ph-lg', + 'ti ti-copy': 'ph-copy ph-bold ph-lg', + 'ti ti-cpu': 'ph-cpu ph-bold ph-lg', + 'ti ti-crop': 'ph-crop ph-bold ph-lg', + 'ti ti-crown': 'ph-crown ph-bold ph-lg', + 'ti ti-dashboard': 'ph-gauge ph-bold ph-lg', + 'ti ti-database': 'ph-database ph-bold ph-lg', + 'ti ti-device-desktop': 'ph-desktop ph-bold ph-lg', + 'ti ti-device-floppy': 'ph-floppy-disk ph-bold ph-lg', + 'ti ti-device-gamepad': 'ph-game-controller ph-bold ph-lg', + '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-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', + 'ti ti-dots': 'ph-dots-three ph-bold ph-lg', + 'ti ti-download': 'ph-download ph-bold ph-lg', + 'ti ti-edit': 'ph-pencil-simple-line ph-bold ph-lg', + 'ti ti-equal-double': 'ph-equals ph-bold ph-lg', + 'ti ti-equal-not': 'ph-prohibit ph-bold ph-lg', + 'ti ti-exclamation-circle': 'ph-warning-circle ph-bold ph-lg', + 'ti ti-external-link': 'ph-arrow-square-out ph-bold ph-lg', + 'ti ti-eye': 'ph-eye ph-bold ph-lg', + 'ti ti-eye-exclamation': 'ph-eye-slash ph-bold ph-lg', + 'ti ti-eye-off': 'ti ti-eye-exclamation', + 'ti ti-feather': 'ph-feather ph-bold ph-lg', + 'ti ti-file': 'ph-file ph-bold ph-lg', + 'ti ti-file-invoice': 'ph-newspaper-clipping ph-bold ph-lg', + 'ti ti-file-music': 'ph-file-audio ph-bold ph-lg', + 'ti ti-file-text': 'ph-file-text ph-bold ph-lg', + 'ti ti-file-zip': 'ph-file-zip ph-bold ph-lg', + 'ti ti-filter': 'ph-funnel ph-bold ph-lg', + 'ti ti-flare': 'ph-fire ph-bold ph-lg', + 'ti ti-flask': 'ph-flask ph-bold ph-lg', + 'ti ti-folder': 'ph-folder ph-bold ph-lg', + 'ti ti-folder-plus': 'ph-folder-plus ph-bold ph-lg', + 'ti ti-folder-symlink': 'sk-icons sk-foldermove sk-icons-lg', + 'ti ti-forms': 'ph-textbox ph-bold ph-lg', + 'ti ti-ghost': 'ph-ghost ph-bold ph-lg', + 'ti ti-grid-dots': 'ph-dots-nine ph-bold ph-lg', + 'ti ti-hash': 'ph-hash ph-bold ph-lg', + 'ti ti-heart': 'ph-heart ph-bold ph-lg', + 'ti ti-heart-filled': 'ph-heart ph-bold ph-lg', + 'ti ti-heart-off': 'ph-heart-break ph-bold ph-lg', + 'ti ti-heart-plus': 'ph-heart ph-bold ph-lg', + 'ti ti-help-circle': 'ph-question ph-bold ph-lg', + 'ti ti-home': 'ph-house ph-bold ph-lg', + 'ti ti-hourglass-empty': 'ph-hourglass ph-bold ph-lg', + 'ti ti-id': 'ph-identification-card ph-bold ph-lg', + 'ti ti-info-circle': 'ph-info ph-bold ph-lg', + 'ti ti-key': 'ph-key ph-bold ph-lg', + 'ti ti-language-hiragana': 'ph-translate ph-bold ph-lg', + 'ti ti-leaf': 'ph-leaf ph-bold ph-lg', + 'ti ti-license': 'ph-notebook ph-bold ph-lg', + 'ti ti-link': 'ph-link ph-bold ph-lg', + 'ti ti-link-off': 'ph-link-break ph-bold ph-lg', + 'ti ti-list': 'ph-list ph-bold ph-lg', + '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-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', + 'ti ti-medal': 'ph-trophy ph-bold ph-lg', + '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-off': 'ph-bell-slash 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', + 'ti ti-minus': 'ph-minus ph-bold ph-lg', + 'ti ti-mood-happy': 'ph-smiley ph-bold ph-lg', + 'ti ti-mood-smile': 'ph-smiley ph-bold pg-lg', + 'ti ti-moon': 'ph-moon ph-bold ph-lg', + 'ti ti-movie': 'ph-film-strip ph-bold ph-lg', + 'ti ti-music': 'ph-music-notes ph-bold ph-lg', + 'ti ti-news': 'ph-newspaper ph-bold ph-lg', + 'ti ti-note': 'ph-note ph-bold ph-lg', + 'ti ti-notebook': 'ph-notebook ph-bold ph-lg', + 'ti ti-package': 'ph-package ph-bold ph-lg', + 'ti ti-paint': 'ph-paint-roller ph-bold ph-lg', + 'ti ti-palette': 'ph-palette ph-bold ph-lg', + 'ti ti-paperclip': 'ph-paperclip ph-bold ph-lg', + 'ti ti-password': 'ph-password ph-bold ph-lg', + 'ti ti-pencil': 'ph-pencil-simple ph-bold ph-lg', + 'ti ti-pencil-plus': 'ph-plus ph-bold pg-lg', + 'ti ti-photo-plus': 'ph-image-square ph-bold ph-lg', + 'ti ti-picture-in-picture': 'ph-picture-in-picture ph-bold ph-lg', + 'ti ti-pin': 'ph-push-pin ph-bold ph-lg', + 'ti ti-pinned-off': 'ph-push-pin-slash ph-bold ph-lg', + 'ti ti-plane': 'ph-airplane ph-bold ph-lg', + 'ti ti-plane-arrival': 'ph-airplane-landing ph-bold ph-lg', + 'ti ti-plane-departure': 'ph-airplane-takeoff ph-bold ph-lg', + 'ti ti-planet': 'ph-planet ph-bold ph-lg', + 'ti ti-planet-off': 'ph-globe-simple ph-bold ph-lg', + 'ti ti-player-eject': 'ph-eject ph-bold ph-lg', + 'ti ti-player-pause': 'ph-pause ph-bold ph-lg', + 'ti ti-player-pause-filled': 'ph-pause ph-bold ph-lg', + 'ti ti-player-play': 'ph-play ph-bold ph-lg', + 'ti ti-player-play-filled': 'ph-play ph-bold ph-lg', + 'ti ti-player-stop': 'ph-stop ph-bold ph-lg', + 'ti ti-player-track-next': 'ph-skip-forward ph-bold ph-lg', + 'ti ti-plug': 'ph-plug ph-bold ph-lg', + 'ti ti-plus': 'ph-plus ph-bold ph-lg', + 'ti ti-point': 'ph-circle ph-bold ph-lg', + 'ti ti-power': 'ph-power ph-bold ph-lg', + 'ti ti-presentation': 'ph-presentation ph-bold ph-lg', + 'ti ti-quote': 'ph-quotes ph-bold ph-lg', + 'ti ti-rectangle': 'ph-frame-corners ph-bold ph-lg', + 'ti ti-refresh': 'ph-arrows-counter-clockwise ph-bold ph-lg', + 'ti ti-repeat-off': 'ph-repeat ph-bold ph-lg', + 'ti ti-restore': 'ph-box-arrow-up ph-box ph-lg', + 'ti ti-robot': 'ph-robot ph-bold ph-lg', + 'ti ti-rocket': 'ph-rocket-launch ph-bold ph-lg', + 'ti ti-rocket-off': 'ph-rocket ph-bold ph-lg', + 'ti ti-rss': 'ph-rss ph-bold ph-lg', + 'ti ti-search': 'ph-magnifying-glass ph-bold ph-lg', + 'ti ti-section': 'ph-selection-all ph-bold ph-lg', + 'ti ti-selector': 'ph-caret-up-down ph-bold ph-lg', + 'ti ti-send': 'ph-paper-plane-tilt ph-bold ph-lg', + 'ti ti-server': 'ph-hard-drives ph-bold ph-lg', + 'ti ti-settings': 'ph-gear ph-bold ph-lg', + 'ti ti-share': 'ph-share-network ph-bold ph-lg', + 'ti ti-shield': 'ph-shield ph-bold ph-lg', + 'ti ti-shield-lock': 'ph-shield ph-bold ph-lg', + 'ti ti-snowflake': 'ph-snowflake ph-bold ph-lg', + 'ti ti-sparkles': 'ph-sparkle ph-bold ph-lg', + 'ti ti-speakerphone': 'ph-megaphone ph-bold ph-lg', + 'ti ti-stack-2': 'ph-stack ph-bold ph-lg', + 'ti ti-star': 'ph-star ph-bold ph-lg', + 'ti ti-star-off': 'ph-star-half ph-bold ph-lg', + 'ti ti-sun': 'ph-sun ph-bold ph-lg', + 'ti ti-switch-horizontal': 'ph-arrows-left-right ph-bold ph-lg', + 'ti ti-terminal-2': 'ph-terminal-window ph-bold ph-lg', + 'ti ti-text-caption': 'ph-text-indent ph-bold ph-lg', + 'ti ti-tool': 'ph-wrench ph-bold ph-lg', + 'ti ti-trash': 'ph-trash ph-bold ph-lg', + 'ti ti-trophy': 'ph-trophy ph-bold ph-lg', + '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-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', + 'ti ti-user-off': 'ph-user-minus ph-bold ph-lg', + '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-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', + 'ti ti-volume-3': 'ph-speaker-x ph-bold ph-lg', + 'ti ti-webhook': 'ph-webhooks-logo ph-bold ph-lg', + 'ti ti-whirl': 'ph-globe-hemisphere-west ph-bold ph-lg', + '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-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', + 'ti ti-x': 'ph-x ph-bold ph-lg', + 'ti-help': 'ph-question ph-bold ph-lg', + 'ti-mail': 'ph-envelope ph-bold ph-lg', + 'ti-webhook': 'ph-webhooks-logo ph-bold ph-lg', + 'ti ti-caret-down': 'ph-caret-down ph-bold ph-lg', + 'ti ti-chevron-down': 'ph-caret-down ph-bold ph-lg', + }, + }), + ]; +} diff --git a/packages/misskey-bubble-game/.eslintignore b/packages/misskey-bubble-game/.eslintignore deleted file mode 100644 index 52ea8b3362e9e0ab8baf6e7daeb91a99bef2f424..0000000000000000000000000000000000000000 --- a/packages/misskey-bubble-game/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -/built -/coverage -/.eslintrc.js -/jest.config.ts -/test -/test-d -build.js diff --git a/packages/misskey-bubble-game/.eslintrc.cjs b/packages/misskey-bubble-game/.eslintrc.cjs deleted file mode 100644 index e2e31e9e331e0b135f991e53df66781ab4fbd15e..0000000000000000000000000000000000000000 --- a/packages/misskey-bubble-game/.eslintrc.cjs +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], -}; diff --git a/packages/misskey-bubble-game/build.js b/packages/misskey-bubble-game/build.js index 0b79f4b9159be3e153e39e6d092524f9b624adf5..e626c97a5967d6c20facd1cd107f46f28451a63a 100644 --- a/packages/misskey-bubble-game/build.js +++ b/packages/misskey-bubble-game/build.js @@ -95,7 +95,6 @@ async function watchSrc() { process.on('SIGHUP', resolve); process.on('SIGINT', resolve); process.on('SIGTERM', resolve); - process.on('SIGKILL', resolve); process.on('uncaughtException', reject); process.on('exit', resolve); }).finally(async () => { diff --git a/packages/misskey-bubble-game/eslint.config.js b/packages/misskey-bubble-game/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..86c21a22a3a78a7619a698d3d1c9200868e3e3a3 --- /dev/null +++ b/packages/misskey-bubble-game/eslint.config.js @@ -0,0 +1,27 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + ignores: [ + '**/node_modules', + 'built', + 'coverage', + 'jest.config.ts', + 'test', + 'test-d', + ], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json index a3aad147a98452b8caa126494fc0be70d865b7aa..528eb00b746066cc8c65f9e1aae1982653e60e46 100644 --- a/packages/misskey-bubble-game/package.json +++ b/packages/misskey-bubble-game/package.json @@ -17,18 +17,16 @@ "scripts": { "build": "node ./build.js", "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", - "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", + "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", "typecheck": "tsc --noEmit", "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "1.0.0", "@types/matter-js": "0.19.6", "@types/seedrandom": "3.0.8", "@types/node": "20.11.5", "@typescript-eslint/eslint-plugin": "7.1.0", "@typescript-eslint/parser": "7.1.0", - "eslint": "8.57.0", "nodemon": "3.0.2", "execa": "8.0.1", "typescript": "5.3.3", diff --git a/packages/misskey-js/.eslintignore b/packages/misskey-js/.eslintignore deleted file mode 100644 index 52ea8b3362e9e0ab8baf6e7daeb91a99bef2f424..0000000000000000000000000000000000000000 --- a/packages/misskey-js/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -/built -/coverage -/.eslintrc.js -/jest.config.ts -/test -/test-d -build.js diff --git a/packages/misskey-js/.eslintrc.cjs b/packages/misskey-js/.eslintrc.cjs deleted file mode 100644 index e2e31e9e331e0b135f991e53df66781ab4fbd15e..0000000000000000000000000000000000000000 --- a/packages/misskey-js/.eslintrc.cjs +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], -}; diff --git a/packages/misskey-js/README.md b/packages/misskey-js/README.md index 63d4b36c567c02c857cb28d5c8d45cf2167ced34..4753e2434b0c0137133f8f3ce32c5a6eb25ec0dd 100644 --- a/packages/misskey-js/README.md +++ b/packages/misskey-js/README.md @@ -154,5 +154,5 @@ stream.on('_disconnected_', () => { --- <div align="center"> - <a href="https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md"><img src="https://raw.githubusercontent.com/misskey-dev/assets/main/i-want-you.png" width="300"></a> + <a href="https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md"><img src="https://assets.misskey-hub.net/public/i-want-you.png" width="300"></a> </div> diff --git a/packages/misskey-js/build.js b/packages/misskey-js/build.js index 0b79f4b9159be3e153e39e6d092524f9b624adf5..a80b71646f43800c48d502dc828142c4e0bd367c 100644 --- a/packages/misskey-js/build.js +++ b/packages/misskey-js/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(); @@ -95,7 +95,6 @@ async function watchSrc() { process.on('SIGHUP', resolve); process.on('SIGINT', resolve); process.on('SIGTERM', resolve); - process.on('SIGKILL', resolve); process.on('uncaughtException', reject); process.on('exit', resolve); }).finally(async () => { diff --git a/packages/misskey-js/eslint.config.js b/packages/misskey-js/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..d8173f30e9ac53c6dfe8a1e379aa8cfc9b815fe8 --- /dev/null +++ b/packages/misskey-js/eslint.config.js @@ -0,0 +1,29 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../shared/eslint.config.js'; + +// eslint-disable-next-line import/no-default-export +export default [ + ...sharedConfig, + { + ignores: [ + '**/node_modules', + 'built', + 'coverage', + 'jest.config.ts', + 'test', + 'test-d', + 'generator', + ], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 1b72ec247b55d2ffae4a4dc285bbc4917b87260b..87fda5568eca67598eab813ea6476e5c99eab5f2 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -6,6 +6,11 @@ import { EventEmitter } from 'eventemitter3'; +// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient']; + // @public (undocumented) export type Acct = { username: string; @@ -21,13 +26,38 @@ declare namespace acct { } export { acct } -// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts -// // @public (undocumented) type Ad = components['schemas']['Ad']; // Warning: (ae-forgotten-export) The symbol "operations" needs to be exported by the entry point index.d.ts // +// @public (undocumented) +type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json']; + // @public (undocumented) type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json']; @@ -316,6 +346,33 @@ type AdminSilenceUserRequest = operations['admin___silence-user']['requestBody'] // @public (undocumented) type AdminSuspendUserRequest = operations['admin___suspend-user']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json']; + // @public (undocumented) type AdminUnnsfwUserRequest = operations['admin___unnsfw-user']['requestBody']['content']['application/json']; @@ -509,7 +566,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> = any> extends EventEmitter<Channel['events']> { +export abstract class ChannelConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> { constructor(stream: Stream, channel: string, name?: string); // (undocumented) channel: string; @@ -745,12 +802,12 @@ export type Channels = { user1: boolean; user2: boolean; }) => void; - updateSettings: (payload: { + updateSettings: <K extends ReversiUpdateKey>(payload: { userId: User['id']; - key: string; - value: any; + key: K; + value: ReversiGameDetailed[K]; }) => void; - log: (payload: Record<string, any>) => void; + log: (payload: Record<string, unknown>) => void; }; receives: { putStone: { @@ -759,10 +816,7 @@ export type Channels = { }; ready: boolean; cancel: null | Record<string, never>; - updateSettings: { - key: string; - value: any; - }; + updateSettings: ReversiUpdateSettings<ReversiUpdateKey>; claimTimeIsUp: null | Record<string, never>; }; }; @@ -1134,6 +1188,12 @@ export type Endpoints = Overwrite<Endpoints_2, { req: SigninRequest; res: SigninResponse; }; + 'admin/roles/create': { + req: Overwrite<AdminRolesCreateRequest, { + policies: PartialRolePolicyOverride; + }>; + res: AdminRolesCreateResponse; + }; }>; // @public (undocumented) @@ -1143,6 +1203,7 @@ declare namespace entities { export { ID, DateString, + PureRenote, PageEvent, ModerationLog, ServerStats, @@ -1159,11 +1220,21 @@ declare namespace entities { SignupPendingResponse, SigninRequest, SigninResponse, + PartialRolePolicyOverride, EmptyRequest, EmptyResponse, AdminMetaResponse, AdminAbuseUserReportsRequest, AdminAbuseUserReportsResponse, + AdminAbuseReportNotificationRecipientListRequest, + AdminAbuseReportNotificationRecipientListResponse, + AdminAbuseReportNotificationRecipientShowRequest, + AdminAbuseReportNotificationRecipientShowResponse, + AdminAbuseReportNotificationRecipientCreateRequest, + AdminAbuseReportNotificationRecipientCreateResponse, + AdminAbuseReportNotificationRecipientUpdateRequest, + AdminAbuseReportNotificationRecipientUpdateResponse, + AdminAbuseReportNotificationRecipientDeleteRequest, AdminAccountsCreateRequest, AdminAccountsCreateResponse, AdminAccountsDeleteRequest, @@ -1264,6 +1335,15 @@ declare namespace entities { AdminRolesUpdateDefaultPoliciesRequest, AdminRolesUsersRequest, AdminRolesUsersResponse, + AdminSystemWebhookCreateRequest, + AdminSystemWebhookCreateResponse, + AdminSystemWebhookDeleteRequest, + AdminSystemWebhookListRequest, + AdminSystemWebhookListResponse, + AdminSystemWebhookShowRequest, + AdminSystemWebhookShowResponse, + AdminSystemWebhookUpdateRequest, + AdminSystemWebhookUpdateResponse, AnnouncementsRequest, AnnouncementsResponse, AnnouncementsShowRequest, @@ -1779,7 +1859,9 @@ declare namespace entities { ReversiGameDetailed, MetaLite, MetaDetailedOnly, - MetaDetailed + MetaDetailed, + SystemWebhook, + AbuseReportNotificationRecipient } } export { entities } @@ -1838,7 +1920,7 @@ type FetchExternalResourcesResponse = operations['fetch-external-resources']['re // @public (undocumented) type FetchLike = (input: string, init?: { method?: string; - body?: string; + body?: Blob | FormData | string; credentials?: RequestCredentials; cache?: RequestCache; headers: { @@ -2248,6 +2330,9 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co // @public (undocumented) type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; +// @public (undocumented) +function isPureRenote(note: Note): note is PureRenote; + // @public (undocumented) type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; @@ -2437,10 +2522,40 @@ type ModerationLog = { } | { type: 'unsetUserBanner'; info: ModerationLogPayloads['unsetUserBanner']; +} | { + type: 'createSystemWebhook'; + info: ModerationLogPayloads['createSystemWebhook']; +} | { + type: 'updateSystemWebhook'; + info: ModerationLogPayloads['updateSystemWebhook']; +} | { + type: 'deleteSystemWebhook'; + info: ModerationLogPayloads['deleteSystemWebhook']; +} | { + type: 'createAbuseReportNotificationRecipient'; + info: ModerationLogPayloads['createAbuseReportNotificationRecipient']; +} | { + type: 'updateAbuseReportNotificationRecipient'; + info: ModerationLogPayloads['updateAbuseReportNotificationRecipient']; +} | { + type: 'deleteAbuseReportNotificationRecipient'; + info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient']; +} | { + type: 'deleteAccount'; + info: ModerationLogPayloads['deleteAccount']; +} | { + type: 'deletePage'; + info: ModerationLogPayloads['deletePage']; +} | { + type: 'deleteFlash'; + info: ModerationLogPayloads['deleteFlash']; +} | { + type: 'deleteGalleryPost'; + info: ModerationLogPayloads['deleteGalleryPost']; }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "approve", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"]; // @public (undocumented) type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; @@ -2469,6 +2584,13 @@ type MyAppsResponse = operations['my___apps']['responses']['200']['content']['ap // @public (undocumented) type Note = components['schemas']['Note']; +declare namespace note { + export { + isPureRenote + } +} +export { note } + // @public (undocumented) type NoteFavorite = components['schemas']['NoteFavorite']; @@ -2707,7 +2829,16 @@ type PagesUnlikeRequest = operations['pages___unlike']['requestBody']['content'] type PagesUpdateRequest = operations['pages___update']['requestBody']['content']['application/json']; // @public (undocumented) -function parse(acct: string): Acct; +function parse(_acct: string): Acct; + +// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PartialRolePolicyOverride = Partial<{ + [k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & { + value: RolePolicies[k]; + }; +}>; // @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"]; @@ -2721,6 +2852,15 @@ type PinnedUsersResponse = operations['pinned-users']['responses']['200']['conte // @public (undocumented) type PromoReadRequest = operations['promo___read']['requestBody']['content']['application/json']; +// Warning: (ae-forgotten-export) The symbol "AllNullRecord" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "NonNullableRecord" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> & { + files: []; + fileIds: []; +} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>; + // @public (undocumented) type QueueCount = components['schemas']['QueueCount']; @@ -2800,6 +2940,9 @@ type ReversiShowGameResponse = operations['reversi___show-game']['responses']['2 // @public (undocumented) type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json']; +// @public (undocumented) +export const reversiUpdateKeys: ["map", "bw", "isLlotheo", "canPutEverywhere", "loopedBoard", "timeLimitForEachTurn"]; + // @public (undocumented) type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json']; @@ -2940,7 +3083,7 @@ export class Stream extends EventEmitter<StreamEvents> { constructor(origin: string, user: { token: string; } | null, options?: { - WebSocket?: any; + WebSocket?: WebSocket; }); // (undocumented) close(): void; @@ -2963,9 +3106,9 @@ export class Stream extends EventEmitter<StreamEvents> { // (undocumented) send(typeOrPayload: string): void; // (undocumented) - send(typeOrPayload: string, payload: any): void; + send(typeOrPayload: string, payload: unknown): void; // (undocumented) - send(typeOrPayload: Record<string, any> | any[]): void; + send(typeOrPayload: Record<string, unknown> | unknown[]): void; // (undocumented) state: 'initializing' | 'reconnecting' | 'connected'; // (undocumented) @@ -3000,6 +3143,9 @@ type SwUpdateRegistrationRequest = operations['sw___update-registration']['reque // @public (undocumented) type SwUpdateRegistrationResponse = operations['sw___update-registration']['responses']['200']['content']['application/json']; +// @public (undocumented) +type SystemWebhook = components['schemas']['SystemWebhook']; + // @public (undocumented) type TestRequest = operations['test']['requestBody']['content']['application/json']; @@ -3197,7 +3343,9 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody'][' // Warnings were encountered during analysis: // -// src/entities.ts:25:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:49:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/streaming.types.ts:236:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts +// src/streaming.types.ts:246:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/generator/.eslintrc.cjs b/packages/misskey-js/generator/.eslintrc.cjs deleted file mode 100644 index 6a8b31da9cc6e465b561e9a517ca14d476f61706..0000000000000000000000000000000000000000 --- a/packages/misskey-js/generator/.eslintrc.cjs +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../../shared/.eslintrc.js', - ], -}; diff --git a/packages/misskey-js/generator/eslint.config.js b/packages/misskey-js/generator/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..4bf78c3b910d66ad2eb907f929575e1a3a7d23c2 --- /dev/null +++ b/packages/misskey-js/generator/eslint.config.js @@ -0,0 +1,17 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json index a1c0f41cb28526946b06d2276decf9edc3afc5e4..4a02bcd8ff7cb4c19498728cb4bd87bbb37924fa 100644 --- a/packages/misskey-js/generator/package.json +++ b/packages/misskey-js/generator/package.json @@ -4,15 +4,13 @@ "description": "Misskey TypeGenerator", "type": "module", "scripts": { - "generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix" + "generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "^1.0.0", "@readme/openapi-parser": "2.5.0", "@types/node": "20.9.1", "@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/parser": "6.11.0", - "eslint": "8.53.0", "openapi-types": "12.1.3", "openapi-typescript": "6.7.3", "ts-case-convert": "2.0.2", diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts index 78178d7c7ed4406f8d5802ae0e3dcfcb4ee18ea2..4ae00a4522db6ccfa8adb3608e8cc789d27080ad 100644 --- a/packages/misskey-js/generator/src/generator.ts +++ b/packages/misskey-js/generator/src/generator.ts @@ -20,7 +20,14 @@ async function generateBaseTypes( } lines.push(''); - const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true }); + const generatedTypes = await openapiTS(openApiJsonPath, { + exportType: true, + transform(schemaObject) { + if ('format' in schemaObject && schemaObject.format === 'binary') { + return schemaObject.nullable ? 'Blob | null' : 'Blob'; + } + }, + }); lines.push(generatedTypes); lines.push(''); @@ -56,6 +63,8 @@ async function generateEndpoints( endpointOutputPath: string, ) { const endpoints: Endpoint[] = []; + const endpointReqMediaTypes: EndpointReqMediaType[] = []; + const endpointReqMediaTypesSet = new Set<string>(); // misskey-jsã¯POST固定ã§é€ã£ã¦ã„ã‚‹ã®ã§ã€ã“ã¡ã‚‰ã‚‚決ã‚打ã¡ã™ã‚‹ã€‚別メソッドã«å¯¾å¿œã™ã‚‹ã“ã¨ãŒã‚ã‚Œã°ã“ã¡ã‚‰ã‚‚ç›´ã™å¿…è¦ã‚ã‚Š const paths = openApiDocs.paths ?? {}; @@ -78,13 +87,24 @@ async function generateEndpoints( const supportMediaTypes = Object.keys(reqContent); if (supportMediaTypes.length > 0) { // ã„ã¾ã®ã¨ã“ã‚複数ã®ãƒ¡ãƒ‡ã‚£ã‚¢ã‚¿ã‚¤ãƒ—ã‚’ã¨ã‚‹ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã¯ç„¡ã„ã®ã§æ±ºã‚打ã¡ã™ã‚‹ - endpoint.request = new OperationTypeAlias( + const req = new OperationTypeAlias( operationId, path, supportMediaTypes[0], OperationsAliasType.REQUEST, ); + 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')); } + } else { + endpointReqMediaTypesSet.add('application/json'); + endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); } if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) { @@ -137,6 +157,19 @@ async function generateEndpoints( endpointOutputLine.push('}'); endpointOutputLine.push(''); + function generateEndpointReqMediaTypesType() { + return `Record<keyof Endpoints, ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}>`; + } + + endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`); + + endpointOutputLine.push( + ...endpointReqMediaTypes.map(it => '\t' + it.toLine()), + ); + + endpointOutputLine.push('};'); + endpointOutputLine.push(''); + await writeFile(endpointOutputPath, endpointOutputLine.join('\n')); } @@ -314,6 +347,26 @@ class Endpoint { } } +class EndpointReqMediaType { + public readonly path: string; + public readonly mediaType: string; + + constructor(path: string, request: OperationTypeAlias, mediaType?: undefined); + constructor(path: string, request: undefined, mediaType: string); + constructor(path: string, request: OperationTypeAlias | undefined, mediaType?: string) { + this.path = path; + this.mediaType = mediaType ?? request?.mediaType ?? 'application/json'; + } + + getMediaType(): string { + return this.mediaType; + } + + toLine(): string { + return `'${this.path}': '${this.mediaType}',`; + } +} + async function main() { const generatePath = './built/autogen'; await mkdir(generatePath, { recursive: true }); diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 4ff1a57309a76097c4cf7d71390b64385882bee6..39e687d4af34f29765a6b752bf23c0e8b91f66a7 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.5.0", + "version": "2024.8.0", "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 . --ext .js,.jsx,.ts,.tsx", + "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", "typecheck": "tsc --noEmit", "lint": "pnpm typecheck && pnpm eslint", "jest": "jest --coverage --detectOpenHandles", @@ -35,25 +35,23 @@ "directory": "packages/misskey-js" }, "devDependencies": { - "@microsoft/api-extractor": "7.43.1", - "@misskey-dev/eslint-plugin": "1.0.0", + "@microsoft/api-extractor": "7.47.4", "@swc/jest": "0.2.36", "@types/jest": "29.5.12", - "@types/node": "20.12.7", - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", - "eslint": "8.57.0", + "@types/node": "20.14.12", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", "jest": "29.7.0", "jest-fetch-mock": "3.0.3", "jest-websocket-mock": "2.5.0", "mock-socket": "9.3.1", "ncp": "2.0.0", - "nodemon": "3.1.0", - "execa": "8.0.1", - "tsd": "0.30.7", - "typescript": "5.4.5", - "esbuild": "0.19.11", - "glob": "10.3.12" + "nodemon": "3.1.4", + "execa": "9.3.0", + "tsd": "0.31.1", + "typescript": "5.5.4", + "esbuild": "0.23.0", + "glob": "11.0.0" }, "files": [ "built" diff --git a/packages/misskey-js/src/acct.ts b/packages/misskey-js/src/acct.ts index b25bc564ea7d8e1657bd5828483dcfd57c0680c3..aa8658cdbdf4d72f70c01972f1465014512375c7 100644 --- a/packages/misskey-js/src/acct.ts +++ b/packages/misskey-js/src/acct.ts @@ -3,7 +3,8 @@ export type Acct = { host: string | null; }; -export function parse(acct: string): Acct { +export function parse(_acct: string): Acct { + let acct = _acct; if (acct.startsWith('@')) acct = acct.substring(1); const split = acct.split('@', 2); return { username: split[0], host: split[1] || null }; diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts index 959a634a743f6ad8ea56c8059a95cb89320bd467..ea1df57f3d77cf10564b949a2f235d9025de9eac 100644 --- a/packages/misskey-js/src/api.ts +++ b/packages/misskey-js/src/api.ts @@ -1,7 +1,7 @@ import './autogen/apiClientJSDoc.js'; -import { SwitchCaseResponseType } from './api.types.js'; -import type { Endpoints } from './api.types.js'; +import { endpointReqTypes } from './autogen/endpoint.js'; +import type { SwitchCaseResponseType, Endpoints } from './api.types.js'; export type { SwitchCaseResponseType, @@ -14,6 +14,7 @@ export type APIError = { code: string; message: string; kind: 'client' | 'server'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any info: Record<string, any>; }; @@ -23,12 +24,13 @@ export function isAPIError(reason: Record<PropertyKey, unknown>): reason is APIE export type FetchLike = (input: string, init?: { method?: string; - body?: string; + body?: Blob | FormData | string; credentials?: RequestCredentials; cache?: RequestCache; headers: { [key in string]: string } }) => Promise<{ status: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any json(): Promise<any>; }>; @@ -49,20 +51,56 @@ export class APIClient { this.fetch = opts.fetch ?? ((...args) => fetch(...args)); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private assertIsRecord<T>(obj: T): obj is T & Record<string, any> { + return obj !== null && typeof obj === 'object' && !Array.isArray(obj); + } + public request<E extends keyof Endpoints, P extends Endpoints[E]['req']>( endpoint: E, params: P = {} as P, credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>> { return new Promise((resolve, reject) => { - this.fetch(`${this.origin}/api/${endpoint}`, { - method: 'POST', - body: JSON.stringify({ + let mediaType = 'application/json'; + if (endpoint in endpointReqTypes) { + mediaType = endpointReqTypes[endpoint]; + } + let payload: FormData | string = '{}'; + + if (mediaType === 'application/json') { + payload = JSON.stringify({ ...params, i: credential !== undefined ? credential : this.credential, - }), + }); + } else if (mediaType === 'multipart/form-data') { + payload = new FormData(); + const i = credential !== undefined ? credential : this.credential; + if (i != null) { + payload.append('i', i); + } + if (this.assertIsRecord(params)) { + for (const key in params) { + const value = params[key]; + + if (value == null) continue; + + if (value instanceof File || value instanceof Blob) { + payload.append(key, value); + } else if (typeof value === 'object') { + payload.append(key, JSON.stringify(value)); + } else { + payload.append(key, value); + } + } + } + } + + this.fetch(`${this.origin}/api/${endpoint}`, { + method: 'POST', + body: payload, headers: { - 'Content-Type': 'application/json', + 'Content-Type': endpointReqTypes[endpoint], }, credentials: 'omit', cache: 'no-cache', diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index af0bade5b3b41861aca32adcf71975dd8a3b694f..5ee4194db213b0a0122bcf9f36152d666c9f92d0 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -1,7 +1,8 @@ import { Endpoints as Gen } from './autogen/endpoint.js'; import { UserDetailed } from './autogen/models.js'; -import { UsersShowRequest } from './autogen/entities.js'; +import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js'; import { + PartialRolePolicyOverride, SigninRequest, SigninResponse, SignupPendingRequest, @@ -27,11 +28,13 @@ type StrictExtract<Union, Cond> = Cond extends Union ? Union : never; type IsCaseMatched<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = Endpoints[E]['res'] extends SwitchCase + // eslint-disable-next-line @typescript-eslint/no-explicit-any ? IsNeverType<StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>> extends false ? true : false : false type GetCaseResult<E extends keyof Endpoints, P extends Endpoints[E]['req'], C extends number> = Endpoints[E]['res'] extends SwitchCase + // eslint-disable-next-line @typescript-eslint/no-explicit-any ? StrictExtract<Endpoints[E]['res']['$switch']['$cases'][C], [P, any]>[1] : never @@ -79,5 +82,9 @@ export type Endpoints = Overwrite< req: SigninRequest; res: SigninResponse; }, + '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 68137b103ec83467be1593abfbab91c90cfe2e63..c13485621b5709b631aac1e96ba079547bbbc4ad 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -25,6 +25,66 @@ 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:abuse-report:notification-recipient* + */ + request<E extends 'admin/abuse-report/notification-recipient/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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:abuse-report:notification-recipient* + */ + request<E extends 'admin/abuse-report/notification-recipient/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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**: *write:admin:abuse-report:notification-recipient* + */ + request<E extends 'admin/abuse-report/notification-recipient/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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**: *write:admin:abuse-report:notification-recipient* + */ + request<E extends 'admin/abuse-report/notification-recipient/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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**: *write:admin:abuse-report:notification-recipient* + */ + request<E extends 'admin/abuse-report/notification-recipient/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * @@ -895,6 +955,66 @@ 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**: *write:admin:system-webhook* + */ + request<E extends 'admin/system-webhook/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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**: *write:admin:system-webhook* + */ + request<E extends 'admin/system-webhook/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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**: *write:admin:system-webhook* + */ + request<E extends 'admin/system-webhook/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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**: *write:admin:system-webhook* + */ + request<E extends 'admin/system-webhook/show', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + 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**: *write:admin:system-webhook* + */ + request<E extends 'admin/system-webhook/update', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 9f0ff8364c0ca18a0b380544e1ea8d150a01e46f..628825d5044c7f0dae683a8aafe0e833a80583b8 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -4,6 +4,15 @@ import type { AdminMetaResponse, AdminAbuseUserReportsRequest, AdminAbuseUserReportsResponse, + AdminAbuseReportNotificationRecipientListRequest, + AdminAbuseReportNotificationRecipientListResponse, + AdminAbuseReportNotificationRecipientShowRequest, + AdminAbuseReportNotificationRecipientShowResponse, + AdminAbuseReportNotificationRecipientCreateRequest, + AdminAbuseReportNotificationRecipientCreateResponse, + AdminAbuseReportNotificationRecipientUpdateRequest, + AdminAbuseReportNotificationRecipientUpdateResponse, + AdminAbuseReportNotificationRecipientDeleteRequest, AdminAccountsCreateRequest, AdminAccountsCreateResponse, AdminAccountsDeleteRequest, @@ -104,6 +113,15 @@ import type { AdminRolesUpdateDefaultPoliciesRequest, AdminRolesUsersRequest, AdminRolesUsersResponse, + AdminSystemWebhookCreateRequest, + AdminSystemWebhookCreateResponse, + AdminSystemWebhookDeleteRequest, + AdminSystemWebhookListRequest, + AdminSystemWebhookListResponse, + AdminSystemWebhookShowRequest, + AdminSystemWebhookShowResponse, + AdminSystemWebhookUpdateRequest, + AdminSystemWebhookUpdateResponse, AnnouncementsRequest, AnnouncementsResponse, AnnouncementsShowRequest, @@ -573,6 +591,11 @@ import type { export type Endpoints = { 'admin/meta': { req: EmptyRequest; res: AdminMetaResponse }; 'admin/abuse-user-reports': { req: AdminAbuseUserReportsRequest; res: AdminAbuseUserReportsResponse }; + 'admin/abuse-report/notification-recipient/list': { req: AdminAbuseReportNotificationRecipientListRequest; res: AdminAbuseReportNotificationRecipientListResponse }; + 'admin/abuse-report/notification-recipient/show': { req: AdminAbuseReportNotificationRecipientShowRequest; res: AdminAbuseReportNotificationRecipientShowResponse }; + 'admin/abuse-report/notification-recipient/create': { req: AdminAbuseReportNotificationRecipientCreateRequest; res: AdminAbuseReportNotificationRecipientCreateResponse }; + 'admin/abuse-report/notification-recipient/update': { req: AdminAbuseReportNotificationRecipientUpdateRequest; res: AdminAbuseReportNotificationRecipientUpdateResponse }; + 'admin/abuse-report/notification-recipient/delete': { req: AdminAbuseReportNotificationRecipientDeleteRequest; res: EmptyResponse }; 'admin/accounts/create': { req: AdminAccountsCreateRequest; res: AdminAccountsCreateResponse }; 'admin/accounts/delete': { req: AdminAccountsDeleteRequest; res: EmptyResponse }; 'admin/accounts/find-by-email': { req: AdminAccountsFindByEmailRequest; res: AdminAccountsFindByEmailResponse }; @@ -652,6 +675,11 @@ export type Endpoints = { 'admin/roles/unassign': { req: AdminRolesUnassignRequest; res: EmptyResponse }; 'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse }; 'admin/roles/users': { req: AdminRolesUsersRequest; res: AdminRolesUsersResponse }; + 'admin/system-webhook/create': { req: AdminSystemWebhookCreateRequest; res: AdminSystemWebhookCreateResponse }; + 'admin/system-webhook/delete': { req: AdminSystemWebhookDeleteRequest; res: EmptyResponse }; + 'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse }; + 'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse }; + 'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse }; 'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse }; 'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse }; 'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse }; @@ -954,3 +982,398 @@ export type Endpoints = { 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse }; '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', + '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', +}; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 356cafae34e9d8e944494ec2fc4d4e23db921223..0228ad47e60e6a3e1c10b1f707f3fbed95c162c3 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -7,6 +7,15 @@ export type EmptyResponse = Record<string, unknown> | undefined; export type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json']; export type AdminAbuseUserReportsRequest = operations['admin___abuse-user-reports']['requestBody']['content']['application/json']; export type AdminAbuseUserReportsResponse = operations['admin___abuse-user-reports']['responses']['200']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientListRequest = operations['admin___abuse-report___notification-recipient___list']['requestBody']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientListResponse = operations['admin___abuse-report___notification-recipient___list']['responses']['200']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientShowRequest = operations['admin___abuse-report___notification-recipient___show']['requestBody']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientShowResponse = operations['admin___abuse-report___notification-recipient___show']['responses']['200']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientCreateRequest = operations['admin___abuse-report___notification-recipient___create']['requestBody']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientCreateResponse = operations['admin___abuse-report___notification-recipient___create']['responses']['200']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientUpdateRequest = operations['admin___abuse-report___notification-recipient___update']['requestBody']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientUpdateResponse = operations['admin___abuse-report___notification-recipient___update']['responses']['200']['content']['application/json']; +export type AdminAbuseReportNotificationRecipientDeleteRequest = operations['admin___abuse-report___notification-recipient___delete']['requestBody']['content']['application/json']; export type AdminAccountsCreateRequest = operations['admin___accounts___create']['requestBody']['content']['application/json']; export type AdminAccountsCreateResponse = operations['admin___accounts___create']['responses']['200']['content']['application/json']; export type AdminAccountsDeleteRequest = operations['admin___accounts___delete']['requestBody']['content']['application/json']; @@ -107,6 +116,15 @@ export type AdminRolesUnassignRequest = operations['admin___roles___unassign'][' export type AdminRolesUpdateDefaultPoliciesRequest = operations['admin___roles___update-default-policies']['requestBody']['content']['application/json']; export type AdminRolesUsersRequest = operations['admin___roles___users']['requestBody']['content']['application/json']; export type AdminRolesUsersResponse = operations['admin___roles___users']['responses']['200']['content']['application/json']; +export type AdminSystemWebhookCreateRequest = operations['admin___system-webhook___create']['requestBody']['content']['application/json']; +export type AdminSystemWebhookCreateResponse = operations['admin___system-webhook___create']['responses']['200']['content']['application/json']; +export type AdminSystemWebhookDeleteRequest = operations['admin___system-webhook___delete']['requestBody']['content']['application/json']; +export type AdminSystemWebhookListRequest = operations['admin___system-webhook___list']['requestBody']['content']['application/json']; +export type AdminSystemWebhookListResponse = operations['admin___system-webhook___list']['responses']['200']['content']['application/json']; +export type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']['requestBody']['content']['application/json']; +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 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']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index a6e5fbe6890e3678fbabc84b2a4bc4502ff76cce..04574849d44de0aadb05d652f5f7361120c54c58 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -51,3 +51,5 @@ export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; export type MetaLite = components['schemas']['MetaLite']; export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly']; export type MetaDetailed = components['schemas']['MetaDetailed']; +export type SystemWebhook = components['schemas']['SystemWebhook']; +export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index f2d86d2ca5bc2e64c3350fcbff2ebd112a775791..562c4981909a544728764f74eaf622d129add204 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -30,6 +30,56 @@ export type paths = { */ post: operations['admin___abuse-user-reports']; }; + '/admin/abuse-report/notification-recipient/list': { + /** + * admin/abuse-report/notification-recipient/list + * @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:abuse-report:notification-recipient* + */ + post: operations['admin___abuse-report___notification-recipient___list']; + }; + '/admin/abuse-report/notification-recipient/show': { + /** + * admin/abuse-report/notification-recipient/show + * @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:abuse-report:notification-recipient* + */ + post: operations['admin___abuse-report___notification-recipient___show']; + }; + '/admin/abuse-report/notification-recipient/create': { + /** + * admin/abuse-report/notification-recipient/create + * @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**: *write:admin:abuse-report:notification-recipient* + */ + post: operations['admin___abuse-report___notification-recipient___create']; + }; + '/admin/abuse-report/notification-recipient/update': { + /** + * admin/abuse-report/notification-recipient/update + * @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**: *write:admin:abuse-report:notification-recipient* + */ + post: operations['admin___abuse-report___notification-recipient___update']; + }; + '/admin/abuse-report/notification-recipient/delete': { + /** + * admin/abuse-report/notification-recipient/delete + * @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**: *write:admin:abuse-report:notification-recipient* + */ + post: operations['admin___abuse-report___notification-recipient___delete']; + }; '/admin/accounts/create': { /** * admin/accounts/create @@ -742,6 +792,56 @@ export type paths = { */ post: operations['admin___roles___users']; }; + '/admin/system-webhook/create': { + /** + * admin/system-webhook/create + * @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**: *write:admin:system-webhook* + */ + post: operations['admin___system-webhook___create']; + }; + '/admin/system-webhook/delete': { + /** + * admin/system-webhook/delete + * @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**: *write:admin:system-webhook* + */ + post: operations['admin___system-webhook___delete']; + }; + '/admin/system-webhook/list': { + /** + * admin/system-webhook/list + * @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**: *write:admin:system-webhook* + */ + post: operations['admin___system-webhook___list']; + }; + '/admin/system-webhook/show': { + /** + * admin/system-webhook/show + * @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**: *write:admin:system-webhook* + */ + post: operations['admin___system-webhook___show']; + }; + '/admin/system-webhook/update': { + /** + * admin/system-webhook/update + * @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**: *write:admin:system-webhook* + */ + post: operations['admin___system-webhook___update']; + }; '/announcements': { /** * announcements @@ -4121,7 +4221,8 @@ export type components = { userId: string | null; }) | null; localOnly?: boolean; - reactionAcceptance: string | null; + /** @enum {string|null} */ + reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; reactionEmojis: { [key: string]: string; }; @@ -4342,7 +4443,7 @@ export type components = { id: string; /** Format: date-time */ createdAt: string; - /** @example lenna.jpg */ + /** @example 192.jpg */ name: string; /** @example image/jpeg */ type: string; @@ -4641,6 +4742,7 @@ export type components = { maintainerName: string | null; maintainerEmail: string | null; isSilenced: boolean; + isMediaSilenced: boolean; /** Format: url */ iconUrl: string | null; /** Format: url */ @@ -4714,6 +4816,8 @@ export type components = { title: string; summary: string; script: string; + /** @enum {string} */ + visibility: 'private' | 'public'; likedCount: number | null; isLiked?: boolean; }; @@ -4831,6 +4935,7 @@ export type components = { canHideAds: boolean; driveCapacityMb: number; alwaysMarkNsfw: boolean; + canUpdateBioMedia: boolean; pinLimit: number; antennaLimit: number; wordMuteLimit: number; @@ -4986,6 +5091,11 @@ export type components = { serverRules: string[]; themeColor: string | null; policies: components['schemas']['RolePolicies']; + /** + * @default local + * @enum {string} + */ + noteSearchableScope: 'local' | 'global'; }; MetaDetailedOnly: { features?: { @@ -5008,6 +5118,32 @@ export type components = { cacheRemoteSensitiveFiles: boolean; }; MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly']; + SystemWebhook: { + id: string; + isActive: boolean; + /** Format: date-time */ + updatedAt: string; + /** Format: date-time */ + latestSentAt: string | null; + latestStatus: number | null; + name: string; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; + url: string; + secret: string; + }; + AbuseReportNotificationRecipient: { + id: string; + isActive: boolean; + /** Format: date-time */ + updatedAt: string; + name: string; + /** @enum {string} */ + method: 'email' | 'webhook'; + userId?: string; + user?: components['schemas']['UserLite']; + systemWebhookId?: string; + systemWebhook?: components['schemas']['SystemWebhook']; + }; }; responses: never; parameters: never; @@ -5061,6 +5197,7 @@ export type operations = { enableServiceWorker: boolean; translatorAvailable: boolean; silencedHosts?: string[]; + mediaSilencedHosts: string[]; pinnedUsers: string[]; hiddenTags: string[]; blockedHosts: string[]; @@ -5282,17 +5419,17 @@ export type operations = { }; }; /** - * admin/accounts/create + * admin/abuse-report/notification-recipient/list * @description No description provided. * - * **Credential required**: *No* + * **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:abuse-report:notification-recipient* */ - admin___accounts___create: { + 'admin___abuse-report___notification-recipient___list': { requestBody: { content: { 'application/json': { - username: string; - password: string; + method?: ('email' | 'webhook')[]; }; }; }; @@ -5300,7 +5437,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['MeDetailed']; + 'application/json': components['schemas']['AbuseReportNotificationRecipient'][]; }; }; /** @description Client error */ @@ -5336,24 +5473,27 @@ export type operations = { }; }; /** - * admin/accounts/delete + * admin/abuse-report/notification-recipient/show * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:account* + * **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:abuse-report:notification-recipient* */ - admin___accounts___delete: { + 'admin___abuse-report___notification-recipient___show': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - userId: string; + id: string; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['AbuseReportNotificationRecipient']; + }; }; /** @description Client error */ 400: { @@ -5388,16 +5528,24 @@ export type operations = { }; }; /** - * admin/accounts/find-by-email + * admin/abuse-report/notification-recipient/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:account* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* */ - 'admin___accounts___find-by-email': { + 'admin___abuse-report___notification-recipient___create': { requestBody: { content: { 'application/json': { - email: string; + isActive: boolean; + name: string; + /** @enum {string} */ + method: 'email' | 'webhook'; + /** Format: misskey:id */ + userId?: string; + /** Format: misskey:id */ + systemWebhookId?: string; }; }; }; @@ -5405,7 +5553,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['UserDetailedNotMe']; + 'application/json': components['schemas']['AbuseReportNotificationRecipient']; }; }; /** @description Client error */ @@ -5441,24 +5589,26 @@ export type operations = { }; }; /** - * admin/ad/create + * admin/abuse-report/notification-recipient/update * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:ad* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* */ - admin___ad___create: { + 'admin___abuse-report___notification-recipient___update': { requestBody: { content: { 'application/json': { - url: string; - memo: string; - place: string; - priority: string; - ratio: number; - expiresAt: number; - startsAt: number; - imageUrl: string; - dayOfWeek: number; + /** Format: misskey:id */ + id: string; + isActive: boolean; + name: string; + /** @enum {string} */ + method: 'email' | 'webhook'; + /** Format: misskey:id */ + userId?: string; + /** Format: misskey:id */ + systemWebhookId?: string; }; }; }; @@ -5466,7 +5616,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['Ad']; + 'application/json': components['schemas']['AbuseReportNotificationRecipient']; }; }; /** @description Client error */ @@ -5502,12 +5652,13 @@ export type operations = { }; }; /** - * admin/ad/delete + * admin/abuse-report/notification-recipient/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:ad* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:abuse-report:notification-recipient* */ - admin___ad___delete: { + 'admin___abuse-report___notification-recipient___delete': { requestBody: { content: { 'application/json': { @@ -5554,23 +5705,17 @@ export type operations = { }; }; /** - * admin/ad/list + * admin/accounts/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:ad* + * **Credential required**: *No* */ - admin___ad___list: { + admin___accounts___create: { requestBody: { content: { 'application/json': { - /** @default 10 */ - limit?: number; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; - /** @default null */ - publishing?: boolean | null; + username: string; + password: string; }; }; }; @@ -5578,7 +5723,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['Ad'][]; + 'application/json': components['schemas']['MeDetailed']; }; }; /** @description Client error */ @@ -5614,26 +5759,17 @@ export type operations = { }; }; /** - * admin/ad/update + * admin/accounts/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:ad* + * **Credential required**: *Yes* / **Permission**: *write:admin:account* */ - admin___ad___update: { + admin___accounts___delete: { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - id: string; - memo: string; - url: string; - imageUrl: string; - place: string; - priority: string; - ratio: number; - expiresAt: number; - startsAt: number; - dayOfWeek: number; + userId: string; }; }; }; @@ -5675,39 +5811,16 @@ export type operations = { }; }; /** - * admin/announcements/create + * admin/accounts/find-by-email * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *read:admin:account* */ - admin___announcements___create: { + 'admin___accounts___find-by-email': { requestBody: { content: { 'application/json': { - title: string; - text: string; - imageUrl: string | null; - /** - * @default info - * @enum {string} - */ - icon?: 'info' | 'warning' | 'error' | 'success'; - /** - * @default normal - * @enum {string} - */ - display?: 'normal' | 'banner' | 'dialog'; - /** @default false */ - forExistingUsers?: boolean; - /** @default false */ - silence?: boolean; - /** @default false */ - needConfirmationToRead?: boolean; - /** - * Format: misskey:id - * @default null - */ - userId?: string | null; + email: string; }; }; }; @@ -5715,20 +5828,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string | null; - title: string; - text: string; - imageUrl: string | null; - }; + 'application/json': components['schemas']['UserDetailedNotMe']; }; }; /** @description Client error */ @@ -5764,24 +5864,33 @@ export type operations = { }; }; /** - * admin/announcements/delete + * admin/ad/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - admin___announcements___delete: { + admin___ad___create: { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - id: string; + url: string; + memo: string; + place: string; + priority: string; + ratio: number; + expiresAt: number; + startsAt: number; + imageUrl: string; + dayOfWeek: number; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Ad']; + }; }; /** @description Client error */ 400: { @@ -5816,45 +5925,83 @@ export type operations = { }; }; /** - * admin/announcements/list + * admin/ad/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - admin___announcements___list: { + admin___ad___delete: { requestBody: { content: { 'application/json': { - /** @default 10 */ - limit?: number; /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; - /** Format: misskey:id */ - userId?: string | null; + id: string; }; }; }; responses: { - /** @description OK (with results) */ - 200: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { content: { - 'application/json': ({ - /** - * Format: id - * @example xxxxxxxxxx - */ - id: string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string | null; - text: string; - title: string; - imageUrl: string | null; - reads: number; - })[]; + '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/ad/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:ad* + */ + admin___ad___list: { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default null */ + publishing?: boolean | null; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Ad'][]; }; }; /** @description Client error */ @@ -5890,28 +6037,26 @@ export type operations = { }; }; /** - * admin/announcements/update + * admin/ad/update * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* + * **Credential required**: *Yes* / **Permission**: *write:admin:ad* */ - admin___announcements___update: { + admin___ad___update: { requestBody: { content: { 'application/json': { /** Format: misskey:id */ id: string; - title?: string; - text?: string; - imageUrl?: string | null; - /** @enum {string} */ - icon?: 'info' | 'warning' | 'error' | 'success'; - /** @enum {string} */ - display?: 'normal' | 'banner' | 'dialog'; - forExistingUsers?: boolean; - silence?: boolean; - needConfirmationToRead?: boolean; - isActive?: boolean; + memo?: string; + url?: string; + imageUrl?: string; + place?: string; + priority?: string; + ratio?: number; + expiresAt?: number; + startsAt?: number; + dayOfWeek?: number; }; }; }; @@ -5953,26 +6098,61 @@ export type operations = { }; }; /** - * admin/avatar-decorations/create + * admin/announcements/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - 'admin___avatar-decorations___create': { + admin___announcements___create: { requestBody: { content: { 'application/json': { - name: string; - description: string; - url: string; - roleIdsThatCanBeUsedThisDecoration?: string[]; + title: string; + text: string; + imageUrl: string | null; + /** + * @default info + * @enum {string} + */ + icon?: 'info' | 'warning' | 'error' | 'success'; + /** + * @default normal + * @enum {string} + */ + display?: 'normal' | 'banner' | 'dialog'; + /** @default false */ + forExistingUsers?: boolean; + /** @default false */ + silence?: boolean; + /** @default false */ + needConfirmationToRead?: boolean; + /** + * Format: misskey:id + * @default null + */ + userId?: string | null; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + title: string; + text: string; + imageUrl: string | null; + }; + }; }; /** @description Client error */ 400: { @@ -6007,12 +6187,12 @@ export type operations = { }; }; /** - * admin/avatar-decorations/delete + * admin/announcements/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - 'admin___avatar-decorations___delete': { + admin___announcements___delete: { requestBody: { content: { 'application/json': { @@ -6059,12 +6239,12 @@ export type operations = { }; }; /** - * admin/avatar-decorations/list + * admin/announcements/list * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *read:admin:announcements* */ - 'admin___avatar-decorations___list': { + admin___announcements___list: { requestBody: { content: { 'application/json': { @@ -6076,6 +6256,11 @@ export type operations = { untilId?: string; /** Format: misskey:id */ userId?: string | null; + /** + * @default active + * @enum {string} + */ + status?: 'all' | 'active' | 'archived'; }; }; }; @@ -6093,10 +6278,10 @@ export type operations = { createdAt: string; /** Format: date-time */ updatedAt: string | null; - name: string; - description: string; - url: string; - roleIdsThatCanBeUsedThisDecoration: string[]; + text: string; + title: string; + imageUrl: string | null; + reads: number; })[]; }; }; @@ -6133,21 +6318,28 @@ export type operations = { }; }; /** - * admin/avatar-decorations/update + * admin/announcements/update * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* + * **Credential required**: *Yes* / **Permission**: *write:admin:announcements* */ - 'admin___avatar-decorations___update': { + admin___announcements___update: { requestBody: { content: { 'application/json': { /** Format: misskey:id */ id: string; - name?: string; - description?: string; - url?: string; - roleIdsThatCanBeUsedThisDecoration?: string[]; + title?: string; + text?: string; + imageUrl?: string | null; + /** @enum {string} */ + icon?: 'info' | 'warning' | 'error' | 'success'; + /** @enum {string} */ + display?: 'normal' | 'banner' | 'dialog'; + forExistingUsers?: boolean; + silence?: boolean; + needConfirmationToRead?: boolean; + isActive?: boolean; }; }; }; @@ -6189,17 +6381,19 @@ export type operations = { }; }; /** - * admin/delete-all-files-of-a-user + * admin/avatar-decorations/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user* + * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - 'admin___delete-all-files-of-a-user': { + 'admin___avatar-decorations___create': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - userId: string; + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration?: string[]; }; }; }; @@ -6241,17 +6435,17 @@ export type operations = { }; }; /** - * admin/unset-user-avatar + * admin/avatar-decorations/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar* + * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - 'admin___unset-user-avatar': { + 'admin___avatar-decorations___delete': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - userId: string; + id: string; }; }; }; @@ -6293,24 +6487,46 @@ export type operations = { }; }; /** - * admin/unset-user-banner + * admin/avatar-decorations/list * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner* + * **Credential required**: *Yes* / **Permission**: *read:admin:avatar-decorations* */ - 'admin___unset-user-banner': { + 'admin___avatar-decorations___list': { requestBody: { content: { 'application/json': { + /** @default 10 */ + limit?: number; /** Format: misskey:id */ - userId: string; + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** Format: misskey:id */ + userId?: string | null; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** + * Format: id + * @example xxxxxxxxxx + */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string | null; + name: string; + description: string; + url: string; + roleIdsThatCanBeUsedThisDecoration: string[]; + })[]; + }; }; /** @description Client error */ 400: { @@ -6345,14 +6561,226 @@ export type operations = { }; }; /** - * admin/drive/clean-remote-files + * admin/avatar-decorations/update * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:drive* + * **Credential required**: *Yes* / **Permission**: *write:admin:avatar-decorations* */ - 'admin___drive___clean-remote-files': { - responses: { - /** @description OK (without any results) */ + 'admin___avatar-decorations___update': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + id: string; + name?: string; + description?: string; + url?: string; + roleIdsThatCanBeUsedThisDecoration?: 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/delete-all-files-of-a-user + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:delete-all-files-of-a-user* + */ + 'admin___delete-all-files-of-a-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/unset-user-avatar + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-avatar* + */ + 'admin___unset-user-avatar': { + 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/unset-user-banner + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-banner* + */ + 'admin___unset-user-banner': { + 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/drive/clean-remote-files + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:drive* + */ + 'admin___drive___clean-remote-files': { + responses: { + /** @description OK (without any results) */ 204: { content: never; }; @@ -6543,7 +6971,7 @@ export type operations = { * @example 15eca7fba0480996e2245f5185bf39f2 */ md5: string; - /** @example lenna.jpg */ + /** @example 192.jpg */ name: string; /** @example image/jpeg */ type: string; @@ -9374,6 +9802,7 @@ export type operations = { perUserListTimelineCacheMax?: number; notesPerOneAd?: number; silencedHosts?: string[] | null; + mediaSilencedHosts?: string[] | null; /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */ summalyProxy?: string | null; urlPreviewEnabled?: boolean; @@ -9478,15 +9907,302 @@ export type operations = { * admin/update-user-note * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:user-note* + * **Credential required**: *Yes* / **Permission**: *write:admin:user-note* + */ + 'admin___update-user-note': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + text: 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/roles/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* + */ + admin___roles___create: { + requestBody: { + content: { + 'application/json': { + name: string; + description: string; + color: string | null; + iconUrl: string | null; + /** @enum {string} */ + target: 'manual' | 'conditional'; + condFormula: Record<string, never>; + isPublic: boolean; + isModerator: boolean; + isAdministrator: boolean; + /** @default false */ + isExplorable?: boolean; + asBadge: boolean; + canEditMembersByModerator: boolean; + displayOrder: number; + policies: Record<string, never>; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role']; + }; + }; + /** @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/roles/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* + */ + admin___roles___delete: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: 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/roles/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + */ + admin___roles___list: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role'][]; + }; + }; + /** @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/roles/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + */ + admin___roles___show: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roleId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Role']; + }; + }; + /** @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/roles/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - 'admin___update-user-note': { + admin___roles___update: { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - userId: string; - text: string; + roleId: string; + name?: string; + description?: string; + color?: string | null; + iconUrl?: string | null; + /** @enum {string} */ + target?: 'manual' | 'conditional'; + condFormula?: Record<string, never>; + isPublic?: boolean; + isModerator?: boolean; + isAdministrator?: boolean; + isExplorable?: boolean; + asBadge?: boolean; + canEditMembersByModerator?: boolean; + displayOrder?: number; + policies?: Record<string, never>; }; }; }; @@ -9528,40 +10244,27 @@ export type operations = { }; }; /** - * admin/roles/create + * admin/roles/assign * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - admin___roles___create: { + admin___roles___assign: { requestBody: { content: { 'application/json': { - name: string; - description: string; - color: string | null; - iconUrl: string | null; - /** @enum {string} */ - target: 'manual' | 'conditional'; - condFormula: Record<string, never>; - isPublic: boolean; - isModerator: boolean; - isAdministrator: boolean; - /** @default false */ - isExplorable?: boolean; - asBadge: boolean; - canEditMembersByModerator: boolean; - displayOrder: number; - policies: Record<string, never>; + /** Format: misskey:id */ + roleId: string; + /** Format: misskey:id */ + userId: string; + expiresAt?: number | null; }; }; }; responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['Role']; - }; + /** @description OK (without any results) */ + 204: { + content: never; }; /** @description Client error */ 400: { @@ -9596,17 +10299,19 @@ export type operations = { }; }; /** - * admin/roles/delete + * admin/roles/unassign * @description No description provided. * * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - admin___roles___delete: { + admin___roles___unassign: { requestBody: { content: { 'application/json': { /** Format: misskey:id */ roleId: string; + /** Format: misskey:id */ + userId: string; }; }; }; @@ -9648,19 +10353,24 @@ export type operations = { }; }; /** - * admin/roles/list + * admin/roles/update-default-policies * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + * **Credential required**: *Yes* / **Permission**: *write:admin:roles* */ - admin___roles___list: { - responses: { - /** @description OK (with results) */ - 200: { - content: { - 'application/json': components['schemas']['Role'][]; + 'admin___roles___update-default-policies': { + requestBody: { + content: { + 'application/json': { + policies: Record<string, never>; }; }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; /** @description Client error */ 400: { content: { @@ -9694,17 +10404,23 @@ export type operations = { }; }; /** - * admin/roles/show + * admin/roles/users * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:admin:roles* + * **Credential required**: *No* / **Permission**: *read:admin:roles* */ - admin___roles___show: { + admin___roles___users: { requestBody: { content: { 'application/json': { /** Format: misskey:id */ roleId: string; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; }; }; }; @@ -9712,7 +10428,15 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['Role']; + 'application/json': ({ + /** Format: misskey:id */ + id: string; + /** Format: date-time */ + createdAt: string; + user: components['schemas']['UserDetailed']; + /** Format: date-time */ + expiresAt: string | null; + })[]; }; }; /** @description Client error */ @@ -9748,39 +10472,30 @@ export type operations = { }; }; /** - * admin/roles/update + * admin/system-webhook/create * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ - admin___roles___update: { + 'admin___system-webhook___create': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - roleId: string; + isActive: boolean; name: string; - description: string; - color: string | null; - iconUrl: string | null; - /** @enum {string} */ - target: 'manual' | 'conditional'; - condFormula: Record<string, never>; - isPublic: boolean; - isModerator: boolean; - isAdministrator: boolean; - isExplorable?: boolean; - asBadge: boolean; - canEditMembersByModerator: boolean; - displayOrder: number; - policies: Record<string, never>; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; + url: string; + secret: string; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['SystemWebhook']; + }; }; /** @description Client error */ 400: { @@ -9815,20 +10530,18 @@ export type operations = { }; }; /** - * admin/roles/assign + * admin/system-webhook/delete * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ - admin___roles___assign: { + 'admin___system-webhook___delete': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - roleId: string; - /** Format: misskey:id */ - userId: string; - expiresAt?: number | null; + id: string; }; }; }; @@ -9870,26 +10583,27 @@ export type operations = { }; }; /** - * admin/roles/unassign + * admin/system-webhook/list * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ - admin___roles___unassign: { + 'admin___system-webhook___list': { requestBody: { content: { 'application/json': { - /** Format: misskey:id */ - roleId: string; - /** Format: misskey:id */ - userId: string; + isActive?: boolean; + on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['SystemWebhook'][]; + }; }; /** @description Client error */ 400: { @@ -9924,23 +10638,27 @@ export type operations = { }; }; /** - * admin/roles/update-default-policies + * admin/system-webhook/show * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:roles* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ - 'admin___roles___update-default-policies': { + 'admin___system-webhook___show': { requestBody: { content: { 'application/json': { - policies: Record<string, never>; + /** Format: misskey:id */ + id: string; }; }; }; responses: { - /** @description OK (without any results) */ - 204: { - content: never; + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['SystemWebhook']; + }; }; /** @description Client error */ 400: { @@ -9975,23 +10693,23 @@ export type operations = { }; }; /** - * admin/roles/users + * admin/system-webhook/update * @description No description provided. * - * **Credential required**: *No* / **Permission**: *read:admin:roles* + * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties. + * **Credential required**: *Yes* / **Permission**: *write:admin:system-webhook* */ - admin___roles___users: { + 'admin___system-webhook___update': { requestBody: { content: { 'application/json': { /** Format: misskey:id */ - roleId: string; - /** Format: misskey:id */ - sinceId?: string; - /** Format: misskey:id */ - untilId?: string; - /** @default 10 */ - limit?: number; + id: string; + isActive: boolean; + name: string; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; + url: string; + secret: string; }; }; }; @@ -9999,15 +10717,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': ({ - /** Format: misskey:id */ - id: string; - /** Format: date-time */ - createdAt: string; - user: components['schemas']['UserDetailed']; - /** Format: date-time */ - expiresAt: string | null; - })[]; + 'application/json': components['schemas']['SystemWebhook']; }; }; /** @description Client error */ @@ -13141,7 +13851,7 @@ export type operations = { 'application/json': { /** Format: misskey:id */ clipId: string; - name: string; + name?: string; isPublic?: boolean; description?: string | null; }; @@ -13409,6 +14119,8 @@ export type operations = { type?: string | null; /** @enum {string|null} */ sort?: '+createdAt' | '-createdAt' | '+name' | '-name' | '+size' | '-size' | null; + /** @default */ + searchQuery?: string; }; }; }; @@ -13591,7 +14303,7 @@ export type operations = { * Format: binary * @description The file contents. */ - file: string; + file: Blob; }; }; }; @@ -14008,6 +14720,8 @@ export type operations = { * @default null */ folderId?: string | null; + /** @default */ + searchQuery?: string; }; }; }; @@ -15990,9 +16704,9 @@ export type operations = { 'application/json': { /** Format: misskey:id */ postId: string; - title: string; + title?: string; description?: string | null; - fileIds: string[]; + fileIds?: string[]; /** @default false */ isSensitive?: boolean; }; @@ -16590,6 +17304,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16731,6 +17451,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16792,6 +17518,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16899,6 +17631,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -16952,6 +17690,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17191,6 +17935,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -17244,6 +17994,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -18705,6 +19461,12 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; /** @description Internal server error */ 500: { content: { @@ -19942,12 +20704,11 @@ export type operations = { 'application/json': { /** Format: misskey:id */ webhookId: string; - name: string; - url: string; - /** @default */ - secret?: string; - on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[]; - active: boolean; + name?: string; + url?: string; + secret?: string | null; + on?: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[]; + active?: boolean; }; }; }; @@ -23604,16 +24365,16 @@ export type operations = { 'application/json': { /** Format: misskey:id */ pageId: string; - title: string; - name: string; + title?: string; + name?: string; summary?: string | null; - content: { + content?: { [key: string]: unknown; }[]; - variables: { + variables?: { [key: string]: unknown; }[]; - script: string; + script?: string; /** Format: misskey:id */ eyeCatchingImageId?: string | null; /** @enum {string} */ diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 518fc75ec6a5e67a4fdb7d76b3f9fc1b7323f612..890b43639d150a75a44d85bafa874ef5f062a27f 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -1,3 +1,21 @@ +import type { operations } from './autogen/types.js'; +import type { + AbuseReportNotificationRecipient, + Ad, + Announcement, + EmojiDetailed, + Flash, + GalleryPost, + InviteCode, + MetaDetailed, + Note, + Page, + Role, + ReversiGameDetailed, + SystemWebhook, + UserLite, +} from './autogen/models.js'; + export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; @@ -139,12 +157,42 @@ export const moderationLogTypes = [ 'deleteAvatarDecoration', 'unsetUserAvatar', 'unsetUserBanner', + 'createSystemWebhook', + 'updateSystemWebhook', + 'deleteSystemWebhook', + 'createAbuseReportNotificationRecipient', + 'updateAbuseReportNotificationRecipient', + 'deleteAbuseReportNotificationRecipient', + 'deleteAccount', + 'deletePage', + 'deleteFlash', + 'deleteGalleryPost', ] as const; +// See: packages/backend/src/core/ReversiService.ts@L410 +export const reversiUpdateKeys = [ + 'map', + 'bw', + 'isLlotheo', + 'canPutEverywhere', + 'loopedBoard', + 'timeLimitForEachTurn', +] as const satisfies (keyof ReversiGameDetailed)[]; + +export type ReversiUpdateKey = typeof reversiUpdateKeys[number]; + +type AvatarDecoration = UserLite['avatarDecorations'][number]; + +type ReceivedAbuseReport = { + reportId: AbuseReportNotificationRecipient['id']; + report: operations['admin___abuse-user-reports']['responses'][200]['content']['application/json']; + forwarded: boolean; +}; + export type ModerationLogPayloads = { updateServerSettings: { - before: any | null; - after: any | null; + before: MetaDetailed | null; + after: MetaDetailed | null; }; suspend: { userId: string; @@ -170,16 +218,16 @@ export type ModerationLogPayloads = { }; addCustomEmoji: { emojiId: string; - emoji: any; + emoji: EmojiDetailed; }; updateCustomEmoji: { emojiId: string; - before: any; - after: any; + before: EmojiDetailed; + after: EmojiDetailed; }; deleteCustomEmoji: { emojiId: string; - emoji: any; + emoji: EmojiDetailed; }; assignRole: { userId: string; @@ -198,16 +246,16 @@ export type ModerationLogPayloads = { }; createRole: { roleId: string; - role: any; + role: Role; }; updateRole: { roleId: string; - before: any; - after: any; + before: Role; + after: Role; }; deleteRole: { roleId: string; - role: any; + role: Role; }; clearQueue: Record<string, never>; promoteQueue: Record<string, never>; @@ -222,39 +270,39 @@ export type ModerationLogPayloads = { noteUserId: string; noteUserUsername: string; noteUserHost: string | null; - note: any; + note: Note; }; createGlobalAnnouncement: { announcementId: string; - announcement: any; + announcement: Announcement; }; createUserAnnouncement: { announcementId: string; - announcement: any; + announcement: Announcement; userId: string; userUsername: string; userHost: string | null; }; updateGlobalAnnouncement: { announcementId: string; - before: any; - after: any; + before: Announcement; + after: Announcement; }; updateUserAnnouncement: { announcementId: string; - before: any; - after: any; + before: Announcement; + after: Announcement; userId: string; userUsername: string; userHost: string | null; }; deleteGlobalAnnouncement: { announcementId: string; - announcement: any; + announcement: Announcement; }; deleteUserAnnouncement: { announcementId: string; - announcement: any; + announcement: Announcement; userId: string; userUsername: string; userHost: string | null; @@ -292,37 +340,37 @@ export type ModerationLogPayloads = { }; resolveAbuseReport: { reportId: string; - report: any; + report: ReceivedAbuseReport; forwarded: boolean; }; createInvitation: { - invitations: any[]; + invitations: InviteCode[]; }; createAd: { adId: string; - ad: any; + ad: Ad; }; updateAd: { adId: string; - before: any; - after: any; + before: Ad; + after: Ad; }; deleteAd: { adId: string; - ad: any; + ad: Ad; }; createAvatarDecoration: { avatarDecorationId: string; - avatarDecoration: any; + avatarDecoration: AvatarDecoration; }; updateAvatarDecoration: { avatarDecorationId: string; - before: any; - after: any; + before: AvatarDecoration; + after: AvatarDecoration; }; deleteAvatarDecoration: { avatarDecorationId: string; - avatarDecoration: any; + avatarDecoration: AvatarDecoration; }; unsetUserAvatar: { userId: string; @@ -336,4 +384,53 @@ export type ModerationLogPayloads = { userHost: string | null; fileId: string; }; + createSystemWebhook: { + systemWebhookId: string; + webhook: SystemWebhook; + }; + updateSystemWebhook: { + systemWebhookId: string; + before: SystemWebhook; + after: SystemWebhook; + }; + deleteSystemWebhook: { + systemWebhookId: string; + webhook: SystemWebhook; + }; + createAbuseReportNotificationRecipient: { + recipientId: string; + recipient: AbuseReportNotificationRecipient; + }; + updateAbuseReportNotificationRecipient: { + recipientId: string; + before: AbuseReportNotificationRecipient; + after: AbuseReportNotificationRecipient; + }; + deleteAbuseReportNotificationRecipient: { + recipientId: string; + recipient: AbuseReportNotificationRecipient; + }; + deleteAccount: { + userId: string; + userUsername: string; + userHost: string | null; + }; + deletePage: { + pageId: string; + pageUserId: string; + pageUserUsername: string; + page: Page; + }; + deleteFlash: { + flashId: string; + flashUserId: string; + flashUserUsername: string; + flash: Flash; + }; + deleteGalleryPost: { + postId: string; + postUserId: string; + postUserUsername: string; + post: GalleryPost; + }; }; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 5a19bf74461bd84310153e40443a4c9a08ad6b7d..6a405f81a5514f8092cb61448c161078001b5255 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -1,5 +1,15 @@ import { ModerationLogPayloads } from './consts.js'; -import { Announcement, EmojiDetailed, MeDetailed, Page, User, UserDetailedNotMe } from './autogen/models.js'; +import { + Announcement, + EmojiDetailed, + MeDetailed, + Note, + Page, + Role, + RolePolicies, + User, + UserDetailedNotMe, +} from './autogen/models.js'; export * from './autogen/entities.js'; export * from './autogen/models.js'; @@ -7,9 +17,23 @@ export * from './autogen/models.js'; export type ID = string; export type DateString = string; +type NonNullableRecord<T> = { + [P in keyof T]-?: NonNullable<T[P]>; +}; +type AllNullRecord<T> = { + [P in keyof T]: null; +}; + +export type PureRenote = + Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> + & AllNullRecord<Pick<Note, 'reply' | 'replyId' | 'text' | 'cw' | 'poll'>> + & { files: []; fileIds: []; } + & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>; + export type PageEvent = { pageId: Page['id']; event: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any var: any; userId: User['id']; user: User; @@ -137,6 +161,36 @@ export type ModerationLog = { } | { type: 'unsetUserBanner'; info: ModerationLogPayloads['unsetUserBanner']; +} | { + type: 'createSystemWebhook'; + info: ModerationLogPayloads['createSystemWebhook']; +} | { + type: 'updateSystemWebhook'; + info: ModerationLogPayloads['updateSystemWebhook']; +} | { + type: 'deleteSystemWebhook'; + info: ModerationLogPayloads['deleteSystemWebhook']; +} | { + type: 'createAbuseReportNotificationRecipient'; + info: ModerationLogPayloads['createAbuseReportNotificationRecipient']; +} | { + type: 'updateAbuseReportNotificationRecipient'; + info: ModerationLogPayloads['updateAbuseReportNotificationRecipient']; +} | { + type: 'deleteAbuseReportNotificationRecipient'; + info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient']; +} | { + type: 'deleteAccount'; + info: ModerationLogPayloads['deleteAccount']; +} | { + type: 'deletePage'; + info: ModerationLogPayloads['deletePage']; +} | { + type: 'deleteFlash'; + info: ModerationLogPayloads['deleteFlash']; +} | { + type: 'deleteGalleryPost'; + info: ModerationLogPayloads['deleteGalleryPost']; }); export type ServerStats = { @@ -224,3 +278,7 @@ export type SigninResponse = { id: User['id'], i: string, }; + +type Values<T extends Record<PropertyKey, unknown>> = T[keyof T]; + +export type PartialRolePolicyOverride = Partial<{[k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & { value: RolePolicies[k] }}>; diff --git a/packages/misskey-js/src/index.ts b/packages/misskey-js/src/index.ts index 28007a8ade3c9f6911510f9a823ce3ecb009001d..ace9738e6a19ffd6d2e6a253dbc54455ee09df54 100644 --- a/packages/misskey-js/src/index.ts +++ b/packages/misskey-js/src/index.ts @@ -22,6 +22,7 @@ export const mutedNoteReasons = consts.mutedNoteReasons; export const followingVisibilities = consts.followingVisibilities; export const followersVisibilities = consts.followersVisibilities; export const moderationLogTypes = consts.moderationLogTypes; +export const reversiUpdateKeys = consts.reversiUpdateKeys; // api extractor not supported yet //export * as api from './api.js'; @@ -29,4 +30,5 @@ export const moderationLogTypes = consts.moderationLogTypes; import * as api from './api.js'; import * as entities from './entities.js'; import * as acct from './acct.js'; -export { api, entities, acct }; +import * as note from './note.js'; +export { api, entities, acct, note }; diff --git a/packages/misskey-js/src/note.ts b/packages/misskey-js/src/note.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c8298c7e4f5c47639d7bd70535b1c5c8aab32b7 --- /dev/null +++ b/packages/misskey-js/src/note.ts @@ -0,0 +1,12 @@ +import type { Note, PureRenote } from './entities.js'; + +export function isPureRenote(note: Note): note is PureRenote { + return ( + note.renote != null && + note.reply == null && + note.text == null && + note.cw == null && + (note.fileIds == null || note.fileIds.length === 0) && + note.poll == null + ); +} diff --git a/packages/misskey-js/src/streaming.ts b/packages/misskey-js/src/streaming.ts index 0f2685778205caaa435f56c8ce40b01ad3aa8ff4..d1d131cfc134d95aa34416dff09817bd989d8ed9 100644 --- a/packages/misskey-js/src/streaming.ts +++ b/packages/misskey-js/src/streaming.ts @@ -15,7 +15,7 @@ export function urlQuery(obj: Record<string, string | number | boolean | undefin .join('&'); } -type AnyOf<T extends Record<any, any>> = T[keyof T]; +type AnyOf<T extends Record<PropertyKey, unknown>> = T[keyof T]; type StreamEvents = { _connected_: void; @@ -25,6 +25,7 @@ type StreamEvents = { /** * Misskey stream connection */ +// eslint-disable-next-line import/no-default-export export default class Stream extends EventEmitter<StreamEvents> { private stream: _ReconnectingWebsocket.default; public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; @@ -34,7 +35,7 @@ export default class Stream extends EventEmitter<StreamEvents> { private idCounter = 0; constructor(origin: string, user: { token: string; } | null, options?: { - WebSocket?: any; + WebSocket?: WebSocket; }) { super(); @@ -51,6 +52,7 @@ export default class Stream extends EventEmitter<StreamEvents> { this.send = this.send.bind(this); this.close = this.close.bind(this); + // eslint-disable-next-line no-param-reassign options = options ?? { }; const query = urlQuery({ @@ -91,8 +93,8 @@ export default class Stream extends EventEmitter<StreamEvents> { this.sharedConnectionPools.push(pool); } - const connection = new SharedConnection(this, channel, pool, name); - this.sharedConnections.push(connection); + const connection = new SharedConnection<Channels[C]>(this, channel, pool, name); + this.sharedConnections.push(connection as unknown as SharedConnection); return connection; } @@ -106,7 +108,7 @@ export default class Stream extends EventEmitter<StreamEvents> { private connectToChannel<C extends keyof Channels>(channel: C, params: Channels[C]['params']): NonSharedConnection<Channels[C]> { const connection = new NonSharedConnection(this, channel, this.genId(), params); - this.nonSharedConnections.push(connection); + this.nonSharedConnections.push(connection as unknown as NonSharedConnection); return connection; } @@ -174,9 +176,9 @@ export default class Stream extends EventEmitter<StreamEvents> { * ! ストリーム上ã®ã‚„ã‚Šå–ã‚Šã¯ã™ã¹ã¦JSONã§è¡Œã‚ã‚Œã¾ã™ ! */ 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 { + public send(typeOrPayload: string, payload: unknown): void + public send(typeOrPayload: Record<string, unknown> | unknown[]): void + public send(typeOrPayload: string | Record<string, unknown> | unknown[], payload?: unknown): void { if (typeof typeOrPayload === 'string') { this.stream.send(JSON.stringify({ type: typeOrPayload, @@ -211,7 +213,7 @@ class Pool { public id: string; protected stream: Stream; public users = 0; - private disposeTimerId: any; + private disposeTimerId: ReturnType<typeof setTimeout> | null = null; private isConnected = false; constructor(stream: Stream, channel: string, id: string) { @@ -275,7 +277,7 @@ class Pool { } } -export abstract class Connection<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> { +export abstract class Connection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends EventEmitter<Channel['events']> { public channel: string; protected stream: Stream; public abstract id: string; @@ -291,7 +293,9 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends this.stream = stream; this.channel = channel; - this.name = name; + if (name !== undefined) { + this.name = name; + } } public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void { @@ -307,7 +311,7 @@ export abstract class Connection<Channel extends AnyOf<Channels> = any> extends public abstract dispose(): void; } -class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> { +class SharedConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends Connection<Channel> { private pool: Pool; public get id(): string { @@ -326,11 +330,11 @@ class SharedConnection<Channel extends AnyOf<Channels> = any> extends Connection public dispose(): void { this.pool.dec(); this.removeAllListeners(); - this.stream.removeSharedConnection(this); + this.stream.removeSharedConnection(this as unknown as SharedConnection); } } -class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connection<Channel> { +class NonSharedConnection<Channel extends AnyOf<Channels> = AnyOf<Channels>> extends Connection<Channel> { public id: string; protected params: Channel['params']; @@ -357,6 +361,6 @@ class NonSharedConnection<Channel extends AnyOf<Channels> = any> extends Connect public dispose(): void { this.removeAllListeners(); this.stream.send('disconnect', { id: this.id }); - this.stream.disconnectToChannel(this); + this.stream.disconnectToChannel(this as unknown as NonSharedConnection); } } diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 912ad56f63ec2e5c7107f16a2611af1260f554fe..7455bdcd7fccec9adf97e8cafbf328efa308f8a8 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -21,6 +21,14 @@ import { ServerStatsLog, ReversiGameDetailed, } from './entities.js'; +import { + ReversiUpdateKey, +} from './consts.js'; + +type ReversiUpdateSettings<K extends ReversiUpdateKey> = { + key: K; + value: ReversiGameDetailed[K]; +}; export type Channels = { main: { @@ -51,6 +59,7 @@ export type Channels = { registryUpdated: (payload: { scope?: string[]; key: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any | null; }) => void; driveFileCreated: (payload: DriveFile) => void; @@ -224,8 +233,8 @@ export type Channels = { ended: (payload: { winnerId: User['id'] | null; game: ReversiGameDetailed; }) => void; canceled: (payload: { userId: User['id']; }) => void; changeReadyStates: (payload: { user1: boolean; user2: boolean; }) => void; - updateSettings: (payload: { userId: User['id']; key: string; value: any; }) => void; - log: (payload: Record<string, any>) => void; + updateSettings: <K extends ReversiUpdateKey>(payload: { userId: User['id']; key: K; value: ReversiGameDetailed[K]; }) => void; + log: (payload: Record<string, unknown>) => void; }; receives: { putStone: { @@ -234,10 +243,7 @@ export type Channels = { }; ready: boolean; cancel: null | Record<string, never>; - updateSettings: { - key: string; - value: any; - }; + updateSettings: ReversiUpdateSettings<ReversiUpdateKey>; claimTimeIsUp: null | Record<string, never>; } } diff --git a/packages/misskey-js/test-d/api.ts b/packages/misskey-js/test-d/api.ts index 4b72ff4e9da51db010c8060567124f628dfb9c8c..ca6d8dcb8876253b9e408ecf3b552af4ea9967b2 100644 --- a/packages/misskey-js/test-d/api.ts +++ b/packages/misskey-js/test-d/api.ts @@ -11,7 +11,7 @@ describe('API', () => { expectType<Misskey.entities.MetaResponse>(res); }); - test('conditional respose type (meta)', async () => { + test('conditional response type (meta)', async () => { const cli = new Misskey.api.APIClient({ origin: 'https://misskey.test', credential: 'TOKEN' @@ -30,7 +30,7 @@ describe('API', () => { expectType<Misskey.entities.MetaResponse>(res4); }); - test('conditional respose type (users/show)', async () => { + test('conditional response type (users/show)', async () => { const cli = new Misskey.api.APIClient({ origin: 'https://misskey.test', credential: 'TOKEN' diff --git a/packages/misskey-js/test/api.ts b/packages/misskey-js/test/api.ts index fa31d23faab752ada4bb37d2bc924cbf4d21bd24..1a7574de25e75a289438c6e654d801a0e5fee8cc 100644 --- a/packages/misskey-js/test/api.ts +++ b/packages/misskey-js/test/api.ts @@ -5,13 +5,19 @@ enableFetchMocks(); function getFetchCall(call: any[]) { const { body, method } = call[1]; - if (body != null && typeof body != 'string') { + const contentType = call[1].headers['Content-Type']; + if ( + body == null || + (contentType === 'application/json' && typeof body !== 'string') || + (contentType === 'multipart/form-data' && !(body instanceof FormData)) + ) { throw new Error('invalid body'); } return { url: call[0], method: method, - body: JSON.parse(body as any) + contentType: contentType, + body: body instanceof FormData ? Object.fromEntries(body.entries()) : JSON.parse(body), }; } @@ -45,6 +51,7 @@ describe('API', () => { expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ url: 'https://misskey.test/api/i', method: 'POST', + contentType: 'application/json', body: { i: 'TOKEN' } }); }); @@ -78,10 +85,52 @@ describe('API', () => { expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ url: 'https://misskey.test/api/notes/show', method: 'POST', + contentType: 'application/json', body: { i: 'TOKEN', noteId: 'aaaaa' } }); }); + test('multipart/form-data', async () => { + fetchMock.resetMocks(); + fetchMock.mockResponse(async (req) => { + if (req.method == 'POST' && req.url == 'https://misskey.test/api/drive/files/create') { + if (req.headers.get('Content-Type')?.includes('multipart/form-data')) { + return JSON.stringify({ id: 'foo' }); + } else { + return { status: 400 }; + } + } else { + return { status: 404 }; + } + }); + + const cli = new APIClient({ + origin: 'https://misskey.test', + credential: 'TOKEN', + }); + + const testFile = new File([], 'foo.txt'); + + const res = await cli.request('drive/files/create', { + file: testFile, + name: null, // nullã®ãƒ‘ラメータã¯æ¶ˆãˆã‚‹ + }); + + expect(res).toEqual({ + id: 'foo' + }); + + expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ + url: 'https://misskey.test/api/drive/files/create', + method: 'POST', + contentType: 'multipart/form-data', + body: { + i: 'TOKEN', + file: testFile, + } + }); + }); + test('204 No Content 㧠null ãŒè¿”ã‚‹', async () => { fetchMock.resetMocks(); fetchMock.mockResponse(async (req) => { @@ -104,6 +153,7 @@ describe('API', () => { expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({ url: 'https://misskey.test/api/reset-password', method: 'POST', + contentType: 'application/json', body: { i: 'TOKEN', token: 'aaa', password: 'aaa' } }); }); @@ -209,4 +259,42 @@ describe('API', () => { expect(isAPIError(e)).toEqual(false); } }); + + test('admin/roles/create ã®åž‹ãŒåˆã†', async() => { + fetchMock.resetMocks(); + fetchMock.mockResponse(async () => { + return { + // 本æ¥è¿”ã™ã¹ã値ã¯`Role`åž‹ã ãŒã€ãƒ†ã‚¹ãƒˆãªã®ã§ãŠèŒ¶ã‚’æ¿ã™ + status: 200, + body: '{}' + }; + }); + + const cli = new APIClient({ + origin: 'https://misskey.test', + credential: 'TOKEN', + }); + await cli.request('admin/roles/create', { + name: 'aaa', + asBadge: false, + canEditMembersByModerator: false, + color: '#123456', + condFormula: {}, + description: '', + displayOrder: 0, + iconUrl: '', + isAdministrator: false, + isExplorable: false, + isModerator: false, + isPublic: false, + policies: { + ltlAvailable: { + value: true, + priority: 0, + useDefault: false, + }, + }, + target: 'manual', + }); + }) }); diff --git a/packages/misskey-js/tsconfig.json b/packages/misskey-js/tsconfig.json index 6e34e332e000246724fd39812527733960a20185..f7bbc4730454e937f210a032cfcd90f978b445a8 100644 --- a/packages/misskey-js/tsconfig.json +++ b/packages/misskey-js/tsconfig.json @@ -15,6 +15,7 @@ "experimentalDecorators": true, "noImplicitReturns": true, "esModuleInterop": true, + "exactOptionalPropertyTypes": true, "typeRoots": [ "./node_modules/@types" ], diff --git a/packages/misskey-reversi/.eslintignore b/packages/misskey-reversi/.eslintignore deleted file mode 100644 index 52ea8b3362e9e0ab8baf6e7daeb91a99bef2f424..0000000000000000000000000000000000000000 --- a/packages/misskey-reversi/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules -/built -/coverage -/.eslintrc.js -/jest.config.ts -/test -/test-d -build.js diff --git a/packages/misskey-reversi/.eslintrc.cjs b/packages/misskey-reversi/.eslintrc.cjs deleted file mode 100644 index db37a0109871fba27cae540702f545531e9d36e4..0000000000000000000000000000000000000000 --- a/packages/misskey-reversi/.eslintrc.cjs +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - root: true, - parserOptions: { - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: [ - '../shared/.eslintrc.js', - ], -}; diff --git a/packages/misskey-reversi/build.js b/packages/misskey-reversi/build.js index 0b79f4b9159be3e153e39e6d092524f9b624adf5..e626c97a5967d6c20facd1cd107f46f28451a63a 100644 --- a/packages/misskey-reversi/build.js +++ b/packages/misskey-reversi/build.js @@ -95,7 +95,6 @@ async function watchSrc() { process.on('SIGHUP', resolve); process.on('SIGINT', resolve); process.on('SIGTERM', resolve); - process.on('SIGKILL', resolve); process.on('uncaughtException', reject); process.on('exit', resolve); }).finally(async () => { diff --git a/packages/misskey-reversi/eslint.config.js b/packages/misskey-reversi/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..3f81df714508b46e3d547e601ca7ce3dcd5e007b --- /dev/null +++ b/packages/misskey-reversi/eslint.config.js @@ -0,0 +1,23 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + ignores: [ + '**/node_modules', + 'built', + ], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json index 45a61208610d5295c50a892aef443e34090a16b7..c6db6e6221ae812d6f3044e0a0e86a184e143455 100644 --- a/packages/misskey-reversi/package.json +++ b/packages/misskey-reversi/package.json @@ -17,16 +17,14 @@ "scripts": { "build": "node ./build.js", "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", - "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", + "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", "typecheck": "tsc --noEmit", "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "1.0.0", "@types/node": "20.11.5", "@typescript-eslint/eslint-plugin": "7.1.0", "@typescript-eslint/parser": "7.1.0", - "eslint": "8.57.0", "execa": "8.0.1", "nodemon": "3.0.2", "typescript": "5.3.3", diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js deleted file mode 100644 index 58247877ae275e7b1c81cef33998e7856a5548be..0000000000000000000000000000000000000000 --- a/packages/shared/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - root: true, - ignorePatterns: ['**/.eslintrc.cjs'], - extends: [ - 'plugin:@misskey-dev/recommended', - ], -}; diff --git a/packages/shared/eslint.config.js b/packages/shared/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..e9d27c4a7288961baf6b27f0e989860ca3cb8d90 --- /dev/null +++ b/packages/shared/eslint.config.js @@ -0,0 +1,28 @@ +import globals from 'globals'; +import pluginMisskey from '@misskey-dev/eslint-plugin'; + +export default [ + ...pluginMisskey.configs['recommended'], + { + files: ['**/*.cjs'], + languageOptions: { + parserOptions: { + sourceType: 'commonjs', + }, + }, + }, + { + files: ['**/*.js', '**/*.jsx'], + languageOptions: { + parserOptions: { + sourceType: 'module', + }, + }, + }, + { + files: ['build.js'], + languageOptions: { + globals: globals.node, + }, + }, +]; diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000000000000000000000000000000000000..bedb411a912437aa07bf5bdc07a9b6f36359522e --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/sw/.eslintrc.cjs b/packages/sw/.eslintrc.cjs deleted file mode 100644 index b1fd6b5edc80c3c35b4864c87496f44a4b313a67..0000000000000000000000000000000000000000 --- a/packages/sw/.eslintrc.cjs +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - root: true, - env: { - node: false, - }, - parserOptions: { - parser: '@typescript-eslint/parser', - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - extends: ['../shared/.eslintrc.js'], - globals: { - require: false, - _DEV_: false, - _LANGS_: false, - _VERSION_: false, - _ENV_: false, - _PERF_PREFIX_: false, - }, -}; diff --git a/packages/sw/build.js b/packages/sw/build.js index eb9a944f4765e809f5f03b9965f287ce3c5b57b3..9522d061e020d5414433c18aff784070f7539153 100644 --- a/packages/sw/build.js +++ b/packages/sw/build.js @@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url'; import * as esbuild from 'esbuild'; import locales from '../../locales/index.js'; -import meta from '../../package.json' assert { type: "json" }; +import meta from '../../package.json' with { type: "json" }; const watch = process.argv[2]?.includes('watch'); const __dirname = fileURLToPath(new URL('.', import.meta.url)) diff --git a/packages/sw/eslint.config.js b/packages/sw/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..c62a2eadc6d5f03feef800d0bf4dcefae5441d98 --- /dev/null +++ b/packages/sw/eslint.config.js @@ -0,0 +1,32 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + ignores: ['build.js'], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), + require: false, + _DEV_: false, + _LANGS_: false, + _VERSION_: false, + _ENV_: false, + _PERF_PREFIX_: false, + }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/sw/package.json b/packages/sw/package.json index cb59a7023862df94f713019309dd44e0a14acc2a..9174f50ae3b30b2d9fdf423783909b9098107084 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -9,18 +9,16 @@ "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { - "esbuild": "0.20.2", + "esbuild": "0.23.0", "idb-keyval": "6.2.1", "misskey-js": "workspace:*" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "1.0.0", - "@typescript-eslint/parser": "7.7.1", + "@typescript-eslint/parser": "7.17.0", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67", - "eslint": "8.57.0", "eslint-plugin-import": "2.29.1", - "nodemon": "3.1.0", - "typescript": "5.4.5" + "nodemon": "3.1.4", + "typescript": "5.5.4" }, "type": "module" } diff --git a/packages/sw/tsconfig.json b/packages/sw/tsconfig.json index f3f354301387748545dc3d119888ae5f378a8e2b..50d4aae19d3edf5f97b5b4ddf093068a34b8e927 100644 --- a/packages/sw/tsconfig.json +++ b/packages/sw/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "allowJs": true, "noEmitOnError": false, - "noImplicitAny": false, "noImplicitReturns": true, "noUnusedParameters": false, "noUnusedLocals": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5ef08947c80075dbbe0b6e4831fcc5c16248e2b..d297feacf1360b034157c18ba5e4ac8a24abd94f 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.38) + version: 6.1.2(postcss@8.4.40) esbuild: - specifier: 0.20.2 - version: 0.20.2 + specifier: 0.23.0 + version: 0.23.0 execa: specifier: 8.0.1 version: 8.0.1 @@ -25,69 +25,75 @@ importers: specifier: 3.3.2 version: 3.3.2 glob: - specifier: 10.3.12 - version: 10.3.12 + specifier: 11.0.0 + version: 11.0.0 ignore-walk: - specifier: 6.0.4 - version: 6.0.4 + specifier: 6.0.5 + version: 6.0.5 js-yaml: specifier: 4.1.0 version: 4.1.0 postcss: - specifier: 8.4.38 - version: 8.4.38 + specifier: 8.4.40 + version: 8.4.40 tar: specifier: 6.2.1 version: 6.2.1 terser: - specifier: 5.30.3 - version: 5.30.3 + specifier: 5.31.3 + version: 5.31.3 typescript: - specifier: 5.4.5 - version: 5.4.5 + specifier: 5.5.4 + version: 5.5.4 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) '@types/node': - specifier: 20.12.7 - version: 20.12.7 + specifier: 20.14.12 + version: 20.14.12 '@typescript-eslint/eslint-plugin': - specifier: 7.7.1 - version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + 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.7.1 - version: 7.7.1(eslint@8.57.0)(typescript@5.4.5) + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) cross-env: specifier: 7.0.3 version: 7.0.3 cypress: - specifier: 13.7.3 - version: 13.7.3 + specifier: 13.13.1 + version: 13.13.1 eslint: - specifier: 8.57.0 - version: 8.57.0 + specifier: 9.8.0 + version: 9.8.0 + globals: + specifier: 15.8.0 + version: 15.8.0 ncp: specifier: 2.0.0 version: 2.0.0 start-server-and-test: - specifier: 2.0.3 - version: 2.0.3 + specifier: 2.0.4 + version: 2.0.4 packages/backend: dependencies: '@aws-sdk/client-s3': - specifier: 3.412.0 - version: 3.412.0 + specifier: 3.620.0 + version: 3.620.0 '@aws-sdk/lib-storage': - specifier: 3.412.0 - version: 3.412.0(@aws-sdk/client-s3@3.412.0) + specifier: 3.620.0 + version: 3.620.0(@aws-sdk/client-s3@3.620.0) '@bull-board/api': - specifier: 5.17.0 - version: 5.17.0(@bull-board/ui@5.17.0) + specifier: 5.21.1 + version: 5.21.1(@bull-board/ui@5.21.1) '@bull-board/fastify': - specifier: 5.17.0 - version: 5.17.0 + specifier: 5.21.1 + version: 5.21.1 '@bull-board/ui': - specifier: 5.17.0 - version: 5.17.0 + specifier: 5.21.1 + version: 5.21.1 '@discordapp/twemoji': specifier: 15.0.3 version: 15.0.3 @@ -107,11 +113,11 @@ importers: specifier: 9.5.0 version: 9.5.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) '@fastify/multipart': - specifier: 8.2.0 - version: 8.2.0 + specifier: 8.3.0 + version: 8.3.0 '@fastify/static': - specifier: 7.0.3 - version: 7.0.3 + specifier: 7.0.4 + version: 7.0.4 '@fastify/view': specifier: 9.1.0 version: 9.1.0 @@ -122,29 +128,29 @@ importers: specifier: 5.1.0 version: 5.1.0 '@napi-rs/canvas': - specifier: ^0.1.52 - version: 0.1.52 + specifier: ^0.1.53 + version: 0.1.53 '@nestjs/common': - specifier: 10.3.8 - version: 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1) + specifier: 10.3.10 + version: 10.3.10(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': - specifier: 10.3.8 - version: 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + 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) '@nestjs/testing': - specifier: 10.3.8 - version: 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)) + 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)) '@peertube/http-signature': specifier: 1.7.0 version: 1.7.0 '@sentry/node': - specifier: ^8.5.0 - version: 8.5.0 + specifier: 8.20.0 + version: 8.20.0 '@sentry/profiling-node': - specifier: ^8.5.0 - version: 8.5.0 + specifier: 8.20.0 + version: 8.20.0 '@simplewebauthn/server': - specifier: 10.0.0 - version: 10.0.0(encoding@0.1.13) + specifier: 10.0.1 + version: 10.0.1(encoding@0.1.13) '@sinonjs/fake-timers': specifier: 11.2.2 version: 11.2.2 @@ -153,10 +159,10 @@ importers: version: 2.5.0 '@swc/cli': specifier: 0.3.12 - version: 0.3.12(@swc/core@1.4.17)(chokidar@3.5.3) + version: 0.3.12(@swc/core@1.6.6)(chokidar@3.5.3) '@swc/core': - specifier: 1.4.17 - version: 1.4.17 + specifier: 1.6.6 + version: 1.6.6 '@transfem-org/sfm-js': specifier: 0.24.5 version: 0.24.5 @@ -167,8 +173,8 @@ importers: specifier: 1.3.8 version: 1.3.8 ajv: - specifier: 8.13.0 - version: 8.13.0 + specifier: 8.17.1 + version: 8.17.1 archiver: specifier: 7.0.1 version: 7.0.1 @@ -188,8 +194,8 @@ importers: specifier: 1.20.2 version: 1.20.2 bullmq: - specifier: 5.7.8 - version: 5.7.8 + specifier: 5.10.4 + version: 5.10.4 cacheable-lookup: specifier: 7.0.0 version: 7.0.0 @@ -220,9 +226,12 @@ importers: deep-email-validator: specifier: 0.1.21 version: 0.1.21 + fast-xml-parser: + specifier: ^4.4.0 + version: 4.4.0 fastify: - specifier: 4.26.2 - version: 4.26.2 + specifier: 4.28.1 + version: 4.28.1 fastify-multer: specifier: ^2.0.3 version: 2.0.3 @@ -233,11 +242,11 @@ importers: specifier: 4.2.2 version: 4.2.2 file-type: - specifier: 19.0.0 - version: 19.0.0 + specifier: 19.3.0 + version: 19.3.0 fluent-ffmpeg: - specifier: 2.1.2 - version: 2.1.2 + specifier: 2.1.3 + version: 2.1.3 form-data: specifier: 4.0.0 version: 4.0.0 @@ -245,11 +254,11 @@ importers: specifier: 10.3.10 version: 10.3.10 got: - specifier: 14.2.1 - version: 14.2.1 + specifier: 14.4.2 + version: 14.4.2 happy-dom: - specifier: 10.0.3 - version: 10.0.3 + specifier: 15.6.1 + version: 15.6.1 hpagent: specifier: 1.2.0 version: 1.2.0 @@ -263,26 +272,26 @@ importers: specifier: 5.4.1 version: 5.4.1 ip-cidr: - specifier: 3.1.0 - version: 3.1.0 + specifier: 4.0.1 + version: 4.0.1 ipaddr.js: specifier: 2.2.0 version: 2.2.0 is-svg: - specifier: 5.0.0 - version: 5.0.0 + specifier: 5.0.1 + version: 5.0.1 js-yaml: specifier: 4.1.0 version: 4.1.0 jsdom: - specifier: 24.0.0 - version: 24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + specifier: 24.1.1 + version: 24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3) json5: specifier: 2.2.3 version: 2.2.3 jsonld: specifier: 8.3.2 - version: 8.3.2(web-streams-polyfill@3.2.1) + version: 8.3.2(web-streams-polyfill@4.0.0) jsrsasign: specifier: 11.1.0 version: 11.1.0 @@ -290,8 +299,8 @@ importers: specifier: workspace:* version: link:../megalodon meilisearch: - specifier: 0.38.0 - version: 0.38.0(encoding@0.1.13) + specifier: 0.41.0 + version: 0.41.0(encoding@0.1.13) microformats-parser: specifier: 2.0.2 version: 2.0.2 @@ -317,8 +326,8 @@ importers: specifier: 3.3.2 version: 3.3.2 nodemailer: - specifier: 6.9.13 - version: 6.9.13 + specifier: 6.9.14 + version: 6.9.14 oauth: specifier: 0.10.0 version: 0.10.0 @@ -332,14 +341,14 @@ importers: specifier: 0.0.14 version: 0.0.14 otpauth: - specifier: 9.2.3 - version: 9.2.3 + specifier: 9.3.1 + version: 9.3.1 parse5: specifier: 7.1.2 version: 7.1.2 pg: - specifier: 8.11.5 - version: 8.11.5 + specifier: 8.12.0 + version: 8.12.0 pkce-challenge: specifier: 4.1.0 version: 4.1.0 @@ -349,9 +358,12 @@ importers: promise-limit: specifier: 2.7.0 version: 2.7.0 + proxy-addr: + specifier: ^2.0.7 + version: 2.0.7 pug: - specifier: 3.0.2 - version: 3.0.2 + specifier: 3.0.3 + version: 3.0.3 punycode: specifier: 2.3.1 version: 2.3.1 @@ -365,8 +377,8 @@ importers: specifier: 3.4.1 version: 3.4.1 re2: - specifier: 1.20.10 - version: 1.20.10 + specifier: 1.21.3 + version: 1.21.3 redis-lock: specifier: 0.1.4 version: 0.1.4 @@ -389,8 +401,8 @@ importers: specifier: 2.7.0 version: 2.7.0 sharp: - specifier: 0.33.3 - version: 0.33.3 + specifier: 0.33.4 + version: 0.33.4 slacc: specifier: 0.0.10 version: 0.0.10 @@ -401,8 +413,8 @@ importers: specifier: 2.1.0 version: 2.1.0 systeminformation: - specifier: 5.22.7 - version: 5.22.7 + specifier: 5.22.11 + version: 5.22.11 tinycolor2: specifier: 1.6.0 version: 1.6.0 @@ -410,17 +422,17 @@ importers: specifier: 0.2.3 version: 0.2.3 tsc-alias: - specifier: 1.8.8 - version: 1.8.8 + specifier: 1.8.10 + version: 1.8.10 tsconfig-paths: specifier: 4.2.0 version: 4.2.0 typeorm: specifier: 0.3.20 - version: 0.3.20(ioredis@5.4.1)(pg@8.11.5) + version: 0.3.20(ioredis@5.4.1)(pg@8.12.0) typescript: - specifier: 5.4.5 - version: 5.4.5 + specifier: 5.5.4 + version: 5.5.4 ulid: specifier: 2.3.0 version: 2.3.0 @@ -434,8 +446,8 @@ importers: specifier: 3.6.7 version: 3.6.7 ws: - specifier: 8.17.0 - version: 8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + specifier: 8.18.0 + version: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) xev: specifier: 3.0.2 version: 3.0.2 @@ -525,18 +537,15 @@ importers: '@jest/globals': specifier: 29.7.0 version: 29.7.0 - '@misskey-dev/eslint-plugin': - specifier: 1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) '@nestjs/platform-express': - specifier: 10.3.8 - version: 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8) + 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) '@simplewebauthn/types': specifier: 10.0.0 version: 10.0.0 '@swc/jest': specifier: 0.2.36 - version: 0.2.36(@swc/core@1.4.17) + version: 0.2.36(@swc/core@1.6.6) '@types/accepts': specifier: 1.3.7 version: 1.3.7 @@ -559,11 +568,11 @@ importers: specifier: 2.1.24 version: 2.1.24 '@types/htmlescape': - specifier: ^1.1.3 + specifier: 1.1.3 version: 1.1.3 '@types/http-link-header': - specifier: 1.0.5 - version: 1.0.5 + specifier: 1.0.7 + version: 1.0.7 '@types/jest': specifier: 29.5.12 version: 29.5.12 @@ -571,11 +580,11 @@ importers: specifier: 4.0.9 version: 4.0.9 '@types/jsdom': - specifier: 21.1.6 - version: 21.1.6 + specifier: 21.1.7 + version: 21.1.7 '@types/jsonld': - specifier: 1.5.13 - version: 1.5.13 + specifier: 1.5.15 + version: 1.5.15 '@types/jsrsasign': specifier: 10.5.14 version: 10.5.14 @@ -586,17 +595,14 @@ importers: specifier: 0.7.34 version: 0.7.34 '@types/node': - specifier: 20.12.7 - version: 20.12.7 - '@types/node-fetch': - specifier: 3.0.3 - version: 3.0.3 + specifier: 20.14.12 + version: 20.14.12 '@types/nodemailer': specifier: 6.4.15 version: 6.4.15 '@types/oauth': - specifier: 0.9.4 - version: 0.9.4 + specifier: 0.9.5 + version: 0.9.5 '@types/oauth2orize': specifier: 1.11.5 version: 1.11.5 @@ -604,8 +610,11 @@ importers: specifier: 0.1.2 version: 0.1.2 '@types/pg': - specifier: 8.11.5 - version: 8.11.5 + specifier: 8.11.6 + version: 8.11.6 + '@types/proxy-addr': + specifier: ^2.0.3 + version: 2.0.3 '@types/pug': specifier: 2.0.10 version: 2.0.10 @@ -652,47 +661,44 @@ importers: specifier: 3.6.3 version: 3.6.3 '@types/ws': - specifier: 8.5.10 - version: 8.5.10 + specifier: 8.5.11 + version: 8.5.11 '@typescript-eslint/eslint-plugin': - specifier: 7.7.1 - version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + 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.7.1 - version: 7.7.1(eslint@8.57.0)(typescript@5.4.5) + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) aws-sdk-client-mock: - specifier: 3.0.1 - version: 3.0.1 + specifier: 4.0.1 + version: 4.0.1 cross-env: specifier: 7.0.3 version: 7.0.3 - eslint: - specifier: 8.57.0 - version: 8.57.0 eslint-plugin-import: specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) + version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) execa: - specifier: 8.0.1 - version: 8.0.1 + specifier: 9.3.0 + version: 9.3.0 fkill: - specifier: ^9.0.0 + specifier: 9.0.0 version: 9.0.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.12.7) + version: 29.7.0(@types/node@20.14.12) jest-mock: specifier: 29.7.0 version: 29.7.0 nodemon: - specifier: 3.1.0 - version: 3.1.0 + specifier: 3.1.4 + version: 3.1.4 pid-port: specifier: 1.0.0 version: 1.0.0 simple-oauth2: - specifier: 5.0.0 - version: 5.0.0 + specifier: 5.1.0 + version: 5.1.0 packages/frontend: dependencies: @@ -713,16 +719,16 @@ importers: version: 2.1.1 '@rollup/plugin-json': specifier: 6.1.0 - version: 6.1.0(rollup@4.17.2) + version: 6.1.0(rollup@4.19.1) '@rollup/plugin-replace': - specifier: 5.0.5 - version: 5.0.5(rollup@4.17.2) + specifier: 5.0.7 + version: 5.0.7(rollup@4.19.1) '@rollup/pluginutils': specifier: 5.1.0 - version: 5.1.0(rollup@4.17.2) + version: 5.1.0(rollup@4.19.1) '@syuilo/aiscript': - specifier: 0.18.0 - version: 0.18.0 + specifier: 0.19.0 + version: 0.19.0 '@transfem-org/sfm-js': specifier: 0.24.5 version: 0.24.5 @@ -730,14 +736,14 @@ importers: specifier: 15.1.1 version: 15.1.1 '@vitejs/plugin-vue': - specifier: 5.0.4 - version: 5.0.4(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5)) + 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)) '@vue/compiler-sfc': - specifier: 3.4.26 - version: 3.4.26 + specifier: 3.4.37 + version: 3.4.37 aiscript-vscode: - specifier: github:aiscript-dev/aiscript-vscode#v0.1.9 - version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02 + 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 @@ -751,29 +757,29 @@ importers: specifier: 1.9.3 version: 1.9.3 chart.js: - specifier: 4.4.2 - version: 4.4.2 + specifier: 4.4.3 + version: 4.4.3 chartjs-adapter-date-fns: specifier: 3.0.0 - version: 3.0.0(chart.js@4.4.2)(date-fns@2.30.0) + version: 3.0.0(chart.js@4.4.3)(date-fns@2.30.0) chartjs-chart-matrix: specifier: 2.0.1 - version: 2.0.1(chart.js@4.4.2) + version: 2.0.1(chart.js@4.4.3) chartjs-plugin-gradient: specifier: 0.6.1 - version: 0.6.1(chart.js@4.4.2) + version: 0.6.1(chart.js@4.4.3) chartjs-plugin-zoom: specifier: 2.0.1 - version: 2.0.1(chart.js@4.4.2) + version: 2.0.1(chart.js@4.4.3) chromatic: - specifier: 11.3.0 - version: 11.3.0 + specifier: 11.5.6 + version: 11.5.6 compare-versions: - specifier: 6.1.0 - version: 6.1.0 + specifier: 6.1.1 + version: 6.1.1 cropperjs: - specifier: 2.0.0-beta.5 - version: 2.0.0-beta.5 + specifier: 2.0.0-rc.1 + version: 2.0.0-rc.1 date-fns: specifier: 2.30.0 version: 2.30.0 @@ -814,23 +820,23 @@ importers: specifier: workspace:* version: link:../misskey-reversi photoswipe: - specifier: 5.4.3 - version: 5.4.3 + specifier: 5.4.4 + version: 5.4.4 punycode: specifier: 2.3.1 version: 2.3.1 rollup: - specifier: 4.17.2 - version: 4.17.2 + specifier: 4.19.1 + version: 4.19.1 sanitize-html: specifier: 2.13.0 version: 2.13.0 sass: - specifier: 1.76.0 - version: 1.76.0 + specifier: 1.77.8 + version: 1.77.8 shiki: - specifier: 1.4.0 - version: 1.4.0 + specifier: 1.12.0 + version: 1.12.0 strict-event-emitter-types: specifier: 2.0.0 version: 2.0.0 @@ -838,102 +844,99 @@ importers: specifier: 3.1.0 version: 3.1.0 three: - specifier: 0.164.1 - version: 0.164.1 + specifier: 0.167.0 + version: 0.167.0 throttle-debounce: - specifier: 5.0.0 - version: 5.0.0 + specifier: 5.0.2 + version: 5.0.2 tinycolor2: specifier: 1.6.0 version: 1.6.0 tsc-alias: - specifier: 1.8.8 - version: 1.8.8 + specifier: 1.8.10 + version: 1.8.10 tsconfig-paths: specifier: 4.2.0 version: 4.2.0 typescript: - specifier: 5.4.5 - version: 5.4.5 + specifier: 5.5.4 + version: 5.5.4 uuid: - specifier: 9.0.1 - version: 9.0.1 + specifier: 10.0.0 + version: 10.0.0 v-code-diff: - specifier: 1.11.0 - version: 1.11.0(vue@3.4.26(typescript@5.4.5)) + specifier: 1.12.0 + version: 1.12.0(vue@3.4.37(typescript@5.5.4)) vite: - specifier: 5.2.11 - version: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) + specifier: 5.3.5 + version: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) vue: - specifier: 3.4.26 - version: 3.4.26(typescript@5.4.5) + specifier: 3.4.37 + version: 3.4.37(typescript@5.5.4) vuedraggable: specifier: next - version: 4.1.0(vue@3.4.26(typescript@5.4.5)) + version: 4.1.0(vue@3.4.37(typescript@5.5.4)) devDependencies: - '@misskey-dev/eslint-plugin': - specifier: 1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) '@misskey-dev/summaly': specifier: 5.1.0 version: 5.1.0 '@storybook/addon-actions': - specifier: 8.0.9 - version: 8.0.9 + 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)) '@storybook/addon-essentials': - specifier: 8.0.9 - version: 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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)) '@storybook/addon-interactions': - specifier: 8.0.9 - version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + 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)) '@storybook/addon-links': - specifier: 8.0.9 - version: 8.0.9(react@18.3.1) + 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)) '@storybook/addon-mdx-gfm': - specifier: 8.0.9 - version: 8.0.9 + 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)) '@storybook/addon-storysource': - specifier: 8.0.9 - version: 8.0.9 + 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)) '@storybook/blocks': - specifier: 8.0.9 - version: 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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)) '@storybook/components': - specifier: 8.0.9 - version: 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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)) '@storybook/core-events': - specifier: 8.0.9 - version: 8.0.9 + 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)) '@storybook/manager-api': - specifier: 8.0.9 - version: 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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)) '@storybook/preview-api': - specifier: 8.0.9 - version: 8.0.9 + 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)) '@storybook/react': - specifier: 8.0.9 - version: 8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5) + 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) '@storybook/react-vite': - specifier: 8.0.9 - version: 8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)) + 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)) '@storybook/test': - specifier: 8.0.9 - version: 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + 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)) '@storybook/theming': - specifier: 8.0.9 - version: 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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)) '@storybook/types': - specifier: 8.0.9 - version: 8.0.9 + 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)) '@storybook/vue3': - specifier: 8.0.9 - version: 8.0.9(encoding@0.1.13)(vue@3.4.26(typescript@5.4.5)) + 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)) '@storybook/vue3-vite': - specifier: 8.0.9 - version: 8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5)) + 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)) '@testing-library/vue': - specifier: 8.0.3 - version: 8.0.3(@vue/compiler-sfc@3.4.26)(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5)) + 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 @@ -941,20 +944,23 @@ importers: specifier: 1.0.5 version: 1.0.5 '@types/matter-js': - specifier: 0.19.6 - version: 0.19.6 + specifier: 0.19.7 + version: 0.19.7 '@types/micromatch': - specifier: 4.0.7 - version: 4.0.7 + specifier: 4.0.9 + version: 4.0.9 '@types/node': - specifier: 20.12.7 - version: 20.12.7 + specifier: 20.14.12 + version: 20.14.12 '@types/punycode': specifier: 2.1.4 version: 2.1.4 '@types/sanitize-html': specifier: 2.11.0 version: 2.11.0 + '@types/seedrandom': + specifier: 3.0.8 + version: 3.0.8 '@types/throttle-debounce': specifier: 5.0.2 version: 5.0.2 @@ -962,41 +968,38 @@ importers: specifier: 1.4.6 version: 1.4.6 '@types/uuid': - specifier: 9.0.8 - version: 9.0.8 + specifier: 10.0.0 + version: 10.0.0 '@types/ws': - specifier: 8.5.10 - version: 8.5.10 + specifier: 8.5.11 + version: 8.5.11 '@typescript-eslint/eslint-plugin': - specifier: 7.7.1 - version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + 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.7.1 - version: 7.7.1(eslint@8.57.0)(typescript@5.4.5) + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) '@vitest/coverage-v8': - specifier: 0.34.6 - version: 0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) + 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)) '@vue/runtime-core': - specifier: 3.4.26 - version: 3.4.26 + specifier: 3.4.37 + version: 3.4.37 acorn: - specifier: 8.11.3 - version: 8.11.3 + specifier: 8.12.1 + version: 8.12.1 cross-env: specifier: 7.0.3 version: 7.0.3 cypress: - specifier: 13.8.1 - version: 13.8.1 - eslint: - specifier: 8.57.0 - version: 8.57.0 + specifier: 13.13.1 + version: 13.13.1 eslint-plugin-import: specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) + version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) eslint-plugin-vue: - specifier: 9.25.0 - version: 9.25.0(eslint@8.57.0) + specifier: 9.27.0 + version: 9.27.0(eslint@9.8.0) fast-glob: specifier: 3.3.2 version: 3.3.2 @@ -1007,53 +1010,56 @@ importers: specifier: 0.12.2 version: 0.12.2 micromatch: - specifier: 4.0.5 - version: 4.0.5 + specifier: 4.0.7 + version: 4.0.7 msw: - specifier: 2.2.14 - version: 2.2.14(typescript@5.4.5) + specifier: 2.3.4 + version: 2.3.4(typescript@5.5.4) msw-storybook-addon: - specifier: 2.0.1 - version: 2.0.1(msw@2.2.14(typescript@5.4.5)) + specifier: 2.0.3 + version: 2.0.3(msw@2.3.4(typescript@5.5.4)) nodemon: - specifier: 3.1.0 - version: 3.1.0 + specifier: 3.1.4 + version: 3.1.4 prettier: - specifier: 3.2.5 - version: 3.2.5 + specifier: 3.3.3 + version: 3.3.3 react: specifier: 18.3.1 version: 18.3.1 react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) + seedrandom: + specifier: 3.0.5 + version: 3.0.5 start-server-and-test: - specifier: 2.0.3 - version: 2.0.3 + specifier: 2.0.4 + version: 2.0.4 storybook: - specifier: 8.0.9 - version: 8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3) + 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) 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(@storybook/blocks@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/components@8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.0.9)(@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.0.9)(@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/types@8.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(3rvqj7p7l43ansgshs3zbslm7u) vite-plugin-turbosnap: specifier: 1.0.3 version: 1.0.3 vitest: - specifier: 0.34.6 - version: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) + 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) vitest-fetch-mock: specifier: 0.2.2 - version: 0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.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.77.8)(terser@5.31.3)) vue-component-type-helpers: - specifier: 2.0.16 - version: 2.0.16 + specifier: 2.0.29 + version: 2.0.29 vue-eslint-parser: - specifier: 9.4.2 - version: 9.4.2(eslint@8.57.0) + specifier: 9.4.3 + version: 9.4.3(eslint@9.8.0) vue-tsc: - specifier: 2.0.16 - version: 2.0.16(typescript@5.4.5) + specifier: 2.0.29 + version: 2.0.29(typescript@5.5.4) packages/megalodon: dependencies: @@ -1068,7 +1074,7 @@ importers: version: 29.5.12 '@types/oauth': specifier: ^0.9.4 - version: 0.9.4 + version: 0.9.5 '@types/object-assign-deep': specifier: ^0.4.3 version: 0.4.3 @@ -1080,7 +1086,7 @@ importers: version: 9.0.8 '@types/ws': specifier: ^8.5.10 - version: 8.5.10 + version: 8.5.11 axios: specifier: 1.6.0 version: 1.6.0 @@ -1113,7 +1119,7 @@ importers: version: 9.0.1 ws: specifier: 8.14.2 - version: 8.14.2(bufferutil@4.0.7)(utf-8-validate@6.0.3) + version: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) devDependencies: '@typescript-eslint/eslint-plugin': specifier: ^6.12.0 @@ -1129,7 +1135,7 @@ importers: version: 9.1.0(eslint@8.57.0) jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@20.12.7) + version: 29.7.0(@types/node@20.14.12) jest-worker: specifier: ^29.7.0 version: 29.7.0 @@ -1138,10 +1144,10 @@ importers: version: 4.17.21 prettier: specifier: ^3.1.0 - version: 3.2.5 + version: 3.3.3 ts-jest: specifier: ^29.1.1 - version: 29.1.2(@babel/core@7.23.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.20.2)(jest@29.7.0(@types/node@20.12.7))(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.0)(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) @@ -1158,9 +1164,6 @@ importers: specifier: 3.0.5 version: 3.0.5 devDependencies: - '@misskey-dev/eslint-plugin': - specifier: 1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) '@types/matter-js': specifier: 0.19.6 version: 0.19.6 @@ -1172,16 +1175,13 @@ importers: version: 3.0.8 '@typescript-eslint/eslint-plugin': specifier: 7.1.0 - version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) + version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3) '@typescript-eslint/parser': specifier: 7.1.0 - version: 7.1.0(eslint@8.57.0)(typescript@5.3.3) + version: 7.1.0(eslint@9.8.0)(typescript@5.3.3) esbuild: specifier: 0.19.11 version: 0.19.11 - eslint: - specifier: 8.57.0 - version: 8.57.0 execa: specifier: 8.0.1 version: 8.0.1 @@ -1205,41 +1205,35 @@ importers: version: 4.4.0 devDependencies: '@microsoft/api-extractor': - specifier: 7.43.1 - version: 7.43.1(@types/node@20.12.7) - '@misskey-dev/eslint-plugin': - specifier: 1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) + specifier: 7.47.4 + version: 7.47.4(@types/node@20.14.12) '@swc/jest': specifier: 0.2.36 - version: 0.2.36(@swc/core@1.4.17) + version: 0.2.36(@swc/core@1.6.13) '@types/jest': specifier: 29.5.12 version: 29.5.12 '@types/node': - specifier: 20.12.7 - version: 20.12.7 + specifier: 20.14.12 + version: 20.14.12 '@typescript-eslint/eslint-plugin': - specifier: 7.7.1 - version: 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) + 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.7.1 - version: 7.7.1(eslint@8.57.0)(typescript@5.4.5) + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) esbuild: - specifier: 0.19.11 - version: 0.19.11 - eslint: - specifier: 8.57.0 - version: 8.57.0 + specifier: 0.23.0 + version: 0.23.0 execa: - specifier: 8.0.1 - version: 8.0.1 + specifier: 9.3.0 + version: 9.3.0 glob: - specifier: 10.3.12 - version: 10.3.12 + specifier: 11.0.0 + version: 11.0.0 jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.12.7) + version: 29.7.0(@types/node@20.14.12) jest-fetch-mock: specifier: 3.0.3 version: 3.0.3(encoding@0.1.13) @@ -1253,20 +1247,17 @@ importers: specifier: 2.0.0 version: 2.0.0 nodemon: - specifier: 3.1.0 - version: 3.1.0 + specifier: 3.1.4 + version: 3.1.4 tsd: - specifier: 0.30.7 - version: 0.30.7 + specifier: 0.31.1 + version: 0.31.1 typescript: - specifier: 5.4.5 - version: 5.4.5 + specifier: 5.5.4 + version: 5.5.4 packages/misskey-js/generator: devDependencies: - '@misskey-dev/eslint-plugin': - specifier: ^1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3))(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0))(eslint@8.53.0) '@readme/openapi-parser': specifier: 2.5.0 version: 2.5.0(openapi-types@12.1.3) @@ -1275,13 +1266,10 @@ importers: version: 20.9.1 '@typescript-eslint/eslint-plugin': specifier: 6.11.0 - version: 6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3) + 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) '@typescript-eslint/parser': specifier: 6.11.0 - version: 6.11.0(eslint@8.53.0)(typescript@5.3.3) - eslint: - specifier: 8.53.0 - version: 8.53.0 + version: 6.11.0(eslint@9.8.0)(typescript@5.3.3) openapi-types: specifier: 12.1.3 version: 12.1.3 @@ -1304,24 +1292,18 @@ importers: specifier: 1.2.2 version: 1.2.2 devDependencies: - '@misskey-dev/eslint-plugin': - specifier: 1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0) '@types/node': specifier: 20.11.5 version: 20.11.5 '@typescript-eslint/eslint-plugin': specifier: 7.1.0 - version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) + version: 7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3) '@typescript-eslint/parser': specifier: 7.1.0 - version: 7.1.0(eslint@8.57.0)(typescript@5.3.3) + version: 7.1.0(eslint@9.8.0)(typescript@5.3.3) esbuild: specifier: 0.19.11 version: 0.19.11 - eslint: - specifier: 8.57.0 - version: 8.57.0 execa: specifier: 8.0.1 version: 8.0.1 @@ -1338,8 +1320,8 @@ importers: packages/sw: dependencies: esbuild: - specifier: 0.20.2 - version: 0.20.2 + specifier: 0.23.0 + version: 0.23.0 idb-keyval: specifier: 6.2.1 version: 6.2.1 @@ -1347,34 +1329,24 @@ importers: specifier: workspace:* version: link:../misskey-js devDependencies: - '@misskey-dev/eslint-plugin': - specifier: 1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.0) '@typescript-eslint/parser': - specifier: 7.7.1 - version: 7.7.1(eslint@8.57.0)(typescript@5.4.5) + specifier: 7.17.0 + version: 7.17.0(eslint@9.8.0)(typescript@5.5.4) '@typescript/lib-webworker': specifier: npm:@types/serviceworker@0.0.67 version: '@types/serviceworker@0.0.67' - eslint: - specifier: 8.57.0 - version: 8.57.0 eslint-plugin-import: specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) + version: 2.29.1(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) nodemon: - specifier: 3.1.0 - version: 3.1.0 + specifier: 3.1.4 + version: 3.1.4 typescript: - specifier: 5.4.5 - version: 5.4.5 + specifier: 5.5.4 + version: 5.5.4 packages: - '@aashutoshrathi/word-wrap@1.2.6': - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - '@adobe/css-tools@4.3.3': resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} @@ -1398,221 +1370,237 @@ packages: resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==} hasBin: true - '@aws-crypto/crc32@3.0.0': - resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} - '@aws-crypto/crc32c@3.0.0': - resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==} + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} - '@aws-crypto/ie11-detection@3.0.0': - resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} - '@aws-crypto/sha1-browser@3.0.0': - resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==} + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - '@aws-crypto/sha256-browser@3.0.0': - resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} - '@aws-crypto/sha256-js@3.0.0': - resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - '@aws-crypto/supports-web-crypto@3.0.0': - resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-crypto/util@3.0.0': - resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} + '@aws-sdk/client-s3@3.620.0': + resolution: {integrity: sha512-kf3Lqvuq/ciUn4myQjd1a9nhVg95+FEWkIq7pdkgxFoKow8HKj3nuiwI7zYBRTfk0RKXRkJca3GE+3RXpeZSiA==} + engines: {node: '>=16.0.0'} - '@aws-sdk/client-s3@3.412.0': - resolution: {integrity: sha512-sNrlx9sSBmFUCqMgTznwk9Fee3PJat0nZ3RIDR5Crhsld/eexxrqb6TYKsxzFfBfXTL/oPh+/S5driRV2xsB8A==} - engines: {node: '>=14.0.0'} + '@aws-sdk/client-sso-oidc@3.620.0': + resolution: {integrity: sha512-CWL8aJa6rrNaQXNsLhblGZzbFBrRz4BXAsFBbyqAZEmryr9q/IC7z/ww3nq8CD2UsW+bn89U/XcoP5r1KWUHuQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.620.0 - '@aws-sdk/client-sso@3.410.0': - resolution: {integrity: sha512-MC9GrgwtlOuSL2WS3DRM3dQ/5y+49KSMMJRH6JiEcU5vE0dX/OtEcX+VfEwpi73x5pSfIjm7xnzjzOFx+sQBIg==} - engines: {node: '>=14.0.0'} + '@aws-sdk/client-sso@3.620.0': + resolution: {integrity: sha512-J1CvF7u39XwtCK9rPlkW2AA631EPqkb4PjOOj9aZ9LjQmkJ0DkL+9tEqU2XIWcjDd2Z3hS3LBuS8uN7upIkEnQ==} + engines: {node: '>=16.0.0'} - '@aws-sdk/client-sts@3.410.0': - resolution: {integrity: sha512-e6VMrBJtnTxxUXwDmkADGIvyppmDMFf4+cGGA68tVCUm1cFNlCI6M/67bVSIPN/WVKAAfhEL5O2vVXCM7aatYg==} - engines: {node: '>=14.0.0'} + '@aws-sdk/client-sts@3.620.0': + resolution: {integrity: sha512-pG4SqDHZV/ZbpoVoVtpxo6ZZoqVDbVItC3QUO73UJ3Gemxznd/Ck7kAsyb6/dJkV/Aqm3gt2O5UL7vzQLNHSjw==} + engines: {node: '>=16.0.0'} - '@aws-sdk/credential-provider-env@3.410.0': - resolution: {integrity: sha512-c7TB9LbN0PkFOsXI0lcRJnqPNOmc4VBvrHf8jP/BkTDg4YUoKQKOFd4d0SqzODmlZiAyoMQVZTR4ISZo95Zj4Q==} - engines: {node: '>=14.0.0'} + '@aws-sdk/core@3.620.0': + resolution: {integrity: sha512-5D9tMahxIDDFLULS9/ULa0HuIu7CZSshfj6wmDSmigXzkWyUvHoVIrme2z6eM3Icat/MO3d4WEy3445Vk385gQ==} + engines: {node: '>=16.0.0'} - '@aws-sdk/credential-provider-ini@3.410.0': - resolution: {integrity: sha512-D8rcr5bRCFD0f42MPQ7K6TWZq5d3pfqrKINL1/bpfkK5BJbvq1BGYmR88UC6CLpTRtZ1LHY2HgYG0fp/2zjjww==} - engines: {node: '>=14.0.0'} + '@aws-sdk/credential-provider-env@3.609.0': + resolution: {integrity: sha512-v69ZCWcec2iuV9vLVJMa6fAb5xwkzN4jYIT8yjo2c4Ia/j976Q+TPf35Pnz5My48Xr94EFcaBazrWedF+kwfuQ==} + engines: {node: '>=16.0.0'} - '@aws-sdk/credential-provider-node@3.410.0': - resolution: {integrity: sha512-0wmVm33T/j1FS7MZ/j+WsPlgSc0YnCXnpbWSov1Mn6R86SHI2b2JhdIPRRE4XbGfyW2QGNUl2CwoZVaqhXeF5g==} - engines: {node: '>=14.0.0'} + '@aws-sdk/credential-provider-http@3.620.0': + resolution: {integrity: sha512-BI2BdrSKDmB/2ouB/NJR0PT0x/+5fmoF6XOE78hFBb4F5w/yynGgcJY936dF+oREfpME6ehjB2b0okGg78Scpw==} + engines: {node: '>=16.0.0'} - '@aws-sdk/credential-provider-process@3.410.0': - resolution: {integrity: sha512-BMju1hlDCDNkkSZpKF5SQ8G0WCLRj6/Jvw9QmudLHJuVwYJXEW1r2AsVMg98OZ3hB9G+MAvHruHZIbMiNmUMXQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/credential-provider-ini@3.620.0': + resolution: {integrity: sha512-P9fYi6dzZIl8ITC7qAPf5DX9omI3LfA91g3KH+0OUmS3ctP7tN+gNo3HmqlzoqnwPe0pXn1FumYAe1qFl6Yjjg==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.620.0 - '@aws-sdk/credential-provider-sso@3.410.0': - resolution: {integrity: sha512-zEaoY/sY+KYTlQUkp9dvveAHf175b8RIt0DsQkDrRPtrg/RBHR00r5rFvz9+nrwsR8546RaBU7h/zzTaQGhmcA==} - engines: {node: '>=14.0.0'} + '@aws-sdk/credential-provider-node@3.620.0': + resolution: {integrity: sha512-or8ahy4ysURcWgKX00367DMDTTyMynDEl+FQh4wce66fMyePhFVuoPcRgXzWsi8KYmL95sPCfJFNqBMyFNcgvQ==} + engines: {node: '>=16.0.0'} - '@aws-sdk/credential-provider-web-identity@3.410.0': - resolution: {integrity: sha512-cE0l8LmEHdWbDkdPNgrfdYSgp4/cIVXrjUKI1QCATA729CrHZ/OQjB/maOBOrMHO9YTiggko887NkslVvwVB7w==} - engines: {node: '>=14.0.0'} + '@aws-sdk/credential-provider-process@3.614.0': + resolution: {integrity: sha512-Q0SI0sTRwi8iNODLs5+bbv8vgz8Qy2QdxbCHnPk/6Cx6LMf7i3dqmWquFbspqFRd8QiqxStrblwxrUYZi09tkA==} + engines: {node: '>=16.0.0'} - '@aws-sdk/lib-storage@3.412.0': - resolution: {integrity: sha512-uAdVtNuip06rJOs28zVrYXLNeHfKraxvJRTzTA+DW1dXkzh70GTKqDKHWH9IJkW/xMTE6wGSM+fDs8jsMOn/yA==} - engines: {node: '>=14.0.0'} + '@aws-sdk/credential-provider-sso@3.620.0': + resolution: {integrity: sha512-xtIj2hmq3jcKwvGmqhoYapbWeQfFyoQgKBtwD6nx0M6oS5lbFH4rzHhj0gBwatZDjMa35cWtcYVUJCv2/9mWvA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.609.0': + resolution: {integrity: sha512-U+PG8NhlYYF45zbr1km3ROtBMYqyyj/oK8NRp++UHHeuavgrP+4wJ4wQnlEaKvJBjevfo3+dlIBcaeQ7NYejWg==} + engines: {node: '>=16.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.0.0 + '@aws-sdk/client-sts': ^3.609.0 - '@aws-sdk/middleware-bucket-endpoint@3.410.0': - resolution: {integrity: sha512-pUGrpFgCKf9fDHu01JJhhw+MUImheS0HFlZwNG37OMubkxUAbCdmYGewGxfTCUvWyZJtx9bVjrSu6gG7w+RARg==} - engines: {node: '>=14.0.0'} + '@aws-sdk/lib-storage@3.620.0': + resolution: {integrity: sha512-xUeKH8RrPQqE6J49Yun0qCdu5SGaN5LgyyjWutLeElqR0G3jhmPVrRzFXsHDXbr9S0QVE4V8DjeC17bkEdG4dw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-s3': ^3.620.0 - '@aws-sdk/middleware-expect-continue@3.410.0': - resolution: {integrity: sha512-e5YqGCNmW99GZjEPPujJ02RlEZql19U40oORysBhVF7mKz8BBvF3s8l37tvu37oxebDEkh1u/2cm2+ggOXxLjQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-bucket-endpoint@3.620.0': + resolution: {integrity: sha512-eGLL0W6L3HDb3OACyetZYOWpHJ+gLo0TehQKeQyy2G8vTYXqNTeqYhuI6up9HVjBzU9eQiULVQETmgQs7TFaRg==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.410.0': - resolution: {integrity: sha512-IK7KlvEKtrQVBfmAp/MmGd0wbWLuN2GZwwfAmsU0qFb0f5vOVUbKDsu6tudtDKCBG9uXyTEsx3/QGvoK2zDy+g==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-expect-continue@3.620.0': + resolution: {integrity: sha512-QXeRFMLfyQ31nAHLbiTLtk0oHzG9QLMaof5jIfqcUwnOkO8YnQdeqzakrg1Alpy/VQ7aqzIi8qypkBe2KXZz0A==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-host-header@3.410.0': - resolution: {integrity: sha512-ED/OVcyITln5rrxnajZP+V0PN1nug+gSDHJDqdDo/oLy7eiDr/ZWn3nlWW7WcMplQ1/Jnb+hK0UetBp/25XooA==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-flexible-checksums@3.620.0': + resolution: {integrity: sha512-ftz+NW7qka2sVuwnnO1IzBku5ccP+s5qZGeRTPgrKB7OzRW85gthvIo1vQR2w+OwHFk7WJbbhhWwbCbktnP4UA==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-location-constraint@3.410.0': - resolution: {integrity: sha512-jAftSpOpw/5AdpOJ/cGiXCb+Vv22KXR5QZmxmllUDsnlm18672tpRaI2plmu/1d98CVvqhY61eSklFMrIf2c4w==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-host-header@3.620.0': + resolution: {integrity: sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-logger@3.410.0': - resolution: {integrity: sha512-YtmKYCVtBfScq3/UFJk+aSZOktKJBNZL9DaSc2aPcy/goCVsYDOkGwtHk0jIkC1JRSNCkVTqL7ya60sSr8zaQQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-location-constraint@3.609.0': + resolution: {integrity: sha512-xzsdoTkszGVqGVPjUmgoP7TORiByLueMHieI1fhQL888WPdqctwAx3ES6d/bA9Q/i8jnc6hs+Fjhy8UvBTkE9A==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-recursion-detection@3.410.0': - resolution: {integrity: sha512-KWaes5FLzRqj28vaIEE4Bimpga2E596WdPF2HaH6zsVMJddoRDsc3ZX9ZhLOGrXzIO1RqBd0QxbLrM0S/B2aOQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-logger@3.609.0': + resolution: {integrity: sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-sdk-s3@3.410.0': - resolution: {integrity: sha512-K2sG2V1ZkezYMCIy3uMt0MwtflcfIwLptwm0iFLaYitiINZQ1tcslk9ggAjyTHg0rslDSI4/zjkhy8VHFOV7HA==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-recursion-detection@3.620.0': + resolution: {integrity: sha512-nh91S7aGK3e/o1ck64sA/CyoFw+gAYj2BDOnoNa6ouyCrVJED96ZXWbhye/fz9SgmNUZR2g7GdVpiLpMKZoI5w==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-sdk-sts@3.410.0': - resolution: {integrity: sha512-YfBpctDocRR4CcROoDueJA7D+aMLBV8nTFfmVNdLLLgyuLZ/AUR11VQSu1lf9gQZKl8IpKE/BLf2fRE/qV1ZuA==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-sdk-s3@3.620.0': + resolution: {integrity: sha512-AAZ6NLVOx/bP97PYj/afCMeySzxOHocgJG3ZXh6f8MnJcGpZgx8NyRm0vtiYUTFrS2JtU4xV05Dl3j4afV3s4A==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-signing@3.410.0': - resolution: {integrity: sha512-KBAZ/eoAJUSJv5us2HsKwK2OszG2s9FEyKpEhgnHLcbbKzW873zHBH5GcOGEQu4AWArTy2ndzJu3FF+9/J9hJQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-signing@3.620.0': + resolution: {integrity: sha512-gxI7rubiaanUXaLfJ4NybERa9MGPNg2Ycl/OqANsozrBnR3Pw8vqy3EuVImQOyn2pJ2IFvl8ZPoSMHf4pX56FQ==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-ssec@3.410.0': - resolution: {integrity: sha512-DNsjVTXoxIh+PuW9o45CFaMiconbuZRm19MC3NA1yNCaCj3ZxD5OdXAutq6UjQdrx8UG4EjUlCJEEvBKmboITw==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-ssec@3.609.0': + resolution: {integrity: sha512-GZSD1s7+JswWOTamVap79QiDaIV7byJFssBW68GYjyRS5EBjNfwA/8s+6uE6g39R3ojyTbYOmvcANoZEhSULXg==} + engines: {node: '>=16.0.0'} - '@aws-sdk/middleware-user-agent@3.410.0': - resolution: {integrity: sha512-ZayDtLfvCZUohSxQc/49BfoU/y6bDHLfLdyyUJbJ54Sv8zQcrmdyKvCBFUZwE6tHQgAmv9/ZT18xECMl+xiONA==} - engines: {node: '>=14.0.0'} + '@aws-sdk/middleware-user-agent@3.620.0': + resolution: {integrity: sha512-bvS6etn+KsuL32ubY5D3xNof1qkenpbJXf/ugGXbg0n98DvDFQ/F+SMLxHgbnER5dsKYchNnhmtI6/FC3HFu/A==} + engines: {node: '>=16.0.0'} - '@aws-sdk/signature-v4-multi-region@3.412.0': - resolution: {integrity: sha512-ijxOeYpNDuk2T940S9HYcZ1C+wTP9vqp1Cw37zw9whVY2mKV3Vr7i+44D4FQ5HhWULgdwhjD7IctbNxPIPzUZQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/region-config-resolver@3.614.0': + resolution: {integrity: sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==} + engines: {node: '>=16.0.0'} - '@aws-sdk/token-providers@3.410.0': - resolution: {integrity: sha512-d5Nc0xydkH/X0LA1HDyhGY5sEv4LuADFk+QpDtT8ogLilcre+b1jpdY8Sih/gd1KoGS1H+d1tz2hSGwUHAbUbw==} - engines: {node: '>=14.0.0'} + '@aws-sdk/signature-v4-multi-region@3.620.0': + resolution: {integrity: sha512-yu1pTCqIbkSdaOvmyfW9vV9jWe3pDApkQPZLg4VEN5dXDWRtgQ/amv88myyCEoG14irUN1tsbvytcKzGyEXnhA==} + engines: {node: '>=16.0.0'} - '@aws-sdk/types@3.410.0': - resolution: {integrity: sha512-D7iaUCszv/v04NDaZUmCmekamy6VD/lKozm/3gS9+dkfU6cC2CsNoUfPV8BlV6dPdw0oWgF91am3I1stdvfVrQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/token-providers@3.614.0': + resolution: {integrity: sha512-okItqyY6L9IHdxqs+Z116y5/nda7rHxLvROxtAJdLavWTYDydxrZstImNgGWTeVdmc0xX2gJCI77UYUTQWnhRw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sso-oidc': ^3.614.0 - '@aws-sdk/types@3.413.0': - resolution: {integrity: sha512-j1xib0f/TazIFc5ySIKOlT1ujntRbaoG4LJFeEezz4ji03/wSJMI8Vi4KjzpBp8J1tTu0oRDnsxRIGixsUBeYQ==} - engines: {node: '>=14.0.0'} + '@aws-sdk/types@3.609.0': + resolution: {integrity: sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==} + engines: {node: '>=16.0.0'} - '@aws-sdk/util-arn-parser@3.310.0': - resolution: {integrity: sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==} - engines: {node: '>=14.0.0'} + '@aws-sdk/util-arn-parser@3.568.0': + resolution: {integrity: sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==} + engines: {node: '>=16.0.0'} - '@aws-sdk/util-endpoints@3.410.0': - resolution: {integrity: sha512-iNiqJyC7N3+8zFwnXUqcWSxrZecVZLToo1iTQQdeYL2af1IcOtRgb7n8jpAI/hmXhBSx2+3RI+Y7pxyFo1vu+w==} - engines: {node: '>=14.0.0'} + '@aws-sdk/util-endpoints@3.614.0': + resolution: {integrity: sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw==} + engines: {node: '>=16.0.0'} '@aws-sdk/util-locate-window@3.208.0': resolution: {integrity: sha512-iua1A2+P7JJEDHVgvXrRJSvsnzG7stYSGQnBVphIUlemwl6nN5D+QrgbjECtrbxRz8asYFHSzhdhECqN+tFiBg==} engines: {node: '>=14.0.0'} - '@aws-sdk/util-user-agent-browser@3.410.0': - resolution: {integrity: sha512-i1G/XGpXGMRT2zEiAhi1xucJsfCWk8nNYjk/LbC0sA+7B9Huri96YAzVib12wkHPsJQvZxZC6CpQDIHWm4lXMA==} + '@aws-sdk/util-user-agent-browser@3.609.0': + resolution: {integrity: sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==} - '@aws-sdk/util-user-agent-node@3.410.0': - resolution: {integrity: sha512-bK70t1jHRl8HrJXd4hEIwc5PBZ7U0w+81AKFnanIVKZwZedd6nLibUXDTK14z/Jp2GFcBqd4zkt2YLGkRt/U4A==} - engines: {node: '>=14.0.0'} + '@aws-sdk/util-user-agent-node@3.614.0': + resolution: {integrity: sha512-15ElZT88peoHnq5TEoEtZwoXTXRxNrk60TZNdpl/TUBJ5oNJ9Dqb5Z4ryb8ofN6nm9aFf59GVAerFDz8iUoHBA==} + engines: {node: '>=16.0.0'} peerDependencies: aws-crt: '>=1.0.0' peerDependenciesMeta: aws-crt: optional: true - '@aws-sdk/util-utf8-browser@3.259.0': - resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} - - '@aws-sdk/xml-builder@3.310.0': - resolution: {integrity: sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==} - engines: {node: '>=14.0.0'} + '@aws-sdk/xml-builder@3.609.0': + resolution: {integrity: sha512-l9XxNcA4HX98rwCC2/KoiWcmEiRfZe4G+mYwDbCFT87JIMj6GBhLDkAzr/W8KAaA2IDr8Vc6J8fZPgVulxxfMA==} + engines: {node: '>=16.0.0'} '@babel/code-frame@7.23.5': resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.24.7': + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.23.5': resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} engines: {node: '>=6.9.0'} - '@babel/core@7.23.5': - resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==} + '@babel/compat-data@7.24.7': + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} engines: {node: '>=6.9.0'} - '@babel/core@7.24.0': - resolution: {integrity: sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==} + '@babel/core@7.23.5': + resolution: {integrity: sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==} engines: {node: '>=6.9.0'} - '@babel/generator@7.23.5': - resolution: {integrity: sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==} + '@babel/core@7.24.7': + resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} engines: {node: '>=6.9.0'} - '@babel/generator@7.23.6': - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + '@babel/generator@7.24.7': + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.22.5': - resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + '@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.22.15': - resolution: {integrity: sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==} + '@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'} - '@babel/helper-compilation-targets@7.23.6': - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + '@babel/helper-compilation-targets@7.24.7': + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.23.5': - resolution: {integrity: sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==} + '@babel/helper-create-class-features-plugin@7.24.7': + resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.22.15': - resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + '@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.4.3': - resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} + '@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 @@ -1620,44 +1608,70 @@ packages: 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==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.23.0': - resolution: {integrity: sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==} + '@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'} + '@babel/helper-module-imports@7.24.7': + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.23.3': resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.22.5': - resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + '@babel/helper-module-transforms@7.24.7': + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} + 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-remap-async-to-generator@7.22.20': - resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + '@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.22.20': - resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + '@babel/helper-replace-supers@7.24.7': + resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1666,71 +1680,83 @@ packages: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} - '@babel/helper-skip-transparent-expression-wrappers@7.22.5': - resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + '@babel/helper-simple-access@7.24.7': + 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-string-parser@7.23.4': - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + '@babel/helper-split-export-declaration@7.24.7': + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.24.7': + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.22.20': - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + '@babel/helper-validator-identifier@7.24.7': + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.23.5': resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.22.20': - resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} + '@babel/helper-validator-option@7.24.7': + 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'} - '@babel/helpers@7.24.0': - resolution: {integrity: sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==} + '@babel/helpers@7.24.7': + 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/parser@7.23.9': - resolution: {integrity: sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==} - engines: {node: '>=6.0.0'} - hasBin: true + '@babel/highlight@7.24.7': + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} - '@babel/parser@7.24.0': - resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} + '@babel/parser@7.24.7': + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.24.5': - resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} - 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.23.3': - resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==} + '@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.23.3': - resolution: {integrity: sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==} + '@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.23.3': - resolution: {integrity: sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==} + '@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 @@ -1778,14 +1804,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.23.3': - resolution: {integrity: sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==} + '@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.23.3': - resolution: {integrity: sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==} + '@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 @@ -1860,92 +1886,92 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-arrow-functions@7.23.3': - resolution: {integrity: sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==} + '@babel/plugin-transform-arrow-functions@7.24.7': + resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.23.4': - resolution: {integrity: sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==} + '@babel/plugin-transform-async-generator-functions@7.24.7': + resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.23.3': - resolution: {integrity: sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==} + '@babel/plugin-transform-async-to-generator@7.24.7': + resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.23.3': - resolution: {integrity: sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==} + '@babel/plugin-transform-block-scoped-functions@7.24.7': + resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.23.4': - resolution: {integrity: sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==} + '@babel/plugin-transform-block-scoping@7.24.7': + resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.23.3': - resolution: {integrity: sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==} + '@babel/plugin-transform-class-properties@7.24.7': + resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.23.4': - resolution: {integrity: sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==} + '@babel/plugin-transform-class-static-block@7.24.7': + resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.23.5': - resolution: {integrity: sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==} + '@babel/plugin-transform-classes@7.24.7': + resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.23.3': - resolution: {integrity: sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==} + '@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 - '@babel/plugin-transform-destructuring@7.23.3': - resolution: {integrity: sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==} + '@babel/plugin-transform-destructuring@7.24.7': + resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-dotall-regex@7.23.3': - resolution: {integrity: sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==} + '@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 - '@babel/plugin-transform-duplicate-keys@7.23.3': - resolution: {integrity: sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==} + '@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 - '@babel/plugin-transform-dynamic-import@7.23.4': - resolution: {integrity: sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==} + '@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 - '@babel/plugin-transform-exponentiation-operator@7.23.3': - resolution: {integrity: sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==} + '@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 - '@babel/plugin-transform-export-namespace-from@7.23.4': - resolution: {integrity: sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==} + '@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 @@ -1956,176 +1982,176 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.23.3': - resolution: {integrity: sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==} + '@babel/plugin-transform-for-of@7.24.7': + resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.23.3': - resolution: {integrity: sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==} + '@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 - '@babel/plugin-transform-json-strings@7.23.4': - resolution: {integrity: sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==} + '@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 - '@babel/plugin-transform-literals@7.23.3': - resolution: {integrity: sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==} + '@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 - '@babel/plugin-transform-logical-assignment-operators@7.23.4': - resolution: {integrity: sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==} + '@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 - '@babel/plugin-transform-member-expression-literals@7.23.3': - resolution: {integrity: sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==} + '@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 - '@babel/plugin-transform-modules-amd@7.23.3': - resolution: {integrity: sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==} + '@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 - '@babel/plugin-transform-modules-commonjs@7.23.3': - resolution: {integrity: sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==} + '@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 - '@babel/plugin-transform-modules-systemjs@7.23.3': - resolution: {integrity: sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==} + '@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 - '@babel/plugin-transform-modules-umd@7.23.3': - resolution: {integrity: sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==} + '@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 - '@babel/plugin-transform-named-capturing-groups-regex@7.22.5': - resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + '@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 - '@babel/plugin-transform-new-target@7.23.3': - resolution: {integrity: sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==} + '@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 - '@babel/plugin-transform-nullish-coalescing-operator@7.23.4': - resolution: {integrity: sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==} + '@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 - '@babel/plugin-transform-numeric-separator@7.23.4': - resolution: {integrity: sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==} + '@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 - '@babel/plugin-transform-object-rest-spread@7.23.4': - resolution: {integrity: sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==} + '@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 - '@babel/plugin-transform-object-super@7.23.3': - resolution: {integrity: sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==} + '@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 - '@babel/plugin-transform-optional-catch-binding@7.23.4': - resolution: {integrity: sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==} + '@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 - '@babel/plugin-transform-optional-chaining@7.23.4': - resolution: {integrity: sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==} + '@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 - '@babel/plugin-transform-parameters@7.23.3': - resolution: {integrity: sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==} + '@babel/plugin-transform-parameters@7.24.7': + resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.23.3': - resolution: {integrity: sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==} + '@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 - '@babel/plugin-transform-private-property-in-object@7.23.4': - resolution: {integrity: sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==} + '@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 - '@babel/plugin-transform-property-literals@7.23.3': - resolution: {integrity: sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==} + '@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.23.3': - resolution: {integrity: sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==} + '@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.23.3': - resolution: {integrity: sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==} + '@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.23.3': - resolution: {integrity: sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==} + '@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.23.3': - resolution: {integrity: sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==} + '@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.23.3': - resolution: {integrity: sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==} + '@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.23.3': - resolution: {integrity: sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==} + '@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.23.3': - resolution: {integrity: sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==} + '@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 @@ -2136,32 +2162,32 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-escapes@7.23.3': - resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} + '@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.23.3': - resolution: {integrity: sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==} + '@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.23.3': - resolution: {integrity: sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==} + '@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.23.3': - resolution: {integrity: sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==} + '@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.23.5': - resolution: {integrity: sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==} + '@babel/preset-env@7.24.7': + resolution: {integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -2204,20 +2230,20 @@ packages: resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.23.5': - resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} + '@babel/template@7.24.7': + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.24.0': - resolution: {integrity: sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==} + '@babel/traverse@7.23.5': + resolution: {integrity: sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==} engines: {node: '>=6.9.0'} - '@babel/types@7.23.5': - resolution: {integrity: sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==} + '@babel/traverse@7.24.7': + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} engines: {node: '>=6.9.0'} - '@babel/types@7.24.0': - resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + '@babel/types@7.24.7': + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} engines: {node: '>=6.9.0'} '@base2/pretty-print-object@1.0.1': @@ -2226,16 +2252,16 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@bull-board/api@5.17.0': - resolution: {integrity: sha512-qU+AiZIaYa//rkt1x7jDowtYa8u7/dLsDfEWgenZMkgvUszZ1kxJszdCtGapsDTVyPmnXgTRxpOWcR6sAYwSNQ==} + '@bull-board/api@5.21.1': + resolution: {integrity: sha512-anzTfhOJ93eraT/GYeyxWpxRMarHwuevn6pPBfdOj0LC2eg98OPnkfdMSjcrpL3qrqsxON0RslS7kuPfCEnX6A==} peerDependencies: - '@bull-board/ui': 5.17.0 + '@bull-board/ui': 5.21.1 - '@bull-board/fastify@5.17.0': - resolution: {integrity: sha512-73YrPc7ERTWSOQRgBP6a7BPscWfcHd8U+Zq0auMdL/KkjPhG9GxapbfnovGZDDahJL/p/4YQb6ULu03zdtOrEA==} + '@bull-board/fastify@5.21.1': + resolution: {integrity: sha512-We33yolc70SALjDdF3cjEaLn1L/vrw85eFCsrviESaW3dFVIdB+xn0fdqMFK6NnaC0JjBa3Ypfev4Co+eaZ+1A==} - '@bull-board/ui@5.17.0': - resolution: {integrity: sha512-Vj+yWPjrjx3Iqh2N/ZBDhK2d2yJD44dfvIxm+SnXQb4ne312j117TpViInceysxGtbbAOlAW6hq6JvsDoRl7KQ==} + '@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==} @@ -2243,6 +2269,9 @@ packages: '@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==} @@ -2250,38 +2279,38 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@cropper/element-canvas@2.0.0-beta.5': - resolution: {integrity: sha512-TS+NTVQAyLKBFLIjzFcaFK6V5GaNCNSp8FBjApTD/AosV/dPRlNCsgmdJ/BugwJTJUSowVnLrPmulI35z4npAg==} + '@cropper/element-canvas@2.0.0-rc.1': + resolution: {integrity: sha512-jRt9OM7cls+zch8U2m7pA9wp8dNOz0EtedGKkqH+DInUYw1+UtonEJirrxyl1YHRgOme5M5DTsDmkUSrUiZN6g==} - '@cropper/element-crosshair@2.0.0-beta.5': - resolution: {integrity: sha512-en3EjiqS/O/uVPVLUanx2ZxvE2n3J5VxGABvBTwQimX4c3kNixq8TUVlsaLdcG7jbehxFpT3S19+tiuZudHqxg==} + '@cropper/element-crosshair@2.0.0-rc.1': + resolution: {integrity: sha512-xfLelqM8EnRZUf7xEE88RWQQx5erUv7jrzni52bAw3/Ua8HXIz3uAMnkrGKOTBj8K4Rv/mNJY8k1DVAEfHY6Lg==} - '@cropper/element-grid@2.0.0-beta.5': - resolution: {integrity: sha512-uKQExNTOMOGo5d6Tv1NJDbjJHRR/0NgqeROUSt2J8g9ymPP+/MoFdCCf+Nj/KM5pk7/fBEV3HhzUnO8jh1hZfQ==} + '@cropper/element-grid@2.0.0-rc.1': + resolution: {integrity: sha512-U/BYPl76upd9sXT+pZTFoQzUqWyNxdGs4YR2UtaVCfTMHLDTrssPAedmqEEnHgbqVcr325sIEfVmwWVA+v+8Dg==} - '@cropper/element-handle@2.0.0-beta.5': - resolution: {integrity: sha512-SlaV5/qbEBQLHnuaGD8J0EqSp797m/MMB8V10EUZpv6cznSRxg/SXOj6ROE0ePzo5+0i96Dw+8ZukLilDgc1Xw==} + '@cropper/element-handle@2.0.0-rc.1': + resolution: {integrity: sha512-GuOHbjkg5CP1+oFzWQeD7VZffUE86dp4gKv5egLxkBEwnQp1VQxjO7L1Wkgj+KsQymoDczsl+x4bF12KDyDg2g==} - '@cropper/element-image@2.0.0-beta.5': - resolution: {integrity: sha512-369ztVaoRS2DN8SaiHZ/bRCz0Snw9ss7PZrX1OQK86fwVhCoeRqCHj48ayfLMdchx+J3RbM5f2g8ePf7ejwOKw==} + '@cropper/element-image@2.0.0-rc.1': + resolution: {integrity: sha512-ttzawKbUkR2A9U3bc2AN/jbNdszBP/yb83PIc5jekjOs+Z7kUBVdOo1SLIewpQ0DjUzhfCRXWUowP1McVQUXZw==} - '@cropper/element-selection@2.0.0-beta.5': - resolution: {integrity: sha512-l8DvOBAZYytTarpkfhCglhxD+zDQ2acVwIzGwp5r9xR+ERleJHxr2rHYVhowRHT/JZRd94DJBlye91c1uO/XGg==} + '@cropper/element-selection@2.0.0-rc.1': + resolution: {integrity: sha512-AcRHRbsyt9xRfBD1QRyNDTS+vaYg6uAeuqhk/Ra58pqxlhtoimAV3oQ7uc/edwOlK60f/DxtKCc8rSOYFQ85bQ==} - '@cropper/element-shade@2.0.0-beta.5': - resolution: {integrity: sha512-LGSVLAD1lasFrS+Pd7JnQSJRCMSNnc40UCcjLhscDuRcRHK/ViMglnwCfFxeGnS26kugbDLF5IbYDCLCbykUog==} + '@cropper/element-shade@2.0.0-rc.1': + resolution: {integrity: sha512-nHv2WujETENoIfxWQn7TYiOnXm5YUnZsoG4r6njK5cxj0gIUfPudUSbjWCQSuB2oxxpeEK8oyTdfOZtP9cxK4g==} - '@cropper/element-viewer@2.0.0-beta.5': - resolution: {integrity: sha512-i4cc+L+j8Gq1L8g1BQWfQ842QxH5T9v2EkIeGuW66SVSBglafxu8gxmSOyRD3hDAMHM3wbJ+XVmFwBHZzlYCvQ==} + '@cropper/element-viewer@2.0.0-rc.1': + resolution: {integrity: sha512-xTj0BObCygbVWXc7t7FYZ9k2eFyWN360it5uGeAkImXcwINRQGTFcLLOjs6i3SwedI7F1a1yNcTBfoT1B/sNAg==} - '@cropper/element@2.0.0-beta.5': - resolution: {integrity: sha512-+pHX/iYw+Y/HxgpcjvSPBc3+hvJaycznbZdWifnChmDkpLStd6Xu9gO2ful9sSL0uGSjQxUYV4xPyikYJOnfug==} + '@cropper/element@2.0.0-rc.1': + resolution: {integrity: sha512-OPKgjUgYC2Xmv77vEqtAR6bdfKOW+v9FrSjr4re3u95rcVj6NJ0JidIta41Ipp8KydHTXSmLetq4XDrA+vuIJQ==} - '@cropper/elements@2.0.0-beta.5': - resolution: {integrity: sha512-KWa5/dmJpLcKDJpNlbEQzO9Shz+f4aB0I3y97CqqTf8JSGS6CEKOd9uLywd1eow1r4O0Hwo65ktXPwAEhMWDZg==} + '@cropper/elements@2.0.0-rc.1': + resolution: {integrity: sha512-6qbtCq3iL3dETVav2XA03a8iLkHXWMIqHFxViMjlLr9CSuDjjaS5wp0JDuGtPv5FHxjsjyQ8Yayt8Ak5p09Zxg==} - '@cropper/utils@2.0.0-beta.5': - resolution: {integrity: sha512-xE7Klel/4WSjhLUeldfROwbWqV/1A3YKrQLqTrs5/X0ath7B05Fmvhr3TNFvN51v2KSx46Ug6xDJzmbg772m1g==} + '@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==} @@ -2315,12 +2344,18 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.20.2': - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + '@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'} @@ -2333,12 +2368,18 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.20.2': - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.23.0': + resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -2351,12 +2392,18 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.20.2': - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.23.0': + resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -2369,12 +2416,18 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.20.2': - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.23.0': + resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -2387,12 +2440,18 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.20.2': - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.23.0': + resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -2405,12 +2464,18 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.20.2': - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.23.0': + resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -2423,12 +2488,18 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.20.2': - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.23.0': + resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -2441,12 +2512,18 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.20.2': - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.0': + resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -2459,12 +2536,18 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.20.2': - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.23.0': + resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -2477,12 +2560,18 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.20.2': - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.23.0': + resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -2495,12 +2584,18 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.20.2': - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.23.0': + resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -2513,12 +2608,18 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.20.2': - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.23.0': + resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -2531,12 +2632,18 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.20.2': - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.23.0': + resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -2549,12 +2656,18 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.20.2': - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.23.0': + resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -2567,12 +2680,18 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.20.2': - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.23.0': + resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -2585,12 +2704,18 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.20.2': - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.23.0': + resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -2603,13 +2728,19 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.20.2': - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.18.20': + '@esbuild/linux-x64@0.23.0': + resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} cpu: [x64] @@ -2621,12 +2752,24 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.20.2': - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.23.0': + resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==} + 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-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -2639,12 +2782,18 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.20.2': - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.0': + resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -2657,12 +2806,18 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.20.2': - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.23.0': + resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -2675,12 +2830,18 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.20.2': - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.23.0': + resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -2693,12 +2854,18 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.20.2': - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.23.0': + resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -2711,34 +2878,60 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.20.2': - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.0': + resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==} + 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} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.10.0': - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + '@eslint-community/regexpp@4.11.0': + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint-community/regexpp@4.6.2': + resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/compat@1.1.1': + resolution: {integrity: sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-array@0.17.1': + resolution: {integrity: sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@2.1.4': resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@eslint/js@8.53.0': - resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/eslintrc@3.1.0': + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@8.57.0': resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/js@9.8.0': + resolution: {integrity: sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + 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==} @@ -2781,8 +2974,8 @@ packages: '@fastify/http-proxy@9.5.0': resolution: {integrity: sha512-1iqIdV10d5k9YtfHq9ylX5zt1NiM50fG+rIX40qt00R694sqWso3ukyTFZVk33SDoSiBW8roB7n11RUVUoN+Ag==} - '@fastify/multipart@8.2.0': - resolution: {integrity: sha512-OZ8nsyyoS2TV7Yeu3ZdrdDGsKUTAbfjrKC9jSxGgT2qdgek+BxpWX31ZubTrWMNZyU5xwk4ox6AvTjAbYWjrWg==} + '@fastify/multipart@8.3.0': + resolution: {integrity: sha512-A8h80TTyqUzaMVH0Cr9Qcm6RxSkVqmhK/MVBYHYeRRSUbUYv08WecjWKSlG2aSnD4aGI841pVxAjC+G1GafUeQ==} '@fastify/reply-from@9.0.1': resolution: {integrity: sha512-q9vFNUiXZTY1x8omDPe59os2MYq+3y7KgO/kZoXpZlnud+45Nd8Ot/svEvrUATzjkizIggfS4K8LR9zXDyZZKg==} @@ -2793,8 +2986,8 @@ packages: '@fastify/static@6.12.0': resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==} - '@fastify/static@7.0.3': - resolution: {integrity: sha512-2tmTdF+uFCykasutaO6k4/wOt7eXyi7m3dGuCPo5micXzv0qt6ttb/nWnDYL/BlXjYGfp1JI4a1gyluTIylvQA==} + '@fastify/static@7.0.4': + resolution: {integrity: sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==} '@fastify/view@8.2.0': resolution: {integrity: sha512-hBSiBofCnJNlPHEMZWpO1SL84eqOaqujJ1hR3jntFyZZCkweH5jMs12DKYyGesjVll7SJFRRxPUBB8kmUmneRQ==} @@ -2812,11 +3005,8 @@ packages: '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} - '@hapi/hoek@10.0.1': - resolution: {integrity: sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==} - - '@hapi/hoek@11.0.2': - resolution: {integrity: sha512-aKmlCO57XFZ26wso4rJsW4oTUnrgTFw2jh3io7CAtO9w4UltBNwRXvXIVzzyfkaaLRo3nluP/19msA8vDUUuKw==} + '@hapi/hoek@11.0.4': + resolution: {integrity: sha512-PnsP5d4q7289pS2T2EgGz147BFJ2Jpb4yrEdkpz2IhgEUzos1S7HTl7ezWh1yfYzYlj89KzLdCRkqsP6SIryeQ==} '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -2830,13 +3020,10 @@ packages: '@hexagon/base64@1.1.27': resolution: {integrity: sha512-PdUmzpvcUM3Rh39kvz9RdbPVYhMjBjdV7Suw7ZduP7urRLsZR8l5tzgSWKm7TExwBYDFwTnYrZbnE0rQ3N5NLQ==} - '@humanwhocodes/config-array@0.11.13': - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} - engines: {node: '>=10.10.0'} - '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -2846,20 +3033,22 @@ packages: resolution: {integrity: sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==} engines: {node: '>=10.10.0'} - '@humanwhocodes/object-schema@2.0.1': - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead - '@humanwhocodes/object-schema@2.0.2': - resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + '@humanwhocodes/retry@0.3.0': + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.33.3': - resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==} + '@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'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.33.3': - resolution: {integrity: sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==} + '@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'} cpu: [x64] os: [darwin] @@ -2912,55 +3101,55 @@ packages: cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.33.3': - resolution: {integrity: sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==} + '@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'} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.33.3': - resolution: {integrity: sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==} + '@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'} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.33.3': - resolution: {integrity: sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==} - 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-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'} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.3': - resolution: {integrity: sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==} + '@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'} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.3': - resolution: {integrity: sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==} + '@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'} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.33.3': - resolution: {integrity: sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==} + '@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'} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.3': - resolution: {integrity: sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==} + '@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'} cpu: [wasm32] - '@img/sharp-win32-ia32@0.33.3': - resolution: {integrity: sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==} + '@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'} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.3': - resolution: {integrity: sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==} + '@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'} cpu: [x64] os: [win32] @@ -3078,8 +3267,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.0': - resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.1': + resolution: {integrity: sha512-pdoMZ9QaPnVlSM+SdU/wgg0nyD/8wQ7y90ttO2CMCyrrm7RxveYIJ5eNfjPaoMFqW41LZra7QO9j+xV4Y18Glw==} peerDependencies: typescript: '>= 4.3.x' vite: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -3091,6 +3280,10 @@ packages: 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'} + '@jridgewell/resolve-uri@3.1.0': resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} @@ -3099,8 +3292,12 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.5': - resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} '@jridgewell/sourcemap-codec@1.4.14': resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} @@ -3111,6 +3308,9 @@ packages: '@jridgewell/trace-mapping@0.3.18': resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} @@ -3140,29 +3340,31 @@ packages: '@types/react': '>=16' react: '>=16' - '@microsoft/api-extractor-model@7.28.14': - resolution: {integrity: sha512-Bery/c8A8SsKPSvA82cTTuy/+OcxZbLRmKhPkk91/AJOQzxZsShcrmHFAGeiEqSIrv1nPZ3tKq9kfMLdCHmsqg==} + '@microsoft/api-extractor-model@7.29.4': + resolution: {integrity: sha512-LHOMxmT8/tU1IiiiHOdHFF83Qsi+V8d0kLfscG4EvQE9cafiR8blOYr8SfkQKWB1wgEilQgXJX3MIA4vetDLZw==} - '@microsoft/api-extractor@7.43.1': - resolution: {integrity: sha512-ohg40SsvFFgzHFAtYq5wKJc8ZDyY46bphjtnSvhSSlXpPTG7GHwyyXkn48UZiUCBwr2WC7TRC1Jfwz7nreuiyQ==} + '@microsoft/api-extractor@7.47.4': + resolution: {integrity: sha512-HKm+P4VNzWwvq1Ey+Jfhhj/3MjsD+ka2hbt8L5AcRM95lu1MFOYnz3XlU7Gr79Q/ZhOb7W/imAKeYrOI0bFydg==} hasBin: true - '@microsoft/tsdoc-config@0.16.2': - resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + '@microsoft/tsdoc-config@0.17.0': + resolution: {integrity: sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==} - '@microsoft/tsdoc@0.14.2': - resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + '@microsoft/tsdoc@0.15.0': + resolution: {integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==} '@misskey-dev/browser-image-resizer@2024.1.0': resolution: {integrity: sha512-4EnO0zLW5NDtng3Gaz5MuT761uiuoOuplwX18wBqgj8w56LTU5BjLn/vbHwDIIe0j2gwqDYhMb7bDjmr1/Fomg==} - '@misskey-dev/eslint-plugin@1.0.0': - resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + '@misskey-dev/eslint-plugin@2.0.3': + resolution: {integrity: sha512-Gd7chezh53Gq4BZ+Tw/uRi4t5DocXR+vTFuTSRpwrZjbyTk6tWMHLV0EtaGY/6GT+Q6WH3UMJYExsFyHFK+JPw==} peerDependencies: - '@typescript-eslint/eslint-plugin': '>= 6' - '@typescript-eslint/parser': '>= 6' - eslint: '>= 3' + '@eslint/compat': '>= 1' + '@typescript-eslint/eslint-plugin': '>= 7' + '@typescript-eslint/parser': '>= 7' + eslint: '>= 8' eslint-plugin-import: '>= 2' + globals: '>= 15' '@misskey-dev/sharp-read-bmp@1.2.0': resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==} @@ -3204,77 +3406,70 @@ packages: cpu: [x64] os: [win32] - '@mswjs/cookies@1.1.0': - resolution: {integrity: sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==} - engines: {node: '>=18'} - - '@mswjs/interceptors@0.26.15': - resolution: {integrity: sha512-HM47Lu1YFmnYHKMBynFfjCp0U/yRskHj/8QEJW0CBEPOlw8Gkmjfll+S9b8M7V5CNDw2/ciRxjjnWeaCiblSIQ==} + '@mswjs/interceptors@0.29.1': + resolution: {integrity: sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==} engines: {node: '>=18'} - '@napi-rs/canvas-android-arm64@0.1.52': - resolution: {integrity: sha512-x/K471KbASPVh5mfBUxokza66J0FNIlOgMNANWAf5C8HiATb487KecEhSkUQvvTS3WLYC9uSqIPHFgwF+tir3w==} + '@napi-rs/canvas-android-arm64@0.1.53': + resolution: {integrity: sha512-2YhxfVsZguATlRWE0fZdTx35SE9+r5D7HV5GPNDataZOKmHf+zZ5//dspuuBSbOriQdoicaFrgXKCUqI0pK3WQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@0.1.52': - resolution: {integrity: sha512-4OgVRD7TW02q5Q7lWLLjT+pYJ9ZHkQUTBOuXbPQ5wB0Wnh3RIq/aMY6thoXDZDzdR5vV3a5TUtbZUJ0aqLq3NA==} + '@napi-rs/canvas-darwin-arm64@0.1.53': + resolution: {integrity: sha512-ls+CWLMusf4RAGo5BvIIzA6dNcc0elwVp6LKjHfQECHA8KKmvdB58YuE5BQcTlb2rzk0SEKtBC/Th3NI2oNdfg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.52': - resolution: {integrity: sha512-3fgeGJ3j2X6Mtmn0QYf3iA+A6y1ePnsayakc2emEokzf03ErrPczONw3vjnTQo53JLPMzEnfPGAffdktU/ssPA==} + '@napi-rs/canvas-darwin-x64@0.1.53': + resolution: {integrity: sha512-ZAgcoCH5+5OKS2P8Lxx+jbkAPKkyLD2x6OvSrHg1U6ppdxmLA+CkJlRl8w45HCXwuyIiP7OeymECRtiNYTwznQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.52': - resolution: {integrity: sha512-aaDEEK5XwHUrPt0q4SR8l7Va0vtn50KmSs+itxP+o7RNk3Nuch8fINHOXyhMyhwNYgv1tfiJVyHsJhD0E6lXGA==} + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.53': + resolution: {integrity: sha512-p9km/3C/loDxu3AvA8/vtpIS1BGMd/Ehkl2Iu/v/Gw8N/KUIt3HUvTS7AKApyVE28bxTfq96wJQjtcT8jzDncw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@0.1.52': - resolution: {integrity: sha512-tzuwM7Amt5mkrp4csQjYWkFzwFdiCm7RNdJ5usX8syzKSXmozqWzLHjzo/2ozdSQNUy6wyzRrxkG4Rh6g0OpOA==} + '@napi-rs/canvas-linux-arm64-gnu@0.1.53': + resolution: {integrity: sha512-QKK+sykEiYwjwd+ogyLcpcnH38DNZ8KViBlnfEpoGA2Wa+21/cWQKfMxnbgb/rbvm5tazJinZcihFvH577WQ5g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-arm64-musl@0.1.52': - resolution: {integrity: sha512-HQCtJlDT0dFp3uUZVzZOZ1VLMO7lbLRc548MjMxPpojit2ZdGopFzJ8jDSr4iszHrTO1SM1AxPaCM3pRvCAtjw==} + '@napi-rs/canvas-linux-arm64-musl@0.1.53': + resolution: {integrity: sha512-2N41U0X8RnrTKzpTtPv1ozlYkJtPsUdbfF3uP/KEd/BsULGd8Y8ghkGMS6CM+821au4ex0dPrWOOdT9wC1rSqQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@napi-rs/canvas-linux-x64-gnu@0.1.52': - resolution: {integrity: sha512-z5sBEw0PVWPH/MIQL8hOR8C3YYVlu8lqtRUcYajigMfXAhbMiNqDWTjuIWGMz3nIydDjZmn8KTxw/D4a0HFPqQ==} + '@napi-rs/canvas-linux-x64-gnu@0.1.53': + resolution: {integrity: sha512-7XjuTvDKCODtf/vMwF43VGDrjfgwYKgS91ggdcX3UrJaBYWyWu/+eqNvNj+zdXSe/0x+YOjf5jG4m8xIXdBMQA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-linux-x64-musl@0.1.52': - resolution: {integrity: sha512-G1+JdWFhHLyHhULJS51xTEhB7EL0ZiAUQwQaRi4/w75OOYDQ91O+o4miaxDHiV0hZuxBhHtZU6ftV2Zl3RMguw==} + '@napi-rs/canvas-linux-x64-musl@0.1.53': + resolution: {integrity: sha512-970WEvB8vmj+uxvgdBZ+AGFV7uq9GJhXrqG5PGQ5lWciHX0P0d/OhS2F7TITgFR0LsKDQZ7XQgzMxsYOfwZ0FQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@napi-rs/canvas-win32-x64-msvc@0.1.52': - resolution: {integrity: sha512-hMI626VsCC/wv29qHF78N7TSG+auatOp08DHln0Zdif5y1NJ14NU/rNUhzlTW8Zc6ssw+AMDJ3KKYYWYYg1aoA==} + '@napi-rs/canvas-win32-x64-msvc@0.1.53': + resolution: {integrity: sha512-rLFQCSJaWg/sv54Aap9nAhaodi4Vyb4un50EgW+PNkk8icMziU6KLRKirGBdQr9ZdxnshAPeQXD1g2ArStujKA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/canvas@0.1.52': - resolution: {integrity: sha512-xeW9EghZLDPZuqWJ4l1+eG3ld0i9J7SpV2zlgi34MPt/FE9K2XWGCfnLr0gHGOBkcI3YOVhI13I0HqRAkMPdVw==} + '@napi-rs/canvas@0.1.53': + resolution: {integrity: sha512-XsEZi97+kKykmAiPpY+IpZoHxJY1srqFZp8jDt1/RySzC0kB0iZYt/VMIFqQKpLCARZjD7SOAz2AULtwYlesCA==} engines: {node: '>= 10'} - '@ndelangen/get-tarball@3.0.7': - resolution: {integrity: sha512-NqGfTZIZpRFef1GoVaShSSRwDC3vde3ThtTeqFdcYd6ipKqnfEVhjK2hUeHjCQUcptyZr2TONqcloFXM+5QBrQ==} - - '@nestjs/common@10.3.8': - resolution: {integrity: sha512-P+vPEIvqx2e+fonsYVlFXKvoChyJ8Tq+lfpqdVFqblovHbFr3kZ/nYX0cPs+XuW6bnRT8tz0SSR9XBGU43kJhw==} + '@nestjs/common@10.3.10': + resolution: {integrity: sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -3286,8 +3481,8 @@ packages: class-validator: optional: true - '@nestjs/core@10.3.8': - resolution: {integrity: sha512-AxF4tpYLDNn5Wfb3C4bNaaHJ4pREH5FJrSisR2A5zkYpQFORFs0Tc36lOFPMwBTy8Iv2wUwWLUVc5ftBnxEv4w==} + '@nestjs/core@10.3.10': + resolution: {integrity: sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/microservices': ^10.0.0 @@ -3303,14 +3498,14 @@ packages: '@nestjs/websockets': optional: true - '@nestjs/platform-express@10.3.8': - resolution: {integrity: sha512-sifLoxgEJvAgbim1UuW6wyScMfkS9SVQRH+lN33N/9ZvZSjO6NSDLOe+wxqsnZkia+QrjFC0qy0ITRAsggfqbg==} + '@nestjs/platform-express@10.3.10': + resolution: {integrity: sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 - '@nestjs/testing@10.3.8': - resolution: {integrity: sha512-hpX9das2TdFTKQ4/2ojhjI6YgXtCfXRKui3A4Qaj54VVzc5+mtK502Jj18Vzji98o9MVS6skmYu+S/UvW3U6Fw==} + '@nestjs/testing@10.3.10': + resolution: {integrity: sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 @@ -3322,6 +3517,10 @@ packages: '@nestjs/platform-express': optional: true + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3359,19 +3558,19 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@opentelemetry/api-logs@0.51.1': - resolution: {integrity: sha512-E3skn949Pk1z2XtXu/lxf6QAZpawuTM/IUEXcAzpiUkTd73Hmvw26FiN3cJuTmkpM5hZzHwkomVdtrh/n/zzwA==} + '@opentelemetry/api-logs@0.52.1': + resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} - '@opentelemetry/api@1.8.0': - resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@1.24.1': - resolution: {integrity: sha512-R5r6DO4kgEOVBxFXhXjwospLQkv+sYxwCfjvoZBe7Zm6KKXAV9kDSJhi/D1BweowdZmO+sdbENLs374gER8hpQ==} + '@opentelemetry/context-async-hooks@1.25.1': + resolution: {integrity: sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' + '@opentelemetry/api': '>=1.0.0 <1.10.0' '@opentelemetry/core@1.24.1': resolution: {integrity: sha512-wMSGfsdmibI88K9wB498zXY04yThPexo8jvwNNlm542HZB7XrrMRBbAyKJqG8qDRJwIBdBrPMi4V9ZPW/sqrcg==} @@ -3379,98 +3578,110 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.9.0' - '@opentelemetry/instrumentation-connect@0.36.0': - resolution: {integrity: sha512-k9++bmJZ9zDEs3u3DnKTn2l7QTiNFg3gPx7G9rW0TPnP+xZoBSBTrEcGYBaqflQlrFG23Q58+X1sM2ayWPv5Fg==} + '@opentelemetry/core@1.25.1': + resolution: {integrity: sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation-connect@0.38.0': + resolution: {integrity: sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.41.0': + resolution: {integrity: sha512-/B7fbMdaf3SYe5f1P973tkqd6s7XZirjpfkoJ63E7nltU30qmlgm9tY5XwZOzAFI0rHS9tbrFI2HFPAvQUFe/A==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-express@0.39.0': - resolution: {integrity: sha512-AG8U7z7D0JcBu/7dDcwb47UMEzj9/FMiJV2iQZqrsZnxR3FjB9J9oIH2iszJYci2eUdp2WbdvtpD9RV/zmME5A==} + '@opentelemetry/instrumentation-fastify@0.38.0': + resolution: {integrity: sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-fastify@0.36.1': - resolution: {integrity: sha512-3Nfm43PI0I+3EX+1YbSy6xbDu276R1Dh1tqAk68yd4yirnIh52Kd5B+nJ8CgHA7o3UKakpBjj6vSzi5vNCzJIA==} + '@opentelemetry/instrumentation-graphql@0.42.0': + resolution: {integrity: sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-graphql@0.40.0': - resolution: {integrity: sha512-LVRdEHWACWOczv2imD+mhUrLMxsEjPPi32vIZJT57zygR5aUiA4em8X3aiGOCycgbMWkIu8xOSGSxdx3JmzN+w==} + '@opentelemetry/instrumentation-hapi@0.40.0': + resolution: {integrity: sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-hapi@0.38.0': - resolution: {integrity: sha512-ZcOqEuwuutTDYIjhDIStix22ECblG/i9pHje23QGs4Q4YS4RMaZ5hKCoQJxW88Z4K7T53rQkdISmoXFKDV8xMg==} + '@opentelemetry/instrumentation-http@0.52.1': + resolution: {integrity: sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.51.1': - resolution: {integrity: sha512-6b3nZnFFEz/3xZ6w8bVxctPUWIPWiXuPQ725530JgxnN1cvYFd8CJ75PrHZNjynmzSSnqBkN3ef4R9N+RpMh8Q==} + '@opentelemetry/instrumentation-ioredis@0.42.0': + resolution: {integrity: sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.40.0': - resolution: {integrity: sha512-Jv/fH7KhpWe4KBirsiqeUJIYrsdR2iu2l4nWhfOlRvaZ+zYIiLEzTQR6QhBbyRoAbU4OuYJzjWusOmmpGBnwng==} + '@opentelemetry/instrumentation-koa@0.42.0': + resolution: {integrity: sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-koa@0.40.0': - resolution: {integrity: sha512-dJc3H/bKMcgUYcQpLF+1IbmUKus0e5Fnn/+ru/3voIRHwMADT3rFSUcGLWSczkg68BCgz0vFWGDTvPtcWIFr7A==} + '@opentelemetry/instrumentation-mongodb@0.46.0': + resolution: {integrity: sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongodb@0.43.0': - resolution: {integrity: sha512-bMKej7Y76QVUD3l55Q9YqizXybHUzF3pujsBFjqbZrRn2WYqtsDtTUlbCK7fvXNPwFInqZ2KhnTqd0gwo8MzaQ==} + '@opentelemetry/instrumentation-mongoose@0.40.0': + resolution: {integrity: sha512-niRi5ZUnkgzRhIGMOozTyoZIvJKNJyhijQI4nF4iFSb+FUx2v5fngfR+8XLmdQAO7xmsD8E5vEGdDVYVtKbZew==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mongoose@0.38.1': - resolution: {integrity: sha512-zaeiasdnRjXe6VhYCBMdkmAVh1S5MmXC/0spet+yqoaViGnYst/DOxPvhwg3yT4Yag5crZNWsVXnA538UjP6Ow==} + '@opentelemetry/instrumentation-mysql2@0.40.0': + resolution: {integrity: sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql2@0.38.1': - resolution: {integrity: sha512-qkpHMgWSDTYVB1vlZ9sspf7l2wdS5DDq/rbIepDwX5BA0N0068JTQqh0CgAh34tdFqSCnWXIhcyOXC2TtRb0sg==} + '@opentelemetry/instrumentation-mysql@0.40.0': + resolution: {integrity: sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-mysql@0.38.1': - resolution: {integrity: sha512-+iBAawUaTfX/HAlvySwozx0C2B6LBfNPXX1W8Z2On1Uva33AGkw2UjL9XgIg1Pj4eLZ9R4EoJ/aFz+Xj4E/7Fw==} + '@opentelemetry/instrumentation-nestjs-core@0.39.0': + resolution: {integrity: sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.37.1': - resolution: {integrity: sha512-ebYQjHZEmGHWEALwwDGhSQVLBaurFnuLIkZD5igPXrt7ohfF4lc5/4al1LO+vKc0NHk8SJWStuRueT86ISA8Vg==} + '@opentelemetry/instrumentation-pg@0.43.0': + resolution: {integrity: sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.41.0': - resolution: {integrity: sha512-BSlhpivzBD77meQNZY9fS4aKgydA8AJBzv2dqvxXFy/Hq64b7HURgw/ztbmwFeYwdF5raZZUifiiNSMLpOJoSA==} + '@opentelemetry/instrumentation-redis-4@0.41.0': + resolution: {integrity: sha512-H7IfGTqW2reLXqput4yzAe8YpDC0fmVNal95GHMLOrS89W+qWUKIqxolSh63hJyfmwPSFwXASzj7wpSk8Az+Dg==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.43.0': - resolution: {integrity: sha512-S1uHE+sxaepgp+t8lvIDuRgyjJWisAb733198kwQTUc9ZtYQ2V2gmyCtR1x21ePGVLoMiX/NWY7WA290hwkjJQ==} + '@opentelemetry/instrumentation@0.46.0': + resolution: {integrity: sha512-a9TijXZZbk0vI5TGLZl+0kxyFfrXHhX6Svtz7Pp2/VBlCSKrazuULEyoJQrOknJyFWNMEmbbJgOciHCCpQcisw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.51.1': - resolution: {integrity: sha512-JIrvhpgqY6437QIqToyozrUG1h5UhwHkaGK/WAX+fkrpyPtc+RO5FkRtUd9BH0MibabHHvqsnBGKfKVijbmp8w==} + '@opentelemetry/instrumentation@0.52.1': + resolution: {integrity: sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -3485,22 +3696,32 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.9.0' + '@opentelemetry/resources@1.25.1': + resolution: {integrity: sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-metrics@1.24.1': resolution: {integrity: sha512-FrAqCbbGao9iKI+Mgh+OsC9+U2YMoXnlDHe06yH7dvavCKzE3S892dGtX54+WhSFVxHR/TMRVJiK/CV93GR0TQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.9.0' - '@opentelemetry/sdk-trace-base@1.24.1': - resolution: {integrity: sha512-zz+N423IcySgjihl2NfjBf0qw1RWe11XIAWVrTNOSSI6dtSPJiVom2zipFB2AEEtJWpv0Iz6DY6+TjnyTV5pWg==} + '@opentelemetry/sdk-trace-base@1.25.1': + resolution: {integrity: sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==} engines: {node: '>=14'} peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' + '@opentelemetry/api': '>=1.0.0 <1.10.0' '@opentelemetry/semantic-conventions@1.24.1': resolution: {integrity: sha512-VkliWlS4/+GHLLW7J/rVBA00uXus1SWvwFvcUDxDwmFxYfg/2VI6ekwdXS28cjI8Qz2ky2BzG8OUHo+WeYIWqw==} engines: {node: '>=14'} + '@opentelemetry/semantic-conventions@1.25.1': + resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==} + engines: {node: '>=14'} + '@opentelemetry/sql-common@0.40.1': resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} engines: {node: '>=14'} @@ -3537,26 +3758,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/instrumentation@5.14.0': - resolution: {integrity: sha512-DeybWvIZzu/mUsOYP9MVd6AyBj+MP7xIMrcuIn25MX8FiQX39QBnET5KhszTAip/ToctUuDwSJ46QkIoyo3RFA==} - - '@radix-ui/react-compose-refs@1.0.1': - resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-slot@1.0.2': - resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - '@types/react': - optional: true + '@prisma/instrumentation@5.17.0': + resolution: {integrity: sha512-c1Sle4ji8aasMcYfBBHFM56We4ljfenVtRmS8aY06BllS7SoU6SmJBwG7vil+GHiR0Yrh+t9iBwt4AY0Jr4KNQ==} '@readme/better-ajv-errors@1.6.0': resolution: {integrity: sha512-9gO9rld84Jgu13kcbKRU+WHseNhaVt76wYMeRDGsUGYxwJtI3RmEJ9LY9dZCYQGI8eUZLuxb5qDja0nqklpFjQ==} @@ -3582,8 +3785,8 @@ packages: rollup: optional: true - '@rollup/plugin-replace@5.0.5': - resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} + '@rollup/plugin-replace@5.0.7': + resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -3600,141 +3803,144 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.17.2': - resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==} + '@rollup/rollup-android-arm-eabi@4.19.1': + resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.17.2': - resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==} + '@rollup/rollup-android-arm64@4.19.1': + resolution: {integrity: sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.17.2': - resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==} + '@rollup/rollup-darwin-arm64@4.19.1': + resolution: {integrity: sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.17.2': - resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==} + '@rollup/rollup-darwin-x64@4.19.1': + resolution: {integrity: sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.17.2': - resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==} + '@rollup/rollup-linux-arm-gnueabihf@4.19.1': + resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.17.2': - resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==} + '@rollup/rollup-linux-arm-musleabihf@4.19.1': + resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.17.2': - resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==} + '@rollup/rollup-linux-arm64-gnu@4.19.1': + resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.17.2': - resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==} + '@rollup/rollup-linux-arm64-musl@4.19.1': + resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.17.2': - resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==} + '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': + resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.17.2': - resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==} + '@rollup/rollup-linux-riscv64-gnu@4.19.1': + resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.17.2': - resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==} + '@rollup/rollup-linux-s390x-gnu@4.19.1': + resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.17.2': - resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==} + '@rollup/rollup-linux-x64-gnu@4.19.1': + resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.17.2': - resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==} + '@rollup/rollup-linux-x64-musl@4.19.1': + resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.17.2': - resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==} + '@rollup/rollup-win32-arm64-msvc@4.19.1': + resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.17.2': - resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==} + '@rollup/rollup-win32-ia32-msvc@4.19.1': + resolution: {integrity: sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.17.2': - resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==} + '@rollup/rollup-win32-x64-msvc@4.19.1': + resolution: {integrity: sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==} cpu: [x64] os: [win32] - '@rushstack/node-core-library@4.1.0': - resolution: {integrity: sha512-qz4JFBZJCf1YN5cAXa1dP6Mki/HrsQxc/oYGAGx29dF2cwF2YMxHoly0FBhMw3IEnxo5fMj0boVfoHVBkpkx/w==} + '@rushstack/node-core-library@5.5.1': + resolution: {integrity: sha512-ZutW56qIzH8xIOlfyaLQJFx+8IBqdbVCZdnj+XT1MorQ1JqqxHse8vbCpEM+2MjsrqcbxcgDIbfggB1ZSQ2A3g==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/rig-package@0.5.2': - resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} + '@rushstack/rig-package@0.5.3': + resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} - '@rushstack/terminal@0.10.1': - resolution: {integrity: sha512-C6Vi/m/84IYJTkfzmXr1+W8Wi3MmBjVF/q3za91Gb3VYjKbpALHVxY6FgH625AnDe5Z0Kh4MHKWA3Z7bqgAezA==} + '@rushstack/terminal@0.13.3': + resolution: {integrity: sha512-fc3zjXOw8E0pXS5t9vTiIPx9gHA0fIdTXsu9mT4WbH+P3mYvnrX0iAQ5a6NvyK1+CqYWBTw/wVNx7SDJkI+WYQ==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/ts-command-line@4.19.2': - resolution: {integrity: sha512-cqmXXmBEBlzo9WtyUrHtF9e6kl0LvBY7aTSVX4jfnBfXWZQWnPq9JTFPlQZ+L/ZwjZ4HrNwQsOVvhe9oOucZkw==} + '@rushstack/ts-command-line@4.22.3': + resolution: {integrity: sha512-edMpWB3QhFFZ4KtSzS8WNjBgR4PXPPOVrOHMbb7kNpmQ1UFS9HdVtjCXg1H5fG+xYAbeE+TMPcVPUyX2p84STA==} - '@sentry/core@8.5.0': - resolution: {integrity: sha512-SO3ddBzGdha+Oflp+IKwBxj+7ds1q69OAT3VsypTd+WUFQdI9DIhR92Bjf+QQZCIzUNOi79VWOh3aOi3f6hMnw==} + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sentry/core@8.20.0': + resolution: {integrity: sha512-R81snuw+67VT4aCxr6ShST/s0Y6FlwN2YczhDwaGyzumn5rlvA6A4JtQDeExduNoDDyv4T3LrmW8wlYZn3CJJw==} engines: {node: '>=14.18'} - '@sentry/node@8.5.0': - resolution: {integrity: sha512-t9cHAx/wLJYtdVf2XlzKlRJGvwdAp1wjzG0tC4E1Znx74OuUS1cFNo5WrGuOi0/YcWSxiJaxBvtUcsWK86fIgw==} + '@sentry/node@8.20.0': + resolution: {integrity: sha512-i4ywT2m0Gw65U3uwI4NwiNcyqp9YF6/RsusfH1pg4YkiL/RYp7FS0MPVgMggfvoue9S3KjCgRVlzTLwFATyPXQ==} engines: {node: '>=14.18'} - '@sentry/opentelemetry@8.5.0': - resolution: {integrity: sha512-AbxFUNjuTKQ9ugZrssmGtPxWkBr4USNoP7GjaaGCNwNzvIVYCa+i8dv7BROJiW2lsxNAremULEbh+nbVmhGxDA==} + '@sentry/opentelemetry@8.20.0': + resolution: {integrity: sha512-NFcLK6+t9wUc4HlGKeuDn6W4KjZxZfZmWlrK2/tgC5KzG1cnVeOnWUrJzGHTa+YDDdIijpjiFUcpXGPkX3rmIg==} engines: {node: '>=14.18'} peerDependencies: - '@opentelemetry/api': ^1.8.0 - '@opentelemetry/core': ^1.24.1 - '@opentelemetry/instrumentation': ^0.51.1 - '@opentelemetry/sdk-trace-base': ^1.23.0 - '@opentelemetry/semantic-conventions': ^1.23.0 + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.25.1 + '@opentelemetry/instrumentation': ^0.52.1 + '@opentelemetry/sdk-trace-base': ^1.25.1 + '@opentelemetry/semantic-conventions': ^1.25.1 - '@sentry/profiling-node@8.5.0': - resolution: {integrity: sha512-nEXJqVNfZWYi4PakQXBZCJeH59UlnBv+zaYftDNUUXttCmzRXpL1ujNm5mJrJHlWjV7tgIFw02HW3nh2yyKOkw==} + '@sentry/profiling-node@8.20.0': + resolution: {integrity: sha512-vQaMYjPM7o0qvmj4atxXZywIDhnxMwTlc6x24eKqT8zN0OFBuIc1nYIacT7pEmd7R6e/mXdiG04GH1Vg0bHfOQ==} engines: {node: '>=14.18'} hasBin: true - '@sentry/types@8.5.0': - resolution: {integrity: sha512-eDgkSmKI4+XL0QZm4H3j/n1RgnrbnjXZmjj+LsfccRZQwbPu9bWlc8q7Y7Ty1gOsoUpX+TecNLp2a8CRID4KHA==} + '@sentry/types@8.20.0': + resolution: {integrity: sha512-6IP278KojOpiAA7vrd1hjhUyn26cl0n0nGsShzic5ztCVs92sTeVRnh7MTB9irDVtAbOEyt/YH6go3h+Jia1pA==} engines: {node: '>=14.18'} - '@sentry/utils@8.5.0': - resolution: {integrity: sha512-fdrCzo8SAYiw9JBhkJPqYqJkDXZ/wICzN7+zcXIuzKNhE1hdoFjeKcPnpUI3bKZCG6e3hT1PTYQXhVw7GIZV9w==} + '@sentry/utils@8.20.0': + resolution: {integrity: sha512-+1I5H8dojURiEUGPliDwheQk8dhjp8uV1sMccR/W/zjFrt4wZyPs+Ttp/V7gzm9LDJoNek9tmELert/jQqWTgg==} engines: {node: '>=14.18'} - '@shikijs/core@1.4.0': - resolution: {integrity: sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==} + '@shikijs/core@1.12.0': + resolution: {integrity: sha512-mc1cLbm6UQ8RxLc0dZES7v5rkH+99LxQp/ZvTqV3NLyYsO/fD6JhEflP1H5b2SDq9gI0+0G36AVZWxvounfR9w==} '@sideway/address@4.1.4': resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} @@ -3745,8 +3951,8 @@ packages: '@sideway/pinpoint@2.0.0': resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - '@simplewebauthn/server@10.0.0': - resolution: {integrity: sha512-w5eIoiF7ltg1sgggjY5Tx654j+DBuyEx2B3869jjmPp0xl2Z4BUP4kJ3yJ6DnZIv+ZYYntT3E6nZXNjPOQbrtw==} + '@simplewebauthn/server@10.0.1': + resolution: {integrity: sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==} engines: {node: '>=20.0.0'} '@simplewebauthn/types@10.0.0': @@ -3763,9 +3969,17 @@ packages: resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} engines: {node: '>=14.16'} - '@sindresorhus/is@6.1.0': - resolution: {integrity: sha512-BuvU07zq3tQ/2SIgBsEuxKYDyDjC0n7Zir52bpHy2xnBbW81+po43aLFPLbeV3HRAheFbGud1qgcqSYfhtHMAg==} - engines: {node: '>=16'} + '@sindresorhus/is@7.0.0': + 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'} '@sinonjs/commons@2.0.0': resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} @@ -3785,277 +3999,345 @@ packages: '@sinonjs/text-encoding@0.7.2': resolution: {integrity: sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==} - '@smithy/abort-controller@2.0.14': - resolution: {integrity: sha512-zXtteuYLWbSXnzI3O6xq3FYvigYZFW8mdytGibfarLL2lxHto9L3ILtGVnVGmFZa7SDh62l39EnU5hesLN87Fw==} - engines: {node: '>=14.0.0'} - '@smithy/abort-controller@2.2.0': resolution: {integrity: sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==} engines: {node: '>=14.0.0'} - '@smithy/chunked-blob-reader-native@2.0.0': - resolution: {integrity: sha512-HM8V2Rp1y8+1343tkZUKZllFhEQPNmpNdgFAncbTsxkZ18/gqjk23XXv3qGyXWp412f3o43ZZ1UZHVcHrpRnCQ==} + '@smithy/abort-controller@3.1.1': + resolution: {integrity: sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==} + engines: {node: '>=16.0.0'} - '@smithy/chunked-blob-reader@2.0.0': - resolution: {integrity: sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==} + '@smithy/chunked-blob-reader-native@3.0.0': + resolution: {integrity: sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg==} - '@smithy/config-resolver@2.0.9': - resolution: {integrity: sha512-QBkGPLUqyPmis9Erz8v4q5lo/ErnF7+GD5WZHa6JZiXopUPfaaM+B21n8gzS5xCkIXZmnwzNQhObP9xQPu8oqQ==} - engines: {node: '>=14.0.0'} + '@smithy/chunked-blob-reader@3.0.0': + resolution: {integrity: sha512-sbnURCwjF0gSToGlsBiAmd1lRCmSn72nu9axfJu5lIx6RUEgHu6GwTMbqCdhQSi0Pumcm5vFxsi9XWXb2mTaoA==} - '@smithy/credential-provider-imds@2.0.11': - resolution: {integrity: sha512-uJJs8dnM5iXkn8a2GaKvlKMhcOJ+oJPYqY9gY3CM/EieCVObIDjxUtR/g8lU/k/A+OauA78GzScAfulmFjPOYA==} - engines: {node: '>=14.0.0'} + '@smithy/config-resolver@3.0.5': + resolution: {integrity: sha512-SkW5LxfkSI1bUC74OtfBbdz+grQXYiPYolyu8VfpLIjEoN/sHVBlLeGXMQ1vX4ejkgfv6sxVbQJ32yF2cl1veA==} + engines: {node: '>=16.0.0'} - '@smithy/eventstream-codec@2.0.8': - resolution: {integrity: sha512-onO4to8ujCKn4m5XagReT9Nc6FlNG5vveuvjp1H7AtaG7njdet1LOl6/jmUOkskF2C/w+9jNw3r9Ak+ghOvN0A==} + '@smithy/core@2.3.1': + resolution: {integrity: sha512-BC7VMXx/1BCmRPCVzzn4HGWAtsrb7/0758EtwOGFJQrlSwJBEjCcDLNZLFoL/68JexYa2s+KmgL/UfmXdG6v1w==} + engines: {node: '>=16.0.0'} - '@smithy/eventstream-serde-browser@2.0.8': - resolution: {integrity: sha512-/RGlkKUnC0sd+xKBKH/2APSBRmVMZTeLOKZMhrZmrO+ONoU+DwyMr/RLJ6WnmBKN+2ebjffM4pcIJTKLNNDD8g==} - engines: {node: '>=14.0.0'} + '@smithy/credential-provider-imds@3.2.0': + resolution: {integrity: sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==} + engines: {node: '>=16.0.0'} - '@smithy/eventstream-serde-config-resolver@2.0.8': - resolution: {integrity: sha512-EyAEj258eMUv9zcMvBbqrInh2eHRYuiwQAjXDMxZFCyP+JePzQB6O++3wFwjQeRKMFFgZipNgnEXfReII4+NAw==} - engines: {node: '>=14.0.0'} + '@smithy/eventstream-codec@3.1.2': + resolution: {integrity: sha512-0mBcu49JWt4MXhrhRAlxASNy0IjDRFU+aWNDRal9OtUJvJNiwDuyKMUONSOjLjSCeGwZaE0wOErdqULer8r7yw==} - '@smithy/eventstream-serde-node@2.0.8': - resolution: {integrity: sha512-FMBatSUSKwh6aguKVJokXfJaV8nqsuCkCZHb9MP9zah0ZF+ohbTLeeed7DQGeTVBueVIVWEzIsShPxtxBv7MMQ==} - engines: {node: '>=14.0.0'} + '@smithy/eventstream-serde-browser@3.0.5': + resolution: {integrity: sha512-dEyiUYL/ekDfk+2Ra4GxV+xNnFoCmk1nuIXg+fMChFTrM2uI/1r9AdiTYzPqgb72yIv/NtAj6C3dG//1wwgakQ==} + engines: {node: '>=16.0.0'} - '@smithy/eventstream-serde-universal@2.0.8': - resolution: {integrity: sha512-6InMXH8BUKoEDa6CAuxR4Gn8Gf2vBfVtjA9A6zDKZClYHT+ANUJS+2EtOBc5wECJJGk4KLn5ajQyrt9MBv5lcw==} - engines: {node: '>=14.0.0'} + '@smithy/eventstream-serde-config-resolver@3.0.3': + resolution: {integrity: sha512-NVTYjOuYpGfrN/VbRQgn31x73KDLfCXCsFdad8DiIc3IcdxL+dYA9zEQPyOP7Fy2QL8CPy2WE4WCUD+ZsLNfaQ==} + engines: {node: '>=16.0.0'} - '@smithy/fetch-http-handler@2.1.4': - resolution: {integrity: sha512-SL24M9W5ERByoXaVicRx+bj9GJVujDnPn+QO7GY7adhY0mPGa6DSF58pVKsgIh4r5Tx/k3SWCPlH4BxxSxA/fQ==} + '@smithy/eventstream-serde-node@3.0.4': + resolution: {integrity: sha512-mjlG0OzGAYuUpdUpflfb9zyLrBGgmQmrobNT8b42ZTsGv/J03+t24uhhtVEKG/b2jFtPIHF74Bq+VUtbzEKOKg==} + engines: {node: '>=16.0.0'} - '@smithy/hash-blob-browser@2.0.8': - resolution: {integrity: sha512-IgvRlBMfg/qLg321a59T1yTdEEbaizLrEVsU3DHj65DAO4lFRMF5f+l7vuV+je6m1G9wSD5GQXLturX8qlGb4g==} + '@smithy/eventstream-serde-universal@3.0.4': + resolution: {integrity: sha512-Od9dv8zh3PgOD7Vj4T3HSuox16n0VG8jJIM2gvKASL6aCtcS8CfHZDWe1Ik3ZXW6xBouU+45Q5wgoliWDZiJ0A==} + engines: {node: '>=16.0.0'} - '@smithy/hash-node@2.0.8': - resolution: {integrity: sha512-yZL/nmxZzjZV5/QX5JWSgXlt0HxuMTwFO89CS++jOMMPiCMZngf6VYmtNdccs8IIIAMmfQeTzwu07XgUE/Zd3Q==} - engines: {node: '>=14.0.0'} + '@smithy/fetch-http-handler@3.2.4': + resolution: {integrity: sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==} - '@smithy/hash-stream-node@2.0.8': - resolution: {integrity: sha512-82zC6I9ZJycbEZH8TVyXyBx9c2ZIPQDgBvM0x5AFPUl/i1AxwKKX+lwYRnzgkF//cYhIIoJaCfJ9mjSMPRGvCQ==} - engines: {node: '>=14.0.0'} + '@smithy/hash-blob-browser@3.1.2': + resolution: {integrity: sha512-hAbfqN2UbISltakCC2TP0kx4LqXBttEv2MqSPE98gVuDFMf05lU+TpC41QtqGP3Ff5A3GwZMPfKnEy0VmEUpmg==} + + '@smithy/hash-node@3.0.3': + resolution: {integrity: sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==} + engines: {node: '>=16.0.0'} - '@smithy/invalid-dependency@2.0.8': - resolution: {integrity: sha512-88VOS7W3KzUz/bNRc+Sl/F/CDIasFspEE4G39YZRHIh9YmsXF7GUyVaAKURfMNulTie62ayk6BHC9O0nOBAVgQ==} + '@smithy/hash-stream-node@3.1.2': + resolution: {integrity: sha512-PBgDMeEdDzi6JxKwbfBtwQG9eT9cVwsf0dZzLXoJF4sHKHs5HEo/3lJWpn6jibfJwT34I1EBXpBnZE8AxAft6g==} + engines: {node: '>=16.0.0'} + + '@smithy/invalid-dependency@3.0.3': + resolution: {integrity: sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==} '@smithy/is-array-buffer@2.0.0': resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} engines: {node: '>=14.0.0'} - '@smithy/md5-js@2.0.8': - resolution: {integrity: sha512-1VVECXEiuJvjXv+mudiaUFKYwgDLOWz5MTTy8RzbrPiU3GiOb3/o5/urdkYpqmgoMfxdvxxOw/Adjv2dV2q2Yg==} + '@smithy/is-array-buffer@3.0.0': + resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} + engines: {node: '>=16.0.0'} - '@smithy/middleware-content-length@2.0.10': - resolution: {integrity: sha512-EGSbysyA4jH0p3xI6G0jdXoj9Iz9GUnAta6aEaHtXm3wVWtenRf80y2TeVvNkVSr5jwKOdSCjKIRI2l1A/oZLA==} - engines: {node: '>=14.0.0'} + '@smithy/md5-js@3.0.3': + resolution: {integrity: sha512-O/SAkGVwpWmelpj/8yDtsaVe6sINHLB1q8YE/+ZQbDxIw3SRLbTZuRaI10K12sVoENdnHqzPp5i3/H+BcZ3m3Q==} - '@smithy/middleware-endpoint@2.0.8': - resolution: {integrity: sha512-yOpogfG2d2V0cbJdAJ6GLAWkNOc9pVsL5hZUfXcxJu408N3CUCsXzIAFF6+70ZKSE+lCfG3GFErcSXv/UfUbjw==} - engines: {node: '>=14.0.0'} + '@smithy/middleware-content-length@3.0.5': + resolution: {integrity: sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw==} + engines: {node: '>=16.0.0'} - '@smithy/middleware-retry@2.0.11': - resolution: {integrity: sha512-pknfokumZ+wvBERSuKAI2vVr+aK3ZgPiWRg6+0ZG4kKJogBRpPmDGWw+Jht0izS9ZaEbIobNzueIb4wD33JJVg==} - engines: {node: '>=14.0.0'} + '@smithy/middleware-endpoint@3.1.0': + resolution: {integrity: sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==} + engines: {node: '>=16.0.0'} - '@smithy/middleware-serde@2.0.8': - resolution: {integrity: sha512-Is0sm+LiNlgsc0QpstDzifugzL9ehno1wXp109GgBgpnKTK3j+KphiparBDI4hWTtH9/7OUsxuspNqai2yyhcg==} - engines: {node: '>=14.0.0'} + '@smithy/middleware-retry@3.0.13': + resolution: {integrity: sha512-zvCLfaRYCaUmjbF2yxShGZdolSHft7NNCTA28HVN9hKcEbOH+g5irr1X9s+in8EpambclGnevZY4A3lYpvDCFw==} + engines: {node: '>=16.0.0'} - '@smithy/middleware-stack@2.0.1': - resolution: {integrity: sha512-UexsfY6/oQZRjTQL56s9AKtMcR60tBNibSgNYX1I2WXaUaXg97W9JCkFyth85TzBWKDBTyhLfenrukS/kyu54A==} - engines: {node: '>=14.0.0'} + '@smithy/middleware-serde@3.0.3': + resolution: {integrity: sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==} + engines: {node: '>=16.0.0'} - '@smithy/node-config-provider@2.0.11': - resolution: {integrity: sha512-CaR1dciSSGKttjhcefpytYjsfI/Yd5mqL8am4wfmyFCDxSiPsvnEWHl8UjM/RbcAjX0klt+CeIKPSHEc0wGvJA==} - engines: {node: '>=14.0.0'} + '@smithy/middleware-stack@3.0.3': + resolution: {integrity: sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==} + engines: {node: '>=16.0.0'} + + '@smithy/node-config-provider@3.1.4': + resolution: {integrity: sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ==} + engines: {node: '>=16.0.0'} '@smithy/node-http-handler@2.5.0': resolution: {integrity: sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==} engines: {node: '>=14.0.0'} - '@smithy/property-provider@2.0.9': - resolution: {integrity: sha512-25pPZ8f8DeRwYI5wbPRZaoMoR+3vrw8DwbA0TjP+GsdiB2KxScndr4HQehiJ5+WJ0giOTWhLz0bd+7Djv1qpUQ==} - engines: {node: '>=14.0.0'} + '@smithy/node-http-handler@3.1.4': + resolution: {integrity: sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==} + engines: {node: '>=16.0.0'} - '@smithy/protocol-http@3.0.10': - resolution: {integrity: sha512-6+tjNk7rXW7YTeGo9qwxXj/2BFpJTe37kTj3EnZCoX/nH+NP/WLA7O83fz8XhkGqsaAhLUPo/bB12vvd47nsmg==} - engines: {node: '>=14.0.0'} + '@smithy/property-provider@3.1.3': + resolution: {integrity: sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==} + engines: {node: '>=16.0.0'} '@smithy/protocol-http@3.3.0': resolution: {integrity: sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==} engines: {node: '>=14.0.0'} - '@smithy/querystring-builder@2.0.14': - resolution: {integrity: sha512-lQ4pm9vTv9nIhl5jt6uVMPludr6syE2FyJmHsIJJuOD7QPIJnrf9HhUGf1iHh9KJ4CUv21tpOU3X6s0rB6uJ0g==} - engines: {node: '>=14.0.0'} + '@smithy/protocol-http@4.1.0': + resolution: {integrity: sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==} + engines: {node: '>=16.0.0'} '@smithy/querystring-builder@2.2.0': resolution: {integrity: sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==} engines: {node: '>=14.0.0'} - '@smithy/querystring-parser@2.0.8': - resolution: {integrity: sha512-ArbanNuR7O/MmTd90ZqhDqGOPPDYmxx3huHxD+R3cuCnazcK/1tGQA+SnnR5307T7ZRb5WTpB6qBggERuibVSA==} - engines: {node: '>=14.0.0'} + '@smithy/querystring-builder@3.0.3': + resolution: {integrity: sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==} + engines: {node: '>=16.0.0'} - '@smithy/service-error-classification@2.0.1': - resolution: {integrity: sha512-QHa9+t+v4s0cMuDCcbjIJN67mNZ42/+fc3jKe8P6ZMPXZl5ksKk6a8vhZ/m494GZng5eFTc3OePv+NF9cG83yg==} - engines: {node: '>=14.0.0'} + '@smithy/querystring-parser@3.0.3': + resolution: {integrity: sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==} + engines: {node: '>=16.0.0'} - '@smithy/shared-ini-file-loader@2.0.10': - resolution: {integrity: sha512-jWASteSezRKohJ7GdA7pHDvmr7Q7tw3b5mu3xLHIkZy/ICftJ+O7aqNaF8wklhI7UNFoQ7flFRM3Rd0KA+1BbQ==} - engines: {node: '>=14.0.0'} + '@smithy/service-error-classification@3.0.3': + resolution: {integrity: sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==} + engines: {node: '>=16.0.0'} - '@smithy/signature-v4@2.0.5': - resolution: {integrity: sha512-ABIzXmUDXK4n2c9cXjQLELgH2RdtABpYKT+U131e2I6RbCypFZmxIHmIBufJzU2kdMCQ3+thBGDWorAITFW04A==} - engines: {node: '>=14.0.0'} + '@smithy/shared-ini-file-loader@3.1.4': + resolution: {integrity: sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ==} + engines: {node: '>=16.0.0'} - '@smithy/smithy-client@2.1.5': - resolution: {integrity: sha512-7S865uKzsxApM8W8Q6zkij7tcUFgaG8PuADMFdMt1yL/ku3d0+s6Zwrg3N7iXCPM08Gu/mf0BIfTXIu/9i450Q==} - engines: {node: '>=14.0.0'} + '@smithy/signature-v4@4.1.0': + resolution: {integrity: sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==} + engines: {node: '>=16.0.0'} + + '@smithy/smithy-client@3.1.11': + resolution: {integrity: sha512-l0BpyYkciNyMaS+PnFFz4aO5sBcXvGLoJd7mX9xrMBIm2nIQBVvYgp2ZpPDMzwjKCavsXu06iuCm0F6ZJZc6yQ==} + engines: {node: '>=16.0.0'} '@smithy/types@2.12.0': resolution: {integrity: sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==} engines: {node: '>=14.0.0'} - '@smithy/types@2.6.0': - resolution: {integrity: sha512-PgqxJq2IcdMF9iAasxcqZqqoOXBHufEfmbEUdN1pmJrJltT42b0Sc8UiYSWWzKkciIp9/mZDpzYi4qYG1qqg6g==} - engines: {node: '>=14.0.0'} + '@smithy/types@3.3.0': + resolution: {integrity: sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==} + engines: {node: '>=16.0.0'} - '@smithy/url-parser@2.0.8': - resolution: {integrity: sha512-wQw7j004ScCrBRJ+oNPXlLE9mtofxyadSZ9D8ov/rHkyurS7z1HTNuyaGRj6OvKsEk0SVQsuY0C9+EfM75XTkw==} + '@smithy/url-parser@3.0.3': + resolution: {integrity: sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==} - '@smithy/util-base64@2.0.0': - resolution: {integrity: sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==} - engines: {node: '>=14.0.0'} + '@smithy/util-base64@3.0.0': + resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} + engines: {node: '>=16.0.0'} - '@smithy/util-body-length-browser@2.0.0': - resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==} + '@smithy/util-body-length-browser@3.0.0': + resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} - '@smithy/util-body-length-node@2.1.0': - resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==} - engines: {node: '>=14.0.0'} + '@smithy/util-body-length-node@3.0.0': + resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} + engines: {node: '>=16.0.0'} '@smithy/util-buffer-from@2.0.0': resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==} engines: {node: '>=14.0.0'} - '@smithy/util-config-provider@2.0.0': - resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} - engines: {node: '>=14.0.0'} + '@smithy/util-buffer-from@3.0.0': + resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-config-provider@3.0.0': + resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} + engines: {node: '>=16.0.0'} - '@smithy/util-defaults-mode-browser@2.0.9': - resolution: {integrity: sha512-JONLJVQWT8165XoSV36ERn3SVlZLJJ4D6IeGsCSePv65Uxa93pzSLE0UMSR9Jwm4zix7rst9AS8W5QIypZWP8Q==} + '@smithy/util-defaults-mode-browser@3.0.13': + resolution: {integrity: sha512-ZIRSUsnnMRStOP6OKtW+gCSiVFkwnfQF2xtf32QKAbHR6ACjhbAybDvry+3L5qQYdh3H6+7yD/AiUE45n8mTTw==} engines: {node: '>= 10.0.0'} - '@smithy/util-defaults-mode-node@2.0.11': - resolution: {integrity: sha512-tmqjNsfj+bgZN6jXBe6efZnukzILA7BUytHkzqikuRLNtR+0VVchQHvawD0w6vManh76rO81ydhioe7i4oBzuA==} + '@smithy/util-defaults-mode-node@3.0.13': + resolution: {integrity: sha512-voUa8TFJGfD+U12tlNNLCDlXibt9vRdNzRX45Onk/WxZe7TS+hTOZouEZRa7oARGicdgeXvt1A0W45qLGYdy+g==} engines: {node: '>= 10.0.0'} - '@smithy/util-hex-encoding@2.0.0': - resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} - engines: {node: '>=14.0.0'} + '@smithy/util-endpoints@2.0.5': + resolution: {integrity: sha512-ReQP0BWihIE68OAblC/WQmDD40Gx+QY1Ez8mTdFMXpmjfxSyz2fVQu3A4zXRfQU9sZXtewk3GmhfOHswvX+eNg==} + engines: {node: '>=16.0.0'} - '@smithy/util-middleware@2.0.1': - resolution: {integrity: sha512-LnsBMi0Mg3gfz/TpNGLv2Jjcz2ra1OX5HR/4IaCepIYmtPQzqMWDdhX/XTW1LS8OZ0xbQuyQPcHkQ+2XkhWOVQ==} - engines: {node: '>=14.0.0'} + '@smithy/util-hex-encoding@3.0.0': + resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} + engines: {node: '>=16.0.0'} - '@smithy/util-retry@2.0.1': - resolution: {integrity: sha512-naj4X0IafJ9yJnVJ58QgSMkCNLjyQOnyrnKh/T0f+0UOUxJiT8vuFn/hS7B/pNqbo2STY7PyJ4J4f+5YqxwNtA==} - engines: {node: '>= 14.0.0'} + '@smithy/util-middleware@3.0.3': + resolution: {integrity: sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==} + engines: {node: '>=16.0.0'} - '@smithy/util-stream@2.0.11': - resolution: {integrity: sha512-2MeWfqSpZKdmEJ+tH8CJQSgzLWhH5cmdE24X7JB0hiamXrOmswWGGuPvyj/9sQCTclo57pNxLR2p7KrP8Ahiyg==} - engines: {node: '>=14.0.0'} + '@smithy/util-retry@3.0.3': + resolution: {integrity: sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==} + engines: {node: '>=16.0.0'} - '@smithy/util-uri-escape@2.0.0': - resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} - engines: {node: '>=14.0.0'} + '@smithy/util-stream@3.1.3': + resolution: {integrity: sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==} + engines: {node: '>=16.0.0'} '@smithy/util-uri-escape@2.2.0': resolution: {integrity: sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==} engines: {node: '>=14.0.0'} + '@smithy/util-uri-escape@3.0.0': + resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} + engines: {node: '>=16.0.0'} + '@smithy/util-utf8@2.0.0': resolution: {integrity: sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==} engines: {node: '>=14.0.0'} - '@smithy/util-waiter@2.0.8': - resolution: {integrity: sha512-t9yaoofNhdEhNlyDeV5al/JJEFJ62HIQBGktgCUE63MvKn6imnbkh1qISsYMyMYVLwhWCpZ3Xa3R1LA+SnWcng==} - engines: {node: '>=14.0.0'} + '@smithy/util-utf8@3.0.0': + resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-waiter@3.1.2': + resolution: {integrity: sha512-4pP0EV3iTsexDx+8PPGAKCQpd/6hsQBaQhqWzU4hqKPHN5epPsxKbvUTIiYIHTxaKt6/kEaqPBpu/ufvfbrRzw==} + engines: {node: '>=16.0.0'} '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} - '@storybook/addon-actions@8.0.9': - resolution: {integrity: sha512-+I3VTvlKdj8puHeS2tyaOVv9syDiNLneVZbTfqN+UDOK2i42NwvZr8PVwjTzMlEj9eePJdCZgiipz55xwts5bw==} + '@storybook/addon-actions@8.2.6': + resolution: {integrity: sha512-iCsf3V28/jJ95w2zd8aSvR4denoA2UYV3fpNCTGOURqICyKOG3cyVxvqKp8Hhcwn7trNOsK+HlL6q5gpv56ViA==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-backgrounds@8.0.9': - resolution: {integrity: sha512-pCDecACrVyxPaJKEWS0sHsRb8xw+IPCSxDM1TkjaAQ6zZ468A/dcUnqW+LVK8bSXgQwWzn23wqnqPFSy5yptuQ==} + '@storybook/addon-backgrounds@8.2.6': + resolution: {integrity: sha512-61NFowA6EmCw+Eyzp0U4fat9MlPDdnT7aoDyzqSImLwWLITY9IvmWuTeo7XKJZN3fe22z1r7cZseKdYrtaHcKw==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-controls@8.0.9': - resolution: {integrity: sha512-wWdmd62UP/sfPm8M7aJjEA+kEXTUIR/QsYi9PoYBhBZcXiikZ4kNan7oD7GfsnzGGKHrBVfwQhO+TqaENGYytA==} + '@storybook/addon-controls@8.2.6': + resolution: {integrity: sha512-EHUwHy+oZZv3pXzN7fuXWrS/meHFjqcELY3RBvOyEkGf21agl6co6R1tnf6d5N5QoYAGfIbDO7dkauSL2RfNAw==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-docs@8.0.9': - resolution: {integrity: sha512-x7hX7UuzJtClu6XwU3SfpyFhuckVcgqgD6BU6Ihxl0zs+i4xp6iKVXYSnHFMRM1sgoeT8TjPxab35Ke8w8BVRw==} + '@storybook/addon-docs@8.2.6': + resolution: {integrity: sha512-qe7hxntaezqjKdU9QS+Q9NFL6i/uNdBxdvOnCKgPhBAY/zY6yhk5t3sOvonynPK5nkaNAowfSNPIzNxAXlJ1sA==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-essentials@8.0.9': - resolution: {integrity: sha512-mwAgdfrOsTuTDcagvM7veBh+iayZIWmKOazzkhrIWbhYcrXOsweigD2UOVeHgAiAzJK49znr4FXTCKcE1hOWcw==} + '@storybook/addon-essentials@8.2.6': + resolution: {integrity: sha512-diGjGZcZNov+RCAVQBTm8JKP2kUtMRuJIQFBeXdPWpu6hYBk6lw1FlAf2GywWGCvdny1pJT90hfoD33qUMNuDg==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-highlight@8.0.9': - resolution: {integrity: sha512-vaRHGDbx7dpNpQECAHk5wczlZO3ntstprGlqnZt0o7ylz6xB5+pTQwTuIFty0hwKv+3TPcskzzifATUyEOEmyg==} + '@storybook/addon-highlight@8.2.6': + resolution: {integrity: sha512-03cV9USsfP3bS4wYV06DYcIaGPfoheQe53Q0Jr1B2yJUVyIPKvmO2nGjLBsqzeL3Wl7vSfLQn0/dUdxCcbqLsw==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-interactions@8.0.9': - resolution: {integrity: sha512-AMIdNcyM6DDAWvMitBJMqp1iPZND8AXB4QT4VZHGMKG2ngHNKktriEKpTfcRkfKPGTJs9T+71dWfm6/R4tticw==} + '@storybook/addon-interactions@8.2.6': + resolution: {integrity: sha512-YXpHf8jWPz9HJV+Fw4GaunaCWeE6uqF24aLXdAd8xuhN1UfWJeNV6AwAvFQ0hTLqvmz0yMhX/5JXDKeKESoYDA==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-links@8.0.9': - resolution: {integrity: sha512-FVt+AdW3JFSqbJzkKiqKsMRWqHXqEvCBqFs7lNfk3OW0w0jfv1iREtrxE0dVdJoUFQC9V/2Im/EpJ7UB3C2bNQ==} + '@storybook/addon-links@8.2.6': + resolution: {integrity: sha512-CUuU3nk8wyZ3bljCmOG/OCKazan+bPuNbCph8N763zyzdEx5M/CbBxV9d3pi3zjYpix7txlqrl2/YdMCejfyFw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.2.6 peerDependenciesMeta: react: optional: true - '@storybook/addon-mdx-gfm@8.0.9': - resolution: {integrity: sha512-AoEx+OGKANtVZgKyWKrQhGpMpDuc2S7PnOlNLUiDYzmj8ABAGPmEJmqeb/VHVgqLQSjhOW1fMsQ4fYsecvMxTQ==} + '@storybook/addon-mdx-gfm@8.2.6': + resolution: {integrity: sha512-PFVfJeuydxlV1VmxEuKNQ7z2vCDvQtHa2GB0ANM11ahxDSUz8QxsO0Y/L3LOn2JjJGYiVFrsHAaC+8NW43iArQ==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-measure@8.0.9': - resolution: {integrity: sha512-91svOOGEXmGG4USglwXLE3wtlUVgtbKJVxTKX7xRI+AC5JEEaKByVzP17/X8Qn/8HilUL7AfSQ0kCoqtPSJ5cA==} + '@storybook/addon-measure@8.2.6': + resolution: {integrity: sha512-neI8YeSOAtOmzasLxo6O8ZLr2ebMaD7XVF+kYatl5+SpyuwwvUGcP9NkKe5S+mB8V2zxFUIsXS74XrhmQhRoaQ==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-outline@8.0.9': - resolution: {integrity: sha512-fQ+jm356TgUnz81IxsC99/aOesbLw3N5OQRJpo/A6kqbLMzlq3ybVzuXYCKC3f0ArgQRNh4NoMeJBMRFMtaWRw==} + '@storybook/addon-outline@8.2.6': + resolution: {integrity: sha512-uAlPtqDWlq7MQQ4zJT80qdjbSdLF/zsvtPhidX6h9cjLKNPWAv79xJQ14AJHaMv+Hzy5xKnM4wdEhgPbzKabQg==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-storysource@8.0.9': - resolution: {integrity: sha512-5m3K2Rs4fQtKtqwrq4CDS1jK2wzWOlnxhE2ArX5XTWytb1am65CEPxfYTEQkvZH9oPGwX3cXytPCziynqysFMQ==} + '@storybook/addon-storysource@8.2.6': + resolution: {integrity: sha512-8H2kvRIM12oXN4kO/oowABu88IOY9Je7PphCUUs/nIfTIB+Ck1GrLxx5fCNNCSwUrTBEZY8bDOfxmkf9d4qngw==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-toolbars@8.0.9': - resolution: {integrity: sha512-nNSBnnBOhQ+EJwkrIkK4ZBYPcozNmEH770CZ/6NK85SUJ6WEBZapE6ru33jIUokFGEvlOlNCeai0GUc++cQP8w==} + '@storybook/addon-toolbars@8.2.6': + resolution: {integrity: sha512-0JmRirMpxHS6VZzBk0kY871xWTpkk3TN4S1sxoFf5fcnCfVTHDjEJ5Ws/QWru1RJlIZHuJKRdQIA6Vuq5X+KfQ==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/addon-viewport@8.0.9': - resolution: {integrity: sha512-Ao4+D56cO7biaw+iTlMU1FBec1idX0cmdosDeCFZin06MSawcPkeBlRBeruaSQYdLes8TBMdZPFgfuqI5yIk6g==} + '@storybook/addon-viewport@8.2.6': + resolution: {integrity: sha512-IAxH9H8tVFzSmZhKf5E+EALiAdkp19RzGqP/rWluD8LH7oW5HumQE/4oN0ZhVMy1RxYsCKFYjWyAp7AuxeMRSw==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/blocks@8.0.9': - resolution: {integrity: sha512-F2zSrfSwzTFN7qW3zB80tG+EXtmfmCDC6Ird0F7tolszb6tOqJcAcBOwQbE2O0wI63sLu21qxzXgaKBMkiWvJg==} + '@storybook/blocks@8.2.6': + resolution: {integrity: sha512-nMlZJjVTyfOJ6xwORptsNuS1AZZlDbJUVXc2R8uukGd5GIXxxCdrPk4NvUsjfQslMT9LhYuFld3z62FATsM2rw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + 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 peerDependenciesMeta: react: optional: true react-dom: optional: true - '@storybook/builder-manager@8.0.9': - resolution: {integrity: sha512-/PxDwZIfMc/PSRZcasb6SIdGr3azIlenzx7dBF7Imt8i4jLHiAf1t00GvghlfJsvsrn4DNp95rbRbXTDyTj7tQ==} + '@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.0.9': - resolution: {integrity: sha512-7hEQFZIIz7VvxdySDpPE96iMvZxQvRZcRdhaNGeE+8Y2pyc3DgYE4WY3sjr+LUoB0a6TYLpAIKqbXwtLz0R+PQ==} + '@storybook/builder-vite@8.2.6': + resolution: {integrity: sha512-3PrsPZAedpQUbzRBEl23Fi1zG5bkQD76JsygVwmfiSm4Est4K8kW2AIB2ht9cIfKXh3mfQkyQlxXKHeQEHeQwQ==} peerDependencies: '@preact/preset-vite': '*' + storybook: ^8.2.6 typescript: '>= 4.3.x' vite: ^4.0.0 || ^5.0.0 vite-plugin-glimmerx: '*' @@ -4067,48 +4349,64 @@ packages: vite-plugin-glimmerx: optional: true - '@storybook/channels@8.0.9': - resolution: {integrity: sha512-7Lcfyy5CsLWWGhMPO9WG4jZ/Alzp0AjepFhEreYHRPtQrfttp6qMAjE/g1aHgun0qHCYWxwqIG4NLR/hqDNrXQ==} + '@storybook/channels@8.1.11': + resolution: {integrity: sha512-fu5FTqo6duOqtJFa6gFzKbiSLJoia+8Tibn3xFfB6BeifWrH81hc+AZq0lTmHo5qax2G5t8ZN8JooHjMw6k2RA==} - '@storybook/cli@8.0.9': - resolution: {integrity: sha512-lilYTKn8F5YOePijqfRYFa5v2mHVIJxPCIgTn+OXAmAFbcizZ6P8P6niU4J/NXulgx68Ln1M7hYhFtTP25hVTw==} - hasBin: true + '@storybook/client-logger@8.1.11': + resolution: {integrity: sha512-DVMh2usz3yYmlqCLCiCKy5fT8/UR9aTh+gSqwyNFkGZrIM4otC5A8eMXajXifzotQLT5SaOEnM3WzHwmpvMIEA==} - '@storybook/client-logger@8.0.9': - resolution: {integrity: sha512-LzV/RHkbf07sRc1Jc0ff36RlapKf9Ul7/+9VMvVbI3hshH1CpmrZK4t/tsIdpX/EVOdJ1Gg5cES06PnleOAIPA==} + '@storybook/codemod@8.2.6': + resolution: {integrity: sha512-+mFJ6R+JhJLpU7VPDlXU5Yn6nqIBq745GaEosnIiFOdNo3jaxJ58wq/sGhbQvoCHPUxMA+sDQvR7pS62YFoLRQ==} - '@storybook/codemod@8.0.9': - resolution: {integrity: sha512-VBeGpSZSQpL6iyLLqceJSNGhdCqcNwv+xC/aWdDFOkmuE1YfbmNNwpa9QYv4ZFJ2QjUsm4iTWG60qK+9NXeSKA==} + '@storybook/components@8.2.6': + resolution: {integrity: sha512-H8ckH1AnLkHtMtvJ3J8LxnmDtHxkJ7NJacGctHMRrsBIvdKTVwlT4su5nAVVJlan/PrEou+jESfw+OjjBYE5PA==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/components@8.0.9': - resolution: {integrity: sha512-JcwBGADzIJs0PSzqykrrD2KHzNG9wtexUOKuidt+FSv9szpUhe3qBAXIHpdfBRl7mOJ9TRZ5rt+mukEnfncdzA==} + '@storybook/core-common@8.1.11': + resolution: {integrity: sha512-Ix0nplD4I4DrV2t9B+62jaw1baKES9UbR/Jz9LVKFF9nsua3ON0aVe73dOjMxFWBngpzBYWe+zYBTZ7aQtDH4Q==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + '@storybook/core-events@8.1.11': + resolution: {integrity: sha512-vXaNe2KEW9BGlLrg0lzmf5cJ0xt+suPjWmEODH5JqBbrdZ67X6ApA2nb6WcxDQhykesWCuFN5gp1l+JuDOBi7A==} - '@storybook/core-common@8.0.9': - resolution: {integrity: sha512-Jmue+sfHFb4GTYBzyWYw1MygoJiQSfISIrKmNIzAmZ+oR9EOr+jpu/i/bH+uetZ2Hqg1AGhj1VB7OtJp9HQyWw==} + '@storybook/core-events@8.2.6': + resolution: {integrity: sha512-bmtm7sHBExKCSGiCIyhwfHKFIsdrRQqd8ZEb/iNWsR93AxHszcf/adYAVynencdWKipw1haIWBNaiDhnsOBVPA==} + peerDependencies: + storybook: ^8.2.6 + + '@storybook/core-server@8.1.11': + resolution: {integrity: sha512-L6dzQTmR0np/kagNONvvlm6lSvF1FNc9js3vxsEEPnEypLbhx8bDZaHmuhmBpYUzKyUMpRVQTE/WgjHLuBBuxA==} - '@storybook/core-events@8.0.9': - resolution: {integrity: sha512-DxSUx7wG9Qe3OFUBnv3OrYq48J8UWNo2DUR5/JecJCtp3n++L4fAEW3J0IF5FfxpQDMQSp1yTNjZ2PaWCMd2ag==} + '@storybook/core@8.2.6': + resolution: {integrity: sha512-XY71g3AcpD6IiER9k9Lt+vlUMYfPIYgWekd7e0Ggzz2gJkPuLunKEdQccLGDSHf5OFAobHhrTJc7ZsvWhmDMag==} - '@storybook/core-server@8.0.9': - resolution: {integrity: sha512-BIe1T5YUBl0GYxEjRoTQsvXD2pyuzL8rPTUD41zlzSQM0R8U6Iant9SzRms4u0+rKUm2mGxxKuODlUo5ewqaGA==} + '@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==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/csf-plugin@8.0.9': - resolution: {integrity: sha512-pXaNCNi++kxKsqSWwvx215fPx8cNqvepLVxQ7B69qXLHj80DHn0Q3DFBO3sLXNiQMJ2JK4OYcTxMfuOiyzszKw==} + '@storybook/csf-tools@8.1.11': + resolution: {integrity: sha512-6qMWAg/dBwCVIHzANM9lSHoirwqSS+wWmv+NwAs0t9S94M75IttHYxD3IyzwaSYCC5llp0EQFvtXXAuSfFbibg==} - '@storybook/csf-tools@8.0.9': - resolution: {integrity: sha512-PiNMhL97giLytTdQwuhsZ92buVk4gy9H/8DtrDhUc45/1OmF95gogm6T2Yap729SIFwgpOcuq/U3aVo6d6swVQ==} + '@storybook/csf@0.1.11': + resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==} - '@storybook/csf@0.1.6': - resolution: {integrity: sha512-JjWnBptVhBYJ14yq+cHs66BXjykRUWQ5TlD1RhPxMOtavynYyV/Q+QR98/N+XB+mcPtFMm5I2DvNkpj0/Dk8Mw==} + '@storybook/csf@0.1.9': + resolution: {integrity: sha512-JlZ6v/iFn+iKohKGpYXnMeNeTiiAMeFoDhYnPLIC8GnyyIWqEI9wJYrOK9i9rxlJ8NZAH/ojGC/u/xVC41qSgQ==} - '@storybook/docs-mdx@3.0.0': - resolution: {integrity: sha512-NmiGXl2HU33zpwTv1XORe9XG9H+dRUC1Jl11u92L4xr062pZtrShLmD4VKIsOQujxhhOrbxpwhNOt+6TdhyIdQ==} + '@storybook/docs-mdx@3.1.0-next.0': + resolution: {integrity: sha512-t4syFIeSyufieNovZbLruPt2DmRKpbwL4fERCZ1MifWDRIORCKLc4NCEHy+IqvIqd71/SJV2k4B51nF7vlJfmQ==} - '@storybook/docs-tools@8.0.9': - resolution: {integrity: sha512-OzogAeOmeHea/MxSPKRBWtOQVNSpoq+OOpimO9YRA5h5GBRJ2TUOGT44Gny6QT4ll5AvQA8fIiq9KezKcLekAg==} + '@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==} @@ -4120,85 +4418,121 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@storybook/instrumenter@8.0.9': - resolution: {integrity: sha512-Gw74dgpTU/2p7FG0s7DuVdqCbJ2MEcSuRJjDo7HcXRYcvWp7I6Ly+C0v7N5VaoS+kbBVerAhLKIHZgG/LZf1og==} + '@storybook/instrumenter@8.2.6': + resolution: {integrity: sha512-RxtpcMTUSq8/wPM6cR6EXVrPEiNuRbC71cIFVFZagOFYvnnOKwSPV+GOLPK0wxMbGB4c5/+Xe8ADefmZTvxOsA==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/manager-api@8.0.9': - resolution: {integrity: sha512-99b3yKArDSvfabXL7QE3nA95e4DdW/5H/ZCcr6/E2qCQJayZ6G1v/WWamKXbiaTpkndulFmcb/+ZmnDXcweIIQ==} + '@storybook/manager-api@8.1.11': + resolution: {integrity: sha512-QSgwKfAw01K9YvvZj30iGBMgQ4YaCT3vojmttuqdH5ukyXkiO7pENLJj4Y+alwUeSi0g+SJeadCI3PXySBHOGg==} - '@storybook/manager@8.0.9': - resolution: {integrity: sha512-+NnRo+5JQFGNqveKrLtC0b+Z08Tae4m44iq292bPeZMpr9OkFsIkU0PBPsHTHPkrqC/zZXRNsCsTEgvu3p2OIA==} + '@storybook/manager-api@8.2.6': + resolution: {integrity: sha512-uv36h/b5RhlajWtEg4cVPBYV8gZs6juux0nIE+6G9i7vt8Ild6gM9tW1KNabgZcaHFiyWJYCNWxJZoKjgUmXDg==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/node-logger@8.0.9': - resolution: {integrity: sha512-5ajMdZFrYrjGLJOVDq7dlEQNFsgeLHymt4dCK9MulL/ciXykmXUZXE3Bye0wFy+I2qqDVvrvR8uzCvSFvm5MAQ==} + '@storybook/manager@8.1.11': + resolution: {integrity: sha512-e02y9dmxowo7cTKYm9am7UO6NOHoHy6Xi7xZf/UA932qLwFZUtk5pnwIEFaZWI3OQsRUCGhP+FL5zizU7uVZeg==} - '@storybook/preview-api@8.0.9': - resolution: {integrity: sha512-zHfX34bkAMzzmE7vbDzaqFwSW6ExiBD0HiO1L/IsHF55f0f7xV7IH8uJyFRrDTvAoW3ReSxZDMvvPpeydFPKGA==} + '@storybook/node-logger@8.1.11': + resolution: {integrity: sha512-wdzFo7B2naGhS52L3n1qBkt5BfvQjs8uax6B741yKRpiGgeAN8nz8+qelkD25MbSukxvbPgDot7WJvsMU/iCzg==} - '@storybook/preview@8.0.9': - resolution: {integrity: sha512-tFsR8xc8AYBZZrZw8enklFbSQt7ZAV+rv20BoxwDhd3q7fjXyK7O4moGPqUwBZ7rukTG13nPoISxr+VXAk/HYA==} + '@storybook/preview-api@8.1.11': + resolution: {integrity: sha512-8ZChmFV56GKppCJ0hnBd/kNTfGn2gWVq1242kuet13pbJtBpvOhyq4W01e/Yo14tAPXvgz8dSnMvWLbJx4QfhQ==} - '@storybook/react-dom-shim@8.0.9': - resolution: {integrity: sha512-8011KlRuG3obr5pZZ7bcEyYYNWF3tR596YadoMd267NPoHKvwAbKL1L/DNgb6kiYjZDUf9QfaKSCWW31k0kcRQ==} + '@storybook/preview-api@8.2.6': + resolution: {integrity: sha512-5vTj2ndX5ng4nDntZYe+r8UwLjCIGFymhq5/r2adAvRKL+Bo4zQDWGO7bhvGJk16do2THb2JvPz49ComW9LLZw==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + storybook: ^8.2.6 - '@storybook/react-vite@8.0.9': - resolution: {integrity: sha512-FT5KeulUH6grfzOJOxJCxpv9+81UVDrT9UPcgiFhQT9rKtsgmltezThwbHknByZNw3WWnf+ieidMLEis9hd73A==} + '@storybook/preview@8.1.11': + resolution: {integrity: sha512-K/9NZmjnL0D1BROkTNWNoPqgL2UaocALRSqCARmkBLgU2Rn/FuZgEclHkWlYo6pUrmLNK+bZ+XzpNMu12iTbpg==} + + '@storybook/react-dom-shim@8.2.6': + resolution: {integrity: sha512-B+x8UAEQPDp1yhN3tMh09NvSL38QNfJB7PAyLgKrfE7xIAzvewq+RLW2DfGkoZCy+Zr7QSHm1p7NOgud8+sQCg==} + 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/react-vite@8.2.6': + resolution: {integrity: sha512-BpbteaIzsJZL1QN3iR7uuslrPfdtbZYXPhcU9awpfl5pW5MOQThuvl7728mwT8V7KdANeikJPgsnlETOb/afDA==} engines: {node: '>=18.0.0'} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + 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 vite: ^4.0.0 || ^5.0.0 - '@storybook/react@8.0.9': - resolution: {integrity: sha512-NeQ6suZG3HKikwe3Tx9cAIaRx7uP8FKCmlVvIiBg4LTTI5orCt94PPakvuZukZcbkqvcCnEBkebAzwUpn8PiJw==} + '@storybook/react@8.2.6': + resolution: {integrity: sha512-awJlzfiAMrf8l9AgiLhjXEJ+HvS3VKPxNNQaRwBELGq/vigjJe656tMrhvg4OIlJXtlS+6XPshd2knLwjIWNLw==} engines: {node: '>=18.0.0'} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + 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 typescript: '>= 4.2.x' peerDependenciesMeta: typescript: optional: true - '@storybook/router@8.0.9': - resolution: {integrity: sha512-aAOWxbM9J4mt+cp4o88T2PB29mgBBTOzU37/pUsTHYnKnR9XI4npXEXdN8Gv+ryqM0kj0AbBpz/llFlnR2MNNA==} + '@storybook/router@8.1.11': + resolution: {integrity: sha512-nU5lsBvy0L8wBYOkjagh29ztZicDATpZNYrHuavlhQ2jznmmHdJvXKYk+VrMAbthjQ6ZBqfeeMNPR1UlnqR5Rw==} - '@storybook/source-loader@8.0.9': - resolution: {integrity: sha512-FDnpxIGE5nIYT15pvYe6rz95TSBrdLcDll7lOHNyZisWt19MI3wZU3YkVsFNRBuFrebo+FjVU3wHyoV81ur1Qw==} + '@storybook/source-loader@8.2.6': + resolution: {integrity: sha512-mOVf+TJhlQywCymFMs7l604CxEZRKZRKVQojrrgU6CH6EhhLx/q6BT8tf1CakY9JO3Ey+PhUMBBCerYiDaHLcQ==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/telemetry@8.0.9': - resolution: {integrity: sha512-AGGfcup06t+wxhBIkHd0iybieOh9PDVZQJ9oPct5JGB39+ni9wvs0WOD+MYlHbsjp8id7+aGkh6mYuYOvfck+Q==} + '@storybook/telemetry@8.1.11': + resolution: {integrity: sha512-Jqvm7HcZismKzPuebhyLECO6KjGiSk4ycbca1WUM/TUvifxCXqgoUPlHHQEEfaRdHS63/MSqtMNjLsQRLC/vNQ==} - '@storybook/test@8.0.9': - resolution: {integrity: sha512-bRd5tBJnPzR6UKbDXONWnFWtdkNOY99HMLDUWe5fTRo50GwkrpFBVqPflhdkruEeof0kAbBUbnoN2CIYgtnAFw==} + '@storybook/test@8.2.6': + resolution: {integrity: sha512-nTzNxReBcMRlX1+8PNU/MuA9ArFbeQhfZXMBIwJJoHOhnNe1knYpyn1++xINxAHKOh0BBhQ0NIMoKdcGmW3V6w==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/theming@8.0.9': - resolution: {integrity: sha512-jgfDuYoiNMMirQiASN3Eg0hGDXsEtpdAcMxyShqYGwu9elxgD9yUnYC2nSckYsM74a3ZQ3JaViZ9ZFSe2FHmeQ==} + '@storybook/theming@8.1.11': + resolution: {integrity: sha512-Chn/opjO6Rl1isNobutYqAH2PjKNkj09YBw/8noomk6gElSa3JbUTyaG/+JCHA6OG/9kUsqoKDb5cZmAKNq/jA==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + 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/types@8.0.9': - resolution: {integrity: sha512-ew0EXzk9k4B557P1qIWYrnvUcgaE0WWA5qQS0AU8l+fRTp5nvl9O3SP/zNIB0SN1qDFO7dXr3idTNTyIikTcEQ==} + '@storybook/theming@8.2.6': + resolution: {integrity: sha512-ICnYuLIVsYifVCMQljdHgrp+5vAquNybHxDGWiPeOxBicotwHF8rLhTckD2CdVQbMp0jk6r6jetvjXbFJ2MbvQ==} + peerDependencies: + storybook: ^8.2.6 + + '@storybook/types@8.1.11': + resolution: {integrity: sha512-k9N5iRuY2+t7lVRL6xeu6diNsxO3YI3lS4Juv3RZ2K4QsE/b3yG5ElfJB8DjHDSHwRH4ORyrU71KkOCUVfvtnw==} + + '@storybook/types@8.2.6': + resolution: {integrity: sha512-9Kb5+nui8M7TP/EDGwiuOAHYQPg9U6iQl0OWwgbDIYGBpldwlCwVKAoQWzXz/LlhQijULXIpe1cLvEvJN2Uwhg==} + peerDependencies: + storybook: ^8.2.6 - '@storybook/vue3-vite@8.0.9': - resolution: {integrity: sha512-IkzYsEyCo5HIvLWbJeGrBu/VIN4u+LvdIAz7vcFqVVXBtTUhy+9/8caLx8fdnM0FWgKcBRQs8HnjBB2V0lOFcg==} + '@storybook/vue3-vite@8.1.11': + resolution: {integrity: sha512-q0bqh8XEEunaTmp4YiDqM2+YZLwEIevTb5PnNe7G7f2qOiSCE1ncBDnBK717UlCd+iYr34NTztgV2/jIhz1i5w==} engines: {node: '>=18.0.0'} peerDependencies: vite: ^4.0.0 || ^5.0.0 - '@storybook/vue3@8.0.9': - resolution: {integrity: sha512-EqVdS62YbOCAE0wJrQKW0sHpM90be8N8Mvmj+HzB0QYhJNtFqP9ehwbcTfwEKtaVGudisHgGBOzNoSKDlxFaag==} + '@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==} engines: {node: '>=18.0.0'} peerDependencies: + storybook: ^8.2.6 vue: ^3.0.0 '@swc/cli@0.3.12': @@ -4224,8 +4558,14 @@ packages: cpu: [arm64] os: [darwin] - '@swc/core-darwin-arm64@1.4.17': - resolution: {integrity: sha512-HVl+W4LezoqHBAYg2JCqR+s9ife9yPfgWSj37iIawLWzOmuuJ7jVdIB7Ee2B75bEisSEKyxRlTl6Y1Oq3owBgw==} + '@swc/core-darwin-arm64@1.6.13': + resolution: {integrity: sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-arm64@1.6.6': + resolution: {integrity: sha512-5DA8NUGECcbcK1YLKJwNDKqdtTYDVnkfDU1WvQSXq/rU+bjYCLtn5gCe8/yzL7ISXA6rwqPU1RDejhbNt4ARLQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] @@ -4236,8 +4576,14 @@ packages: cpu: [x64] os: [darwin] - '@swc/core-darwin-x64@1.4.17': - resolution: {integrity: sha512-WYRO9Fdzq4S/he8zjW5I95G1zcvyd9yyD3Tgi4/ic84P5XDlSMpBDpBLbr/dCPjmSg7aUXxNQqKqGkl6dQxYlA==} + '@swc/core-darwin-x64@1.6.13': + resolution: {integrity: sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-darwin-x64@1.6.6': + resolution: {integrity: sha512-2nbh/RHpweNRsJiYDFk1KcX7UtaKgzzTNUjwtvK5cp0wWrpbXmPvdlWOx3yzwoiSASDFx78242JHHXCIOlEdsw==} engines: {node: '>=10'} cpu: [x64] os: [darwin] @@ -4254,8 +4600,14 @@ packages: cpu: [arm] os: [linux] - '@swc/core-linux-arm-gnueabihf@1.4.17': - resolution: {integrity: sha512-cgbvpWOvtMH0XFjvwppUCR+Y+nf6QPaGu6AQ5hqCP+5Lv2zO5PG0RfasC4zBIjF53xgwEaaWmGP5/361P30X8Q==} + '@swc/core-linux-arm-gnueabihf@1.6.13': + resolution: {integrity: sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm-gnueabihf@1.6.6': + resolution: {integrity: sha512-YgytuyUfR7b0z0SRHKV+ylr83HmgnROgeT7xryEkth6JGpAEHooCspQ4RrWTU8+WKJ7aXiZlGXPgybQ4TiS+TA==} engines: {node: '>=10'} cpu: [arm] os: [linux] @@ -4266,8 +4618,14 @@ packages: cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-gnu@1.4.17': - resolution: {integrity: sha512-l7zHgaIY24cF9dyQ/FOWbmZDsEj2a9gRFbmgx2u19e3FzOPuOnaopFj0fRYXXKCmtdx+anD750iBIYnTR+pq/Q==} + '@swc/core-linux-arm64-gnu@1.6.13': + resolution: {integrity: sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.6.6': + resolution: {integrity: sha512-yGwx9fddzEE0iURqRVwKBQ4IwRHE6hNhl15WliHpi/PcYhzmYkUIpcbRXjr0dssubXAVPVnx6+jZVDSbutvnfg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -4278,8 +4636,14 @@ packages: cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.4.17': - resolution: {integrity: sha512-qhH4gr9gAlVk8MBtzXbzTP3BJyqbAfUOATGkyUtohh85fPXQYuzVlbExix3FZXTwFHNidGHY8C+ocscI7uDaYw==} + '@swc/core-linux-arm64-musl@1.6.13': + resolution: {integrity: sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.6.6': + resolution: {integrity: sha512-a6fMbqzSAsS5KCxFJyg1mD5kwN3ZFO8qQLyJ75R/htZP/eCt05jrhmOI7h2n+1HjiG332jLnZ9S8lkVE5O8Nqw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] @@ -4290,8 +4654,14 @@ packages: cpu: [x64] os: [linux] - '@swc/core-linux-x64-gnu@1.4.17': - resolution: {integrity: sha512-vRDFATL1oN5oZMImkwbgSHEkp8xG1ofEASBypze01W1Tqto8t+yo6gsp69wzCZBlxldsvPpvFZW55Jq0Rn+UnA==} + '@swc/core-linux-x64-gnu@1.6.13': + resolution: {integrity: sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.6.6': + resolution: {integrity: sha512-hRGsUKNzzZle28YF0dYIpN0bt9PceR9LaVBq7x8+l9TAaDLFbgksSxcnU/ubTtsy+WsYSYGn+A83w3xWC0O8CQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -4302,8 +4672,14 @@ packages: cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.4.17': - resolution: {integrity: sha512-zQNPXAXn3nmPqv54JVEN8k2JMEcMTQ6veVuU0p5O+A7KscJq+AGle/7ZQXzpXSfUCXlLMX4wvd+rwfGhh3J4cw==} + '@swc/core-linux-x64-musl@1.6.13': + resolution: {integrity: sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.6.6': + resolution: {integrity: sha512-NokIUtFxJDVv3LzGeEtYMTV3j2dnGKLac59luTeq36DQLZdJQawQIdTbzzWl2jE7lxxTZme+dhsVOH9LxE3ceg==} engines: {node: '>=10'} cpu: [x64] os: [linux] @@ -4314,8 +4690,14 @@ packages: cpu: [arm64] os: [win32] - '@swc/core-win32-arm64-msvc@1.4.17': - resolution: {integrity: sha512-z86n7EhOwyzxwm+DLE5NoLkxCTme2lq7QZlDjbQyfCxOt6isWz8rkW5QowTX8w9Rdmk34ncrjSLvnHOeLY17+w==} + '@swc/core-win32-arm64-msvc@1.6.13': + resolution: {integrity: sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-arm64-msvc@1.6.6': + resolution: {integrity: sha512-lzYdI4qb4k1dFG26yv+9Jaq/bUMAhgs/2JsrLncGjLof86+uj74wKYCQnbzKAsq2hDtS5DqnHnl+//J+miZfGA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] @@ -4326,8 +4708,14 @@ packages: cpu: [ia32] os: [win32] - '@swc/core-win32-ia32-msvc@1.4.17': - resolution: {integrity: sha512-JBwuSTJIgiJJX6wtr4wmXbfvOswHFj223AumUrK544QV69k60FJ9q2adPW9Csk+a8wm1hLxq4HKa2K334UHJ/g==} + '@swc/core-win32-ia32-msvc@1.6.13': + resolution: {integrity: sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.6.6': + resolution: {integrity: sha512-bvl7FMaXIJQ76WZU0ER4+RyfKIMGb6S2MgRkBhJOOp0i7VFx4WLOnrmMzaeoPJaJSkityVKAftfNh7NBzTIydQ==} engines: {node: '>=10'} cpu: [ia32] os: [win32] @@ -4338,17 +4726,32 @@ packages: cpu: [x64] os: [win32] - '@swc/core-win32-x64-msvc@1.4.17': - resolution: {integrity: sha512-jFkOnGQamtVDBm3MF5Kq1lgW8vx4Rm1UvJWRUfg+0gx7Uc3Jp3QMFeMNw/rDNQYRDYPG3yunCC+2463ycd5+dg==} + '@swc/core-win32-x64-msvc@1.6.13': + resolution: {integrity: sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core-win32-x64-msvc@1.6.6': + resolution: {integrity: sha512-WAP0JoCTfgeYKgOeYJoJV4ZS0sQUmU3OwvXa2dYYtMLF7zsNqOiW4niU7QlThBHgUv/qNZm2p6ITEgh3w1cltw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.4.17': - resolution: {integrity: sha512-tq+mdWvodMBNBBZbwFIMTVGYHe9N7zvEaycVVjfvAx20k1XozHbHhRv+9pEVFJjwRxLdXmtvFZd3QZHRAOpoNQ==} + '@swc/core@1.6.13': + resolution: {integrity: sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/core@1.6.6': + resolution: {integrity: sha512-sHfmIUPUXNrQTwFMVCY5V5Ena2GTOeaWjS2GFUpjLhAgVfP90OP67DWow7+cYrfFtqBdILHuWnjkTcd0+uPKlg==} engines: {node: '>=10'} peerDependencies: - '@swc/helpers': ^0.5.0 + '@swc/helpers': '*' peerDependenciesMeta: '@swc/helpers': optional: true @@ -4362,14 +4765,14 @@ packages: peerDependencies: '@swc/core': '*' - '@swc/types@0.1.5': - resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==} + '@swc/types@0.1.9': + resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==} '@swc/wasm@1.2.130': resolution: {integrity: sha512-rNcJsBxS70+pv8YUWwf5fRlWX6JoY/HJc25HD/F8m6Kv7XhJdqPPMhyX6TKkUBPAG7TWlZYoxa+rHAjPy4Cj3Q==} - '@syuilo/aiscript@0.18.0': - resolution: {integrity: sha512-/iY9Vv4LLjtW/KUzId1QwXC4BlpIEPCMcoT7dyRhYdyxtwhS3Hx4b/4j1HYP+n3Pq9XKyW5zvkY72/+DNu4g6Q==} + '@syuilo/aiscript@0.19.0': + resolution: {integrity: sha512-ZWG4s1m6RrFjE7NeIMaxFz769YO1jW5ReTrOROrEO4IHheOrjxxJ/Ffe2TUNqX9/XxDloMwfWplKhfSzx8LGMA==} '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} @@ -4379,16 +4782,16 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@testing-library/dom@9.3.3': - resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} - engines: {node: '>=14'} + '@testing-library/dom@10.1.0': + resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} + 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.2': - resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==} + '@testing-library/jest-dom@6.4.5': + resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: '@jest/globals': '>= 28' @@ -4414,8 +4817,8 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@testing-library/vue@8.0.3': - resolution: {integrity: sha512-wSsbNlZ69ZFQgVlHMtc/ZC/g9BHO7MhyDrd4nHyfEubtMr3kToN/w4/BsSBknGIF8w9UmPbsgbIuq/CbdBHzCA==} + '@testing-library/vue@8.1.0': + resolution: {integrity: sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==} engines: {node: '>=14'} peerDependencies: '@vue/compiler-sfc': '>= 3' @@ -4434,8 +4837,8 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@tsd/typescript@5.3.3': - resolution: {integrity: sha512-CQlfzol0ldaU+ftWuG52vH29uRoKboLinLy84wS8TQOu+m+tWoaUfk4svL4ij2V8M5284KymJBlHUusKj6k34w==} + '@tsd/typescript@5.4.5': + resolution: {integrity: sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==} engines: {node: '>=14.17'} '@twemoji/parser@15.0.0': @@ -4480,12 +4883,6 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/chai-subset@1.3.5': - resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==} - - '@types/chai@4.3.11': - resolution: {integrity: sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==} - '@types/color-convert@2.0.3': resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==} @@ -4504,9 +4901,6 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/cookies@0.9.0': - resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==} - '@types/core-js@2.5.8': resolution: {integrity: sha512-VgnAj6tIAhJhZdJ8/IpxdatM8G4OD3VWGlp6xIxUGENZlpbob9Ty4VVdC1FIEp0aK6DBscDDjyzy5FB60TuNqg==} @@ -4519,6 +4913,9 @@ packages: '@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==} @@ -4555,6 +4952,9 @@ packages: '@types/express@4.17.17': resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@types/find-cache-dir@3.2.1': resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==} @@ -4577,17 +4977,11 @@ packages: '@types/htmlescape@1.1.3': resolution: {integrity: sha512-tuC81YJXGUe0q8WRtBNW+uyx79rkkzWK651ALIXXYq5/u/IxjX4iHneGF2uUqzsNp+F+9J2mFZOv9jiLTtIq0w==} - '@types/http-assert@1.5.5': - resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==} - '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} - '@types/http-errors@2.0.4': - resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} - - '@types/http-link-header@1.0.5': - resolution: {integrity: sha512-AxhIKR8UbyoqCTNp9rRepkktHuUOw3DjfOfDCaO9kwI8AYzjhxyrvZq4+mRw/2daD3hYDknrtSeV6SsPwmc71w==} + '@types/http-link-header@1.0.7': + resolution: {integrity: sha512-snm5oLckop0K3cTDAiBnZDy6ncx9DJ3mCRDvs42C884MbVYPP74Tiq2hFsSDRTyjK6RyDYDIulPiW23ge+g5Lw==} '@types/istanbul-lib-coverage@2.0.4': resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} @@ -4604,8 +4998,8 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/jsdom@21.1.6': - resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==} + '@types/jsdom@21.1.7': + resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} '@types/json-schema@7.0.12': resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} @@ -4616,41 +5010,32 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/jsonld@1.5.13': - resolution: {integrity: sha512-n7fUU6W4kSYK8VQlf/LsE9kddBHPKhODoVOjsZswmve+2qLwBy6naWxs/EiuSZN9NU0N06Ra01FR+j87C62T0A==} + '@types/jsonld@1.5.15': + resolution: {integrity: sha512-PlAFPZjL+AuGYmwlqwKEL0IMP8M8RexH0NIPGfCVWSQ041H2rR/8OlyZSD7KsCVoN8vCfWdtWDBxX8yBVP+xow==} '@types/jsrsasign@10.5.14': resolution: {integrity: sha512-lppSlfK6etu+cuKs40K4rg8As79PH6hzIB+v55zSqImbSH3SE6Fm8MBHCiI91cWlAP3Z4igtJK1VL3fSN09blQ==} - '@types/keygrip@1.0.6': - resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} - '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/koa-compose@3.2.8': - resolution: {integrity: sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==} - - '@types/koa@2.14.0': - resolution: {integrity: sha512-DTDUyznHGNHAl+wd1n0z1jxNajduyTh8R53xoewuerdBzGo6Ogj6F2299BFtrexJw4NtgjsI5SMPCmV9gZwGXA==} - - '@types/koa__router@12.0.3': - resolution: {integrity: sha512-5YUJVv6NwM1z7m6FuYpKfNLTZ932Z6EF6xy2BbtpJSyn13DKNQEkXVffFVSnJHxvwwWh2SAeumpjAYUELqgjyw==} - '@types/lodash@4.14.191': resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} '@types/matter-js@0.19.6': resolution: {integrity: sha512-ffk6tqJM5scla+ThXmnox+mdfCo3qYk6yMjQsNcrbo6eQ5DqorVdtnaL+1agCoYzxUjmHeiNB7poBMAmhuLY7w==} + '@types/matter-js@0.19.7': + resolution: {integrity: sha512-dlh50YEh1lQS4fiCDGBnK75ocHQIq/1E371Qk6hASJImICIivdZQC2GkOqnfBm0Hac2xLk5+yrqRFDAEfj/yLA==} + '@types/mdast@4.0.3': resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} '@types/mdx@2.0.3': resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==} - '@types/micromatch@4.0.7': - resolution: {integrity: sha512-C/FMQ8HJAZhTsDpl4wDKZdMeeW5USjgzOczUwTGbRc1ZopPgOhIEnxY2ZgUrsuyy4DwK1JVOJZKFakv3TbCKiA==} + '@types/micromatch@4.0.9': + resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==} '@types/mime-types@2.1.4': resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} @@ -4673,18 +5058,14 @@ packages: '@types/mysql@2.15.22': resolution: {integrity: sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==} - '@types/node-fetch@3.0.3': - resolution: {integrity: sha512-HhggYPH5N+AQe/OmN6fmhKmRRt2XuNJow+R3pQwJxOOF9GuwM7O2mheyGeIrs5MOIeNjDEdgdoyHBOrFeJBR3g==} - deprecated: This is a stub types definition. node-fetch provides its own type definitions, so you do not need this installed. - '@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==} - '@types/node@20.12.7': - resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} + '@types/node@20.14.12': + resolution: {integrity: sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==} '@types/node@20.9.1': resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} @@ -4701,8 +5082,8 @@ packages: '@types/oauth2orize@1.11.5': resolution: {integrity: sha512-C6hrRoh9hCnqis39OpeUZSwgw+TIzcV0CsxwJMGfQjTx4I1r+CLmuEPzoDJr5NRTfc7OMwHNLkQwrGFLKrJjMQ==} - '@types/oauth@0.9.4': - resolution: {integrity: sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A==} + '@types/oauth@0.9.5': + resolution: {integrity: sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==} '@types/object-assign-deep@0.4.3': resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==} @@ -4713,8 +5094,8 @@ packages: '@types/pg-pool@2.0.4': resolution: {integrity: sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==} - '@types/pg@8.11.5': - resolution: {integrity: sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==} + '@types/pg@8.11.6': + resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} @@ -4725,6 +5106,9 @@ packages: '@types/prop-types@15.7.5': resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + '@types/proxy-addr@2.0.3': + resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==} + '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} @@ -4815,9 +5199,15 @@ packages: '@types/tough-cookie@4.0.2': resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/unist@3.0.2': resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -4830,8 +5220,8 @@ packages: '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} - '@types/ws@8.5.10': - resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + '@types/ws@8.5.11': + resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==} '@types/yargs-parser@21.0.0': resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -4875,8 +5265,8 @@ packages: typescript: optional: true - '@typescript-eslint/eslint-plugin@7.7.1': - resolution: {integrity: sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==} + '@typescript-eslint/eslint-plugin@7.17.0': + resolution: {integrity: sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: '@typescript-eslint/parser': ^7.0.0 @@ -4916,8 +5306,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@7.7.1': - resolution: {integrity: sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==} + '@typescript-eslint/parser@7.17.0': + resolution: {integrity: sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -4938,8 +5328,8 @@ packages: resolution: {integrity: sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/scope-manager@7.7.1': - resolution: {integrity: sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==} + '@typescript-eslint/scope-manager@7.17.0': + resolution: {integrity: sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==} engines: {node: ^18.18.0 || >=20.0.0} '@typescript-eslint/type-utils@6.11.0': @@ -4972,8 +5362,8 @@ packages: typescript: optional: true - '@typescript-eslint/type-utils@7.7.1': - resolution: {integrity: sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==} + '@typescript-eslint/type-utils@7.17.0': + resolution: {integrity: sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -4994,8 +5384,8 @@ packages: resolution: {integrity: sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/types@7.7.1': - resolution: {integrity: sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==} + '@typescript-eslint/types@7.17.0': + resolution: {integrity: sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==} engines: {node: ^18.18.0 || >=20.0.0} '@typescript-eslint/typescript-estree@6.11.0': @@ -5025,8 +5415,8 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@7.7.1': - resolution: {integrity: sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==} + '@typescript-eslint/typescript-estree@7.17.0': + resolution: {integrity: sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: typescript: '*' @@ -5052,8 +5442,8 @@ packages: peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/utils@7.7.1': - resolution: {integrity: sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==} + '@typescript-eslint/utils@7.17.0': + resolution: {integrity: sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 @@ -5070,87 +5460,81 @@ packages: resolution: {integrity: sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/visitor-keys@7.7.1': - resolution: {integrity: sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==} + '@typescript-eslint/visitor-keys@7.17.0': + resolution: {integrity: sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==} engines: {node: ^18.18.0 || >=20.0.0} '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - '@vitejs/plugin-vue@5.0.4': - resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==} + '@vitejs/plugin-vue@5.1.0': + resolution: {integrity: sha512-QMRxARyrdiwi1mj3AW4fLByoHTavreXq0itdEW696EihXglf1MB3D4C2gBvE0jMPH29ZjC3iK8aIaUMLf4EOGA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: vite: ^5.0.0 vue: ^3.2.25 - '@vitest/coverage-v8@0.34.6': - resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} + '@vitest/coverage-v8@1.6.0': + resolution: {integrity: sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==} peerDependencies: - vitest: '>=0.32.0 <1' - - '@vitest/expect@0.34.6': - resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} + vitest: 1.6.0 - '@vitest/expect@1.3.1': - resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} + '@vitest/expect@1.6.0': + resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} - '@vitest/runner@0.34.6': - resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} + '@vitest/runner@1.6.0': + resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} - '@vitest/snapshot@0.34.6': - resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} - - '@vitest/spy@0.34.6': - resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} - - '@vitest/spy@1.3.1': - resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} + '@vitest/snapshot@1.6.0': + resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} '@vitest/spy@1.6.0': resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} - '@vitest/utils@0.34.6': - resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} - - '@vitest/utils@1.3.1': - resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} - '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} '@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/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/typescript@2.2.0': resolution: {integrity: sha512-wC6l4zLiiCLxF+FGaHCbWlQYf4vMsnRxYhcI6WgvaNppOD6r1g+Ef1RKRJUApALWU46Yy/JDU/TbdV6w/X6Liw==} - '@vue/compiler-core@3.4.21': - resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} + '@volar/typescript@2.4.0-alpha.18': + resolution: {integrity: sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==} - '@vue/compiler-core@3.4.25': - resolution: {integrity: sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==} + '@vue/compiler-core@3.4.31': + resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==} - '@vue/compiler-core@3.4.26': - resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==} + '@vue/compiler-core@3.4.34': + resolution: {integrity: sha512-Z0izUf32+wAnQewjHu+pQf1yw00EGOmevl1kE+ljjjMe7oEfpQ+BI3/JNK7yMB4IrUsqLDmPecUrpj3mCP+yJQ==} - '@vue/compiler-dom@3.4.21': - resolution: {integrity: sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==} + '@vue/compiler-core@3.4.37': + resolution: {integrity: sha512-ZDDT/KiLKuCRXyzWecNzC5vTcubGz4LECAtfGPENpo0nrmqJHwuWtRLxk/Sb9RAKtR9iFflFycbkjkY+W/PZUQ==} - '@vue/compiler-dom@3.4.25': - resolution: {integrity: sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg==} + '@vue/compiler-dom@3.4.34': + resolution: {integrity: sha512-3PUOTS1h5cskdOJMExCu2TInXuM0j60DRPpSCJDqOCupCfUZCJoyQmKtRmA8EgDNZ5kcEE7vketamRZfrEuVDw==} - '@vue/compiler-dom@3.4.26': - resolution: {integrity: sha512-4CWbR5vR9fMg23YqFOhr6t6WB1Fjt62d6xdFPyj8pxrYub7d+OgZaObMsoxaF9yBUHPMiPFK303v61PwAuGvZA==} + '@vue/compiler-dom@3.4.37': + resolution: {integrity: sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==} - '@vue/compiler-sfc@3.4.26': - resolution: {integrity: sha512-It1dp+FAOCgluYSVYlDn5DtZBxk1NCiJJfu2mlQqa/b+k8GL6NG/3/zRbJnHdhV2VhxFghaDq5L4K+1dakW6cw==} + '@vue/compiler-sfc@3.4.37': + resolution: {integrity: sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==} - '@vue/compiler-ssr@3.4.26': - resolution: {integrity: sha512-FNwLfk7LlEPRY/g+nw2VqiDKcnDTVdCfBREekF8X74cPLiWHUX6oldktf/Vx28yh4STNy7t+/yuLoMBBF7YDiQ==} + '@vue/compiler-ssr@3.4.37': + resolution: {integrity: sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==} + + '@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==} @@ -5163,28 +5547,36 @@ packages: typescript: optional: true - '@vue/reactivity@3.4.26': - resolution: {integrity: sha512-E/ynEAu/pw0yotJeLdvZEsp5Olmxt+9/WqzvKff0gE67tw73gmbx6tRkiagE/eH0UCubzSlGRebCbidB1CpqZQ==} + '@vue/language-core@2.0.29': + resolution: {integrity: sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.4.37': + resolution: {integrity: sha512-UmdKXGx0BZ5kkxPqQr3PK3tElz6adTey4307NzZ3whZu19i5VavYal7u2FfOmAzlcDVgE8+X0HZ2LxLb/jgbYw==} - '@vue/runtime-core@3.4.26': - resolution: {integrity: sha512-AFJDLpZvhT4ujUgZSIL9pdNcO23qVFh7zWCsNdGQBw8ecLNxOOnPcK9wTTIYCmBJnuPHpukOwo62a2PPivihqw==} + '@vue/runtime-core@3.4.37': + resolution: {integrity: sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==} - '@vue/runtime-dom@3.4.26': - resolution: {integrity: sha512-UftYA2hUXR2UOZD/Fc3IndZuCOOJgFxJsWOxDkhfVcwLbsfh2CdXE2tG4jWxBZuDAs9J9PzRTUFt1PgydEtItw==} + '@vue/runtime-dom@3.4.37': + resolution: {integrity: sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==} - '@vue/server-renderer@3.4.26': - resolution: {integrity: sha512-xoGAqSjYDPGAeRWxeoYwqJFD/gw7mpgzOvSxEmjWaFO2rE6qpbD1PC172YRpvKhrihkyHJkNDADFXTfCyVGhKw==} + '@vue/server-renderer@3.4.37': + resolution: {integrity: sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==} peerDependencies: - vue: 3.4.26 + vue: 3.4.37 - '@vue/shared@3.4.21': - resolution: {integrity: sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==} + '@vue/shared@3.4.31': + resolution: {integrity: sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==} - '@vue/shared@3.4.25': - resolution: {integrity: sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA==} + '@vue/shared@3.4.34': + resolution: {integrity: sha512-x5LmiRLpRsd9KTjAB8MPKf0CDPMcuItjP0gbNqFCIgL1I8iYp4zglhj9w9FPCdIbHG2M91RVeIbArFfFTz9I3A==} - '@vue/shared@3.4.26': - resolution: {integrity: sha512-Fg4zwR0GNnjzodMt3KRy2AWGMKQXByl56+4HjN87soxLNU9P5xcJkstAlIeEF3cU6UYOzmJl1tV0dVPGIljCnQ==} + '@vue/shared@3.4.37': + resolution: {integrity: sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==} '@vue/test-utils@2.4.1': resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==} @@ -5255,8 +5647,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.11.3: - resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} hasBin: true @@ -5280,9 +5672,9 @@ packages: resolution: {integrity: sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==} engines: {node: '>=18'} - aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02: - resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02} - version: 0.1.9 + aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9: + resolution: {tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9} + version: 0.1.11 engines: {vscode: ^1.83.0} ajv-draft-04@1.0.0: @@ -5301,12 +5693,26 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ajv@8.13.0: resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -5389,6 +5795,9 @@ packages: aria-query@5.1.3: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} @@ -5464,6 +5873,9 @@ packages: async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async@0.2.10: + resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} + async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} @@ -5485,8 +5897,8 @@ packages: avvio@8.3.0: resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==} - aws-sdk-client-mock@3.0.1: - resolution: {integrity: sha512-9VAzJLl8mz99KP9HjOm/93d8vznRRUTpJooPBOunRdUAnVYopCe9xmMuu7eVemu8fQ+w6rP7o5bBK1kAFkB2KQ==} + aws-sdk-client-mock@4.0.1: + resolution: {integrity: sha512-yD2mmgy73Xce097G5hIpr1k7j50qzvJ49/+6osGZiCyk4m6cwhb+2x7kKFY1gEMwTzaS8+m8fXv9SB29SkRYyQ==} aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} @@ -5525,18 +5937,18 @@ 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.6: - resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==} + 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.8.6: - resolution: {integrity: sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==} + 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.5.3: - resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} + 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 @@ -5609,10 +6021,6 @@ packages: bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} - body-parser@1.20.1: - resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@1.20.2: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5637,15 +6045,16 @@ packages: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + broadcast-channel@7.0.0: resolution: {integrity: sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==} browser-assert@1.2.1: resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} - browserify-zlib@0.1.4: - resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} - browserslist@4.22.2: resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -5689,8 +6098,12 @@ packages: resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} engines: {node: '>=6.14.2'} - bullmq@5.7.8: - resolution: {integrity: sha512-F/Haeu6AVHkFrfeaU/kLOjhfrH6x3CaKAZlQQ+76fa8l3kfI9oaUHeFMW+1mYVz0NtYPF7PNTWFq4ylAHYcCgA==} + bufferutil@4.0.8: + resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} + engines: {node: '>=6.14.2'} + + bullmq@5.10.4: + resolution: {integrity: sha512-YEssEbWBbPXvSW2YMjIBKZdkIPZsOaTGWo1y2wpCFv/wUY+tRLKiSVuHgv09x0QEieybx844f9//UWuarG1JHg==} buraha@0.0.1: resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==} @@ -5727,6 +6140,10 @@ packages: resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} engines: {node: '>=14.16'} + cacheable-request@12.0.1: + resolution: {integrity: sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==} + engines: {node: '>=18'} + cacheable-request@7.0.2: resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} engines: {node: '>=8'} @@ -5816,8 +6233,8 @@ packages: character-parser@2.2.0: resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} - chart.js@4.4.2: - resolution: {integrity: sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==} + chart.js@4.4.3: + resolution: {integrity: sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==} engines: {pnpm: '>=8'} chartjs-adapter-date-fns@3.0.0: @@ -5859,15 +6276,12 @@ packages: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - chromatic@11.3.0: - resolution: {integrity: sha512-q1ZtJDJrjLGnz60ivpC16gmd7KFzcaA4eTb7gcytCqbaKqlHhCFr1xQmcUDsm14CK7JsqdkFU6S+JQdOd2ZNJg==} + chromatic@11.5.6: + resolution: {integrity: sha512-ycX/hlZLs69BltwwBNsEXr+As6x5/0rlwp6W/CiHMZ3tpm7dmkd+hQCsb8JGHb1h49W3qPOKQ/Lh9evqcJ1yeQ==} hasBin: true peerDependencies: '@chromatic-com/cypress': ^0.*.* || ^1.0.0 @@ -5902,10 +6316,6 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true - cli-spinners@2.7.0: - resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} - engines: {node: '>=6'} - cli-spinners@2.9.2: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} @@ -6018,8 +6428,8 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - compare-versions@6.1.0: - resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} compress-commons@6.0.2: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} @@ -6082,8 +6492,8 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - core-js-compat@3.33.3: - resolution: {integrity: sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==} + core-js-compat@3.37.1: + resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -6113,8 +6523,8 @@ packages: resolution: {integrity: sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==} engines: {node: '>=12.0.0'} - cropperjs@2.0.0-beta.5: - resolution: {integrity: sha512-8RIynsyHV7KyCxbjV4fCQubGiM6sHMgYvRPKkzuUQSTYHK6shoUNvdvbBekwAwS8QRLsxEBcJ5lvl0W3dvkDQA==} + cropperjs@2.0.0-rc.1: + resolution: {integrity: sha512-Y9ciurIuK6G1vy0ErHC8Gt6wHWvsHWJ5fgE60GL6vsuF2WzHwDpH7F1yof40XAEheeSN4v3rD09D1VZ7kiiSOA==} cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} @@ -6134,9 +6544,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} - crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - 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==} @@ -6196,13 +6606,8 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - cypress@13.7.3: - resolution: {integrity: sha512-uoecY6FTCAuIEqLUYkTrxamDBjMHTYak/1O7jtgwboHiTnS1NaMOoR08KcTrbRZFCBvYOiS4tEkQRmsV+xcrag==} - engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} - hasBin: true - - cypress@13.8.1: - resolution: {integrity: sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==} + cypress@13.13.1: + resolution: {integrity: sha512-8F9UjL5MDUdgC/S5hr8CGLHbS5gGht5UOV184qc2pFny43fnkoaKxlzH/U6//zmGu/xRTaKimNfjknLT8+UDFg==} engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} hasBin: true @@ -6253,6 +6658,15 @@ packages: supports-color: optional: true + debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + 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'} @@ -6326,10 +6740,6 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - del@6.1.1: - resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} - engines: {node: '>=10'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -6384,6 +6794,10 @@ packages: resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} engines: {node: '>=0.3.1'} + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + dijkstrajs@1.0.2: resolution: {integrity: sha512-QV6PMaHTCNmKSeP6QoXhVTw9snc9VD8MulTT0Bd99Pacp4SS1cjcrYPgBPmibqKVtMJJfqC6XvOXgPMEEPH/fg==} @@ -6435,9 +6849,6 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - duplexify@3.7.1: - resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -6455,8 +6866,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} hasBin: true @@ -6500,6 +6911,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@5.0.0: + resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -6522,8 +6937,8 @@ packages: es-get-iterator@1.1.3: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - es-module-lexer@0.9.3: - resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} @@ -6554,11 +6969,16 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true + esbuild@0.23.0: + resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -6634,8 +7054,8 @@ packages: '@typescript-eslint/parser': optional: true - eslint-plugin-vue@9.25.0: - resolution: {integrity: sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==} + eslint-plugin-vue@9.27.0: + resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 @@ -6647,20 +7067,32 @@ packages: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint@8.53.0: - resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true + eslint@9.8.0: + resolution: {integrity: sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6674,6 +7106,10 @@ packages: resolution: {integrity: sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==} engines: {node: '>=0.10'} + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -6736,6 +7172,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + execa@9.3.0: + resolution: {integrity: sha512-l6JFbqnHEadBoVAVpN5dl2yCyfX28WoBAGaoQcNmLLSedOxTxcn2Qa83s8I/PA5i56vWru2OHOtrwF7Om2vqlg==} + engines: {node: ^18.19.0 || >=20.5.0} + executable@4.1.1: resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} engines: {node: '>=4'} @@ -6751,10 +7191,6 @@ packages: exponential-backoff@3.1.1: resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} - express@4.18.2: - resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} - engines: {node: '>= 0.10.0'} - express@4.19.2: resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==} engines: {node: '>= 0.10.0'} @@ -6817,10 +7253,17 @@ packages: fast-uri@2.2.0: resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} + fast-uri@3.0.1: + resolution: {integrity: sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==} + fast-xml-parser@4.2.5: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} hasBin: true + fast-xml-parser@4.4.0: + resolution: {integrity: sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==} + hasBin: true + fastify-multer@2.0.3: resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==} engines: {node: '>=10.17.0'} @@ -6835,11 +7278,8 @@ packages: resolution: {integrity: sha512-F4o8ZIMVx4YoxGfwrZys6wyjl40gF3Yv6AWWRy62ozFAyZBSS831/uyyCAqKYw3tR73g180ryG98yih6To1PUQ==} engines: {node: '>= 10'} - fastify@4.26.2: - resolution: {integrity: sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==} - - fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + fastify@4.28.1: + resolution: {integrity: sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==} fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -6847,6 +7287,9 @@ 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==} @@ -6865,10 +7308,18 @@ packages: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + file-system-cache@2.3.0: resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} @@ -6876,8 +7327,8 @@ packages: resolution: {integrity: sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - file-type@19.0.0: - resolution: {integrity: sha512-s7cxa7/leUWLiXO78DVVfBVse+milos9FitauDLG1pI7lNaJ2+5lzPnr2N24ym+84HVwJL6hVuGfgVE+ALvU8Q==} + file-type@19.3.0: + resolution: {integrity: sha512-mROwiKLZf/Kwa/2Rol+OOZQn1eyTkPB3ZTwC0ExY6OLFCbgxHYZvBm7xI77NvfZFMKBsmuXfmLJnD4eEftEhrA==} engines: {node: '>=18'} filelist@1.0.4: @@ -6895,6 +7346,10 @@ packages: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} engines: {node: '>= 0.8'} @@ -6934,20 +7389,24 @@ packages: resolution: {integrity: sha512-MdYSsbdCaIRjzo5edthZtWmEZVMfr1qrtYZUHIdO3swCE+CoZA8S5l0s4jDsYlTa9ZiXv0pTgpzE7s4N8NeUOA==} engines: {node: '>=18'} - flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} - flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + 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.2: - resolution: {integrity: sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==} - engines: {node: '>=0.8.0'} + fluent-ffmpeg@2.1.3: + resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} + engines: {node: '>=18'} follow-redirects@1.15.2: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} @@ -6999,9 +7458,6 @@ packages: from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} @@ -7022,8 +7478,8 @@ packages: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} - fs-minipass@3.0.2: - resolution: {integrity: sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==} + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} fs.realpath@1.0.0: @@ -7058,10 +7514,6 @@ packages: get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} - get-npm-tarball-url@2.0.3: - resolution: {integrity: sha512-R/PW6RqyaBQNWYaSyfrh54/qtcnOp22FHCCiRhSSZj0FP3KQWCsxxt0DzIdVTbwTqe9CtQfvl/FPD4UIPt4pqw==} - engines: {node: '>=12.17'} - get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -7082,6 +7534,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-symbol-description@1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} @@ -7124,17 +7580,24 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - glob@10.3.12: - resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} - engines: {node: '>=16 || 14 >=14.17'} + glob@10.4.2: + resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} + engines: {node: '>=16 || 14 >=14.18'} + hasBin: true + + glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -7148,6 +7611,14 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.8.0: + resolution: {integrity: sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==} + engines: {node: '>=18'} + globalthis@1.0.3: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} @@ -7156,6 +7627,10 @@ 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==} @@ -7167,8 +7642,8 @@ packages: resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} engines: {node: '>=14.16'} - got@14.2.1: - resolution: {integrity: sha512-KOaPMremmsvx6l9BLC04LYE6ZFW4x7e4HkTe3LwBmtuYYQwpeS4XKqzhubTIkaQ1Nr+eXxeori0zuwupXMovBQ==} + got@14.4.2: + resolution: {integrity: sha512-+Te/qEZ6hr7i+f0FNgXx/6WQteSM/QqueGvxeYQQFm0GDfoxLVJ/oiwUKYMTeioColWUTdewZ06hmrBjw6F7tw==} engines: {node: '>=20'} graceful-fs@4.2.11: @@ -7184,10 +7659,6 @@ packages: resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - gunzip-maybe@1.4.2: - resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} - hasBin: true - hammerjs@2.0.8: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} @@ -7200,6 +7671,10 @@ packages: happy-dom@10.0.3: resolution: {integrity: sha512-WkCP+Z5fX6U5PY+yHP3ElV5D9PoxRAHRWPFq3pG9rg/6Hjf5ak7dozAgSCywsTRUq2qfa8vV8OQvUy5pRXy8EQ==} + happy-dom@15.6.1: + resolution: {integrity: sha512-dsMHLsJHZYhXeExP47B2siAfKNVxptlwFss3/bq/9sG3iBt0P2WYFBq68JgMR5vB5gsN2Ev0feTTPD/+rosUNQ==} + engines: {node: '>=18.0.0'} + hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -7310,8 +7785,8 @@ packages: resolution: {integrity: sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==} engines: {node: '>=6.0.0'} - http-proxy-agent@7.0.0: - resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} http-signature@1.3.6: @@ -7338,6 +7813,14 @@ packages: 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'} + human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} @@ -7354,6 +7837,10 @@ 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==} + engines: {node: '>=18.18.0'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -7371,8 +7858,8 @@ packages: ignore-by-default@1.0.1: resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - ignore-walk@6.0.4: - resolution: {integrity: sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==} + ignore-walk@6.0.5: + resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} ignore@5.3.1: @@ -7386,11 +7873,11 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} - import-in-the-middle@1.4.2: - resolution: {integrity: sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==} + import-in-the-middle@1.10.0: + resolution: {integrity: sha512-Z1jumVdF2GwnnYfM0a/y2ts7mZbwFMgt5rRuVmLgobgahC6iKgN5MBuXjzfTIOUpq5LSU10vJIPpVKe0X89fIw==} - import-in-the-middle@1.7.4: - resolution: {integrity: sha512-Lk+qzWmiQuRPPulGQeK5qq0v32k2bHnWrRPFgqyvhw7Kkov5L6MOLOIU3pcWeujc9W4q54Cp3Q2WV16eQkc7Bg==} + import-in-the-middle@1.7.1: + resolution: {integrity: sha512-1LrZPDtW+atAxH42S6288qyDFNQ2YCty+2mxEPRtfazH6Z5QwkaBSTS2ods7hnVJioF6rkRfNoA6A/MstpFXLg==} import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} @@ -7444,13 +7931,13 @@ packages: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} - ip-address@7.1.0: - resolution: {integrity: sha512-V9pWC/VJf2lsXqP7IWJ+pe3P1/HCYGBMZrrnT62niLGjAfCbeiwXMUxaeHvnVlz19O27pvXP4azs+Pj/A0x+SQ==} - engines: {node: '>= 10'} + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} - ip-cidr@3.1.0: - resolution: {integrity: sha512-HUCn4snshEX1P8cja/IyU3qk8FVDW8T5zZcegDFbu4w7NojmAhk5NcOgj3M8+0fmumo1afJTPDtJlzsxLdOjtg==} - engines: {node: '>=10.0.0'} + ip-cidr@4.0.1: + resolution: {integrity: sha512-V5Nce94SVJ7NtyT/UKUeTM7sY3V7TEk48hURhtBgTiGduOa5t6p9Hd+zBOGvr4Gu7iWPxFVYNl017p0akQA84w==} + engines: {node: '>=16.14.0'} ip-regex@4.3.0: resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} @@ -7514,9 +8001,6 @@ packages: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} - is-deflate@1.0.0: - resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -7548,10 +8032,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-gzip@1.0.0: - resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} - engines: {node: '>=0.10.0'} - is-installed-globally@0.4.0: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} @@ -7589,10 +8069,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} @@ -7641,12 +8117,16 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} - is-svg@5.0.0: - resolution: {integrity: sha512-sRl7J0oX9yUNamSdc8cwgzh9KBLnQXNzGmW0RVHwg/jEYjGNYHC6UvnYD8+hAeut9WwxRvhG9biK7g/wDGxcMw==} + is-svg@5.0.1: + resolution: {integrity: sha512-mLYxDsfisQWdS4+gSblAwhATDoNMS/tx8G7BKA+aBIf7F0m1iUwMvuKAo6mW4WMleQAEE50I1Zqef9yMMfHk3w==} engines: {node: '>=14.16'} is-symbol@1.0.4: @@ -7664,6 +8144,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} @@ -7720,6 +8204,10 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.4: + resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==} + engines: {node: '>=10'} + istanbul-reports@3.1.6: resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} engines: {node: '>=8'} @@ -7732,6 +8220,14 @@ packages: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} + jackspeak@3.4.0: + resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} + engines: {node: '>=14'} + + jackspeak@4.0.1: + resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==} + engines: {node: 20 || >=22} + jake@10.8.5: resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} engines: {node: '>=10'} @@ -7889,6 +8385,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.0: + resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -7916,8 +8415,8 @@ packages: '@babel/preset-env': optional: true - jsdom@24.0.0: - resolution: {integrity: sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==} + jsdom@24.1.1: + resolution: {integrity: sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==} engines: {node: '>=18'} peerDependencies: canvas: ^2.11.2 @@ -7966,6 +8465,7 @@ packages: json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} + hasBin: true jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} @@ -7998,9 +8498,6 @@ packages: jsrsasign@11.1.0: resolution: {integrity: sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==} - jssha@3.3.1: - resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==} - jstransformer@1.0.0: resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} @@ -8081,8 +8578,8 @@ packages: enquirer: optional: true - local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + local-pkg@0.5.0: + resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} engines: {node: '>=14'} locate-path@3.0.0: @@ -8109,9 +8606,6 @@ packages: lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -8157,6 +8651,10 @@ packages: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} + lru-cache@11.0.0: + resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==} + engines: {node: 20 || >=22} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -8189,9 +8687,8 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - magic-string@0.30.7: - resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} - engines: {node: '>=12'} + magicast@0.3.4: + resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} mailcheck@1.1.1: resolution: {integrity: sha512-3WjL8+ZDouZwKlyJBMp/4LeziLFXgleOdsYu87piGcMLqhBzCsy2QFdbtAwv757TFC/rtqd738fgJw1tFQCSgA==} @@ -8235,8 +8732,8 @@ packages: markdown-table@3.0.3: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} - markdown-to-jsx@7.3.2: - resolution: {integrity: sha512-B+28F5ucp83aQm+OxNrPkS8z0tMKaeHiy0lHJs3LqCyDQFtWuenaIrkaVTgAm1pf1AU85LXltva86hlaT17i8Q==} + markdown-to-jsx@7.4.7: + resolution: {integrity: sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg==} engines: {node: '>= 10'} peerDependencies: react: '>= 0.14.0' @@ -8292,8 +8789,8 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - meilisearch@0.38.0: - resolution: {integrity: sha512-bHaq8nYxSKw9/Qslq1Zes5g9tHgFkxy/I9o8942wv2PqlNOT0CzptIkh/x98N52GikoSZOXSQkgt6oMjtf5uZw==} + meilisearch@0.41.0: + resolution: {integrity: sha512-5KcGLxEXD7E+uNO7R68rCbGSHgCqeM3Q3RFFLSsN7ZrIgr8HPDXVAIlP4LHggAZfk0FkSzo8VSXifHCwa2k80g==} memoizerific@1.11.3: resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} @@ -8404,8 +8901,8 @@ packages: micromark@4.0.0: resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} - micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} mime-db@1.52.0: @@ -8453,6 +8950,10 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + minimatch@3.0.8: resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} @@ -8514,13 +9015,14 @@ packages: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -8572,13 +9074,13 @@ packages: msgpackr@1.10.1: resolution: {integrity: sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==} - msw-storybook-addon@2.0.1: - resolution: {integrity: sha512-pZ3JDQ9HkGQ3XDMIHvMcDSI4Vbp/LHmwHwiZu+pHLzimtI1vhAo1swjFEDAEJuBcozljYvREEC4sS7rQHPNtWg==} + msw-storybook-addon@2.0.3: + resolution: {integrity: sha512-CzHmGO32JeOPnyUnRWnB0PFTXCY1HKfHiEB/6fYoUYiFm2NYosLjzs9aBd3XJUryYEN0avJqMNh7nCRDxE5JjQ==} peerDependencies: msw: ^2.0.0 - msw@2.2.14: - resolution: {integrity: sha512-64i8rNCa1xzDK8ZYsTrVMli05D687jty8+Th+PU5VTbJ2/4P7fkQFVyDQ6ZFT5FrNR8z2BHhbY47fKNvfHrumA==} + msw@2.3.4: + resolution: {integrity: sha512-sHMlwrajgmZSA2l1o7qRSe+azm/I+x9lvVVcOxAzi4vCtH8uVPJk1K5BQYDkzGl+tt0RvM9huEXXdeGrgcc79g==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -8605,8 +9107,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.18.0: - resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} + nan@2.20.0: + resolution: {integrity: sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==} nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} @@ -8701,8 +9203,8 @@ packages: resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} hasBin: true - node-gyp@10.0.1: - resolution: {integrity: sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==} + node-gyp@10.1.0: + resolution: {integrity: sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==} engines: {node: ^16.14.0 || >=18.0.0} hasBin: true @@ -8712,8 +9214,8 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - nodemailer@6.9.13: - resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==} + nodemailer@6.9.14: + resolution: {integrity: sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==} engines: {node: '>=6.0.0'} nodemon@3.0.2: @@ -8721,8 +9223,8 @@ packages: engines: {node: '>=10'} hasBin: true - nodemon@3.1.0: - resolution: {integrity: sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==} + nodemon@3.1.4: + resolution: {integrity: sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==} engines: {node: '>=10'} hasBin: true @@ -8763,6 +9265,10 @@ packages: 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'} + npm-run-path@2.0.2: resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} engines: {node: '>=4'} @@ -8775,11 +9281,15 @@ packages: resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nwsapi@2.2.9: - resolution: {integrity: sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==} + nwsapi@2.2.12: + resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} oauth2orize-pkce@0.1.2: resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==} @@ -8868,12 +9378,14 @@ packages: resolution: {integrity: sha512-es3mGcDXV6TKPo6n3aohzHm0qxhLyR39MhF6mkD1FwFGjhxnqMqfSIgM0eCpInZvqatve4CxmXcMZw3jnnsaXw==} hasBin: true - opentelemetry-instrumentation-fetch-node@1.2.0: - resolution: {integrity: sha512-aiSt/4ubOTyb1N5C2ZbGrBvaJOXIZhZvpRPYuUVxQJe27wJZqf/o65iPrqgLcgfeOLaQ8cS2Q+762jrYvniTrA==} + opentelemetry-instrumentation-fetch-node@1.2.3: + resolution: {integrity: sha512-Qb11T7KvoCevMaSeuamcLsAD+pZnavkhDnlVL0kRozfhl42dKG5Q3anUklAFKJZjY3twLR+BnRa6DlwwkIE/+A==} engines: {node: '>18.0.0'} + peerDependencies: + '@opentelemetry/api': ^1.6.0 - optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} ora@5.4.1: @@ -8890,8 +9402,8 @@ packages: ospath@1.2.2: resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} - otpauth@9.2.3: - resolution: {integrity: sha512-oAG55Ch4MBL5Jdg+RXfKiRCZ2lCwa/UIQKsmSfYbGGLSI4dErY1HPZv0JGPPESIYGyDO3s9iJqM4HU/1IppMoQ==} + otpauth@9.3.1: + resolution: {integrity: sha512-E6d2tMxPofHNk4sRFp+kqW7vQ+WJGO9VLI2N/W00DnI+ThskU12Qa10kyNSGklrzhN5c+wRUsN4GijVgCU2N9w==} outvariant@1.4.2: resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==} @@ -8920,9 +9432,9 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} p-locate@3.0.0: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} @@ -8952,8 +9464,8 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -8966,6 +9478,10 @@ packages: parse-link-header@2.0.0: resolution: {integrity: sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} @@ -9022,9 +9538,13 @@ packages: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} engines: {node: '>=16 || 14 >=14.17'} - path-scurry@1.10.2: - resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} - engines: {node: '>=16 || 14 >=14.17'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -9042,6 +9562,10 @@ packages: 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==} @@ -9055,8 +9579,9 @@ packages: resolution: {integrity: sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==} engines: {node: '>=14.16'} - peek-stream@1.1.3: - resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + peek-readable@5.1.3: + resolution: {integrity: sha512-kCsc9HwH5RgVA3H3VqkWFyGQwsxUxLdiSX1d5nqAm7hnMFjNFX1VhBLmJoUY0hZNc8gmDNgBkLjfhiWPsziXWA==} + engines: {node: '>=14.16'} pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -9083,9 +9608,6 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.6.0: - resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - pg-protocol@1.6.1: resolution: {integrity: sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==} @@ -9097,8 +9619,8 @@ packages: resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==} engines: {node: '>=10'} - pg@8.11.5: - resolution: {integrity: sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==} + pg@8.12.0: + resolution: {integrity: sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -9109,13 +9631,16 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - photoswipe@5.4.3: - resolution: {integrity: sha512-9UC6oJBK4oXFZ5HcdlcvGkfEHsVrmE4csUdCQhEjHYb3PvPLO3PG7UhnPuOgjxwmhq5s17Un5NUdum01LgBDng==} + photoswipe@5.4.4: + resolution: {integrity: sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==} engines: {node: '>= 0.12.0'} picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -9132,14 +9657,14 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pino-abstract-transport@1.1.0: - resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} - pino-std-serializers@6.1.0: - resolution: {integrity: sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==} + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} - pino@8.17.0: - resolution: {integrity: sha512-ey+Mku+PVPhvxglLXMg1l1zQMwSHuNrKC3MD40EDZbkckJmmuY7DYZLIOwwjZ8ix/Nvhe9dZt5H99cgkot9bAw==} + pino@9.2.0: + resolution: {integrity: sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==} hasBin: true pirates@4.0.5: @@ -9360,6 +9885,10 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.4.40: + resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -9399,8 +9928,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} engines: {node: '>=14'} hasBin: true @@ -9420,6 +9949,10 @@ packages: 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'} + private-ip@2.3.3: resolution: {integrity: sha512-5zyFfekIVUOTVbL92hc8LJOtE/gyGHeREHkJ2yTyByP8Q2YZVoBqLg3EfYLeF0oVvGqtaEX2t2Qovja0/gStXw==} @@ -9501,12 +10034,15 @@ packages: pug-attrs@3.0.0: resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} - pug-code-gen@3.0.2: - resolution: {integrity: sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==} + pug-code-gen@3.0.3: + resolution: {integrity: sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==} pug-error@2.0.0: resolution: {integrity: sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==} + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + pug-filters@4.0.0: resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} @@ -9531,18 +10067,12 @@ packages: pug-walk@2.0.0: resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} - pug@3.0.2: - resolution: {integrity: sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==} - - pump@2.0.1: - resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + pug@3.0.3: + resolution: {integrity: sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==} pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} - pumpify@1.5.1: - resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -9611,10 +10141,6 @@ packages: ratelimiter@3.4.1: resolution: {integrity: sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==} - raw-body@2.5.1: - resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} - engines: {node: '>= 0.8'} - raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -9623,8 +10149,8 @@ packages: resolution: {integrity: sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==} engines: {node: '>=12'} - re2@1.20.10: - resolution: {integrity: sha512-/5JjSPXobSDaKFL6rD5Gb4qD4CVBITQb7NAxfQ/NA7o0HER3SJAPV3lPO2kvzw0/PN1pVJNVATEUk4y9j7oIIA==} + re2@1.21.3: + resolution: {integrity: sha512-GI+KoGkHT4kxTaX+9p0FgNB1XUnCndO9slG5qqeEoZ7kbf6Dk6ohQVpmwKVeSp7LPLn+g6Q3BaCopz4oHuBDuQ==} react-colorful@5.6.1: resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} @@ -9702,10 +10228,6 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - recast@0.23.4: - resolution: {integrity: sha512-qtEDqIZGVcSZCHniWwZWbRy79Dc6Wp3kT/UmDA2RJKBPg7+7k51aQBZirHmUGn5uvHf2rg8DkjizrN26k61ATw==} - engines: {node: '>= 4'} - recast@0.23.6: resolution: {integrity: sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==} engines: {node: '>= 4'} @@ -9821,9 +10343,6 @@ packages: resolution: {integrity: sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==} engines: {node: '>=10'} - resolve@1.19.0: - resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} - resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -9856,20 +10375,25 @@ packages: rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true 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.17.2: - resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==} + rollup@4.19.1: + resolution: {integrity: sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true rrweb-cssom@0.6.0: resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + rss-parser@3.13.0: resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} @@ -9905,8 +10429,8 @@ packages: sanitize-html@2.13.0: resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==} - sass@1.76.0: - resolution: {integrity: sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==} + sass@1.77.8: + resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -9980,8 +10504,8 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} - sharp@0.33.3: - resolution: {integrity: sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==} + 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} shebang-command@1.2.0: @@ -10003,8 +10527,8 @@ packages: shiki@0.14.7: resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} - shiki@1.4.0: - resolution: {integrity: sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==} + shiki@1.12.0: + resolution: {integrity: sha512-BuAxWOm5JhRcbSOl7XCei8wGjgJJonnV0oipUupPY58iULxUGyHhW5CF+9FRMuM1pcJ5cGEJGll1LusX6FwpPA==} shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} @@ -10022,8 +10546,8 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-oauth2@5.0.0: - resolution: {integrity: sha512-8291lo/z5ZdpmiOFzOs1kF3cxn22bMj5FFH+DNUppLJrpoIlM1QnFiE7KpshHu3J3i21TVcx4yW+gXYjdCKDLQ==} + simple-oauth2@5.1.0: + resolution: {integrity: sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==} simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -10123,6 +10647,10 @@ 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'} @@ -10143,8 +10671,8 @@ packages: resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} - sonic-boom@3.7.0: - resolution: {integrity: sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==} + sonic-boom@4.0.1: + resolution: {integrity: sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==} sort-keys-length@1.0.1: resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} @@ -10157,10 +10685,6 @@ packages: sortablejs@1.14.0: resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==} - source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} @@ -10204,8 +10728,8 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sprintf-js@1.1.2: - resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} sshpk@1.17.0: resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} @@ -10226,8 +10750,8 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - start-server-and-test@2.0.3: - resolution: {integrity: sha512-QsVObjfjFZKJE6CS6bSKNwWZCKBG6975/jKRPPGFfFh+yOQglSeGXiNWjzgQNXdphcBI9nXbyso9tPfX4YAUhg==} + start-server-and-test@2.0.4: + resolution: {integrity: sha512-CKNeBTcP0hVqIlNismHMudb9q3lLdAjcVPO13/7gfI66fcJpeIb/o4NzQd1JK/CD+lfWVqr10ZH9Y14+OwlJuw==} engines: {node: '>=16'} hasBin: true @@ -10264,8 +10788,8 @@ packages: react-dom: optional: true - storybook@8.0.9: - resolution: {integrity: sha512-/Mvij0Br5bUwJpCvqAUZMEDIWmdRxEyllvVj8Ukw5lIWJePxfpSsz4px5jg9+R6B9tO8sQSqjg4HJvQ/pZk8Tg==} + storybook@8.2.6: + resolution: {integrity: sha512-8j30wDxQmkcqI0fWcSYFsUCjErsY1yTWbTW+yjbwM8DyW18Cud6CwbFRCxjFsH+2M0CjP6Pqs/m1PGI0vcQscQ==} hasBin: true stream-browserify@3.0.0: @@ -10277,9 +10801,6 @@ packages: stream-parser@0.3.1: resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} - stream-shift@1.0.1: - resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} - stream-wormhole@1.1.0: resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} engines: {node: '>=4.0.0'} @@ -10360,6 +10881,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -10372,8 +10897,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + strip-literal@2.1.0: + resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} strip-outer@2.0.0: resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==} @@ -10386,6 +10911,10 @@ packages: resolution: {integrity: sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==} engines: {node: '>=14.16'} + strtok3@8.0.1: + resolution: {integrity: sha512-HNkTAnNWQj2YBzfTtoC5OQyu1QwPsMwiB7VyQmNvQKCrmEDSvFB857Vh97UY9InGLNRAB91sdS1ztifRo/3hdA==} + engines: {node: '>=16'} + stylehacks@6.1.1: resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==} engines: {node: ^14 || ^16 || >=18.0} @@ -10424,19 +10953,12 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - systeminformation@5.22.7: - resolution: {integrity: sha512-AWxlP05KeHbpGdgvZkcudJpsmChc2Y5Eo/GvxG/iUA/Aws5LZKHAMSeAo+V+nD+nxWZaxrwpWcnx4SH3oxNL3A==} + systeminformation@5.22.11: + resolution: {integrity: sha512-aLws5yi4KCHTb0BVvbodQY5bY8eW4asMRDTxTW46hqw9lGjACX6TlLdJrkdoHYRB0qs+MekqEq1zG7WDnWE8Ug==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true - tar-fs@2.1.1: - resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - tar-stream@3.1.6: resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} @@ -10451,20 +10973,20 @@ packages: telejson@7.2.0: resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} - temp-dir@2.0.0: - resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} - engines: {node: '>=8'} + 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@1.0.1: - resolution: {integrity: sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==} - engines: {node: '>=10'} + tempy@3.1.0: + resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} + engines: {node: '>=14.16'} - terser@5.30.3: - resolution: {integrity: sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==} + terser@5.31.3: + resolution: {integrity: sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==} engines: {node: '>=10'} hasBin: true @@ -10488,28 +11010,22 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - thread-stream@2.3.0: - resolution: {integrity: sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==} + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - three@0.164.1: - resolution: {integrity: sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==} + three@0.167.0: + resolution: {integrity: sha512-9Y1a66fpjqF3rhq7ivKTaKtjQLZ97Hj/lZ00DmZWaKHaQFH4uzYT1znwRDWQOcgMmCcOloQzo61gDmqO8l9xmA==} - throttle-debounce@5.0.0: - resolution: {integrity: sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==} + throttle-debounce@5.0.2: + resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} throttleit@1.0.0: resolution: {integrity: sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==} - through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-invariant@1.3.1: - resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -10523,8 +11039,8 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinypool@0.7.0: - resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} tinyspy@2.2.0: @@ -10549,17 +11065,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toad-cache@3.3.0: - resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==} - engines: {node: '>=12'} - toad-cache@3.7.0: resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} engines: {node: '>=12'} - tocbot@4.21.1: - resolution: {integrity: sha512-IfajhBTeg0HlMXu1f+VMbPef05QpDTsZ9X2Yn1+8npdaXsXg/+wrm9Ze1WG5OS1UDC3qJ5EQN/XOZ3gfXjPFCw==} - toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -10571,12 +11080,16 @@ packages: resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} engines: {node: '>=14.16'} + token-types@6.0.0: + resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} + engines: {node: '>=14.16'} + touch@3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true - tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} tr46@0.0.3: @@ -10638,8 +11151,8 @@ packages: ts-map@1.0.3: resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==} - tsc-alias@1.8.8: - resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==} + tsc-alias@1.8.10: + resolution: {integrity: sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==} hasBin: true tsconfig-paths@3.15.0: @@ -10649,8 +11162,8 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} - tsd@0.30.7: - resolution: {integrity: sha512-oTiJ28D6B/KXoU3ww/Eji+xqHJojiuPVMwA12g4KYX1O72N93Nb6P3P3h2OAhhf92Xl8NIhb/xFmBZd5zw/xUw==} + tsd@0.31.1: + resolution: {integrity: sha512-sSL84A0SFwx2xGMWrxlGaarKFSQszWjJS2vgNDDLwatytzg2aq6ShlwHsBYxRNmjzXISODwMva5ZOdAg/4AoOA==} engines: {node: '>=14.16'} hasBin: true @@ -10660,6 +11173,9 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + tsx@4.4.0: resolution: {integrity: sha512-4fwcEjRUxW20ciSaMB8zkpGwCPxuRGnadDuj/pBk5S9uT29zvWz15PK36GrKJo45mSJomDxVejZ73c6lr3811Q==} engines: {node: '>=18.0.0'} @@ -10679,10 +11195,6 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} - type-fest@0.16.0: - resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} - engines: {node: '>=10'} - type-fest@0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} engines: {node: '>=10'} @@ -10703,12 +11215,16 @@ 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'} - type-fest@4.9.0: - resolution: {integrity: sha512-KS/6lh/ynPGiHD/LnAobrEFq3Ad4pBzOlJ1wAnJx9N4EYoqFhMfLIBjUT2UEx4wg5ZE+cC1ob6DCSpppVo+rtg==} + type-fest@4.20.1: + resolution: {integrity: sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==} engines: {node: '>=16'} type-is@1.6.18: @@ -10813,8 +11329,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.4.5: - resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} hasBin: true @@ -10833,6 +11349,10 @@ packages: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} + uint8array-extras@1.4.0: + resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} + engines: {node: '>=18'} + ulid@2.3.0: resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==} hasBin: true @@ -10866,6 +11386,10 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + unified@11.0.4: resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} @@ -10877,9 +11401,9 @@ packages: resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} + 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==} @@ -10935,6 +11459,10 @@ packages: resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} engines: {node: '>=6.14.2'} + utf-8-validate@6.0.4: + resolution: {integrity: sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==} + engines: {node: '>=6.14.2'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -10945,6 +11473,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -10953,8 +11485,8 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - v-code-diff@1.11.0: - resolution: {integrity: sha512-lBlO+FXw3I3qFKbnlorXZ4sb5cFnrdxlc6lj3Y1CWrbn2LC7PoVbGlwH0W+nvAVX1rdJhhc15rKIQdHyMkXe/w==} + v-code-diff@1.12.0: + resolution: {integrity: sha512-vvdCBG02mIIiW6Gx6jF119hzxELt+6TlJIwchglR1JYzboHePNxIkVBjR/aoAOVlsGa+5Vtb77cd/N84nrXWPA==} peerDependencies: '@vue/composition-api': ^1.4.9 vue: ^2.6.0 || >=3.0.0 @@ -10969,10 +11501,6 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - validator@13.9.0: - resolution: {integrity: sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==} - engines: {node: '>= 0.10'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -10987,16 +11515,16 @@ packages: vfile@6.0.1: resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} - vite-node@0.34.6: - resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} - engines: {node: '>=v14.18.0'} + vite-node@1.6.0: + resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true vite-plugin-turbosnap@1.0.3: resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==} - vite@5.2.11: - resolution: {integrity: sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==} + vite@5.3.5: + resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -11029,22 +11557,22 @@ packages: peerDependencies: vitest: '>=0.16.0' - vitest@0.34.6: - resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} - engines: {node: '>=v14.18.0'} + vitest@1.6.0: + resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.0 + '@vitest/ui': 1.6.0 happy-dom: '*' jsdom: '*' - playwright: '*' - safaridriver: '*' - webdriverio: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@types/node': + optional: true '@vitest/browser': optional: true '@vitest/ui': @@ -11053,12 +11581,6 @@ packages: optional: true jsdom: optional: true - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} @@ -11091,6 +11613,9 @@ packages: vscode-textmate@8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vue-component-meta@2.0.16: resolution: {integrity: sha512-IyIMClUMYcKxAL34GqdPbR4V45MUeHXqQiZlHxeYMV5Qcqp4M+CEmtGpF//XBSS138heDkYkceHAtJQjLUB1Lw==} peerDependencies: @@ -11105,8 +11630,11 @@ packages: vue-component-type-helpers@2.0.16: resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==} - vue-component-type-helpers@2.0.19: - resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} + vue-component-type-helpers@2.0.29: + resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==} + + vue-component-type-helpers@2.1.2: + resolution: {integrity: sha512-URuxnrOhO9lUG4LOAapGWBaa/WOLDzzyAbL+uKZqT7RS+PFy0cdXI2mUSh7GaMts6vtHaeVbGk7trd0FPJi65Q==} vue-demi@0.14.7: resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} @@ -11124,8 +11652,8 @@ packages: peerDependencies: vue: '>=2' - vue-eslint-parser@9.4.2: - resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==} + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: '>=6.0.0' @@ -11144,14 +11672,14 @@ packages: vue-template-compiler@2.7.14: resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} - vue-tsc@2.0.16: - resolution: {integrity: sha512-/gHAWJa216PeEhfxtAToIbxdWgw01wuQzo48ZUqMYVEyNqDp+OYV9xMO5HaPS2P3Ls0+EsjguMZLY4cGobX4Ew==} + vue-tsc@2.0.29: + resolution: {integrity: sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==} hasBin: true peerDependencies: - typescript: '*' + typescript: '>=5.0.0' - vue@3.4.26: - resolution: {integrity: sha512-bUIq/p+VB+0xrJubaemrfhk1/FiW9iX+pDV+62I/XJ6EkspAO9/DXEjbDFoe8pIfOZBqfk45i9BMc41ptP/uRg==} + vue@3.4.37: + resolution: {integrity: sha512-3vXvNfkKTBsSJ7JP+LyR7GBuwQuckbWvuwAid3xbqK9ppsKt/DUvfqgZ48fgOLEfpy1IacL5f8QhUVl77RaI7A==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -11172,6 +11700,9 @@ packages: 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==} @@ -11191,6 +11722,10 @@ packages: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} + web-streams-polyfill@4.0.0: + resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -11264,6 +11799,10 @@ packages: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -11301,8 +11840,8 @@ packages: utf-8-validate: optional: true - ws@8.17.0: - resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -11394,10 +11933,9 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} - z-schema@5.0.5: - resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} - engines: {node: '>=8.0.0'} - hasBin: true + yoctocolors@2.0.2: + resolution: {integrity: sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw==} + engines: {node: '>=18'} zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} @@ -11408,8 +11946,6 @@ packages: snapshots: - '@aashutoshrathi/word-wrap@1.2.6': {} - '@adobe/css-tools@4.3.3': {} '@aiscript-dev/aiscript-languageserver@https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz': @@ -11433,528 +11969,584 @@ snapshots: dependencies: default-browser-id: 3.0.0 - '@aws-crypto/crc32@3.0.0': - dependencies: - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.413.0 - tslib: 1.14.1 - - '@aws-crypto/crc32c@3.0.0': + '@aws-crypto/crc32@5.2.0': dependencies: - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.413.0 - tslib: 1.14.1 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.609.0 + tslib: 2.6.3 - '@aws-crypto/ie11-detection@3.0.0': + '@aws-crypto/crc32c@5.2.0': dependencies: - tslib: 1.14.1 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.609.0 + tslib: 2.6.3 - '@aws-crypto/sha1-browser@3.0.0': + '@aws-crypto/sha1-browser@5.2.0': dependencies: - '@aws-crypto/ie11-detection': 3.0.0 - '@aws-crypto/supports-web-crypto': 3.0.0 - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.413.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.609.0 '@aws-sdk/util-locate-window': 3.208.0 - '@aws-sdk/util-utf8-browser': 3.259.0 - tslib: 1.14.1 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.3 - '@aws-crypto/sha256-browser@3.0.0': + '@aws-crypto/sha256-browser@5.2.0': dependencies: - '@aws-crypto/ie11-detection': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-crypto/supports-web-crypto': 3.0.0 - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.413.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.609.0 '@aws-sdk/util-locate-window': 3.208.0 - '@aws-sdk/util-utf8-browser': 3.259.0 - tslib: 1.14.1 + '@smithy/util-utf8': 2.0.0 + tslib: 2.6.3 - '@aws-crypto/sha256-js@3.0.0': + '@aws-crypto/sha256-js@5.2.0': dependencies: - '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.413.0 - tslib: 1.14.1 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.609.0 + tslib: 2.6.3 - '@aws-crypto/supports-web-crypto@3.0.0': + '@aws-crypto/supports-web-crypto@5.2.0': dependencies: - tslib: 1.14.1 + tslib: 2.6.3 - '@aws-crypto/util@3.0.0': + '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.413.0 - '@aws-sdk/util-utf8-browser': 3.259.0 - tslib: 1.14.1 - - '@aws-sdk/client-s3@3.412.0': - dependencies: - '@aws-crypto/sha1-browser': 3.0.0 - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.410.0 - '@aws-sdk/credential-provider-node': 3.410.0 - '@aws-sdk/middleware-bucket-endpoint': 3.410.0 - '@aws-sdk/middleware-expect-continue': 3.410.0 - '@aws-sdk/middleware-flexible-checksums': 3.410.0 - '@aws-sdk/middleware-host-header': 3.410.0 - '@aws-sdk/middleware-location-constraint': 3.410.0 - '@aws-sdk/middleware-logger': 3.410.0 - '@aws-sdk/middleware-recursion-detection': 3.410.0 - '@aws-sdk/middleware-sdk-s3': 3.410.0 - '@aws-sdk/middleware-signing': 3.410.0 - '@aws-sdk/middleware-ssec': 3.410.0 - '@aws-sdk/middleware-user-agent': 3.410.0 - '@aws-sdk/signature-v4-multi-region': 3.412.0 - '@aws-sdk/types': 3.410.0 - '@aws-sdk/util-endpoints': 3.410.0 - '@aws-sdk/util-user-agent-browser': 3.410.0 - '@aws-sdk/util-user-agent-node': 3.410.0 - '@aws-sdk/xml-builder': 3.310.0 - '@smithy/config-resolver': 2.0.9 - '@smithy/eventstream-serde-browser': 2.0.8 - '@smithy/eventstream-serde-config-resolver': 2.0.8 - '@smithy/eventstream-serde-node': 2.0.8 - '@smithy/fetch-http-handler': 2.1.4 - '@smithy/hash-blob-browser': 2.0.8 - '@smithy/hash-node': 2.0.8 - '@smithy/hash-stream-node': 2.0.8 - '@smithy/invalid-dependency': 2.0.8 - '@smithy/md5-js': 2.0.8 - '@smithy/middleware-content-length': 2.0.10 - '@smithy/middleware-endpoint': 2.0.8 - '@smithy/middleware-retry': 2.0.11 - '@smithy/middleware-serde': 2.0.8 - '@smithy/middleware-stack': 2.0.1 - '@smithy/node-config-provider': 2.0.11 - '@smithy/node-http-handler': 2.5.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/smithy-client': 2.1.5 - '@smithy/types': 2.6.0 - '@smithy/url-parser': 2.0.8 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.9 - '@smithy/util-defaults-mode-node': 2.0.11 - '@smithy/util-retry': 2.0.1 - '@smithy/util-stream': 2.0.11 + '@aws-sdk/types': 3.609.0 '@smithy/util-utf8': 2.0.0 - '@smithy/util-waiter': 2.0.8 - fast-xml-parser: 4.2.5 - tslib: 2.6.2 + tslib: 2.6.3 + + '@aws-sdk/client-s3@3.620.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.620.0(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/client-sts': 3.620.0 + '@aws-sdk/core': 3.620.0 + '@aws-sdk/credential-provider-node': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/middleware-bucket-endpoint': 3.620.0 + '@aws-sdk/middleware-expect-continue': 3.620.0 + '@aws-sdk/middleware-flexible-checksums': 3.620.0 + '@aws-sdk/middleware-host-header': 3.620.0 + '@aws-sdk/middleware-location-constraint': 3.609.0 + '@aws-sdk/middleware-logger': 3.609.0 + '@aws-sdk/middleware-recursion-detection': 3.620.0 + '@aws-sdk/middleware-sdk-s3': 3.620.0 + '@aws-sdk/middleware-signing': 3.620.0 + '@aws-sdk/middleware-ssec': 3.609.0 + '@aws-sdk/middleware-user-agent': 3.620.0 + '@aws-sdk/region-config-resolver': 3.614.0 + '@aws-sdk/signature-v4-multi-region': 3.620.0 + '@aws-sdk/types': 3.609.0 + '@aws-sdk/util-endpoints': 3.614.0 + '@aws-sdk/util-user-agent-browser': 3.609.0 + '@aws-sdk/util-user-agent-node': 3.614.0 + '@aws-sdk/xml-builder': 3.609.0 + '@smithy/config-resolver': 3.0.5 + '@smithy/core': 2.3.1 + '@smithy/eventstream-serde-browser': 3.0.5 + '@smithy/eventstream-serde-config-resolver': 3.0.3 + '@smithy/eventstream-serde-node': 3.0.4 + '@smithy/fetch-http-handler': 3.2.4 + '@smithy/hash-blob-browser': 3.1.2 + '@smithy/hash-node': 3.0.3 + '@smithy/hash-stream-node': 3.1.2 + '@smithy/invalid-dependency': 3.0.3 + '@smithy/md5-js': 3.0.3 + '@smithy/middleware-content-length': 3.0.5 + '@smithy/middleware-endpoint': 3.1.0 + '@smithy/middleware-retry': 3.0.13 + '@smithy/middleware-serde': 3.0.3 + '@smithy/middleware-stack': 3.0.3 + '@smithy/node-config-provider': 3.1.4 + '@smithy/node-http-handler': 3.1.4 + '@smithy/protocol-http': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.13 + '@smithy/util-defaults-mode-node': 3.0.13 + '@smithy/util-endpoints': 2.0.5 + '@smithy/util-retry': 3.0.3 + '@smithy/util-stream': 3.1.3 + '@smithy/util-utf8': 3.0.0 + '@smithy/util-waiter': 3.1.2 + tslib: 2.6.3 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.410.0': - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/middleware-host-header': 3.410.0 - '@aws-sdk/middleware-logger': 3.410.0 - '@aws-sdk/middleware-recursion-detection': 3.410.0 - '@aws-sdk/middleware-user-agent': 3.410.0 - '@aws-sdk/types': 3.410.0 - '@aws-sdk/util-endpoints': 3.410.0 - '@aws-sdk/util-user-agent-browser': 3.410.0 - '@aws-sdk/util-user-agent-node': 3.410.0 - '@smithy/config-resolver': 2.0.9 - '@smithy/fetch-http-handler': 2.1.4 - '@smithy/hash-node': 2.0.8 - '@smithy/invalid-dependency': 2.0.8 - '@smithy/middleware-content-length': 2.0.10 - '@smithy/middleware-endpoint': 2.0.8 - '@smithy/middleware-retry': 2.0.11 - '@smithy/middleware-serde': 2.0.8 - '@smithy/middleware-stack': 2.0.1 - '@smithy/node-config-provider': 2.0.11 - '@smithy/node-http-handler': 2.5.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/smithy-client': 2.1.5 - '@smithy/types': 2.6.0 - '@smithy/url-parser': 2.0.8 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.9 - '@smithy/util-defaults-mode-node': 2.0.11 - '@smithy/util-retry': 2.0.1 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 + '@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0)': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sts': 3.620.0 + '@aws-sdk/core': 3.620.0 + '@aws-sdk/credential-provider-node': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/middleware-host-header': 3.620.0 + '@aws-sdk/middleware-logger': 3.609.0 + '@aws-sdk/middleware-recursion-detection': 3.620.0 + '@aws-sdk/middleware-user-agent': 3.620.0 + '@aws-sdk/region-config-resolver': 3.614.0 + '@aws-sdk/types': 3.609.0 + '@aws-sdk/util-endpoints': 3.614.0 + '@aws-sdk/util-user-agent-browser': 3.609.0 + '@aws-sdk/util-user-agent-node': 3.614.0 + '@smithy/config-resolver': 3.0.5 + '@smithy/core': 2.3.1 + '@smithy/fetch-http-handler': 3.2.4 + '@smithy/hash-node': 3.0.3 + '@smithy/invalid-dependency': 3.0.3 + '@smithy/middleware-content-length': 3.0.5 + '@smithy/middleware-endpoint': 3.1.0 + '@smithy/middleware-retry': 3.0.13 + '@smithy/middleware-serde': 3.0.3 + '@smithy/middleware-stack': 3.0.3 + '@smithy/node-config-provider': 3.1.4 + '@smithy/node-http-handler': 3.1.4 + '@smithy/protocol-http': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.13 + '@smithy/util-defaults-mode-node': 3.0.13 + '@smithy/util-endpoints': 2.0.5 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-retry': 3.0.3 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.410.0': - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/credential-provider-node': 3.410.0 - '@aws-sdk/middleware-host-header': 3.410.0 - '@aws-sdk/middleware-logger': 3.410.0 - '@aws-sdk/middleware-recursion-detection': 3.410.0 - '@aws-sdk/middleware-sdk-sts': 3.410.0 - '@aws-sdk/middleware-signing': 3.410.0 - '@aws-sdk/middleware-user-agent': 3.410.0 - '@aws-sdk/types': 3.410.0 - '@aws-sdk/util-endpoints': 3.410.0 - '@aws-sdk/util-user-agent-browser': 3.410.0 - '@aws-sdk/util-user-agent-node': 3.410.0 - '@smithy/config-resolver': 2.0.9 - '@smithy/fetch-http-handler': 2.1.4 - '@smithy/hash-node': 2.0.8 - '@smithy/invalid-dependency': 2.0.8 - '@smithy/middleware-content-length': 2.0.10 - '@smithy/middleware-endpoint': 2.0.8 - '@smithy/middleware-retry': 2.0.11 - '@smithy/middleware-serde': 2.0.8 - '@smithy/middleware-stack': 2.0.1 - '@smithy/node-config-provider': 2.0.11 - '@smithy/node-http-handler': 2.5.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/smithy-client': 2.1.5 - '@smithy/types': 2.6.0 - '@smithy/url-parser': 2.0.8 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.9 - '@smithy/util-defaults-mode-node': 2.0.11 - '@smithy/util-retry': 2.0.1 - '@smithy/util-utf8': 2.0.0 - fast-xml-parser: 4.2.5 - tslib: 2.6.2 + '@aws-sdk/client-sso@3.620.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.620.0 + '@aws-sdk/middleware-host-header': 3.620.0 + '@aws-sdk/middleware-logger': 3.609.0 + '@aws-sdk/middleware-recursion-detection': 3.620.0 + '@aws-sdk/middleware-user-agent': 3.620.0 + '@aws-sdk/region-config-resolver': 3.614.0 + '@aws-sdk/types': 3.609.0 + '@aws-sdk/util-endpoints': 3.614.0 + '@aws-sdk/util-user-agent-browser': 3.609.0 + '@aws-sdk/util-user-agent-node': 3.614.0 + '@smithy/config-resolver': 3.0.5 + '@smithy/core': 2.3.1 + '@smithy/fetch-http-handler': 3.2.4 + '@smithy/hash-node': 3.0.3 + '@smithy/invalid-dependency': 3.0.3 + '@smithy/middleware-content-length': 3.0.5 + '@smithy/middleware-endpoint': 3.1.0 + '@smithy/middleware-retry': 3.0.13 + '@smithy/middleware-serde': 3.0.3 + '@smithy/middleware-stack': 3.0.3 + '@smithy/node-config-provider': 3.1.4 + '@smithy/node-http-handler': 3.1.4 + '@smithy/protocol-http': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.13 + '@smithy/util-defaults-mode-node': 3.0.13 + '@smithy/util-endpoints': 2.0.5 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-retry': 3.0.3 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-env@3.410.0': - dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/property-provider': 2.0.9 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/client-sts@3.620.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.620.0(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/core': 3.620.0 + '@aws-sdk/credential-provider-node': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/middleware-host-header': 3.620.0 + '@aws-sdk/middleware-logger': 3.609.0 + '@aws-sdk/middleware-recursion-detection': 3.620.0 + '@aws-sdk/middleware-user-agent': 3.620.0 + '@aws-sdk/region-config-resolver': 3.614.0 + '@aws-sdk/types': 3.609.0 + '@aws-sdk/util-endpoints': 3.614.0 + '@aws-sdk/util-user-agent-browser': 3.609.0 + '@aws-sdk/util-user-agent-node': 3.614.0 + '@smithy/config-resolver': 3.0.5 + '@smithy/core': 2.3.1 + '@smithy/fetch-http-handler': 3.2.4 + '@smithy/hash-node': 3.0.3 + '@smithy/invalid-dependency': 3.0.3 + '@smithy/middleware-content-length': 3.0.5 + '@smithy/middleware-endpoint': 3.1.0 + '@smithy/middleware-retry': 3.0.13 + '@smithy/middleware-serde': 3.0.3 + '@smithy/middleware-stack': 3.0.3 + '@smithy/node-config-provider': 3.1.4 + '@smithy/node-http-handler': 3.1.4 + '@smithy/protocol-http': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.13 + '@smithy/util-defaults-mode-node': 3.0.13 + '@smithy/util-endpoints': 2.0.5 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-retry': 3.0.3 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + transitivePeerDependencies: + - aws-crt - '@aws-sdk/credential-provider-ini@3.410.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.410.0 - '@aws-sdk/credential-provider-process': 3.410.0 - '@aws-sdk/credential-provider-sso': 3.410.0 - '@aws-sdk/credential-provider-web-identity': 3.410.0 - '@aws-sdk/types': 3.410.0 - '@smithy/credential-provider-imds': 2.0.11 - '@smithy/property-provider': 2.0.9 - '@smithy/shared-ini-file-loader': 2.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/core@3.620.0': + dependencies: + '@smithy/core': 2.3.1 + '@smithy/protocol-http': 4.1.0 + '@smithy/signature-v4': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + fast-xml-parser: 4.2.5 + tslib: 2.6.3 + + '@aws-sdk/credential-provider-env@3.609.0': + dependencies: + '@aws-sdk/types': 3.609.0 + '@smithy/property-provider': 3.1.3 + '@smithy/types': 3.3.0 + tslib: 2.6.3 + + '@aws-sdk/credential-provider-http@3.620.0': + dependencies: + '@aws-sdk/types': 3.609.0 + '@smithy/fetch-http-handler': 3.2.4 + '@smithy/node-http-handler': 3.1.4 + '@smithy/property-provider': 3.1.3 + '@smithy/protocol-http': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/util-stream': 3.1.3 + tslib: 2.6.3 + + '@aws-sdk/credential-provider-ini@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)': + dependencies: + '@aws-sdk/client-sts': 3.620.0 + '@aws-sdk/credential-provider-env': 3.609.0 + '@aws-sdk/credential-provider-http': 3.620.0 + '@aws-sdk/credential-provider-process': 3.614.0 + '@aws-sdk/credential-provider-sso': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0)) + '@aws-sdk/credential-provider-web-identity': 3.609.0(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/types': 3.609.0 + '@smithy/credential-provider-imds': 3.2.0 + '@smithy/property-provider': 3.1.3 + '@smithy/shared-ini-file-loader': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-node@3.410.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.410.0 - '@aws-sdk/credential-provider-ini': 3.410.0 - '@aws-sdk/credential-provider-process': 3.410.0 - '@aws-sdk/credential-provider-sso': 3.410.0 - '@aws-sdk/credential-provider-web-identity': 3.410.0 - '@aws-sdk/types': 3.410.0 - '@smithy/credential-provider-imds': 2.0.11 - '@smithy/property-provider': 2.0.9 - '@smithy/shared-ini-file-loader': 2.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/credential-provider-node@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)': + dependencies: + '@aws-sdk/credential-provider-env': 3.609.0 + '@aws-sdk/credential-provider-http': 3.620.0 + '@aws-sdk/credential-provider-ini': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/credential-provider-process': 3.614.0 + '@aws-sdk/credential-provider-sso': 3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0)) + '@aws-sdk/credential-provider-web-identity': 3.609.0(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/types': 3.609.0 + '@smithy/credential-provider-imds': 3.2.0 + '@smithy/property-provider': 3.1.3 + '@smithy/shared-ini-file-loader': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' - aws-crt - '@aws-sdk/credential-provider-process@3.410.0': + '@aws-sdk/credential-provider-process@3.614.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/property-provider': 2.0.9 - '@smithy/shared-ini-file-loader': 2.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/property-provider': 3.1.3 + '@smithy/shared-ini-file-loader': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/credential-provider-sso@3.410.0': + '@aws-sdk/credential-provider-sso@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))': dependencies: - '@aws-sdk/client-sso': 3.410.0 - '@aws-sdk/token-providers': 3.410.0 - '@aws-sdk/types': 3.410.0 - '@smithy/property-provider': 2.0.9 - '@smithy/shared-ini-file-loader': 2.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/client-sso': 3.620.0 + '@aws-sdk/token-providers': 3.614.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0)) + '@aws-sdk/types': 3.609.0 + '@smithy/property-provider': 3.1.3 + '@smithy/shared-ini-file-loader': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-web-identity@3.410.0': + '@aws-sdk/credential-provider-web-identity@3.609.0(@aws-sdk/client-sts@3.620.0)': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/property-provider': 2.0.9 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/client-sts': 3.620.0 + '@aws-sdk/types': 3.609.0 + '@smithy/property-provider': 3.1.3 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/lib-storage@3.412.0(@aws-sdk/client-s3@3.412.0)': + '@aws-sdk/lib-storage@3.620.0(@aws-sdk/client-s3@3.620.0)': dependencies: - '@aws-sdk/client-s3': 3.412.0 - '@smithy/abort-controller': 2.0.14 - '@smithy/middleware-endpoint': 2.0.8 - '@smithy/smithy-client': 2.1.5 + '@aws-sdk/client-s3': 3.620.0 + '@smithy/abort-controller': 3.1.1 + '@smithy/middleware-endpoint': 3.1.0 + '@smithy/smithy-client': 3.1.11 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 - tslib: 2.6.2 + tslib: 2.6.3 + + '@aws-sdk/middleware-bucket-endpoint@3.620.0': + dependencies: + '@aws-sdk/types': 3.609.0 + '@aws-sdk/util-arn-parser': 3.568.0 + '@smithy/node-config-provider': 3.1.4 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + '@smithy/util-config-provider': 3.0.0 + tslib: 2.6.3 + + '@aws-sdk/middleware-expect-continue@3.620.0': + dependencies: + '@aws-sdk/types': 3.609.0 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 + + '@aws-sdk/middleware-flexible-checksums@3.620.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-sdk/types': 3.609.0 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 - '@aws-sdk/middleware-bucket-endpoint@3.410.0': + '@aws-sdk/middleware-host-header@3.620.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@aws-sdk/util-arn-parser': 3.310.0 - '@smithy/node-config-provider': 2.0.11 - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - '@smithy/util-config-provider': 2.0.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-expect-continue@3.410.0': + '@aws-sdk/middleware-location-constraint@3.609.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-flexible-checksums@3.410.0': + '@aws-sdk/middleware-logger@3.609.0': dependencies: - '@aws-crypto/crc32': 3.0.0 - '@aws-crypto/crc32c': 3.0.0 - '@aws-sdk/types': 3.410.0 - '@smithy/is-array-buffer': 2.0.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-host-header@3.410.0': + '@aws-sdk/middleware-recursion-detection@3.620.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-location-constraint@3.410.0': + '@aws-sdk/middleware-sdk-s3@3.620.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@aws-sdk/util-arn-parser': 3.568.0 + '@smithy/node-config-provider': 3.1.4 + '@smithy/protocol-http': 4.1.0 + '@smithy/signature-v4': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-stream': 3.1.3 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 - '@aws-sdk/middleware-logger@3.410.0': + '@aws-sdk/middleware-signing@3.620.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/property-provider': 3.1.3 + '@smithy/protocol-http': 4.1.0 + '@smithy/signature-v4': 4.1.0 + '@smithy/types': 3.3.0 + '@smithy/util-middleware': 3.0.3 + tslib: 2.6.3 - '@aws-sdk/middleware-recursion-detection@3.410.0': + '@aws-sdk/middleware-ssec@3.609.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-sdk-s3@3.410.0': + '@aws-sdk/middleware-user-agent@3.620.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@aws-sdk/util-arn-parser': 3.310.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@aws-sdk/util-endpoints': 3.614.0 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-sdk-sts@3.410.0': + '@aws-sdk/region-config-resolver@3.614.0': dependencies: - '@aws-sdk/middleware-signing': 3.410.0 - '@aws-sdk/types': 3.410.0 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/node-config-provider': 3.1.4 + '@smithy/types': 3.3.0 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.3 + tslib: 2.6.3 - '@aws-sdk/middleware-signing@3.410.0': + '@aws-sdk/signature-v4-multi-region@3.620.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/property-provider': 2.0.9 - '@smithy/protocol-http': 3.0.10 - '@smithy/signature-v4': 2.0.5 - '@smithy/types': 2.6.0 - '@smithy/util-middleware': 2.0.1 - tslib: 2.6.2 + '@aws-sdk/middleware-sdk-s3': 3.620.0 + '@aws-sdk/types': 3.609.0 + '@smithy/protocol-http': 4.1.0 + '@smithy/signature-v4': 4.1.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-ssec@3.410.0': + '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/client-sso-oidc': 3.620.0(@aws-sdk/client-sts@3.620.0) + '@aws-sdk/types': 3.609.0 + '@smithy/property-provider': 3.1.3 + '@smithy/shared-ini-file-loader': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/middleware-user-agent@3.410.0': + '@aws-sdk/types@3.609.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@aws-sdk/util-endpoints': 3.410.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/signature-v4-multi-region@3.412.0': + '@aws-sdk/util-arn-parser@3.568.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/protocol-http': 3.0.10 - '@smithy/signature-v4': 2.0.5 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.6.3 - '@aws-sdk/token-providers@3.410.0': - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/middleware-host-header': 3.410.0 - '@aws-sdk/middleware-logger': 3.410.0 - '@aws-sdk/middleware-recursion-detection': 3.410.0 - '@aws-sdk/middleware-user-agent': 3.410.0 - '@aws-sdk/types': 3.410.0 - '@aws-sdk/util-endpoints': 3.410.0 - '@aws-sdk/util-user-agent-browser': 3.410.0 - '@aws-sdk/util-user-agent-node': 3.410.0 - '@smithy/config-resolver': 2.0.9 - '@smithy/fetch-http-handler': 2.1.4 - '@smithy/hash-node': 2.0.8 - '@smithy/invalid-dependency': 2.0.8 - '@smithy/middleware-content-length': 2.0.10 - '@smithy/middleware-endpoint': 2.0.8 - '@smithy/middleware-retry': 2.0.11 - '@smithy/middleware-serde': 2.0.8 - '@smithy/middleware-stack': 2.0.1 - '@smithy/node-config-provider': 2.0.11 - '@smithy/node-http-handler': 2.5.0 - '@smithy/property-provider': 2.0.9 - '@smithy/protocol-http': 3.0.10 - '@smithy/shared-ini-file-loader': 2.0.10 - '@smithy/smithy-client': 2.1.5 - '@smithy/types': 2.6.0 - '@smithy/url-parser': 2.0.8 - '@smithy/util-base64': 2.0.0 - '@smithy/util-body-length-browser': 2.0.0 - '@smithy/util-body-length-node': 2.1.0 - '@smithy/util-defaults-mode-browser': 2.0.9 - '@smithy/util-defaults-mode-node': 2.0.11 - '@smithy/util-retry': 2.0.1 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.410.0': - dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 - - '@aws-sdk/types@3.413.0': - dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 - - '@aws-sdk/util-arn-parser@3.310.0': - dependencies: - tslib: 2.6.2 - - '@aws-sdk/util-endpoints@3.410.0': + '@aws-sdk/util-endpoints@3.614.0': dependencies: - '@aws-sdk/types': 3.410.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/types': 3.3.0 + '@smithy/util-endpoints': 2.0.5 + tslib: 2.6.3 '@aws-sdk/util-locate-window@3.208.0': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 - '@aws-sdk/util-user-agent-browser@3.410.0': + '@aws-sdk/util-user-agent-browser@3.609.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/types': 2.6.0 + '@aws-sdk/types': 3.609.0 + '@smithy/types': 3.3.0 bowser: 2.11.0 - tslib: 2.6.2 + tslib: 2.6.3 - '@aws-sdk/util-user-agent-node@3.410.0': + '@aws-sdk/util-user-agent-node@3.614.0': dependencies: - '@aws-sdk/types': 3.410.0 - '@smithy/node-config-provider': 2.0.11 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-sdk/types': 3.609.0 + '@smithy/node-config-provider': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@aws-sdk/util-utf8-browser@3.259.0': + '@aws-sdk/xml-builder@3.609.0': dependencies: - tslib: 2.6.2 - - '@aws-sdk/xml-builder@3.310.0': - dependencies: - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 '@babel/code-frame@7.23.5': dependencies: '@babel/highlight': 7.23.4 chalk: 2.4.2 + '@babel/code-frame@7.24.7': + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.0 + '@babel/compat-data@7.23.5': {} + '@babel/compat-data@7.24.7': {} + '@babel/core@7.23.5': dependencies: '@ampproject/remapping': 2.2.1 '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.5 + '@babel/generator': 7.24.7 '@babel/helper-compilation-targets': 7.22.15 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.5) '@babel/helpers': 7.23.5 - '@babel/parser': 7.23.9 + '@babel/parser': 7.24.7 '@babel/template': 7.22.15 '@babel/traverse': 7.23.5 - '@babel/types': 7.23.5 + '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/core@7.24.0': + '@babel/core@7.24.7': dependencies: '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) - '@babel/helpers': 7.24.0 - '@babel/parser': 7.24.5 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.7) + '@babel/helpers': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 convert-source-map: 2.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.23.5': + '@babel/generator@7.24.7': dependencies: - '@babel/types': 7.23.5 - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 - jsesc: 2.5.2 - - '@babel/generator@7.23.6': - dependencies: - '@babel/types': 7.24.0 - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 + '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 - '@babel/helper-annotate-as-pure@7.22.5': + '@babel/helper-annotate-as-pure@7.24.7': dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 - '@babel/helper-builder-binary-assignment-operator-visitor@7.22.15': + '@babel/helper-builder-binary-assignment-operator-visitor@7.24.7': dependencies: - '@babel/types': 7.24.0 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color '@babel/helper-compilation-targets@7.22.15': dependencies: @@ -11964,40 +12556,42 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-compilation-targets@7.23.6': + '@babel/helper-compilation-targets@7.24.7': dependencies: - '@babel/compat-data': 7.23.5 - '@babel/helper-validator-option': 7.23.5 + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 browserslist: 4.23.0 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.23.5(@babel/core@7.24.0)': - dependencies: - '@babel/core': 7.24.0 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-member-expression-to-functions': 7.23.0 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 + '@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.22.15(@babel/core@7.24.0)': + '@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-annotate-as-pure': 7.22.5 + '@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.4.3(@babel/core@7.24.0)': + '@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@8.1.1) + '@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: @@ -12005,22 +12599,45 @@ snapshots: '@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.0 - '@babel/types': 7.24.0 + '@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.0 + '@babel/types': 7.24.7 - '@babel/helper-member-expression-to-functions@7.23.0': + '@babel/helper-hoist-variables@7.24.7': dependencies: - '@babel/types': 7.24.0 + '@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.0 + '@babel/types': 7.24.7 + + '@babel/helper-module-imports@7.24.7': + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.5)': dependencies: @@ -12029,125 +12646,156 @@ snapshots: '@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.22.20 + '@babel/helper-validator-identifier': 7.24.7 - '@babel/helper-module-transforms@7.23.3(@babel/core@7.24.0)': + '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@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.22.20 + '@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.22.5': + '@babel/helper-optimise-call-expression@7.24.7': dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 '@babel/helper-plugin-utils@7.22.5': {} - '@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.0)': + '@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.0 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-wrap-function': 7.22.20 + '@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.22.20(@babel/core@7.24.0)': + '@babel/helper-replace-supers@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-member-expression-to-functions': 7.23.0 - '@babel/helper-optimise-call-expression': 7.22.5 + '@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.0 + '@babel/types': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers@7.22.5': + '@babel/helper-simple-access@7.24.7': dependencies: - '@babel/types': 7.24.0 + '@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.0 + '@babel/types': 7.24.7 - '@babel/helper-string-parser@7.23.4': {} + '@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.22.20': {} + '@babel/helper-validator-identifier@7.24.7': {} '@babel/helper-validator-option@7.23.5': {} - '@babel/helper-wrap-function@7.22.20': + '@babel/helper-validator-option@7.24.7': {} + + '@babel/helper-wrap-function@7.24.7': dependencies: - '@babel/helper-function-name': 7.23.0 - '@babel/template': 7.24.0 - '@babel/types': 7.24.0 + '@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.23.5 + '@babel/types': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/helpers@7.24.0': + '@babel/helpers@7.24.7': dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 - transitivePeerDependencies: - - supports-color + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 '@babel/highlight@7.23.4': dependencies: - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - '@babel/parser@7.23.9': + '@babel/highlight@7.24.7': dependencies: - '@babel/types': 7.24.0 + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 - '@babel/parser@7.24.0': + '@babel/parser@7.24.7': dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 - '@babel/parser@7.24.5': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/types': 7.24.0 + '@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.23.3(@babel/core@7.24.0)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.24.0) + '@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.23.3(@babel/core@7.24.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.22.5 + '@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.0)': + '@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.0 + '@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.0)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.5)': @@ -12155,54 +12803,60 @@ snapshots: '@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.0)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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.0)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@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.0)': + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-flow@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-flow@7.23.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-assertions@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-attributes@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@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.0)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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)': @@ -12210,9 +12864,9 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.23.5)': @@ -12220,9 +12874,9 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-jsx@7.23.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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)': @@ -12230,9 +12884,9 @@ snapshots: '@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.0)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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)': @@ -12240,9 +12894,9 @@ snapshots: '@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.0)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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)': @@ -12250,9 +12904,9 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.0)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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)': @@ -12260,9 +12914,9 @@ snapshots: '@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.0)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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)': @@ -12270,9 +12924,9 @@ snapshots: '@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.0)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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)': @@ -12280,24 +12934,24 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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.0)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@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.0)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.23.5)': @@ -12305,435 +12959,471 @@ snapshots: '@babel/core': 7.23.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-syntax-typescript@7.23.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@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.0)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-async-generator-functions@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.0) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.0) + '@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.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.0) + '@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.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-block-scoping@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-class-properties@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.0) + '@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.23.5(@babel/core@7.24.0)': + '@babel/plugin-transform-classes@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-optimise-call-expression': 7.22.5 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.0) - '@babel/helper-split-export-declaration': 7.22.6 + '@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.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/template': 7.24.0 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/template': 7.24.7 - '@babel/plugin-transform-destructuring@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-dotall-regex@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-dynamic-import@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.0) + '@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/plugin-transform-exponentiation-operator@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-export-namespace-from@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.0) + '@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/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-flow-strip-types@7.23.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.24.0) + '@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/plugin-transform-for-of@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@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/plugin-transform-function-name@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-function-name@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-plugin-utils': 7.22.5 + '@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/plugin-transform-json-strings@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.0) + '@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/plugin-transform-literals@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-literals@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-logical-assignment-operators@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.0) + '@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/plugin-transform-member-expression-literals@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-modules-amd@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-simple-access': 7.22.5 + '@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/plugin-transform-modules-systemjs@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-identifier': 7.22.20 + '@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 + transitivePeerDependencies: + - supports-color - '@babel/plugin-transform-modules-umd@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.0)': + '@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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-new-target@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.0) + '@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/plugin-transform-numeric-separator@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.0) + '@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/plugin-transform-object-rest-spread@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/compat-data': 7.23.5 - '@babel/core': 7.24.0 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.24.0) + '@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/plugin-transform-object-super@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.0) + '@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/plugin-transform-optional-catch-binding@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.0) + '@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/plugin-transform-optional-chaining@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-optional-chaining@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.0) + '@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/plugin-transform-parameters@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-private-methods@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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-private-property-in-object@7.23.4(@babel/core@7.24.0)': + '@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.0) + '@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/plugin-transform-property-literals@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-regenerator@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 regenerator-transform: 0.15.2 - '@babel/plugin-transform-reserved-words@7.23.3(@babel/core@7.24.0)': - dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-transform-shorthand-properties@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-spread@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - - '@babel/plugin-transform-sticky-regex@7.23.3(@babel/core@7.24.0)': - dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-transform-template-literals@7.23.3(@babel/core@7.24.0)': - dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - - '@babel/plugin-transform-typeof-symbol@7.23.3(@babel/core@7.24.0)': - dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-typescript@7.23.5(@babel/core@7.24.0)': + '@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-annotate-as-pure': 7.22.5 - '@babel/helper-create-class-features-plugin': 7.23.5(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.24.0) + '@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/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-unicode-property-regex@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-unicode-regex@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-typeof-symbol@7.24.7(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-unicode-sets-regex@7.23.3(@babel/core@7.24.0)': + '@babel/plugin-transform-typescript@7.23.5(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.0) - '@babel/helper-plugin-utils': 7.22.5 + '@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/preset-env@7.23.5(@babel/core@7.24.0)': - dependencies: - '@babel/compat-data': 7.23.5 - '@babel/core': 7.24.0 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.0) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-import-assertions': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-syntax-import-attributes': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.0) - '@babel/plugin-transform-arrow-functions': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-async-generator-functions': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-async-to-generator': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-block-scoped-functions': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-class-static-block': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-classes': 7.23.5(@babel/core@7.24.0) - '@babel/plugin-transform-computed-properties': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-destructuring': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-dotall-regex': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-duplicate-keys': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-dynamic-import': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-exponentiation-operator': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-export-namespace-from': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-for-of': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-function-name': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-json-strings': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-literals': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-logical-assignment-operators': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-member-expression-literals': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-modules-amd': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-modules-systemjs': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-modules-umd': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.0) - '@babel/plugin-transform-new-target': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-numeric-separator': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-object-rest-spread': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-object-super': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-optional-catch-binding': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-parameters': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-private-property-in-object': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-property-literals': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-regenerator': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-reserved-words': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-shorthand-properties': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-spread': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-sticky-regex': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-template-literals': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-typeof-symbol': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-unicode-escapes': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-unicode-property-regex': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-unicode-regex': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-unicode-sets-regex': 7.23.3(@babel/core@7.24.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.0) - babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.24.0) - babel-plugin-polyfill-corejs3: 0.8.6(@babel/core@7.24.0) - babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.24.0) - core-js-compat: 3.33.3 + '@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + + '@babel/plugin-transform-unicode-property-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-unicode-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-unicode-sets-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/preset-env@7.24.7(@babel/core@7.24.7)': + 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/preset-flow@7.23.3(@babel/core@7.24.0)': + '@babel/preset-flow@7.23.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.24.0) + '@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/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.0)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/types': 7.24.0 + '@babel/core': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/types': 7.24.7 esutils: 2.0.3 - '@babel/preset-typescript@7.23.3(@babel/core@7.24.0)': + '@babel/preset-typescript@7.23.3(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.23.5 - '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-typescript': 7.23.5(@babel/core@7.24.0) + '@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/register@7.22.15(@babel/core@7.24.0)': + '@babel/register@7.22.15(@babel/core@7.24.7)': dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 clone-deep: 4.0.1 find-cache-dir: 2.1.0 make-dir: 2.1.0 @@ -12749,77 +13439,77 @@ snapshots: '@babel/template@7.22.15': dependencies: '@babel/code-frame': 7.23.5 - '@babel/parser': 7.23.9 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 '@babel/template@7.24.0': dependencies: - '@babel/code-frame': 7.23.5 - '@babel/parser': 7.24.5 - '@babel/types': 7.24.0 + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + + '@babel/template@7.24.7': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 '@babel/traverse@7.23.5': dependencies: '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.5 + '@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/parser': 7.23.9 - '@babel/types': 7.23.5 - debug: 4.3.4(supports-color@8.1.1) + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.5(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/traverse@7.24.0': - dependencies: - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@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/parser': 7.24.5 - '@babel/types': 7.24.0 - debug: 4.3.4(supports-color@8.1.1) + '@babel/traverse@7.24.7': + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@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) globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/types@7.23.5': - dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 - to-fast-properties: 2.0.0 - - '@babel/types@7.24.0': + '@babel/types@7.24.7': dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 '@base2/pretty-print-object@1.0.1': {} '@bcoe/v8-coverage@0.2.3': {} - '@bull-board/api@5.17.0(@bull-board/ui@5.17.0)': + '@bull-board/api@5.21.1(@bull-board/ui@5.21.1)': dependencies: - '@bull-board/ui': 5.17.0 + '@bull-board/ui': 5.21.1 redis-info: 3.1.0 - '@bull-board/fastify@5.17.0': + '@bull-board/fastify@5.21.1': dependencies: - '@bull-board/api': 5.17.0(@bull-board/ui@5.17.0) - '@bull-board/ui': 5.17.0 + '@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 - ejs: 3.1.9 + ejs: 3.1.10 - '@bull-board/ui@5.17.0': + '@bull-board/ui@5.21.1': dependencies: - '@bull-board/api': 5.17.0(@bull-board/ui@5.17.0) + '@bull-board/api': 5.21.1(@bull-board/ui@5.21.1) '@bundled-es-modules/cookie@2.0.0': dependencies: @@ -12829,76 +13519,81 @@ snapshots: dependencies: statuses: 2.0.1 + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@canvas/image-data@1.0.0': {} '@colors/colors@1.5.0': optional: true - '@cropper/element-canvas@2.0.0-beta.5': + '@cropper/element-canvas@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@cropper/element': 2.0.0-rc.1 + '@cropper/utils': 2.0.0-rc.1 - '@cropper/element-crosshair@2.0.0-beta.5': + '@cropper/element-crosshair@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@cropper/element': 2.0.0-rc.1 + '@cropper/utils': 2.0.0-rc.1 - '@cropper/element-grid@2.0.0-beta.5': + '@cropper/element-grid@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@cropper/element': 2.0.0-rc.1 + '@cropper/utils': 2.0.0-rc.1 - '@cropper/element-handle@2.0.0-beta.5': + '@cropper/element-handle@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@cropper/element': 2.0.0-rc.1 + '@cropper/utils': 2.0.0-rc.1 - '@cropper/element-image@2.0.0-beta.5': + '@cropper/element-image@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/element-canvas': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@cropper/element': 2.0.0-rc.1 + '@cropper/element-canvas': 2.0.0-rc.1 + '@cropper/utils': 2.0.0-rc.1 - '@cropper/element-selection@2.0.0-beta.5': + '@cropper/element-selection@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/element-canvas': 2.0.0-beta.5 - '@cropper/element-image': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@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-shade@2.0.0-beta.5': + '@cropper/element-shade@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/element-canvas': 2.0.0-beta.5 - '@cropper/element-selection': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@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-viewer@2.0.0-beta.5': + '@cropper/element-viewer@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/element-canvas': 2.0.0-beta.5 - '@cropper/element-image': 2.0.0-beta.5 - '@cropper/element-selection': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@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-beta.5': + '@cropper/element@2.0.0-rc.1': dependencies: - '@cropper/utils': 2.0.0-beta.5 + '@cropper/utils': 2.0.0-rc.1 - '@cropper/elements@2.0.0-beta.5': + '@cropper/elements@2.0.0-rc.1': dependencies: - '@cropper/element': 2.0.0-beta.5 - '@cropper/element-canvas': 2.0.0-beta.5 - '@cropper/element-crosshair': 2.0.0-beta.5 - '@cropper/element-grid': 2.0.0-beta.5 - '@cropper/element-handle': 2.0.0-beta.5 - '@cropper/element-image': 2.0.0-beta.5 - '@cropper/element-selection': 2.0.0-beta.5 - '@cropper/element-shade': 2.0.0-beta.5 - '@cropper/element-viewer': 2.0.0-beta.5 + '@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/utils@2.0.0-beta.5': {} + '@cropper/utils@2.0.0-rc.1': {} '@cypress/request@3.0.0': dependencies: @@ -12917,7 +13612,7 @@ snapshots: performance-now: 2.1.0 qs: 6.10.4 safe-buffer: 5.2.1 - tough-cookie: 4.1.3 + tough-cookie: 4.1.4 tunnel-agent: 0.6.0 uuid: 8.3.2 @@ -12928,10 +13623,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@3.2.1)': + '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@4.0.0)': dependencies: ky: 0.33.3 - ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@3.2.1) + ky-universal: 0.11.0(ky@0.33.3)(web-streams-polyfill@4.0.0) undici: 5.28.2 transitivePeerDependencies: - web-streams-polyfill @@ -12947,7 +13642,7 @@ snapshots: '@emnapi/runtime@1.1.1': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 optional: true '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.3.1)': @@ -12957,7 +13652,10 @@ snapshots: '@esbuild/aix-ppc64@0.19.11': optional: true - '@esbuild/aix-ppc64@0.20.2': + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.23.0': optional: true '@esbuild/android-arm64@0.18.20': @@ -12966,7 +13664,10 @@ snapshots: '@esbuild/android-arm64@0.19.11': optional: true - '@esbuild/android-arm64@0.20.2': + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.23.0': optional: true '@esbuild/android-arm@0.18.20': @@ -12975,7 +13676,10 @@ snapshots: '@esbuild/android-arm@0.19.11': optional: true - '@esbuild/android-arm@0.20.2': + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.23.0': optional: true '@esbuild/android-x64@0.18.20': @@ -12984,7 +13688,10 @@ snapshots: '@esbuild/android-x64@0.19.11': optional: true - '@esbuild/android-x64@0.20.2': + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.23.0': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -12993,7 +13700,10 @@ snapshots: '@esbuild/darwin-arm64@0.19.11': optional: true - '@esbuild/darwin-arm64@0.20.2': + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.23.0': optional: true '@esbuild/darwin-x64@0.18.20': @@ -13002,7 +13712,10 @@ snapshots: '@esbuild/darwin-x64@0.19.11': optional: true - '@esbuild/darwin-x64@0.20.2': + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.23.0': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -13011,7 +13724,10 @@ snapshots: '@esbuild/freebsd-arm64@0.19.11': optional: true - '@esbuild/freebsd-arm64@0.20.2': + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.23.0': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -13020,7 +13736,10 @@ snapshots: '@esbuild/freebsd-x64@0.19.11': optional: true - '@esbuild/freebsd-x64@0.20.2': + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.23.0': optional: true '@esbuild/linux-arm64@0.18.20': @@ -13029,7 +13748,10 @@ snapshots: '@esbuild/linux-arm64@0.19.11': optional: true - '@esbuild/linux-arm64@0.20.2': + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.23.0': optional: true '@esbuild/linux-arm@0.18.20': @@ -13038,7 +13760,10 @@ snapshots: '@esbuild/linux-arm@0.19.11': optional: true - '@esbuild/linux-arm@0.20.2': + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.23.0': optional: true '@esbuild/linux-ia32@0.18.20': @@ -13047,7 +13772,10 @@ snapshots: '@esbuild/linux-ia32@0.19.11': optional: true - '@esbuild/linux-ia32@0.20.2': + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.23.0': optional: true '@esbuild/linux-loong64@0.18.20': @@ -13056,7 +13784,10 @@ snapshots: '@esbuild/linux-loong64@0.19.11': optional: true - '@esbuild/linux-loong64@0.20.2': + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.23.0': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -13065,7 +13796,10 @@ snapshots: '@esbuild/linux-mips64el@0.19.11': optional: true - '@esbuild/linux-mips64el@0.20.2': + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.23.0': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -13074,7 +13808,10 @@ snapshots: '@esbuild/linux-ppc64@0.19.11': optional: true - '@esbuild/linux-ppc64@0.20.2': + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.23.0': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -13083,7 +13820,10 @@ snapshots: '@esbuild/linux-riscv64@0.19.11': optional: true - '@esbuild/linux-riscv64@0.20.2': + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.23.0': optional: true '@esbuild/linux-s390x@0.18.20': @@ -13092,7 +13832,10 @@ snapshots: '@esbuild/linux-s390x@0.19.11': optional: true - '@esbuild/linux-s390x@0.20.2': + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.23.0': optional: true '@esbuild/linux-x64@0.18.20': @@ -13101,7 +13844,10 @@ snapshots: '@esbuild/linux-x64@0.19.11': optional: true - '@esbuild/linux-x64@0.20.2': + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.23.0': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -13110,7 +13856,13 @@ snapshots: '@esbuild/netbsd-x64@0.19.11': optional: true - '@esbuild/netbsd-x64@0.20.2': + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.23.0': + optional: true + + '@esbuild/openbsd-arm64@0.23.0': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -13119,7 +13871,10 @@ snapshots: '@esbuild/openbsd-x64@0.19.11': optional: true - '@esbuild/openbsd-x64@0.20.2': + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.23.0': optional: true '@esbuild/sunos-x64@0.18.20': @@ -13128,7 +13883,10 @@ snapshots: '@esbuild/sunos-x64@0.19.11': optional: true - '@esbuild/sunos-x64@0.20.2': + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.23.0': optional: true '@esbuild/win32-arm64@0.18.20': @@ -13137,7 +13895,10 @@ snapshots: '@esbuild/win32-arm64@0.19.11': optional: true - '@esbuild/win32-arm64@0.20.2': + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.23.0': optional: true '@esbuild/win32-ia32@0.18.20': @@ -13146,7 +13907,10 @@ snapshots: '@esbuild/win32-ia32@0.19.11': optional: true - '@esbuild/win32-ia32@0.20.2': + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.23.0': optional: true '@esbuild/win32-x64@0.18.20': @@ -13155,25 +13919,40 @@ snapshots: '@esbuild/win32-x64@0.19.11': optional: true - '@esbuild/win32-x64@0.20.2': + '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@8.53.0)': - dependencies: - eslint: 8.53.0 - eslint-visitor-keys: 3.4.3 + '@esbuild/win32-x64@0.23.0': + optional: true '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.10.0': {} + '@eslint-community/eslint-utils@4.4.0(eslint@9.8.0)': + dependencies: + eslint: 9.8.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.11.0': {} + + '@eslint-community/regexpp@4.6.2': {} + + '@eslint/compat@1.1.1': {} + + '@eslint/config-array@0.17.1': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.5(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -13184,10 +13963,26 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@8.53.0': {} + '@eslint/eslintrc@3.1.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.5(supports-color@8.1.1) + espree: 10.1.0 + globals: 14.0.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color '@eslint/js@8.57.0': {} + '@eslint/js@9.8.0': {} + + '@eslint/object-schema@2.1.4': {} + '@fal-works/esbuild-plugin-global-externals@2.1.2': {} '@fastify/accept-negotiator@1.0.0': {} @@ -13199,8 +13994,8 @@ snapshots: '@fastify/ajv-compiler@3.5.0': dependencies: - ajv: 8.13.0 - ajv-formats: 2.1.1(ajv@8.13.0) + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) fast-uri: 2.2.0 '@fastify/busboy@1.2.1': @@ -13239,12 +14034,12 @@ snapshots: '@fastify/reply-from': 9.0.1 fast-querystring: 1.1.2 fastify-plugin: 4.5.0 - ws: 8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - utf-8-validate - '@fastify/multipart@8.2.0': + '@fastify/multipart@8.3.0': dependencies: '@fastify/busboy': 2.1.0 '@fastify/deepmerge': 1.3.0 @@ -13280,14 +14075,14 @@ snapshots: glob: 8.1.0 p-limit: 3.1.0 - '@fastify/static@7.0.3': + '@fastify/static@7.0.4': dependencies: '@fastify/accept-negotiator': 1.0.0 '@fastify/send': 2.0.1 content-disposition: 0.5.4 fastify-plugin: 4.5.0 fastq: 1.17.1 - glob: 10.3.12 + glob: 10.4.2 '@fastify/view@8.2.0': dependencies: @@ -13303,13 +14098,11 @@ snapshots: '@hapi/boom@10.0.1': dependencies: - '@hapi/hoek': 11.0.2 + '@hapi/hoek': 11.0.4 '@hapi/bourne@3.0.0': {} - '@hapi/hoek@10.0.1': {} - - '@hapi/hoek@11.0.2': {} + '@hapi/hoek@11.0.4': {} '@hapi/hoek@9.3.0': {} @@ -13321,22 +14114,14 @@ snapshots: dependencies: '@hapi/boom': 10.0.1 '@hapi/bourne': 3.0.0 - '@hapi/hoek': 11.0.2 + '@hapi/hoek': 11.0.4 '@hexagon/base64@1.1.27': {} - '@humanwhocodes/config-array@0.11.13': - dependencies: - '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4(supports-color@8.1.1) - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - '@humanwhocodes/config-array@0.11.14': dependencies: - '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4(supports-color@8.1.1) + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.3.5(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13345,16 +14130,16 @@ snapshots: '@humanwhocodes/momoa@2.0.4': {} - '@humanwhocodes/object-schema@2.0.1': {} + '@humanwhocodes/object-schema@2.0.3': {} - '@humanwhocodes/object-schema@2.0.2': {} + '@humanwhocodes/retry@0.3.0': {} - '@img/sharp-darwin-arm64@0.33.3': + '@img/sharp-darwin-arm64@0.33.4': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.2 optional: true - '@img/sharp-darwin-x64@0.33.3': + '@img/sharp-darwin-x64@0.33.4': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.2 optional: true @@ -13383,45 +14168,45 @@ snapshots: '@img/sharp-libvips-linuxmusl-x64@1.0.2': optional: true - '@img/sharp-linux-arm64@0.33.3': + '@img/sharp-linux-arm64@0.33.4': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.2 optional: true - '@img/sharp-linux-arm@0.33.3': + '@img/sharp-linux-arm@0.33.4': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.2 optional: true - '@img/sharp-linux-s390x@0.33.3': + '@img/sharp-linux-s390x@0.33.4': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.2 optional: true - '@img/sharp-linux-x64@0.33.3': + '@img/sharp-linux-x64@0.33.4': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.2 optional: true - '@img/sharp-linuxmusl-arm64@0.33.3': + '@img/sharp-linuxmusl-arm64@0.33.4': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 optional: true - '@img/sharp-linuxmusl-x64@0.33.3': + '@img/sharp-linuxmusl-x64@0.33.4': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.2 optional: true - '@img/sharp-wasm32@0.33.3': + '@img/sharp-wasm32@0.33.4': dependencies: '@emnapi/runtime': 1.1.1 optional: true - '@img/sharp-win32-ia32@0.33.3': + '@img/sharp-win32-ia32@0.33.4': optional: true - '@img/sharp-win32-x64@0.33.3': + '@img/sharp-win32-x64@0.33.4': optional: true '@inquirer/confirm@3.1.6': @@ -13434,7 +14219,7 @@ snapshots: '@inquirer/figures': 1.0.1 '@inquirer/type': 1.3.1 '@types/mute-stream': 0.0.4 - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/wrap-ansi': 3.0.0 ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -13457,7 +14242,7 @@ snapshots: '@intlify/message-compiler@9.13.1': dependencies: '@intlify/shared': 9.13.1 - source-map-js: 1.0.2 + source-map-js: 1.2.0 '@intlify/shared@9.13.1': {} @@ -13485,7 +14270,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -13498,14 +14283,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.12.7) + jest-config: 29.7.0(@types/node@20.14.12) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -13517,7 +14302,7 @@ snapshots: jest-util: 29.7.0 jest-validate: 29.7.0 jest-watcher: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 pretty-format: 29.7.0 slash: 3.0.0 strip-ansi: 6.0.1 @@ -13534,7 +14319,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -13552,7 +14337,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.12.7 + '@types/node': 20.14.12 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -13574,7 +14359,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 20.12.7 + '@types/node': 20.14.12 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -13621,7 +14406,7 @@ snapshots: '@jest/transform@29.7.0': dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 babel-plugin-istanbul: 6.1.1 @@ -13632,7 +14417,7 @@ snapshots: jest-haste-map: 29.7.0 jest-regex-util: 29.6.3 jest-util: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 pirates: 4.0.5 slash: 3.0.0 write-file-atomic: 4.0.2 @@ -13644,19 +14429,19 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/yargs': 17.0.19 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))': + '@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))': 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.4.5) - vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) + 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) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 '@jridgewell/gen-mapping@0.3.2': dependencies: @@ -13664,14 +14449,22 @@ snapshots: '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.0': {} '@jridgewell/set-array@1.1.2': {} - '@jridgewell/source-map@0.3.5': + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': dependencies: - '@jridgewell/gen-mapping': 0.3.2 - '@jridgewell/trace-mapping': 0.3.18 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/sourcemap-codec@1.4.14': {} @@ -13682,6 +14475,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jsdevtools/ono@7.1.3': {} '@kurkle/color@0.3.2': {} @@ -13704,23 +14502,23 @@ snapshots: '@types/react': 18.0.28 react: 18.3.1 - '@microsoft/api-extractor-model@7.28.14(@types/node@20.12.7)': + '@microsoft/api-extractor-model@7.29.4(@types/node@20.14.12)': dependencies: - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.1.0(@types/node@20.12.7) + '@microsoft/tsdoc': 0.15.0 + '@microsoft/tsdoc-config': 0.17.0 + '@rushstack/node-core-library': 5.5.1(@types/node@20.14.12) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.43.1(@types/node@20.12.7)': + '@microsoft/api-extractor@7.47.4(@types/node@20.14.12)': dependencies: - '@microsoft/api-extractor-model': 7.28.14(@types/node@20.12.7) - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 4.1.0(@types/node@20.12.7) - '@rushstack/rig-package': 0.5.2 - '@rushstack/terminal': 0.10.1(@types/node@20.12.7) - '@rushstack/ts-command-line': 4.19.2(@types/node@20.12.7) + '@microsoft/api-extractor-model': 7.29.4(@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/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) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -13730,43 +14528,31 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@microsoft/tsdoc-config@0.16.2': + '@microsoft/tsdoc-config@0.17.0': dependencies: - '@microsoft/tsdoc': 0.14.2 - ajv: 6.12.6 + '@microsoft/tsdoc': 0.15.0 + ajv: 8.12.0 jju: 1.4.0 - resolve: 1.19.0 + resolve: 1.22.8 - '@microsoft/tsdoc@0.14.2': {} + '@microsoft/tsdoc@0.15.0': {} '@misskey-dev/browser-image-resizer@2024.1.0': {} - '@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3))(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0))(eslint@8.53.0)': - dependencies: - '@typescript-eslint/eslint-plugin': 6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3) - '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) - eslint: 8.53.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0) - - '@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0))(eslint@8.57.0)': - dependencies: - '@typescript-eslint/eslint-plugin': 7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) - '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3) - eslint: 8.57.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0) - - '@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint@8.57.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)': dependencies: - '@typescript-eslint/eslint-plugin': 7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) + '@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: 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 '@misskey-dev/sharp-read-bmp@1.2.0': dependencies: decode-bmp: 0.2.1 decode-ico: 0.4.1 - sharp: 0.33.3 + sharp: 0.33.4 '@misskey-dev/summaly@5.1.0': dependencies: @@ -13808,9 +14594,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2': optional: true - '@mswjs/cookies@1.1.0': {} - - '@mswjs/interceptors@0.26.15': + '@mswjs/interceptors@0.29.1': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -13819,94 +14603,90 @@ snapshots: outvariant: 1.4.2 strict-event-emitter: 0.5.1 - '@napi-rs/canvas-android-arm64@0.1.52': + '@napi-rs/canvas-android-arm64@0.1.53': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.52': + '@napi-rs/canvas-darwin-arm64@0.1.53': optional: true - '@napi-rs/canvas-darwin-x64@0.1.52': + '@napi-rs/canvas-darwin-x64@0.1.53': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.52': + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.53': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.52': + '@napi-rs/canvas-linux-arm64-gnu@0.1.53': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.52': + '@napi-rs/canvas-linux-arm64-musl@0.1.53': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.52': + '@napi-rs/canvas-linux-x64-gnu@0.1.53': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.52': + '@napi-rs/canvas-linux-x64-musl@0.1.53': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.52': + '@napi-rs/canvas-win32-x64-msvc@0.1.53': optional: true - '@napi-rs/canvas@0.1.52': + '@napi-rs/canvas@0.1.53': optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.52 - '@napi-rs/canvas-darwin-arm64': 0.1.52 - '@napi-rs/canvas-darwin-x64': 0.1.52 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.52 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.52 - '@napi-rs/canvas-linux-arm64-musl': 0.1.52 - '@napi-rs/canvas-linux-x64-gnu': 0.1.52 - '@napi-rs/canvas-linux-x64-musl': 0.1.52 - '@napi-rs/canvas-win32-x64-msvc': 0.1.52 - - '@ndelangen/get-tarball@3.0.7': - dependencies: - gunzip-maybe: 1.4.2 - pump: 3.0.0 - tar-fs: 2.1.1 - - '@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@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)': dependencies: iterare: 1.2.1 reflect-metadata: 0.2.2 rxjs: 7.8.1 - tslib: 2.6.2 + tslib: 2.6.3 uid: 2.0.2 - '@nestjs/core@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(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)': dependencies: - '@nestjs/common': 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/common': 10.3.10(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 reflect-metadata: 0.2.2 rxjs: 7.8.1 - tslib: 2.6.2 + tslib: 2.6.3 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8) + '@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) transitivePeerDependencies: - encoding - '@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)': + '@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)': dependencies: - '@nestjs/common': 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@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 cors: 2.8.5 express: 4.19.2 multer: 1.4.4-lts.1 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - supports-color - '@nestjs/testing@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8))': + '@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))': dependencies: - '@nestjs/common': 10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) - tslib: 2.6.2 + '@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 optionalDependencies: - '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8) + '@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) + + '@noble/hashes@1.4.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -13918,13 +14698,13 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 + fastq: 1.17.1 '@npmcli/agent@2.2.0': dependencies: agent-base: 7.1.0 - http-proxy-agent: 7.0.0 - https-proxy-agent: 7.0.2 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 lru-cache: 10.2.2 socks-proxy-agent: 8.0.2 transitivePeerDependencies: @@ -13953,155 +14733,167 @@ snapshots: '@open-draft/until@2.1.0': {} - '@opentelemetry/api-logs@0.51.1': + '@opentelemetry/api-logs@0.52.1': dependencies: - '@opentelemetry/api': 1.8.0 + '@opentelemetry/api': 1.9.0 - '@opentelemetry/api@1.8.0': {} + '@opentelemetry/api@1.9.0': {} - '@opentelemetry/context-async-hooks@1.24.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/context-async-hooks@1.25.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 + '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/core@1.24.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 + '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.24.1 - '@opentelemetry/instrumentation-connect@0.36.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.25.1 + + '@opentelemetry/instrumentation-connect@0.38.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 '@types/connect': 3.4.36 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-express@0.39.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-express@0.41.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-fastify@0.36.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-fastify@0.38.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-graphql@0.40.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-graphql@0.42.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-hapi@0.38.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-hapi@0.40.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-http@0.51.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-http@0.52.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 semver: 7.6.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.40.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-ioredis@0.42.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-koa@0.40.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-koa@0.42.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 - '@types/koa': 2.14.0 - '@types/koa__router': 12.0.3 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongodb@0.43.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-mongodb@0.46.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-metrics': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.24.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mongoose@0.38.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-mongoose@0.40.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql2@0.38.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-mysql2@0.40.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 - '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-mysql@0.38.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-mysql@0.40.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 '@types/mysql': 2.15.22 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.37.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-nestjs-core@0.39.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.41.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-pg@0.43.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 - '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) '@types/pg': 8.6.1 '@types/pg-pool': 2.0.4 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.43.0(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation-redis-4@0.41.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.25.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.46.0(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 + '@opentelemetry/api': 1.9.0 '@types/shimmer': 1.0.5 - import-in-the-middle: 1.4.2 + import-in-the-middle: 1.7.1 require-in-the-middle: 7.3.0 semver: 7.6.0 shimmer: 1.2.1 @@ -14109,12 +14901,12 @@ snapshots: - supports-color optional: true - '@opentelemetry/instrumentation@0.51.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/api-logs': 0.51.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.52.1 '@types/shimmer': 1.0.5 - import-in-the-middle: 1.7.4 + import-in-the-middle: 1.10.0 require-in-the-middle: 7.3.0 semver: 7.6.0 shimmer: 1.2.1 @@ -14123,58 +14915,66 @@ snapshots: '@opentelemetry/redis-common@0.36.2': {} - '@opentelemetry/resources@1.24.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/resources@1.24.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.24.1 - '@opentelemetry/sdk-metrics@1.24.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 + + '@opentelemetry/sdk-metrics@1.24.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.9.0) lodash.merge: 4.6.2 - '@opentelemetry/sdk-trace-base@1.24.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 '@opentelemetry/semantic-conventions@1.24.1': {} - '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.8.0)': + '@opentelemetry/semantic-conventions@1.25.1': {} + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@peculiar/asn1-android@2.3.10': dependencies: '@peculiar/asn1-schema': 2.3.8 asn1js: 3.0.5 - tslib: 2.6.2 + tslib: 2.6.3 '@peculiar/asn1-ecc@2.3.8': dependencies: '@peculiar/asn1-schema': 2.3.8 '@peculiar/asn1-x509': 2.3.8 asn1js: 3.0.5 - tslib: 2.6.2 + tslib: 2.6.3 '@peculiar/asn1-rsa@2.3.8': dependencies: '@peculiar/asn1-schema': 2.3.8 '@peculiar/asn1-x509': 2.3.8 asn1js: 3.0.5 - tslib: 2.6.2 + tslib: 2.6.3 '@peculiar/asn1-schema@2.3.8': dependencies: asn1js: 3.0.5 pvtsutils: 1.3.5 - tslib: 2.6.2 + tslib: 2.6.3 '@peculiar/asn1-x509@2.3.8': dependencies: @@ -14182,7 +14982,7 @@ snapshots: asn1js: 3.0.5 ipaddr.js: 2.2.0 pvtsutils: 1.3.5 - tslib: 2.6.2 + tslib: 2.6.3 '@peertube/http-signature@1.7.0': dependencies: @@ -14192,40 +14992,25 @@ snapshots: '@phc/format@1.0.0': {} - '@phosphor-icons/web@2.1.1': {} - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@prisma/instrumentation@5.14.0': - dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) - transitivePeerDependencies: - - supports-color - - '@radix-ui/react-compose-refs@1.0.1(@types/react@18.0.28)(react@18.3.1)': - dependencies: - '@babel/runtime': 7.23.4 - react: 18.3.1 - optionalDependencies: - '@types/react': 18.0.28 - - '@radix-ui/react-slot@1.0.2(@types/react@18.0.28)(react@18.3.1)': + '@phosphor-icons/web@2.1.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@prisma/instrumentation@5.17.0': dependencies: - '@babel/runtime': 7.23.4 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.0.28)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.0.28 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color - '@readme/better-ajv-errors@1.6.0(ajv@8.13.0)': + '@readme/better-ajv-errors@1.6.0(ajv@8.17.1)': dependencies: '@babel/code-frame': 7.23.5 '@babel/runtime': 7.23.4 '@humanwhocodes/momoa': 2.0.4 - ajv: 8.13.0 + ajv: 8.17.1 chalk: 4.1.2 json-to-ast: 2.1.0 jsonpointer: 5.0.1 @@ -14243,181 +15028,189 @@ snapshots: '@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.13.0) + '@readme/better-ajv-errors': 1.6.0(ajv@8.17.1) '@readme/json-schema-ref-parser': 1.2.0 - ajv: 8.13.0 - ajv-draft-04: 1.0.0(ajv@8.13.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.17.2)': + '@rollup/plugin-json@6.1.0(rollup@4.19.1)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.17.2) + '@rollup/pluginutils': 5.1.0(rollup@4.19.1) optionalDependencies: - rollup: 4.17.2 + rollup: 4.19.1 - '@rollup/plugin-replace@5.0.5(rollup@4.17.2)': + '@rollup/plugin-replace@5.0.7(rollup@4.19.1)': dependencies: - '@rollup/pluginutils': 5.1.0(rollup@4.17.2) - magic-string: 0.30.7 + '@rollup/pluginutils': 5.1.0(rollup@4.19.1) + magic-string: 0.30.10 optionalDependencies: - rollup: 4.17.2 + rollup: 4.19.1 - '@rollup/pluginutils@5.1.0(rollup@4.17.2)': + '@rollup/pluginutils@5.1.0(rollup@4.19.1)': dependencies: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 4.17.2 + rollup: 4.19.1 - '@rollup/rollup-android-arm-eabi@4.17.2': + '@rollup/rollup-android-arm-eabi@4.19.1': optional: true - '@rollup/rollup-android-arm64@4.17.2': + '@rollup/rollup-android-arm64@4.19.1': optional: true - '@rollup/rollup-darwin-arm64@4.17.2': + '@rollup/rollup-darwin-arm64@4.19.1': optional: true - '@rollup/rollup-darwin-x64@4.17.2': + '@rollup/rollup-darwin-x64@4.19.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.17.2': + '@rollup/rollup-linux-arm-gnueabihf@4.19.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.17.2': + '@rollup/rollup-linux-arm-musleabihf@4.19.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.17.2': + '@rollup/rollup-linux-arm64-gnu@4.19.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.17.2': + '@rollup/rollup-linux-arm64-musl@4.19.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.17.2': + '@rollup/rollup-linux-powerpc64le-gnu@4.19.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.17.2': + '@rollup/rollup-linux-riscv64-gnu@4.19.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.17.2': + '@rollup/rollup-linux-s390x-gnu@4.19.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.17.2': + '@rollup/rollup-linux-x64-gnu@4.19.1': optional: true - '@rollup/rollup-linux-x64-musl@4.17.2': + '@rollup/rollup-linux-x64-musl@4.19.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.17.2': + '@rollup/rollup-win32-arm64-msvc@4.19.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.17.2': + '@rollup/rollup-win32-ia32-msvc@4.19.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.17.2': + '@rollup/rollup-win32-x64-msvc@4.19.1': optional: true - '@rushstack/node-core-library@4.1.0(@types/node@20.12.7)': + '@rushstack/node-core-library@5.5.1(@types/node@20.14.12)': dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) fs-extra: 7.0.1 import-lazy: 4.0.0 jju: 1.4.0 resolve: 1.22.8 semver: 7.5.4 - z-schema: 5.0.5 optionalDependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 - '@rushstack/rig-package@0.5.2': + '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.8 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.10.1(@types/node@20.12.7)': + '@rushstack/terminal@0.13.3(@types/node@20.14.12)': dependencies: - '@rushstack/node-core-library': 4.1.0(@types/node@20.12.7) + '@rushstack/node-core-library': 5.5.1(@types/node@20.14.12) supports-color: 8.1.1 optionalDependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 - '@rushstack/ts-command-line@4.19.2(@types/node@20.12.7)': + '@rushstack/ts-command-line@4.22.3(@types/node@20.14.12)': dependencies: - '@rushstack/terminal': 0.10.1(@types/node@20.12.7) + '@rushstack/terminal': 0.13.3(@types/node@20.14.12) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.1 transitivePeerDependencies: - '@types/node' - '@sentry/core@8.5.0': - dependencies: - '@sentry/types': 8.5.0 - '@sentry/utils': 8.5.0 - - '@sentry/node@8.5.0': - dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/context-async-hooks': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-connect': 0.36.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-express': 0.39.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-fastify': 0.36.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-graphql': 0.40.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-hapi': 0.38.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-http': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-ioredis': 0.40.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-koa': 0.40.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mongodb': 0.43.0(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mongoose': 0.38.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mysql': 0.38.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-mysql2': 0.38.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-nestjs-core': 0.37.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation-pg': 0.41.0(@opentelemetry/api@1.8.0) - '@opentelemetry/resources': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 - '@prisma/instrumentation': 5.14.0 - '@sentry/core': 8.5.0 - '@sentry/opentelemetry': 8.5.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/instrumentation@0.51.1(@opentelemetry/api@1.8.0))(@opentelemetry/sdk-trace-base@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/semantic-conventions@1.24.1) - '@sentry/types': 8.5.0 - '@sentry/utils': 8.5.0 + '@sec-ant/readable-stream@0.4.1': {} + + '@sentry/core@8.20.0': + dependencies: + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 + + '@sentry/node@8.20.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.38.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.38.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.42.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.40.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.39.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.43.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.41.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 + '@prisma/instrumentation': 5.17.0 + '@sentry/core': 8.20.0 + '@sentry/opentelemetry': 8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1) + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 + import-in-the-middle: 1.10.0 optionalDependencies: - opentelemetry-instrumentation-fetch-node: 1.2.0 + opentelemetry-instrumentation-fetch-node: 1.2.3(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color - '@sentry/opentelemetry@8.5.0(@opentelemetry/api@1.8.0)(@opentelemetry/core@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/instrumentation@0.51.1(@opentelemetry/api@1.8.0))(@opentelemetry/sdk-trace-base@1.24.1(@opentelemetry/api@1.8.0))(@opentelemetry/semantic-conventions@1.24.1)': + '@sentry/opentelemetry@8.20.0(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.52.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.25.1)': dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/core': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.8.0) - '@opentelemetry/sdk-trace-base': 1.24.1(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 - '@sentry/core': 8.5.0 - '@sentry/types': 8.5.0 - '@sentry/utils': 8.5.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 + '@sentry/core': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 - '@sentry/profiling-node@8.5.0': + '@sentry/profiling-node@8.20.0': dependencies: - '@sentry/core': 8.5.0 - '@sentry/node': 8.5.0 - '@sentry/types': 8.5.0 - '@sentry/utils': 8.5.0 + '@sentry/core': 8.20.0 + '@sentry/node': 8.20.0 + '@sentry/types': 8.20.0 + '@sentry/utils': 8.20.0 detect-libc: 2.0.3 node-abi: 3.62.0 transitivePeerDependencies: - supports-color - '@sentry/types@8.5.0': {} + '@sentry/types@8.20.0': {} - '@sentry/utils@8.5.0': + '@sentry/utils@8.20.0': dependencies: - '@sentry/types': 8.5.0 + '@sentry/types': 8.20.0 - '@shikijs/core@1.4.0': {} + '@shikijs/core@1.12.0': + dependencies: + '@types/hast': 3.0.4 '@sideway/address@4.1.4': dependencies: @@ -14427,7 +15220,7 @@ snapshots: '@sideway/pinpoint@2.0.0': {} - '@simplewebauthn/server@10.0.0(encoding@0.1.13)': + '@simplewebauthn/server@10.0.1(encoding@0.1.13)': dependencies: '@hexagon/base64': 1.1.27 '@levischuck/tiny-cbor': 0.2.2 @@ -14449,7 +15242,11 @@ snapshots: '@sindresorhus/is@5.3.0': {} - '@sindresorhus/is@6.1.0': {} + '@sindresorhus/is@7.0.0': {} + + '@sindresorhus/merge-streams@2.3.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} '@sinonjs/commons@2.0.0': dependencies: @@ -14475,155 +15272,173 @@ snapshots: '@sinonjs/text-encoding@0.7.2': {} - '@smithy/abort-controller@2.0.14': - dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 - '@smithy/abort-controller@2.2.0': dependencies: '@smithy/types': 2.12.0 tslib: 2.6.2 - '@smithy/chunked-blob-reader-native@2.0.0': + '@smithy/abort-controller@3.1.1': dependencies: - '@smithy/util-base64': 2.0.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/chunked-blob-reader@2.0.0': + '@smithy/chunked-blob-reader-native@3.0.0': dependencies: - tslib: 2.6.2 + '@smithy/util-base64': 3.0.0 + tslib: 2.6.3 - '@smithy/config-resolver@2.0.9': + '@smithy/chunked-blob-reader@3.0.0': dependencies: - '@smithy/node-config-provider': 2.0.11 - '@smithy/types': 2.6.0 - '@smithy/util-config-provider': 2.0.0 - '@smithy/util-middleware': 2.0.1 - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/credential-provider-imds@2.0.11': + '@smithy/config-resolver@3.0.5': dependencies: - '@smithy/node-config-provider': 2.0.11 - '@smithy/property-provider': 2.0.9 - '@smithy/types': 2.6.0 - '@smithy/url-parser': 2.0.8 - tslib: 2.6.2 + '@smithy/node-config-provider': 3.1.4 + '@smithy/types': 3.3.0 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.3 + tslib: 2.6.3 - '@smithy/eventstream-codec@2.0.8': + '@smithy/core@2.3.1': dependencies: - '@aws-crypto/crc32': 3.0.0 - '@smithy/types': 2.6.0 - '@smithy/util-hex-encoding': 2.0.0 - tslib: 2.6.2 + '@smithy/middleware-endpoint': 3.1.0 + '@smithy/middleware-retry': 3.0.13 + '@smithy/middleware-serde': 3.0.3 + '@smithy/protocol-http': 4.1.0 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/util-middleware': 3.0.3 + tslib: 2.6.3 - '@smithy/eventstream-serde-browser@2.0.8': + '@smithy/credential-provider-imds@3.2.0': dependencies: - '@smithy/eventstream-serde-universal': 2.0.8 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/node-config-provider': 3.1.4 + '@smithy/property-provider': 3.1.3 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + tslib: 2.6.3 - '@smithy/eventstream-serde-config-resolver@2.0.8': + '@smithy/eventstream-codec@3.1.2': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 3.3.0 + '@smithy/util-hex-encoding': 3.0.0 + tslib: 2.6.3 - '@smithy/eventstream-serde-node@2.0.8': + '@smithy/eventstream-serde-browser@3.0.5': dependencies: - '@smithy/eventstream-serde-universal': 2.0.8 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/eventstream-serde-universal': 3.0.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/eventstream-serde-universal@2.0.8': + '@smithy/eventstream-serde-config-resolver@3.0.3': dependencies: - '@smithy/eventstream-codec': 2.0.8 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/fetch-http-handler@2.1.4': + '@smithy/eventstream-serde-node@3.0.4': dependencies: - '@smithy/protocol-http': 3.0.10 - '@smithy/querystring-builder': 2.0.14 - '@smithy/types': 2.6.0 - '@smithy/util-base64': 2.0.0 - tslib: 2.6.2 + '@smithy/eventstream-serde-universal': 3.0.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/hash-blob-browser@2.0.8': + '@smithy/eventstream-serde-universal@3.0.4': dependencies: - '@smithy/chunked-blob-reader': 2.0.0 - '@smithy/chunked-blob-reader-native': 2.0.0 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/eventstream-codec': 3.1.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/hash-node@2.0.8': + '@smithy/fetch-http-handler@3.2.4': dependencies: - '@smithy/types': 2.6.0 - '@smithy/util-buffer-from': 2.0.0 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 + '@smithy/protocol-http': 4.1.0 + '@smithy/querystring-builder': 3.0.3 + '@smithy/types': 3.3.0 + '@smithy/util-base64': 3.0.0 + tslib: 2.6.3 - '@smithy/hash-stream-node@2.0.8': + '@smithy/hash-blob-browser@3.1.2': dependencies: - '@smithy/types': 2.6.0 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 + '@smithy/chunked-blob-reader': 3.0.0 + '@smithy/chunked-blob-reader-native': 3.0.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/invalid-dependency@2.0.8': + '@smithy/hash-node@3.0.3': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/hash-stream-node@3.1.2': + dependencies: + '@smithy/types': 3.3.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/invalid-dependency@3.0.3': + dependencies: + '@smithy/types': 3.3.0 + tslib: 2.6.3 '@smithy/is-array-buffer@2.0.0': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/md5-js@2.0.8': + '@smithy/is-array-buffer@3.0.0': dependencies: - '@smithy/types': 2.6.0 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/middleware-content-length@2.0.10': + '@smithy/md5-js@3.0.3': dependencies: - '@smithy/protocol-http': 3.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 - '@smithy/middleware-endpoint@2.0.8': + '@smithy/middleware-content-length@3.0.5': dependencies: - '@smithy/middleware-serde': 2.0.8 - '@smithy/types': 2.6.0 - '@smithy/url-parser': 2.0.8 - '@smithy/util-middleware': 2.0.1 - tslib: 2.6.2 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/middleware-retry@2.0.11': + '@smithy/middleware-endpoint@3.1.0': dependencies: - '@smithy/node-config-provider': 2.0.11 - '@smithy/protocol-http': 3.0.10 - '@smithy/service-error-classification': 2.0.1 - '@smithy/types': 2.6.0 - '@smithy/util-middleware': 2.0.1 - '@smithy/util-retry': 2.0.1 - tslib: 2.6.2 - uuid: 8.3.2 + '@smithy/middleware-serde': 3.0.3 + '@smithy/node-config-provider': 3.1.4 + '@smithy/shared-ini-file-loader': 3.1.4 + '@smithy/types': 3.3.0 + '@smithy/url-parser': 3.0.3 + '@smithy/util-middleware': 3.0.3 + tslib: 2.6.3 - '@smithy/middleware-serde@2.0.8': + '@smithy/middleware-retry@3.0.13': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/node-config-provider': 3.1.4 + '@smithy/protocol-http': 4.1.0 + '@smithy/service-error-classification': 3.0.3 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-retry': 3.0.3 + tslib: 2.6.3 + uuid: 9.0.1 - '@smithy/middleware-stack@2.0.1': + '@smithy/middleware-serde@3.0.3': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/node-config-provider@2.0.11': + '@smithy/middleware-stack@3.0.3': dependencies: - '@smithy/property-provider': 2.0.9 - '@smithy/shared-ini-file-loader': 2.0.10 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 + + '@smithy/node-config-provider@3.1.4': + dependencies: + '@smithy/property-provider': 3.1.3 + '@smithy/shared-ini-file-loader': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 '@smithy/node-http-handler@2.5.0': dependencies: @@ -14633,26 +15448,28 @@ snapshots: '@smithy/types': 2.12.0 tslib: 2.6.2 - '@smithy/property-provider@2.0.9': + '@smithy/node-http-handler@3.1.4': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/abort-controller': 3.1.1 + '@smithy/protocol-http': 4.1.0 + '@smithy/querystring-builder': 3.0.3 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/protocol-http@3.0.10': + '@smithy/property-provider@3.1.3': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 '@smithy/protocol-http@3.3.0': dependencies: '@smithy/types': 2.12.0 tslib: 2.6.2 - '@smithy/querystring-builder@2.0.14': + '@smithy/protocol-http@4.1.0': dependencies: - '@smithy/types': 2.6.0 - '@smithy/util-uri-escape': 2.0.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 '@smithy/querystring-builder@2.2.0': dependencies: @@ -14660,226 +15477,234 @@ snapshots: '@smithy/util-uri-escape': 2.2.0 tslib: 2.6.2 - '@smithy/querystring-parser@2.0.8': + '@smithy/querystring-builder@3.0.3': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + '@smithy/util-uri-escape': 3.0.0 + tslib: 2.6.3 - '@smithy/service-error-classification@2.0.1': + '@smithy/querystring-parser@3.0.3': dependencies: - '@smithy/types': 2.6.0 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/shared-ini-file-loader@2.0.10': + '@smithy/service-error-classification@3.0.3': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 - '@smithy/signature-v4@2.0.5': + '@smithy/shared-ini-file-loader@3.1.4': dependencies: - '@smithy/eventstream-codec': 2.0.8 - '@smithy/is-array-buffer': 2.0.0 - '@smithy/types': 2.6.0 - '@smithy/util-hex-encoding': 2.0.0 - '@smithy/util-middleware': 2.0.1 - '@smithy/util-uri-escape': 2.0.0 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/smithy-client@2.1.5': + '@smithy/signature-v4@4.1.0': dependencies: - '@smithy/middleware-stack': 2.0.1 - '@smithy/types': 2.6.0 - '@smithy/util-stream': 2.0.11 - tslib: 2.6.2 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.3 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 + + '@smithy/smithy-client@3.1.11': + dependencies: + '@smithy/middleware-endpoint': 3.1.0 + '@smithy/middleware-stack': 3.0.3 + '@smithy/protocol-http': 4.1.0 + '@smithy/types': 3.3.0 + '@smithy/util-stream': 3.1.3 + tslib: 2.6.3 '@smithy/types@2.12.0': dependencies: tslib: 2.6.2 - '@smithy/types@2.6.0': + '@smithy/types@3.3.0': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/url-parser@2.0.8': + '@smithy/url-parser@3.0.3': dependencies: - '@smithy/querystring-parser': 2.0.8 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/querystring-parser': 3.0.3 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/util-base64@2.0.0': + '@smithy/util-base64@3.0.0': dependencies: - '@smithy/util-buffer-from': 2.0.0 - tslib: 2.6.2 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 - '@smithy/util-body-length-browser@2.0.0': + '@smithy/util-body-length-browser@3.0.0': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/util-body-length-node@2.1.0': + '@smithy/util-body-length-node@3.0.0': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 '@smithy/util-buffer-from@2.0.0': dependencies: '@smithy/is-array-buffer': 2.0.0 - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/util-config-provider@2.0.0': + '@smithy/util-buffer-from@3.0.0': dependencies: - tslib: 2.6.2 + '@smithy/is-array-buffer': 3.0.0 + tslib: 2.6.3 + + '@smithy/util-config-provider@3.0.0': + dependencies: + tslib: 2.6.3 - '@smithy/util-defaults-mode-browser@2.0.9': + '@smithy/util-defaults-mode-browser@3.0.13': dependencies: - '@smithy/property-provider': 2.0.9 - '@smithy/smithy-client': 2.1.5 - '@smithy/types': 2.6.0 + '@smithy/property-provider': 3.1.3 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 bowser: 2.11.0 - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/util-defaults-mode-node@2.0.11': + '@smithy/util-defaults-mode-node@3.0.13': dependencies: - '@smithy/config-resolver': 2.0.9 - '@smithy/credential-provider-imds': 2.0.11 - '@smithy/node-config-provider': 2.0.11 - '@smithy/property-provider': 2.0.9 - '@smithy/smithy-client': 2.1.5 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/config-resolver': 3.0.5 + '@smithy/credential-provider-imds': 3.2.0 + '@smithy/node-config-provider': 3.1.4 + '@smithy/property-provider': 3.1.3 + '@smithy/smithy-client': 3.1.11 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/util-hex-encoding@2.0.0': + '@smithy/util-endpoints@2.0.5': dependencies: - tslib: 2.6.2 + '@smithy/node-config-provider': 3.1.4 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/util-middleware@2.0.1': + '@smithy/util-hex-encoding@3.0.0': dependencies: - '@smithy/types': 2.6.0 - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/util-retry@2.0.1': + '@smithy/util-middleware@3.0.3': dependencies: - '@smithy/service-error-classification': 2.0.1 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/util-stream@2.0.11': + '@smithy/util-retry@3.0.3': dependencies: - '@smithy/fetch-http-handler': 2.1.4 - '@smithy/node-http-handler': 2.5.0 - '@smithy/types': 2.6.0 - '@smithy/util-base64': 2.0.0 - '@smithy/util-buffer-from': 2.0.0 - '@smithy/util-hex-encoding': 2.0.0 - '@smithy/util-utf8': 2.0.0 - tslib: 2.6.2 + '@smithy/service-error-classification': 3.0.3 + '@smithy/types': 3.3.0 + tslib: 2.6.3 - '@smithy/util-uri-escape@2.0.0': + '@smithy/util-stream@3.1.3': dependencies: - tslib: 2.6.2 + '@smithy/fetch-http-handler': 3.2.4 + '@smithy/node-http-handler': 3.1.4 + '@smithy/types': 3.3.0 + '@smithy/util-base64': 3.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.6.3 '@smithy/util-uri-escape@2.2.0': dependencies: tslib: 2.6.2 + '@smithy/util-uri-escape@3.0.0': + dependencies: + tslib: 2.6.3 + '@smithy/util-utf8@2.0.0': dependencies: '@smithy/util-buffer-from': 2.0.0 - tslib: 2.6.2 + tslib: 2.6.3 - '@smithy/util-waiter@2.0.8': + '@smithy/util-utf8@3.0.0': dependencies: - '@smithy/abort-controller': 2.0.14 - '@smithy/types': 2.6.0 - tslib: 2.6.2 + '@smithy/util-buffer-from': 3.0.0 + tslib: 2.6.3 + + '@smithy/util-waiter@3.1.2': + dependencies: + '@smithy/abort-controller': 3.1.1 + '@smithy/types': 3.3.0 + tslib: 2.6.3 '@sqltools/formatter@1.2.5': {} - '@storybook/addon-actions@8.0.9': + '@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))': dependencies: - '@storybook/core-events': 8.0.9 '@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) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.0.9': + '@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))': 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) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@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))': dependencies: - '@storybook/blocks': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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) ts-dedent: 2.2.0 - transitivePeerDependencies: - - '@types/react' - - encoding - - react - - react-dom - - supports-color - '@storybook/addon-docs@8.0.9(encoding@0.1.13)': + '@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))': dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 '@mdx-js/react': 3.0.1(@types/react@18.0.28)(react@18.3.1) - '@storybook/blocks': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/client-logger': 8.0.9 - '@storybook/components': 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/csf-plugin': 8.0.9 - '@storybook/csf-tools': 8.0.9 + '@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/global': 5.0.0 - '@storybook/node-logger': 8.0.9 - '@storybook/preview-api': 8.0.9 - '@storybook/react-dom-shim': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.0.9 + '@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)) '@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) ts-dedent: 2.2.0 transitivePeerDependencies: - - encoding - supports-color - '@storybook/addon-essentials@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@storybook/addon-actions': 8.0.9 - '@storybook/addon-backgrounds': 8.0.9 - '@storybook/addon-controls': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/addon-docs': 8.0.9(encoding@0.1.13) - '@storybook/addon-highlight': 8.0.9 - '@storybook/addon-measure': 8.0.9 - '@storybook/addon-outline': 8.0.9 - '@storybook/addon-toolbars': 8.0.9 - '@storybook/addon-viewport': 8.0.9 - '@storybook/core-common': 8.0.9(encoding@0.1.13) - '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/node-logger': 8.0.9 - '@storybook/preview-api': 8.0.9 + '@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) ts-dedent: 2.2.0 transitivePeerDependencies: - - '@types/react' - - encoding - - react - - react-dom - supports-color - '@storybook/addon-highlight@8.0.9': + '@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))': 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/addon-interactions@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': + '@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))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.0.9 - '@storybook/test': 8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) - '@storybook/types': 8.0.9 + '@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)) 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) ts-dedent: 2.2.0 transitivePeerDependencies: - '@jest/globals' @@ -14888,89 +15713,83 @@ snapshots: - jest - vitest - '@storybook/addon-links@8.0.9(react@18.3.1)': + '@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))': dependencies: - '@storybook/csf': 0.1.6 + '@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) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 - '@storybook/addon-mdx-gfm@8.0.9': + '@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))': dependencies: - '@storybook/node-logger': 8.0.9 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) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-measure@8.0.9': + '@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))': dependencies: '@storybook/global': 5.0.0 - tiny-invariant: 1.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) + tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.0.9': + '@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))': 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) ts-dedent: 2.2.0 - '@storybook/addon-storysource@8.0.9': + '@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))': dependencies: - '@storybook/source-loader': 8.0.9 + '@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)) estraverse: 5.3.0 - tiny-invariant: 1.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) + tiny-invariant: 1.3.3 - '@storybook/addon-toolbars@8.0.9': {} + '@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))': + 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/addon-viewport@8.0.9': + '@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))': 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/blocks@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(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))': dependencies: - '@storybook/channels': 8.0.9 - '@storybook/client-logger': 8.0.9 - '@storybook/components': 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/core-events': 8.0.9 - '@storybook/csf': 0.1.6 - '@storybook/docs-tools': 8.0.9(encoding@0.1.13) + '@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/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/preview-api': 8.0.9 - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.0.9 '@types/lodash': 4.14.191 color-convert: 2.0.1 dequal: 2.0.3 lodash: 4.17.21 - markdown-to-jsx: 7.3.2(react@18.3.1) + markdown-to-jsx: 7.4.7(react@18.3.1) 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) telejson: 7.2.0 - tocbot: 4.21.1 ts-dedent: 2.2.0 util-deprecate: 1.0.2 optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - transitivePeerDependencies: - - '@types/react' - - encoding - - supports-color - '@storybook/builder-manager@8.0.9(encoding@0.1.13)': + '@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.0.9(encoding@0.1.13) - '@storybook/manager': 8.0.9 - '@storybook/node-logger': 8.0.9 + '@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.20.2) + '@yarnpkg/esbuild-plugin-pnp': 3.0.0-rc.15(esbuild@0.19.11) browser-assert: 1.2.1 - ejs: 3.1.9 - esbuild: 0.20.2 + ejs: 3.1.10 + esbuild: 0.19.11 esbuild-plugin-alias: 0.2.1 express: 4.19.2 fs-extra: 11.1.1 @@ -14978,187 +15797,158 @@ snapshots: util: 0.12.5 transitivePeerDependencies: - encoding + - prettier - supports-color - '@storybook/builder-vite@8.0.9(encoding@0.1.13)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))': - dependencies: - '@storybook/channels': 8.0.9 - '@storybook/client-logger': 8.0.9 - '@storybook/core-common': 8.0.9(encoding@0.1.13) - '@storybook/core-events': 8.0.9 - '@storybook/csf-plugin': 8.0.9 - '@storybook/node-logger': 8.0.9 - '@storybook/preview': 8.0.9 - '@storybook/preview-api': 8.0.9 - '@storybook/types': 8.0.9 + '@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: 0.9.3 - express: 4.18.2 + 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.7 + magic-string: 0.30.10 ts-dedent: 2.2.0 - vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) + vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - encoding + - prettier - supports-color - '@storybook/channels@8.0.9': + '@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))': dependencies: - '@storybook/client-logger': 8.0.9 - '@storybook/core-events': 8.0.9 - '@storybook/global': 5.0.0 - telejson: 7.2.0 - tiny-invariant: 1.3.1 - - '@storybook/cli@8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)': - dependencies: - '@babel/core': 7.24.0 - '@babel/types': 7.24.0 - '@ndelangen/get-tarball': 3.0.7 - '@storybook/codemod': 8.0.9 - '@storybook/core-common': 8.0.9(encoding@0.1.13) - '@storybook/core-events': 8.0.9 - '@storybook/core-server': 8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3) - '@storybook/csf-tools': 8.0.9 - '@storybook/node-logger': 8.0.9 - '@storybook/telemetry': 8.0.9(encoding@0.1.13) - '@storybook/types': 8.0.9 - '@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 - find-up: 5.0.0 + '@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)) + '@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 - get-npm-tarball-url: 2.0.3 - giget: 1.1.2 - globby: 11.1.0 - jscodeshift: 0.15.1(@babel/preset-env@7.23.5(@babel/core@7.24.0)) - leven: 3.1.0 - ora: 5.4.1 - prettier: 3.2.5 - prompts: 2.4.2 - read-pkg-up: 7.0.1 - semver: 7.6.0 - strip-json-comments: 3.1.1 - tempy: 1.0.1 - tiny-invariant: 1.3.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: - - '@babel/preset-env' - - bufferutil - - encoding - - react - - react-dom - supports-color - - utf-8-validate - '@storybook/client-logger@8.0.9': + '@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.0.9': + '@storybook/codemod@8.2.6(bufferutil@4.0.8)(utf-8-validate@6.0.4)': dependencies: - '@babel/core': 7.24.0 - '@babel/preset-env': 7.23.5(@babel/core@7.24.0) - '@babel/types': 7.24.0 - '@storybook/csf': 0.1.6 - '@storybook/csf-tools': 8.0.9 - '@storybook/node-logger': 8.0.9 - '@storybook/types': 8.0.9 + '@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: 11.1.0 - jscodeshift: 0.15.1(@babel/preset-env@7.23.5(@babel/core@7.24.0)) + 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.2.5 + prettier: 3.3.3 recast: 0.23.6 - tiny-invariant: 1.3.1 + tiny-invariant: 1.3.3 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate - '@storybook/components@8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@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: - '@radix-ui/react-slot': 1.0.2(@types/react@18.0.28)(react@18.3.1) - '@storybook/client-logger': 8.0.9 - '@storybook/csf': 0.1.6 - '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.0.9 - memoizerific: 1.11.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - util-deprecate: 1.0.2 - transitivePeerDependencies: - - '@types/react' + 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.0.9(encoding@0.1.13)': + '@storybook/core-common@8.1.11(encoding@0.1.13)(prettier@3.3.3)': dependencies: - '@storybook/core-events': 8.0.9 - '@storybook/csf-tools': 8.0.9 - '@storybook/node-logger': 8.0.9 - '@storybook/types': 8.0.9 + '@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.20.2 - esbuild-register: 3.5.0(esbuild@0.20.2) + 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.12 + 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: 1.0.1 - tiny-invariant: 1.3.1 + tempy: 3.1.0 + tiny-invariant: 1.3.3 ts-dedent: 2.2.0 util: 0.12.5 + optionalDependencies: + prettier: 3.3.3 transitivePeerDependencies: - encoding - supports-color - '@storybook/core-events@8.0.9': + '@storybook/core-events@8.1.11': dependencies: + '@storybook/csf': 0.1.9 ts-dedent: 2.2.0 - '@storybook/core-server@8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)': + '@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))': + 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-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)': dependencies: '@aw-web-design/x-default-browser': 1.4.126 - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 '@discoveryjs/json-ext': 0.5.7 - '@storybook/builder-manager': 8.0.9(encoding@0.1.13) - '@storybook/channels': 8.0.9 - '@storybook/core-common': 8.0.9(encoding@0.1.13) - '@storybook/core-events': 8.0.9 - '@storybook/csf': 0.1.6 - '@storybook/csf-tools': 8.0.9 - '@storybook/docs-mdx': 3.0.0 + '@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.0.9 - '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/node-logger': 8.0.9 - '@storybook/preview-api': 8.0.9 - '@storybook/telemetry': 8.0.9(encoding@0.1.13) - '@storybook/types': 8.0.9 + '@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 @@ -15167,10 +15957,10 @@ snapshots: cli-table3: 0.6.3 compression: 1.7.4 detect-port: 1.5.1 - express: 4.18.2 + diff: 5.2.0 + express: 4.19.2 fs-extra: 11.1.1 - globby: 11.1.0 - ip: 2.0.1 + globby: 14.0.1 lodash: 4.17.21 open: 8.4.2 pretty-hrtime: 1.0.3 @@ -15178,59 +15968,88 @@ snapshots: read-pkg-up: 7.0.1 semver: 7.6.0 telejson: 7.2.0 - tiny-invariant: 1.3.1 + 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.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + 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/csf-plugin@8.0.9': + '@storybook/core@8.2.6(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 + browser-assert: 1.2.1 + esbuild: 0.19.11 + esbuild-register: 3.5.0(esbuild@0.19.11) + express: 4.19.2 + process: 0.11.10 + recast: 0.23.6 + util: 0.12.5 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@storybook/csf-plugin@8.1.11': dependencies: - '@storybook/csf-tools': 8.0.9 + '@storybook/csf-tools': 8.1.11 unplugin: 1.4.0 transitivePeerDependencies: - supports-color - '@storybook/csf-tools@8.0.9': + '@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.23.6 - '@babel/parser': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 - '@storybook/csf': 0.1.6 - '@storybook/types': 8.0.9 + '@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.6': + '@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.0.0': {} + '@storybook/docs-mdx@3.1.0-next.0': {} - '@storybook/docs-tools@8.0.9(encoding@0.1.13)': + '@storybook/docs-tools@8.1.11(encoding@0.1.13)(prettier@3.3.3)': dependencies: - '@storybook/core-common': 8.0.9(encoding@0.1.13) - '@storybook/core-events': 8.0.9 - '@storybook/preview-api': 8.0.9 - '@storybook/types': 8.0.9 + '@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': {} @@ -15240,27 +16059,24 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.0.9': + '@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))': dependencies: - '@storybook/channels': 8.0.9 - '@storybook/client-logger': 8.0.9 - '@storybook/core-events': 8.0.9 '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.9 '@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) util: 0.12.5 - '@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/manager-api@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@storybook/channels': 8.0.9 - '@storybook/client-logger': 8.0.9 - '@storybook/core-events': 8.0.9 - '@storybook/csf': 0.1.6 + '@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.0.9 - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.0.9 + '@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 @@ -15271,65 +16087,73 @@ snapshots: - react - react-dom - '@storybook/manager@8.0.9': {} + '@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.0.9': {} + '@storybook/node-logger@8.1.11': {} - '@storybook/preview-api@8.0.9': + '@storybook/preview-api@8.1.11': dependencies: - '@storybook/channels': 8.0.9 - '@storybook/client-logger': 8.0.9 - '@storybook/core-events': 8.0.9 - '@storybook/csf': 0.1.6 + '@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.0.9 + '@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.1 + tiny-invariant: 1.3.3 ts-dedent: 2.2.0 util-deprecate: 1.0.2 - '@storybook/preview@8.0.9': {} + '@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))': + 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/react-dom-shim@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@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))': 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/react-vite@8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))': + '@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))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)) - '@rollup/pluginutils': 5.1.0(rollup@4.17.2) - '@storybook/builder-vite': 8.0.9(encoding@0.1.13)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)) - '@storybook/node-logger': 8.0.9 - '@storybook/react': 8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5) + '@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) find-up: 5.0.0 - magic-string: 0.30.7 + 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) tsconfig-paths: 4.2.0 - vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) + vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) transitivePeerDependencies: - '@preact/preset-vite' - - encoding - rollup - supports-color - typescript - vite-plugin-glimmerx - '@storybook/react@8.0.9(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.4.5)': + '@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)': dependencies: - '@storybook/client-logger': 8.0.9 - '@storybook/docs-tools': 8.0.9(encoding@0.1.13) + '@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/global': 5.0.0 - '@storybook/preview-api': 8.0.9 - '@storybook/react-dom-shim': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.0.9 + '@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)) '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 '@types/node': 18.17.15 @@ -15344,34 +16168,32 @@ snapshots: 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) ts-dedent: 2.2.0 type-fest: 2.19.0 util-deprecate: 1.0.2 optionalDependencies: - typescript: 5.4.5 - transitivePeerDependencies: - - encoding - - supports-color + typescript: 5.5.4 - '@storybook/router@8.0.9': + '@storybook/router@8.1.11': dependencies: - '@storybook/client-logger': 8.0.9 + '@storybook/client-logger': 8.1.11 memoizerific: 1.11.3 qs: 6.11.1 - '@storybook/source-loader@8.0.9': + '@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))': dependencies: - '@storybook/csf': 0.1.6 - '@storybook/types': 8.0.9 + '@storybook/csf': 0.1.11 estraverse: 5.3.0 lodash: 4.17.21 - prettier: 3.2.5 + 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.0.9(encoding@0.1.13)': + '@storybook/telemetry@8.1.11(encoding@0.1.13)(prettier@3.3.3)': dependencies: - '@storybook/client-logger': 8.0.9 - '@storybook/core-common': 8.0.9(encoding@0.1.13) - '@storybook/csf-tools': 8.0.9 + '@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 @@ -15379,19 +16201,19 @@ snapshots: read-pkg-up: 7.0.1 transitivePeerDependencies: - encoding + - prettier - supports-color - '@storybook/test@8.0.9(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': + '@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/client-logger': 8.0.9 - '@storybook/core-events': 8.0.9 - '@storybook/instrumenter': 8.0.9 - '@storybook/preview-api': 8.0.9 - '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3)) - '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) - '@vitest/expect': 1.3.1 + '@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' @@ -15400,37 +16222,47 @@ snapshots: - jest - vitest - '@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/theming@8.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) - '@storybook/client-logger': 8.0.9 + '@storybook/client-logger': 8.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/types@8.0.9': + '@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/channels': 8.0.9 + 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.1.11': + dependencies: + '@storybook/channels': 8.1.11 '@types/express': 4.17.17 file-system-cache: 2.3.0 - '@storybook/vue3-vite@8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5))': + '@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))': + 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/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))': dependencies: - '@storybook/builder-vite': 8.0.9(encoding@0.1.13)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3)) - '@storybook/core-server': 8.0.9(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3) - '@storybook/vue3': 8.0.9(encoding@0.1.13)(vue@3.4.26(typescript@5.4.5)) + '@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)) find-package-json: 1.2.0 - magic-string: 0.30.7 - typescript: 5.4.5 - vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) - vue-component-meta: 2.0.16(typescript@5.4.5) - vue-docgen-api: 4.75.1(vue@3.4.26(typescript@5.4.5)) + 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)) transitivePeerDependencies: - '@preact/preset-vite' - bufferutil - encoding + - prettier - react - react-dom - supports-color @@ -15438,26 +16270,42 @@ snapshots: - vite-plugin-glimmerx - vue - '@storybook/vue3@8.0.9(encoding@0.1.13)(vue@3.4.26(typescript@5.4.5))': + '@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.0.9(encoding@0.1.13) + '@storybook/docs-tools': 8.1.11(encoding@0.1.13)(prettier@3.3.3) '@storybook/global': 5.0.0 - '@storybook/preview-api': 8.0.9 - '@storybook/types': 8.0.9 - '@vue/compiler-core': 3.4.21 + '@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.26(typescript@5.4.5) - vue-component-type-helpers: 2.0.19 + vue: 3.4.37(typescript@5.5.4) + vue-component-type-helpers: 2.1.2 transitivePeerDependencies: - encoding + - prettier - supports-color - '@swc/cli@0.3.12(@swc/core@1.4.17)(chokidar@3.5.3)': + '@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))': + 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/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) + ts-dedent: 2.2.0 + type-fest: 2.19.0 + vue: 3.4.37(typescript@5.5.4) + vue-component-type-helpers: 2.1.2 + + '@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)': dependencies: '@mole-inc/bin-wrapper': 8.0.1 - '@swc/core': 1.4.17 + '@swc/core': 1.6.6 '@swc/counter': 0.1.3 commander: 8.3.0 fast-glob: 3.3.2 @@ -15477,13 +16325,19 @@ snapshots: '@swc/core-darwin-arm64@1.3.56': optional: true - '@swc/core-darwin-arm64@1.4.17': + '@swc/core-darwin-arm64@1.6.13': + optional: true + + '@swc/core-darwin-arm64@1.6.6': optional: true '@swc/core-darwin-x64@1.3.56': optional: true - '@swc/core-darwin-x64@1.4.17': + '@swc/core-darwin-x64@1.6.13': + optional: true + + '@swc/core-darwin-x64@1.6.6': optional: true '@swc/core-freebsd-x64@1.3.11': @@ -15494,82 +16348,131 @@ snapshots: '@swc/core-linux-arm-gnueabihf@1.3.56': optional: true - '@swc/core-linux-arm-gnueabihf@1.4.17': + '@swc/core-linux-arm-gnueabihf@1.6.13': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.6.6': optional: true '@swc/core-linux-arm64-gnu@1.3.56': optional: true - '@swc/core-linux-arm64-gnu@1.4.17': + '@swc/core-linux-arm64-gnu@1.6.13': + optional: true + + '@swc/core-linux-arm64-gnu@1.6.6': optional: true '@swc/core-linux-arm64-musl@1.3.56': optional: true - '@swc/core-linux-arm64-musl@1.4.17': + '@swc/core-linux-arm64-musl@1.6.13': + optional: true + + '@swc/core-linux-arm64-musl@1.6.6': optional: true '@swc/core-linux-x64-gnu@1.3.56': optional: true - '@swc/core-linux-x64-gnu@1.4.17': + '@swc/core-linux-x64-gnu@1.6.13': + optional: true + + '@swc/core-linux-x64-gnu@1.6.6': optional: true '@swc/core-linux-x64-musl@1.3.56': optional: true - '@swc/core-linux-x64-musl@1.4.17': + '@swc/core-linux-x64-musl@1.6.13': + optional: true + + '@swc/core-linux-x64-musl@1.6.6': optional: true '@swc/core-win32-arm64-msvc@1.3.56': optional: true - '@swc/core-win32-arm64-msvc@1.4.17': + '@swc/core-win32-arm64-msvc@1.6.13': + optional: true + + '@swc/core-win32-arm64-msvc@1.6.6': optional: true '@swc/core-win32-ia32-msvc@1.3.56': optional: true - '@swc/core-win32-ia32-msvc@1.4.17': + '@swc/core-win32-ia32-msvc@1.6.13': + optional: true + + '@swc/core-win32-ia32-msvc@1.6.6': optional: true '@swc/core-win32-x64-msvc@1.3.56': optional: true - '@swc/core-win32-x64-msvc@1.4.17': + '@swc/core-win32-x64-msvc@1.6.13': + optional: true + + '@swc/core-win32-x64-msvc@1.6.6': optional: true - '@swc/core@1.4.17': + '@swc/core@1.6.13': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.9 + optionalDependencies: + '@swc/core-darwin-arm64': 1.6.13 + '@swc/core-darwin-x64': 1.6.13 + '@swc/core-linux-arm-gnueabihf': 1.6.13 + '@swc/core-linux-arm64-gnu': 1.6.13 + '@swc/core-linux-arm64-musl': 1.6.13 + '@swc/core-linux-x64-gnu': 1.6.13 + '@swc/core-linux-x64-musl': 1.6.13 + '@swc/core-win32-arm64-msvc': 1.6.13 + '@swc/core-win32-ia32-msvc': 1.6.13 + '@swc/core-win32-x64-msvc': 1.6.13 + + '@swc/core@1.6.6': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.5 + '@swc/types': 0.1.9 optionalDependencies: - '@swc/core-darwin-arm64': 1.4.17 - '@swc/core-darwin-x64': 1.4.17 - '@swc/core-linux-arm-gnueabihf': 1.4.17 - '@swc/core-linux-arm64-gnu': 1.4.17 - '@swc/core-linux-arm64-musl': 1.4.17 - '@swc/core-linux-x64-gnu': 1.4.17 - '@swc/core-linux-x64-musl': 1.4.17 - '@swc/core-win32-arm64-msvc': 1.4.17 - '@swc/core-win32-ia32-msvc': 1.4.17 - '@swc/core-win32-x64-msvc': 1.4.17 + '@swc/core-darwin-arm64': 1.6.6 + '@swc/core-darwin-x64': 1.6.6 + '@swc/core-linux-arm-gnueabihf': 1.6.6 + '@swc/core-linux-arm64-gnu': 1.6.6 + '@swc/core-linux-arm64-musl': 1.6.6 + '@swc/core-linux-x64-gnu': 1.6.6 + '@swc/core-linux-x64-musl': 1.6.6 + '@swc/core-win32-arm64-msvc': 1.6.6 + '@swc/core-win32-ia32-msvc': 1.6.6 + '@swc/core-win32-x64-msvc': 1.6.6 '@swc/counter@0.1.3': {} - '@swc/jest@0.2.36(@swc/core@1.4.17)': + '@swc/jest@0.2.36(@swc/core@1.6.13)': + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@swc/core': 1.6.13 + '@swc/counter': 0.1.3 + jsonc-parser: 3.2.0 + + '@swc/jest@0.2.36(@swc/core@1.6.6)': dependencies: '@jest/create-cache-key-function': 29.7.0 - '@swc/core': 1.4.17 + '@swc/core': 1.6.6 '@swc/counter': 0.1.3 jsonc-parser: 3.2.0 - '@swc/types@0.1.5': {} + '@swc/types@0.1.9': + dependencies: + '@swc/counter': 0.1.3 '@swc/wasm@1.2.130': optional: true - '@syuilo/aiscript@0.18.0': + '@syuilo/aiscript@0.19.0': dependencies: seedrandom: 3.0.5 stringz: 2.1.0 @@ -15583,12 +16486,12 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@testing-library/dom@9.3.3': + '@testing-library/dom@10.1.0': 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 + aria-query: 5.3.0 chalk: 4.1.2 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 @@ -15605,11 +16508,11 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7))(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3))': + '@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))': dependencies: '@adobe/css-tools': 4.3.3 '@babel/runtime': 7.23.4 - aria-query: 5.1.3 + aria-query: 5.3.0 chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 @@ -15618,21 +16521,21 @@ snapshots: optionalDependencies: '@jest/globals': 29.7.0 '@types/jest': 29.5.12 - jest: 29.7.0(@types/node@20.12.7) - vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3) + 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@9.3.4)': + '@testing-library/user-event@14.5.2(@testing-library/dom@10.1.0)': dependencies: - '@testing-library/dom': 9.3.4 + '@testing-library/dom': 10.1.0 - '@testing-library/vue@8.0.3(@vue/compiler-sfc@3.4.26)(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5))': + '@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))': dependencies: '@babel/runtime': 7.23.4 - '@testing-library/dom': 9.3.3 - '@vue/test-utils': 2.4.1(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5)) - vue: 3.4.26(typescript@5.4.5) + '@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) optionalDependencies: - '@vue/compiler-sfc': 3.4.26 + '@vue/compiler-sfc': 3.4.37 transitivePeerDependencies: - '@vue/server-renderer' @@ -15644,7 +16547,7 @@ snapshots: '@trysound/sax@0.2.0': {} - '@tsd/typescript@5.3.3': {} + '@tsd/typescript@5.4.5': {} '@twemoji/parser@15.0.0': {} @@ -15652,7 +16555,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/archiver@6.0.2': dependencies: @@ -15664,31 +16567,31 @@ snapshots: '@types/babel__core@7.20.0': dependencies: - '@babel/parser': 7.24.5 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.20.0 '@types/babel__generator@7.6.4': dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 '@types/babel__template@7.4.1': dependencies: - '@babel/parser': 7.24.5 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 '@types/babel__traverse@7.20.0': dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 '@types/bcryptjs@2.4.6': {} '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.35 - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/braces@3.0.1': {} @@ -15696,15 +16599,9 @@ snapshots: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/responselike': 1.0.0 - '@types/chai-subset@1.3.5': - dependencies: - '@types/chai': 4.3.11 - - '@types/chai@4.3.11': {} - '@types/color-convert@2.0.3': dependencies: '@types/color-name': 1.1.1 @@ -15713,28 +16610,21 @@ snapshots: '@types/connect@3.4.35': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/connect@3.4.36': dependencies: - '@types/node': 20.12.7 - - '@types/content-disposition@0.5.8': {} - - '@types/cookie@0.6.0': {} + '@types/node': 20.14.12 - '@types/cookies@0.9.0': - dependencies: - '@types/connect': 3.4.35 - '@types/express': 4.17.17 - '@types/keygrip': 1.0.6 - '@types/node': 20.12.7 + '@types/content-disposition@0.5.8': {} + + '@types/cookie@0.6.0': {} '@types/core-js@2.5.8': {} '@types/cross-spawn@6.0.2': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/debug@4.1.12': dependencies: @@ -15742,6 +16632,8 @@ snapshots: '@types/detect-port@1.3.2': {} + '@types/diff@5.2.1': {} + '@types/disposable-email-domains@1.0.2': {} '@types/doctrine@0.0.3': {} @@ -15767,7 +16659,7 @@ snapshots: '@types/express-serve-static-core@4.17.33': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 @@ -15778,11 +16670,18 @@ snapshots: '@types/qs': 6.9.7 '@types/serve-static': 1.15.1 + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.33 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.1 + '@types/find-cache-dir@3.2.1': {} '@types/fluent-ffmpeg@2.1.24': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/form-data@2.5.0': dependencies: @@ -15791,11 +16690,11 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/graceful-fs@4.1.6': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/hast@3.0.4': dependencies: @@ -15803,15 +16702,11 @@ snapshots: '@types/htmlescape@1.1.3': {} - '@types/http-assert@1.5.5': {} - '@types/http-cache-semantics@4.0.4': {} - '@types/http-errors@2.0.4': {} - - '@types/http-link-header@1.0.5': + '@types/http-link-header@1.0.7': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/istanbul-lib-coverage@2.0.4': {} @@ -15830,9 +16725,9 @@ snapshots: '@types/js-yaml@4.0.9': {} - '@types/jsdom@21.1.6': + '@types/jsdom@21.1.7': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 @@ -15842,46 +16737,27 @@ snapshots: '@types/json5@0.0.29': {} - '@types/jsonld@1.5.13': {} + '@types/jsonld@1.5.15': {} '@types/jsrsasign@10.5.14': {} - '@types/keygrip@1.0.6': {} - '@types/keyv@3.1.4': dependencies: - '@types/node': 20.12.7 - - '@types/koa-compose@3.2.8': - dependencies: - '@types/koa': 2.14.0 - - '@types/koa@2.14.0': - dependencies: - '@types/accepts': 1.3.7 - '@types/content-disposition': 0.5.8 - '@types/cookies': 0.9.0 - '@types/http-assert': 1.5.5 - '@types/http-errors': 2.0.4 - '@types/keygrip': 1.0.6 - '@types/koa-compose': 3.2.8 - '@types/node': 20.12.7 - - '@types/koa__router@12.0.3': - dependencies: - '@types/koa': 2.14.0 + '@types/node': 20.14.12 '@types/lodash@4.14.191': {} '@types/matter-js@0.19.6': {} + '@types/matter-js@0.19.7': {} + '@types/mdast@4.0.3': dependencies: '@types/unist': 3.0.2 '@types/mdx@2.0.3': {} - '@types/micromatch@4.0.7': + '@types/micromatch@4.0.9': dependencies: '@types/braces': 3.0.1 @@ -15897,15 +16773,11 @@ snapshots: '@types/mute-stream@0.0.4': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/mysql@2.15.22': dependencies: - '@types/node': 20.12.7 - - '@types/node-fetch@3.0.3': - dependencies: - node-fetch: 3.3.2 + '@types/node': 20.14.12 '@types/node@18.17.15': {} @@ -15913,7 +16785,7 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.12.7': + '@types/node@20.14.12': dependencies: undici-types: 5.26.5 @@ -15923,7 +16795,7 @@ snapshots: '@types/nodemailer@6.4.15': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/normalize-package-data@2.4.1': {} @@ -15934,11 +16806,11 @@ snapshots: '@types/oauth2orize@1.11.5': dependencies: '@types/express': 4.17.17 - '@types/node': 20.12.7 + '@types/node': 20.14.12 - '@types/oauth@0.9.4': + '@types/oauth@0.9.5': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/object-assign-deep@0.4.3': {} @@ -15946,17 +16818,17 @@ snapshots: '@types/pg-pool@2.0.4': dependencies: - '@types/pg': 8.11.5 + '@types/pg': 8.11.6 - '@types/pg@8.11.5': + '@types/pg@8.11.6': dependencies: - '@types/node': 20.12.7 - pg-protocol: 1.6.0 + '@types/node': 20.14.12 + pg-protocol: 1.6.1 pg-types: 4.0.1 '@types/pg@8.6.1': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 pg-protocol: 1.6.1 pg-types: 2.2.0 @@ -15964,13 +16836,17 @@ snapshots: '@types/prop-types@15.7.5': {} + '@types/proxy-addr@2.0.3': + dependencies: + '@types/node': 20.14.12 + '@types/pug@2.0.10': {} '@types/punycode@2.1.4': {} '@types/qrcode@1.5.5': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/qs@6.9.7': {} @@ -15988,7 +16864,7 @@ snapshots: '@types/readdir-glob@1.1.1': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/rename@1.0.7': {} @@ -15996,7 +16872,7 @@ snapshots: '@types/responselike@1.0.0': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/sanitize-html@2.11.0': dependencies: @@ -16011,7 +16887,7 @@ snapshots: '@types/serve-static@1.15.1': dependencies: '@types/mime': 3.0.1 - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/serviceworker@0.0.67': {} @@ -16041,23 +16917,27 @@ snapshots: '@types/tough-cookie@4.0.2': {} + '@types/tough-cookie@4.0.5': {} + '@types/unist@3.0.2': {} + '@types/uuid@10.0.0': {} + '@types/uuid@9.0.8': {} '@types/vary@1.1.3': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/web-push@3.6.3': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/wrap-ansi@3.0.0': {} - '@types/ws@8.5.10': + '@types/ws@8.5.11': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 '@types/yargs-parser@21.0.0': {} @@ -16067,19 +16947,19 @@ snapshots: '@types/yauzl@2.10.0': dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 optional: true - '@typescript-eslint/eslint-plugin@6.11.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0)(typescript@5.3.3)': + '@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.10.0 - '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + '@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@8.53.0)(typescript@5.3.3) - '@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3) + '@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@8.1.1) - eslint: 8.53.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 @@ -16092,13 +16972,13 @@ snapshots: '@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.10.0 + '@eslint-community/regexpp': 4.11.0 '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.1.6) '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -16110,16 +16990,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0(eslint@9.8.0)(typescript@5.3.3))(eslint@9.8.0)(typescript@5.3.3)': dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3) + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 7.1.0(eslint@9.8.0)(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.1.0 - '@typescript-eslint/type-utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3) - '@typescript-eslint/utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.1.0(eslint@9.8.0)(typescript@5.3.3) + '@typescript-eslint/utils': 7.1.0(eslint@9.8.0)(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.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 @@ -16130,34 +17010,32 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.7.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': + '@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)': dependencies: - '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/scope-manager': 7.7.1 - '@typescript-eslint/type-utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.7.1 - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 7.17.0 + '@typescript-eslint/type-utils': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 7.17.0 + eslint: 9.8.0 graphemer: 1.4.0 ignore: 5.3.1 natural-compare: 1.4.0 - semver: 7.6.0 - ts-api-utils: 1.3.0(typescript@5.4.5) + ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3)': + '@typescript-eslint/parser@6.11.0(eslint@9.8.0)(typescript@5.3.3)': 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@8.1.1) - eslint: 8.53.0 + debug: 4.3.4(supports-color@5.5.0) + eslint: 9.8.0 optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -16169,36 +17047,36 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) eslint: 8.57.0 optionalDependencies: typescript: 5.1.6 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3)': + '@typescript-eslint/parser@7.1.0(eslint@9.8.0)(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.1.0 '@typescript-eslint/types': 7.1.0 '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.1.0 - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + debug: 4.3.4(supports-color@5.5.0) + eslint: 9.8.0 optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5)': + '@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/scope-manager': 7.7.1 - '@typescript-eslint/types': 7.7.1 - '@typescript-eslint/typescript-estree': 7.7.1(typescript@5.4.5) - '@typescript-eslint/visitor-keys': 7.7.1 - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + '@typescript-eslint/scope-manager': 7.17.0 + '@typescript-eslint/types': 7.17.0 + '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 7.17.0 + debug: 4.3.5(supports-color@8.1.1) + eslint: 9.8.0 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -16217,17 +17095,17 @@ snapshots: '@typescript-eslint/types': 7.1.0 '@typescript-eslint/visitor-keys': 7.1.0 - '@typescript-eslint/scope-manager@7.7.1': + '@typescript-eslint/scope-manager@7.17.0': dependencies: - '@typescript-eslint/types': 7.7.1 - '@typescript-eslint/visitor-keys': 7.7.1 + '@typescript-eslint/types': 7.17.0 + '@typescript-eslint/visitor-keys': 7.17.0 - '@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.3.3)': + '@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@8.53.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.53.0 + '@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 @@ -16238,7 +17116,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.1.6) '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.1.6) - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.1.6) optionalDependencies: @@ -16246,27 +17124,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.1.0(eslint@8.57.0)(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.1.0(eslint@9.8.0)(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3) - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + '@typescript-eslint/utils': 7.1.0(eslint@9.8.0)(typescript@5.3.3) + debug: 4.3.4(supports-color@5.5.0) + 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@7.7.1(eslint@8.57.0)(typescript@5.4.5)': + '@typescript-eslint/type-utils@7.17.0(eslint@9.8.0)(typescript@5.5.4)': dependencies: - '@typescript-eslint/typescript-estree': 7.7.1(typescript@5.4.5) - '@typescript-eslint/utils': 7.7.1(eslint@8.57.0)(typescript@5.4.5) - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 - ts-api-utils: 1.3.0(typescript@5.4.5) + '@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4) + '@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + debug: 4.3.5(supports-color@8.1.1) + eslint: 9.8.0 + ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -16276,13 +17154,13 @@ snapshots: '@typescript-eslint/types@7.1.0': {} - '@typescript-eslint/types@7.7.1': {} + '@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.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -16296,7 +17174,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -16311,7 +17189,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.1.0 '@typescript-eslint/visitor-keys': 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -16322,30 +17200,30 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@7.7.1(typescript@5.4.5)': + '@typescript-eslint/typescript-estree@7.17.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 7.7.1 - '@typescript-eslint/visitor-keys': 7.7.1 - debug: 4.3.4(supports-color@8.1.1) + '@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.4.5) + ts-api-utils: 1.3.0(typescript@5.5.4) optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.3.3)': + '@typescript-eslint/utils@6.11.0(eslint@9.8.0)(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) + '@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: 8.53.0 + eslint: 9.8.0 semver: 7.6.0 transitivePeerDependencies: - supports-color @@ -16365,30 +17243,27 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.1.0(eslint@8.57.0)(typescript@5.3.3)': + '@typescript-eslint/utils@7.1.0(eslint@9.8.0)(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@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': 7.1.0 '@typescript-eslint/types': 7.1.0 '@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3) - eslint: 8.57.0 + eslint: 9.8.0 semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/utils@7.7.1(eslint@8.57.0)(typescript@5.4.5)': + '@typescript-eslint/utils@7.17.0(eslint@9.8.0)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@types/json-schema': 7.0.15 - '@types/semver': 7.5.8 - '@typescript-eslint/scope-manager': 7.7.1 - '@typescript-eslint/types': 7.7.1 - '@typescript-eslint/typescript-estree': 7.7.1(typescript@5.4.5) - eslint: 8.57.0 - semver: 7.6.0 + '@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.5.4) + eslint: 9.8.0 transitivePeerDependencies: - supports-color - typescript @@ -16408,84 +17283,59 @@ snapshots: '@typescript-eslint/types': 7.1.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@7.7.1': + '@typescript-eslint/visitor-keys@7.17.0': dependencies: - '@typescript-eslint/types': 7.7.1 + '@typescript-eslint/types': 7.17.0 eslint-visitor-keys: 3.4.3 '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-vue@5.0.4(vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3))(vue@3.4.26(typescript@5.4.5))': + '@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))': dependencies: - vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) - vue: 3.4.26(typescript@5.4.5) + 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) - '@vitest/coverage-v8@0.34.6(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.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.77.8)(terser@5.31.3))': dependencies: '@ampproject/remapping': 2.2.1 '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.4(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 + istanbul-lib-source-maps: 5.0.4 istanbul-reports: 3.1.6 - magic-string: 0.30.7 + magic-string: 0.30.10 + magicast: 0.3.4 picocolors: 1.0.0 std-env: 3.7.0 + strip-literal: 2.1.0 test-exclude: 6.0.0 - v8-to-istanbul: 9.2.0 - vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.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.77.8)(terser@5.31.3) transitivePeerDependencies: - supports-color - '@vitest/expect@0.34.6': + '@vitest/expect@1.6.0': dependencies: - '@vitest/spy': 0.34.6 - '@vitest/utils': 0.34.6 - chai: 4.3.10 - - '@vitest/expect@1.3.1': - dependencies: - '@vitest/spy': 1.3.1 - '@vitest/utils': 1.3.1 + '@vitest/spy': 1.6.0 + '@vitest/utils': 1.6.0 chai: 4.3.10 - '@vitest/runner@0.34.6': + '@vitest/runner@1.6.0': dependencies: - '@vitest/utils': 0.34.6 - p-limit: 4.0.0 + '@vitest/utils': 1.6.0 + p-limit: 5.0.0 pathe: 1.1.2 - '@vitest/snapshot@0.34.6': + '@vitest/snapshot@1.6.0': dependencies: magic-string: 0.30.10 pathe: 1.1.2 pretty-format: 29.7.0 - '@vitest/spy@0.34.6': - dependencies: - tinyspy: 2.2.0 - - '@vitest/spy@1.3.1': - dependencies: - tinyspy: 2.2.0 - '@vitest/spy@1.6.0': dependencies: tinyspy: 2.2.0 - '@vitest/utils@0.34.6': - dependencies: - diff-sequences: 29.6.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - - '@vitest/utils@1.3.1': - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - '@vitest/utils@1.6.0': dependencies: diff-sequences: 29.6.3 @@ -16497,124 +17347,150 @@ snapshots: dependencies: '@volar/source-map': 2.2.0 + '@volar/language-core@2.4.0-alpha.18': + dependencies: + '@volar/source-map': 2.4.0-alpha.18 + '@volar/source-map@2.2.0': dependencies: muggle-string: 0.4.1 + '@volar/source-map@2.4.0-alpha.18': {} + '@volar/typescript@2.2.0': dependencies: '@volar/language-core': 2.2.0 path-browserify: 1.0.1 - '@vue/compiler-core@3.4.21': + '@volar/typescript@2.4.0-alpha.18': dependencies: - '@babel/parser': 7.24.0 - '@vue/shared': 3.4.21 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.0.2 + '@volar/language-core': 2.4.0-alpha.18 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 - '@vue/compiler-core@3.4.25': + '@vue/compiler-core@3.4.31': dependencies: - '@babel/parser': 7.24.5 - '@vue/shared': 3.4.25 + '@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.26': + '@vue/compiler-core@3.4.34': dependencies: - '@babel/parser': 7.24.5 - '@vue/shared': 3.4.26 + '@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-dom@3.4.21': + '@vue/compiler-core@3.4.37': dependencies: - '@vue/compiler-core': 3.4.21 - '@vue/shared': 3.4.21 + '@babel/parser': 7.24.7 + '@vue/shared': 3.4.37 + entities: 5.0.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 - '@vue/compiler-dom@3.4.25': + '@vue/compiler-dom@3.4.34': dependencies: - '@vue/compiler-core': 3.4.25 - '@vue/shared': 3.4.25 + '@vue/compiler-core': 3.4.34 + '@vue/shared': 3.4.34 - '@vue/compiler-dom@3.4.26': + '@vue/compiler-dom@3.4.37': dependencies: - '@vue/compiler-core': 3.4.26 - '@vue/shared': 3.4.26 + '@vue/compiler-core': 3.4.37 + '@vue/shared': 3.4.37 - '@vue/compiler-sfc@3.4.26': + '@vue/compiler-sfc@3.4.37': dependencies: - '@babel/parser': 7.24.5 - '@vue/compiler-core': 3.4.26 - '@vue/compiler-dom': 3.4.26 - '@vue/compiler-ssr': 3.4.26 - '@vue/shared': 3.4.26 + '@babel/parser': 7.24.7 + '@vue/compiler-core': 3.4.37 + '@vue/compiler-dom': 3.4.37 + '@vue/compiler-ssr': 3.4.37 + '@vue/shared': 3.4.37 estree-walker: 2.0.2 magic-string: 0.30.10 - postcss: 8.4.38 + postcss: 8.4.40 source-map-js: 1.2.0 - '@vue/compiler-ssr@3.4.26': + '@vue/compiler-ssr@3.4.37': dependencies: - '@vue/compiler-dom': 3.4.26 - '@vue/shared': 3.4.26 + '@vue/compiler-dom': 3.4.37 + '@vue/shared': 3.4.37 + + '@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.4.5)': + '@vue/language-core@2.0.16(typescript@5.5.4)': dependencies: '@volar/language-core': 2.2.0 - '@vue/compiler-dom': 3.4.25 - '@vue/shared': 3.4.25 + '@vue/compiler-dom': 3.4.34 + '@vue/shared': 3.4.34 computeds: 0.0.1 minimatch: 9.0.4 path-browserify: 1.0.1 vue-template-compiler: 2.7.14 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 - '@vue/reactivity@3.4.26': + '@vue/language-core@2.0.29(typescript@5.5.4)': dependencies: - '@vue/shared': 3.4.26 + '@volar/language-core': 2.4.0-alpha.18 + '@vue/compiler-dom': 3.4.34 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.4.34 + computeds: 0.0.1 + minimatch: 9.0.4 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.5.4 + + '@vue/reactivity@3.4.37': + dependencies: + '@vue/shared': 3.4.37 - '@vue/runtime-core@3.4.26': + '@vue/runtime-core@3.4.37': dependencies: - '@vue/reactivity': 3.4.26 - '@vue/shared': 3.4.26 + '@vue/reactivity': 3.4.37 + '@vue/shared': 3.4.37 - '@vue/runtime-dom@3.4.26': + '@vue/runtime-dom@3.4.37': dependencies: - '@vue/runtime-core': 3.4.26 - '@vue/shared': 3.4.26 + '@vue/reactivity': 3.4.37 + '@vue/runtime-core': 3.4.37 + '@vue/shared': 3.4.37 csstype: 3.1.3 - '@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5))': + '@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4))': dependencies: - '@vue/compiler-ssr': 3.4.26 - '@vue/shared': 3.4.26 - vue: 3.4.26(typescript@5.4.5) + '@vue/compiler-ssr': 3.4.37 + '@vue/shared': 3.4.37 + vue: 3.4.37(typescript@5.5.4) - '@vue/shared@3.4.21': {} + '@vue/shared@3.4.31': {} - '@vue/shared@3.4.25': {} + '@vue/shared@3.4.34': {} - '@vue/shared@3.4.26': {} + '@vue/shared@3.4.37': {} - '@vue/test-utils@2.4.1(@vue/server-renderer@3.4.26(vue@3.4.26(typescript@5.4.5)))(vue@3.4.26(typescript@5.4.5))': + '@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))': dependencies: js-beautify: 1.14.9 - vue: 3.4.26(typescript@5.4.5) + vue: 3.4.37(typescript@5.5.4) vue-component-type-helpers: 1.8.4 optionalDependencies: - '@vue/server-renderer': 3.4.26(vue@3.4.26(typescript@5.4.5)) + '@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.20.2)': + '@yarnpkg/esbuild-plugin-pnp@3.0.0-rc.15(esbuild@0.19.11)': dependencies: - esbuild: 0.20.2 - tslib: 2.6.2 + esbuild: 0.19.11 + tslib: 2.6.3 '@yarnpkg/fslib@2.10.3': dependencies: @@ -16641,22 +17517,22 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-import-assertions@1.9.0(acorn@8.11.3): + acorn-import-assertions@1.9.0(acorn@8.12.1): dependencies: - acorn: 8.11.3 + acorn: 8.12.1 optional: true - acorn-import-attributes@1.9.5(acorn@8.11.3): + acorn-import-attributes@1.9.5(acorn@8.12.1): dependencies: - acorn: 8.11.3 + acorn: 8.12.1 acorn-jsx@5.3.2(acorn@7.4.1): dependencies: acorn: 7.4.1 - acorn-jsx@5.3.2(acorn@8.11.3): + acorn-jsx@5.3.2(acorn@8.12.1): dependencies: - acorn: 8.11.3 + acorn: 8.12.1 acorn-walk@7.2.0: {} @@ -16664,19 +17540,19 @@ snapshots: acorn@7.4.1: {} - acorn@8.11.3: {} + acorn@8.12.1: {} address@1.2.2: {} agent-base@6.0.2: dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color agent-base@7.1.0: dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16690,7 +17566,7 @@ snapshots: clean-stack: 5.2.0 indent-string: 5.0.0 - aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/34bf4e1530efcf1efa855bd04e2dab39735e1b02: + aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9: dependencies: '@aiscript-dev/aiscript-languageserver': https://github.com/aiscript-dev/aiscript-languageserver/releases/download/0.1.6/aiscript-dev-aiscript-languageserver-0.1.6.tgz vscode-languageclient: 9.0.1 @@ -16699,7 +17575,15 @@ snapshots: optionalDependencies: ajv: 8.13.0 - ajv-formats@2.1.1(ajv@8.13.0): + ajv-draft-04@1.0.0(ajv@8.17.1): + 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 @@ -16710,6 +17594,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + ajv@8.13.0: dependencies: fast-deep-equal: 3.1.3 @@ -16717,6 +17608,13 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.1 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -16758,7 +17656,7 @@ snapshots: archiver-utils@5.0.2: dependencies: - glob: 10.3.12 + glob: 10.3.10 graceful-fs: 4.2.11 is-stream: 2.0.1 lazystream: 1.0.1 @@ -16796,6 +17694,10 @@ snapshots: dependencies: deep-equal: 2.2.0 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-buffer-byte-length@1.0.0: dependencies: call-bind: 1.0.2 @@ -16863,7 +17765,7 @@ snapshots: dependencies: pvtsutils: 1.3.5 pvutils: 1.1.3 - tslib: 2.6.2 + tslib: 2.6.3 assert-never@1.2.1: {} @@ -16881,7 +17783,7 @@ snapshots: ast-types@0.16.1: dependencies: - tslib: 2.6.2 + tslib: 2.6.3 astral-regex@2.0.0: {} @@ -16891,6 +17793,8 @@ snapshots: dependencies: tslib: 2.6.2 + async@0.2.10: {} + async@3.2.4: {} asynckit@0.4.0: {} @@ -16905,12 +17809,12 @@ snapshots: dependencies: '@fastify/error': 3.4.0 archy: 1.0.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) fastq: 1.17.1 transitivePeerDependencies: - supports-color - aws-sdk-client-mock@3.0.1: + aws-sdk-client-mock@4.0.1: dependencies: '@types/sinon': 10.0.13 sinon: 16.1.3 @@ -16922,21 +17826,21 @@ snapshots: axios@0.24.0: dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2(debug@4.3.5) transitivePeerDependencies: - debug axios@1.6.0: dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2(debug@4.3.5) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - axios@1.6.2(debug@4.3.4): + axios@1.6.2(debug@4.3.5): dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2(debug@4.3.5) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -16944,9 +17848,9 @@ snapshots: b4a@1.6.4: {} - babel-core@7.0.0-bridge.0(@babel/core@7.24.0): + babel-core@7.0.0-bridge.0(@babel/core@7.24.7): dependencies: - '@babel/core': 7.24.0 + '@babel/core': 7.24.7 babel-jest@29.7.0(@babel/core@7.23.5): dependencies: @@ -16961,6 +17865,20 @@ snapshots: transitivePeerDependencies: - supports-color + babel-jest@29.7.0(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.0 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.24.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + optional: true + babel-plugin-istanbul@6.1.1: dependencies: '@babel/helper-plugin-utils': 7.22.5 @@ -16974,31 +17892,31 @@ snapshots: babel-plugin-jest-hoist@29.6.3: dependencies: '@babel/template': 7.24.0 - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.20.0 - babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.24.0): + babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.7): dependencies: - '@babel/compat-data': 7.23.5 - '@babel/core': 7.24.0 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.0) + '@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.8.6(@babel/core@7.24.0): + babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.7): dependencies: - '@babel/core': 7.24.0 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.0) - core-js-compat: 3.33.3 + '@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.5.3(@babel/core@7.24.0): + babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.7): dependencies: - '@babel/core': 7.24.0 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.0) + '@babel/core': 7.24.7 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.7) transitivePeerDependencies: - supports-color @@ -17018,15 +17936,39 @@ snapshots: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.5) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.5) + babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@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-top-level-await': 7.14.5(@babel/core@7.24.7) + optional: true + babel-preset-jest@29.6.3(@babel/core@7.23.5): dependencies: '@babel/core': 7.23.5 babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.5) + babel-preset-jest@29.6.3(@babel/core@7.24.7): + dependencies: + '@babel/core': 7.24.7 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.7) + optional: true + babel-walk@3.0.0-canary-5: dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 bail@2.0.2: {} @@ -17078,23 +18020,6 @@ snapshots: bn.js@4.12.0: {} - body-parser@1.20.1: - 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.11.0 - raw-body: 2.5.1 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - body-parser@1.20.2: dependencies: bytes: 3.1.2 @@ -17133,6 +18058,10 @@ snapshots: dependencies: fill-range: 7.0.1 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + broadcast-channel@7.0.0: dependencies: '@babel/runtime': 7.23.4 @@ -17142,10 +18071,6 @@ snapshots: browser-assert@1.2.1: {} - browserify-zlib@0.1.4: - dependencies: - pako: 0.2.9 - browserslist@4.22.2: dependencies: caniuse-lite: 1.0.30001566 @@ -17196,14 +18121,19 @@ snapshots: node-gyp-build: 4.6.0 optional: true - bullmq@5.7.8: + bufferutil@4.0.8: + dependencies: + node-gyp-build: 4.8.1 + optional: true + + bullmq@5.10.4: dependencies: cron-parser: 4.8.1 ioredis: 5.4.1 msgpackr: 1.10.1 node-abort-controller: 3.1.1 semver: 7.6.0 - tslib: 2.6.2 + tslib: 2.6.3 uuid: 9.0.1 transitivePeerDependencies: - supports-color @@ -17223,10 +18153,10 @@ snapshots: cacache@18.0.0: dependencies: '@npmcli/fs': 3.1.0 - fs-minipass: 3.0.2 - glob: 10.3.12 + fs-minipass: 3.0.3 + glob: 10.3.10 lru-cache: 10.2.2 - minipass: 7.0.4 + minipass: 7.1.2 minipass-collect: 1.0.2 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 @@ -17249,6 +18179,16 @@ snapshots: normalize-url: 8.0.0 responselike: 3.0.0 + cacheable-request@12.0.1: + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 9.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.1 + responselike: 3.0.0 + cacheable-request@7.0.2: dependencies: clone-response: 1.0.3 @@ -17343,26 +18283,26 @@ snapshots: dependencies: is-regex: 1.1.4 - chart.js@4.4.2: + chart.js@4.4.3: dependencies: '@kurkle/color': 0.3.2 - chartjs-adapter-date-fns@3.0.0(chart.js@4.4.2)(date-fns@2.30.0): + chartjs-adapter-date-fns@3.0.0(chart.js@4.4.3)(date-fns@2.30.0): dependencies: - chart.js: 4.4.2 + chart.js: 4.4.3 date-fns: 2.30.0 - chartjs-chart-matrix@2.0.1(chart.js@4.4.2): + chartjs-chart-matrix@2.0.1(chart.js@4.4.3): dependencies: - chart.js: 4.4.2 + chart.js: 4.4.3 - chartjs-plugin-gradient@0.6.1(chart.js@4.4.2): + chartjs-plugin-gradient@0.6.1(chart.js@4.4.3): dependencies: - chart.js: 4.4.2 + chart.js: 4.4.3 - chartjs-plugin-zoom@2.0.1(chart.js@4.4.2): + chartjs-plugin-zoom@2.0.1(chart.js@4.4.3): dependencies: - chart.js: 4.4.2 + chart.js: 4.4.3 hammerjs: 2.0.8 check-error@1.0.3: @@ -17402,11 +18342,9 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chownr@1.1.4: {} - chownr@2.0.0: {} - chromatic@11.3.0: {} + chromatic@11.5.6: {} ci-info@3.7.1: {} @@ -17431,8 +18369,6 @@ snapshots: parse5-htmlparser2-tree-adapter: 6.0.1 yargs: 16.2.0 - cli-spinners@2.7.0: {} - cli-spinners@2.9.2: {} cli-table3@0.6.3: @@ -17532,7 +18468,7 @@ snapshots: commondir@1.0.1: {} - compare-versions@6.1.0: {} + compare-versions@6.1.1: {} compress-commons@6.0.2: dependencies: @@ -17585,8 +18521,8 @@ snapshots: constantinople@4.0.1: dependencies: - '@babel/parser': 7.23.9 - '@babel/types': 7.23.5 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 content-disposition@0.5.4: dependencies: @@ -17604,7 +18540,7 @@ snapshots: cookie@0.6.0: {} - core-js-compat@3.33.3: + core-js-compat@3.37.1: dependencies: browserslist: 4.23.0 @@ -17624,13 +18560,13 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.3.0 - create-jest@29.7.0(@types/node@20.12.7): + create-jest@29.7.0(@types/node@20.14.12): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.12.7) + jest-config: 29.7.0(@types/node@20.14.12) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -17643,10 +18579,10 @@ snapshots: dependencies: luxon: 3.3.0 - cropperjs@2.0.0-beta.5: + cropperjs@2.0.0-rc.1: dependencies: - '@cropper/elements': 2.0.0-beta.5 - '@cropper/utils': 2.0.0-beta.5 + '@cropper/elements': 2.0.0-rc.1 + '@cropper/utils': 2.0.0-rc.1 cross-env@7.0.3: dependencies: @@ -17676,136 +18612,93 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - crypto-random-string@2.0.0: {} - - css-declaration-sorter@7.2.0(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - - css-select@5.1.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.0.1 - nth-check: 2.1.1 - - css-tree@2.2.1: - dependencies: - mdn-data: 2.0.28 - source-map-js: 1.0.2 - - css-tree@2.3.1: - dependencies: - mdn-data: 2.0.30 - source-map-js: 1.0.2 - - css-what@6.1.0: {} - - css.escape@1.5.1: {} - - cssesc@3.0.0: {} - - cssnano-preset-default@6.1.2(postcss@8.4.38): - dependencies: - browserslist: 4.23.0 - css-declaration-sorter: 7.2.0(postcss@8.4.38) - cssnano-utils: 4.0.2(postcss@8.4.38) - postcss: 8.4.38 - postcss-calc: 9.0.1(postcss@8.4.38) - postcss-colormin: 6.1.0(postcss@8.4.38) - postcss-convert-values: 6.1.0(postcss@8.4.38) - postcss-discard-comments: 6.0.2(postcss@8.4.38) - postcss-discard-duplicates: 6.0.3(postcss@8.4.38) - postcss-discard-empty: 6.0.3(postcss@8.4.38) - postcss-discard-overridden: 6.0.2(postcss@8.4.38) - postcss-merge-longhand: 6.0.5(postcss@8.4.38) - postcss-merge-rules: 6.1.1(postcss@8.4.38) - postcss-minify-font-values: 6.1.0(postcss@8.4.38) - postcss-minify-gradients: 6.0.3(postcss@8.4.38) - postcss-minify-params: 6.1.0(postcss@8.4.38) - postcss-minify-selectors: 6.0.4(postcss@8.4.38) - postcss-normalize-charset: 6.0.2(postcss@8.4.38) - postcss-normalize-display-values: 6.0.2(postcss@8.4.38) - postcss-normalize-positions: 6.0.2(postcss@8.4.38) - postcss-normalize-repeat-style: 6.0.2(postcss@8.4.38) - postcss-normalize-string: 6.0.2(postcss@8.4.38) - postcss-normalize-timing-functions: 6.0.2(postcss@8.4.38) - postcss-normalize-unicode: 6.1.0(postcss@8.4.38) - postcss-normalize-url: 6.0.2(postcss@8.4.38) - postcss-normalize-whitespace: 6.0.2(postcss@8.4.38) - postcss-ordered-values: 6.0.2(postcss@8.4.38) - postcss-reduce-initial: 6.1.0(postcss@8.4.38) - postcss-reduce-transforms: 6.0.2(postcss@8.4.38) - postcss-svgo: 6.0.3(postcss@8.4.38) - postcss-unique-selectors: 6.0.4(postcss@8.4.38) - - cssnano-utils@4.0.2(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - - cssnano@6.1.2(postcss@8.4.38): - dependencies: - cssnano-preset-default: 6.1.2(postcss@8.4.38) - lilconfig: 3.1.1 - postcss: 8.4.38 - - csso@5.0.5: - dependencies: - css-tree: 2.2.1 - - cssstyle@4.0.1: + crypto-random-string@4.0.0: dependencies: - rrweb-cssom: 0.6.0 - - csstype@3.1.3: {} + type-fest: 1.4.0 - cypress@13.7.3: + css-declaration-sorter@7.2.0(postcss@8.4.40): dependencies: - '@cypress/request': 3.0.0 - '@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.4(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 + postcss: 8.4.40 - cypress@13.8.1: + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.0.1 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.0 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.0 + + css-what@6.1.0: {} + + css.escape@1.5.1: {} + + cssesc@3.0.0: {} + + cssnano-preset-default@6.1.2(postcss@8.4.40): + 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) + lilconfig: 3.1.1 + postcss: 8.4.40 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + cssstyle@4.0.1: + dependencies: + rrweb-cssom: 0.6.0 + + csstype@3.1.3: {} + + cypress@13.13.1: dependencies: '@cypress/request': 3.0.0 '@cypress/xvfb': 1.2.4(supports-color@8.1.1) @@ -17823,7 +18716,7 @@ snapshots: commander: 6.2.1 common-tags: 1.8.2 dayjs: 1.11.10 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) enquirer: 2.3.6 eventemitter2: 6.4.7 execa: 4.1.0 @@ -17885,7 +18778,7 @@ snapshots: optionalDependencies: supports-color: 5.5.0 - debug@4.3.4(supports-color@8.1.1): + debug@4.3.5(supports-color@8.1.1): dependencies: ms: 2.1.2 optionalDependencies: @@ -17978,17 +18871,6 @@ snapshots: defu@6.1.4: {} - del@6.1.1: - dependencies: - globby: 11.1.0 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 4.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - delayed-stream@1.0.0: {} denque@2.1.0: {} @@ -18012,7 +18894,7 @@ snapshots: detect-port@1.5.1: dependencies: address: 1.2.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18026,6 +18908,8 @@ snapshots: diff@5.1.0: {} + diff@5.2.0: {} + dijkstrajs@1.0.2: {} dir-glob@3.0.1: @@ -18072,13 +18956,6 @@ snapshots: duplexer@0.1.2: {} - duplexify@3.7.1: - dependencies: - end-of-stream: 1.4.4 - inherits: 2.0.4 - readable-stream: 2.3.7 - stream-shift: 1.0.1 - eastasianwidth@0.2.0: {} ecc-jsbn@0.1.2: @@ -18099,7 +18976,7 @@ snapshots: ee-first@1.1.1: {} - ejs@3.1.9: + ejs@3.1.10: dependencies: jake: 10.8.5 @@ -18134,6 +19011,8 @@ snapshots: entities@4.5.0: {} + entities@5.0.0: {} + env-paths@2.2.1: {} envinfo@7.8.1: {} @@ -18198,7 +19077,7 @@ snapshots: isarray: 2.0.5 stop-iteration-iterator: 1.0.0 - es-module-lexer@0.9.3: {} + es-module-lexer@1.5.4: {} es-set-tostringtag@2.0.1: dependencies: @@ -18218,10 +19097,10 @@ snapshots: esbuild-plugin-alias@0.2.1: {} - esbuild-register@3.5.0(esbuild@0.20.2): + esbuild-register@3.5.0(esbuild@0.19.11): dependencies: - debug: 4.3.4(supports-color@8.1.1) - esbuild: 0.20.2 + debug: 4.3.5(supports-color@8.1.1) + esbuild: 0.19.11 transitivePeerDependencies: - supports-color @@ -18276,31 +19155,58 @@ snapshots: '@esbuild/win32-ia32': 0.19.11 '@esbuild/win32-x64': 0.19.11 - esbuild@0.20.2: + esbuild@0.21.5: optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.23.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.0 + '@esbuild/android-arm': 0.23.0 + '@esbuild/android-arm64': 0.23.0 + '@esbuild/android-x64': 0.23.0 + '@esbuild/darwin-arm64': 0.23.0 + '@esbuild/darwin-x64': 0.23.0 + '@esbuild/freebsd-arm64': 0.23.0 + '@esbuild/freebsd-x64': 0.23.0 + '@esbuild/linux-arm': 0.23.0 + '@esbuild/linux-arm64': 0.23.0 + '@esbuild/linux-ia32': 0.23.0 + '@esbuild/linux-loong64': 0.23.0 + '@esbuild/linux-mips64el': 0.23.0 + '@esbuild/linux-ppc64': 0.23.0 + '@esbuild/linux-riscv64': 0.23.0 + '@esbuild/linux-s390x': 0.23.0 + '@esbuild/linux-x64': 0.23.0 + '@esbuild/netbsd-x64': 0.23.0 + '@esbuild/openbsd-arm64': 0.23.0 + '@esbuild/openbsd-x64': 0.23.0 + '@esbuild/sunos-x64': 0.23.0 + '@esbuild/win32-arm64': 0.23.0 + '@esbuild/win32-ia32': 0.23.0 + '@esbuild/win32-x64': 0.23.0 escalade@3.1.1: {} @@ -18347,91 +19253,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.53.0): - dependencies: - debug: 3.2.7(supports-color@8.1.1) - optionalDependencies: - '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) - eslint: 8.53.0 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.8.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): - dependencies: - debug: 3.2.7(supports-color@8.1.1) - optionalDependencies: - '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3) - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.8.0(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): - dependencies: - debug: 3.2.7(supports-color@8.1.1) - optionalDependencies: - '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5) - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint@8.53.0): + 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): dependencies: - array-includes: 3.1.7 - array.prototype.findlastindex: 1.2.3 - 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: 8.53.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.53.0) - hasown: 2.0.0 - is-core-module: 2.13.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 - semver: 6.3.1 - tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.11.0(eslint@8.53.0)(typescript@5.3.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0): - dependencies: - array-includes: 3.1.7 - array.prototype.findlastindex: 1.2.3 - 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: 8.57.0 + '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4) + eslint: 9.8.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) - hasown: 2.0.0 - is-core-module: 2.13.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 - semver: 6.3.1 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3) transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.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): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -18439,9 +19271,9 @@ snapshots: array.prototype.flatmap: 1.3.2 debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 8.57.0 + eslint: 9.8.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + 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 is-glob: 4.0.3 @@ -18452,22 +19284,22 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.7.1(eslint@8.57.0)(typescript@5.4.5) + '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.5.4) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-vue@9.25.0(eslint@8.57.0): + eslint-plugin-vue@9.27.0(eslint@9.8.0): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - eslint: 8.57.0 + '@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.15 + postcss-selector-parser: 6.0.16 semver: 7.6.0 - vue-eslint-parser: 9.4.2(eslint@8.57.0) + vue-eslint-parser: 9.4.3(eslint@9.8.0) xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color @@ -18479,28 +19311,35 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-scope@8.0.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-visitor-keys@3.4.3: {} - eslint@8.53.0: + eslint-visitor-keys@4.0.0: {} + + eslint@8.57.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) - '@eslint-community/regexpp': 4.10.0 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.11.0 '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.53.0 - '@humanwhocodes/config-array': 0.11.13 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - esquery: 1.4.2 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 6.0.1 @@ -18518,59 +19357,61 @@ snapshots: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color - eslint@8.57.0: + eslint@9.8.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.10.0 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/config-array': 0.17.1 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.8.0 '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@8.1.1) - doctrine: 3.0.0 + debug: 4.3.5(supports-color@8.1.1) escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.4.2 + eslint-scope: 8.0.2 + eslint-visitor-keys: 4.0.0 + espree: 10.1.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 ignore: 5.3.1 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.3 + optionator: 0.9.4 strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color + espree@10.1.0: + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.0.0 + espree@9.6.1: dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -18579,6 +19420,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -18656,7 +19501,7 @@ snapshots: human-signals: 3.0.1 is-stream: 3.0.0 merge-stream: 2.0.0 - npm-run-path: 5.1.0 + npm-run-path: 5.3.0 onetime: 6.0.0 signal-exit: 3.0.7 strip-final-newline: 3.0.0 @@ -18673,6 +19518,21 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + execa@9.3.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 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 5.3.0 + pretty-ms: 9.0.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.0.2 + executable@4.1.1: dependencies: pify: 2.3.0 @@ -18689,42 +19549,6 @@ snapshots: exponential-backoff@3.1.1: {} - express@4.18.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.1 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.5.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.19.2: dependencies: accepts: 1.3.8 @@ -18774,7 +19598,7 @@ snapshots: extract-zip@2.0.1(supports-color@8.1.1): dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -18798,15 +19622,15 @@ snapshots: '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 + micromatch: 4.0.7 fast-json-stable-stringify@2.1.0: {} fast-json-stringify@5.8.0: dependencies: '@fastify/deepmerge': 1.3.0 - ajv: 8.13.0 - ajv-formats: 2.1.1(ajv@8.13.0) + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) fast-deep-equal: 3.1.3 fast-uri: 2.2.0 rfdc: 1.3.0 @@ -18823,10 +19647,16 @@ snapshots: fast-uri@2.2.0: {} + fast-uri@3.0.1: {} + fast-xml-parser@4.2.5: dependencies: strnum: 1.0.5 + fast-xml-parser@4.4.0: + dependencies: + strnum: 1.0.5 + fastify-multer@2.0.3: dependencies: '@fastify/busboy': 1.2.1 @@ -18850,7 +19680,7 @@ snapshots: raw-body: 2.5.2 secure-json-parse: 2.7.0 - fastify@4.26.2: + fastify@4.28.1: dependencies: '@fastify/ajv-compiler': 3.5.0 '@fastify/error': 3.4.0 @@ -18861,20 +19691,16 @@ snapshots: fast-json-stringify: 5.8.0 find-my-way: 8.2.0 light-my-request: 5.11.0 - pino: 8.17.0 + pino: 9.2.0 process-warning: 3.0.0 proxy-addr: 2.0.7 rfdc: 1.3.0 secure-json-parse: 2.7.0 semver: 7.6.0 - toad-cache: 3.3.0 + toad-cache: 3.7.0 transitivePeerDependencies: - supports-color - fastq@1.15.0: - dependencies: - reusify: 1.0.4 - fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -18883,6 +19709,10 @@ 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 @@ -18902,9 +19732,17 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.0.0 + file-entry-cache@6.0.1: dependencies: - flat-cache: 3.0.4 + flat-cache: 3.2.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 file-system-cache@2.3.0: dependencies: @@ -18917,11 +19755,11 @@ snapshots: strtok3: 7.0.0 token-types: 5.0.1 - file-type@19.0.0: + file-type@19.3.0: dependencies: - readable-web-to-node-stream: 3.0.2 - strtok3: 7.0.0 - token-types: 5.0.1 + strtok3: 8.0.1 + token-types: 6.0.0 + uint8array-extras: 1.4.0 filelist@1.0.4: dependencies: @@ -18939,6 +19777,10 @@ snapshots: dependencies: to-regex-range: 5.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + finalhandler@1.2.0: dependencies: debug: 2.6.9 @@ -18998,23 +19840,29 @@ snapshots: ps-list: 8.1.1 taskkill: 5.0.0 - flat-cache@3.0.4: + flat-cache@3.2.0: dependencies: - flatted: 3.2.7 + flatted: 3.3.1 + keyv: 4.5.4 rimraf: 3.0.2 - flatted@3.2.7: {} + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} flow-parser@0.202.0: {} - fluent-ffmpeg@2.1.2: + fluent-ffmpeg@2.1.3: dependencies: - async: 3.2.4 + async: 0.2.10 which: 1.3.1 - follow-redirects@1.15.2(debug@4.3.4): + follow-redirects@1.15.2(debug@4.3.5): optionalDependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) for-each@0.3.3: dependencies: @@ -19053,8 +19901,6 @@ snapshots: from@0.1.7: {} - fs-constants@1.0.0: {} - fs-extra@11.1.1: dependencies: graceful-fs: 4.2.11 @@ -19084,9 +19930,9 @@ snapshots: dependencies: minipass: 3.3.6 - fs-minipass@3.0.2: + fs-minipass@3.0.3: dependencies: - minipass: 5.0.0 + minipass: 7.1.2 fs.realpath@1.0.0: {} @@ -19117,8 +19963,6 @@ snapshots: has-proto: 1.0.1 has-symbols: 1.0.3 - get-npm-tarball-url@2.0.3: {} - get-package-type@0.1.0: {} get-stream@3.0.0: {} @@ -19131,6 +19975,11 @@ snapshots: get-stream@8.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-symbol-description@1.0.0: dependencies: call-bind: 1.0.2 @@ -19185,13 +20034,23 @@ snapshots: minipass: 7.0.4 path-scurry: 1.10.1 - glob@10.3.12: + glob@10.4.2: dependencies: foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.2 + jackspeak: 3.4.0 + minimatch: 9.0.4 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + + glob@11.0.0: + dependencies: + foreground-child: 3.1.1 + jackspeak: 4.0.1 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 2.0.0 glob@7.2.3: dependencies: @@ -19220,6 +20079,10 @@ snapshots: dependencies: type-fest: 0.20.2 + globals@14.0.0: {} + + globals@15.8.0: {} + globalthis@1.0.3: dependencies: define-properties: 1.2.0 @@ -19233,6 +20096,15 @@ 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 @@ -19265,19 +20137,19 @@ snapshots: p-cancelable: 3.0.0 responselike: 3.0.0 - got@14.2.1: + got@14.4.2: dependencies: - '@sindresorhus/is': 6.1.0 + '@sindresorhus/is': 7.0.0 '@szmarczak/http-timer': 5.0.1 cacheable-lookup: 7.0.0 - cacheable-request: 10.2.14 + cacheable-request: 12.0.1 decompress-response: 6.0.0 form-data-encoder: 4.0.2 - get-stream: 8.0.1 http2-wrapper: 2.2.1 lowercase-keys: 3.0.0 p-cancelable: 4.0.1 responselike: 3.0.0 + type-fest: 4.20.1 graceful-fs@4.2.11: {} @@ -19287,15 +20159,6 @@ snapshots: graphql@16.8.1: {} - gunzip-maybe@1.4.2: - dependencies: - browserify-zlib: 0.1.4 - is-deflate: 1.0.0 - is-gzip: 1.0.0 - peek-stream: 1.1.3 - pumpify: 1.5.1 - through2: 2.0.5 - hammerjs@2.0.8: {} handlebars@4.7.7: @@ -19316,6 +20179,12 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 + happy-dom@15.6.1: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + hard-rejection@2.1.0: {} has-bigints@1.0.2: {} @@ -19407,10 +20276,10 @@ snapshots: http-link-header@1.1.3: {} - http-proxy-agent@7.0.0: + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -19432,17 +20301,31 @@ snapshots: http_ece@1.2.0: {} - https-proxy-agent@5.0.1: + 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 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.4: dependencies: - agent-base: 6.0.2 - debug: 4.3.4(supports-color@8.1.1) + agent-base: 7.1.0 + debug: 4.3.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.2: + https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -19454,6 +20337,8 @@ snapshots: human-signals@5.0.0: {} + human-signals@7.0.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -19468,9 +20353,9 @@ snapshots: ignore-by-default@1.0.1: {} - ignore-walk@6.0.4: + ignore-walk@6.0.5: dependencies: - minimatch: 9.0.3 + minimatch: 9.0.4 ignore@5.3.1: {} @@ -19481,20 +20366,20 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@1.4.2: + import-in-the-middle@1.10.0: dependencies: - acorn: 8.11.3 - acorn-import-assertions: 1.9.0(acorn@8.11.3) + acorn: 8.12.1 + acorn-import-attributes: 1.9.5(acorn@8.12.1) cjs-module-lexer: 1.2.2 module-details-from-path: 1.0.3 - optional: true - import-in-the-middle@1.7.4: + import-in-the-middle@1.7.1: dependencies: - acorn: 8.11.3 - acorn-import-attributes: 1.9.5(acorn@8.11.3) + acorn: 8.12.1 + acorn-import-assertions: 1.9.0(acorn@8.12.1) cjs-module-lexer: 1.2.2 module-details-from-path: 1.0.3 + optional: true import-lazy@4.0.0: {} @@ -19536,7 +20421,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -19546,15 +20431,14 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@7.1.0: + ip-address@9.0.5: dependencies: jsbn: 1.1.0 - sprintf-js: 1.1.2 + sprintf-js: 1.1.3 - ip-cidr@3.1.0: + ip-cidr@4.0.1: dependencies: - ip-address: 7.1.0 - jsbn: 1.1.0 + ip-address: 9.0.5 ip-regex@4.3.0: {} @@ -19610,8 +20494,6 @@ snapshots: dependencies: has-tostringtag: 1.0.0 - is-deflate@1.0.0: {} - is-docker@2.2.1: {} is-expression@4.0.0: @@ -19635,8 +20517,6 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-gzip@1.0.0: {} - is-installed-globally@0.4.0: dependencies: global-dirs: 3.0.1 @@ -19667,8 +20547,6 @@ snapshots: is-number@7.0.0: {} - is-path-cwd@2.2.0: {} - is-path-inside@3.0.3: {} is-plain-obj@1.1.0: {} @@ -19702,13 +20580,15 @@ snapshots: is-stream@3.0.0: {} + is-stream@4.0.1: {} + is-string@1.0.7: dependencies: has-tostringtag: 1.0.0 - is-svg@5.0.0: + is-svg@5.0.1: dependencies: - fast-xml-parser: 4.2.5 + fast-xml-parser: 4.4.0 is-symbol@1.0.4: dependencies: @@ -19726,6 +20606,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unicode-supported@2.0.0: {} + is-weakmap@2.0.1: {} is-weakref@1.0.2: @@ -19759,8 +20641,8 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: - '@babel/core': 7.24.0 - '@babel/parser': 7.24.5 + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -19769,8 +20651,8 @@ snapshots: istanbul-lib-instrument@6.0.0: dependencies: - '@babel/core': 7.24.0 - '@babel/parser': 7.24.5 + '@babel/core': 7.24.7 + '@babel/parser': 7.24.7 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.0 @@ -19785,12 +20667,20 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: - supports-color + istanbul-lib-source-maps@5.0.4: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.5(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + istanbul-reports@3.1.6: dependencies: html-escaper: 2.0.2 @@ -19804,6 +20694,18 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@3.4.0: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@4.0.1: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jake@10.8.5: dependencies: async: 3.2.4 @@ -19823,7 +20725,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 chalk: 4.1.2 co: 4.6.0 dedent: 1.3.0 @@ -19843,16 +20745,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.12.7): + jest-cli@29.7.0(@types/node@20.14.12): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.12.7) + create-jest: 29.7.0(@types/node@20.14.12) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.12.7) + jest-config: 29.7.0(@types/node@20.14.12) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -19862,7 +20764,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.12.7): + jest-config@29.7.0(@types/node@20.14.12): dependencies: '@babel/core': 7.23.5 '@jest/test-sequencer': 29.7.0 @@ -19881,13 +20783,13 @@ snapshots: jest-runner: 29.7.0 jest-util: 29.7.0 jest-validate: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 parse-json: 5.2.0 pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -19916,7 +20818,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -19933,14 +20835,14 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 20.12.7 + '@types/node': 20.14.12 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 jest-regex-util: 29.6.3 jest-util: 29.7.0 jest-worker: 29.7.0 - micromatch: 4.0.5 + micromatch: 4.0.7 walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 @@ -19964,7 +20866,7 @@ snapshots: '@types/stack-utils': 2.0.1 chalk: 4.1.2 graceful-fs: 4.2.11 - micromatch: 4.0.5 + micromatch: 4.0.7 pretty-format: 29.7.0 slash: 3.0.0 stack-utils: 2.0.6 @@ -19972,7 +20874,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -20007,7 +20909,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -20035,7 +20937,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -20056,10 +20958,10 @@ snapshots: jest-snapshot@29.7.0: dependencies: '@babel/core': 7.23.5 - '@babel/generator': 7.23.6 + '@babel/generator': 7.24.7 '@babel/plugin-syntax-jsx': 7.23.3(@babel/core@7.23.5) '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.5) - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 '@jest/expect-utils': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 @@ -20081,7 +20983,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.11 @@ -20100,7 +21002,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.7 + '@types/node': 20.14.12 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -20114,17 +21016,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.12.7): + jest@29.7.0(@types/node@20.14.12): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.12.7) + jest-cli: 29.7.0(@types/node@20.14.12) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -20152,6 +21054,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.0: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -20167,60 +21071,89 @@ snapshots: jschardet@3.0.0: {} - jscodeshift@0.15.1(@babel/preset-env@7.23.5(@babel/core@7.24.0)): - dependencies: - '@babel/core': 7.24.0 - '@babel/parser': 7.24.5 - '@babel/plugin-transform-class-properties': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.24.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-optional-chaining': 7.23.4(@babel/core@7.24.0) - '@babel/plugin-transform-private-methods': 7.23.3(@babel/core@7.24.0) - '@babel/preset-flow': 7.23.3(@babel/core@7.24.0) - '@babel/preset-typescript': 7.23.3(@babel/core@7.24.0) - '@babel/register': 7.22.15(@babel/core@7.24.0) - babel-core: 7.0.0-bridge.0(@babel/core@7.24.0) + jscodeshift@0.15.1(@babel/preset-env@7.24.7(@babel/core@7.24.7)): + 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.5 + micromatch: 4.0.7 neo-async: 2.6.2 node-dir: 0.1.17 - recast: 0.23.4 + recast: 0.23.6 temp: 0.8.4 write-file-atomic: 2.4.3 optionalDependencies: - '@babel/preset-env': 7.23.5(@babel/core@7.24.0) + '@babel/preset-env': 7.24.7(@babel/core@7.24.7) transitivePeerDependencies: - supports-color - jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): + jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3): dependencies: 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.0 - https-proxy-agent: 7.0.2 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.9 + nwsapi: 2.2.12 parse5: 7.1.2 - rrweb-cssom: 0.6.0 + 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.7)(utf-8-validate@6.0.3) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4): + dependencies: + 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.3 + 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.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + 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 jsesc@0.5.0: {} @@ -20269,9 +21202,9 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonld@8.3.2(web-streams-polyfill@3.2.1): + jsonld@8.3.2(web-streams-polyfill@4.0.0): dependencies: - '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@3.2.1) + '@digitalbazaar/http-client': 3.4.1(web-streams-polyfill@4.0.0) canonicalize: 1.0.8 lru-cache: 6.0.0 rdf-canonize: 3.4.0 @@ -20296,8 +21229,6 @@ snapshots: jsrsasign@11.1.0: {} - jssha@3.3.1: {} - jstransformer@1.0.0: dependencies: is-promise: 2.2.2 @@ -20328,13 +21259,13 @@ snapshots: kleur@3.0.3: {} - ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@3.2.1): + ky-universal@0.11.0(ky@0.33.3)(web-streams-polyfill@4.0.0): dependencies: abort-controller: 3.0.0 ky: 0.33.3 node-fetch: 3.3.2 optionalDependencies: - web-streams-polyfill: 3.2.1 + web-streams-polyfill: 4.0.0 ky@0.33.3: {} @@ -20380,7 +21311,10 @@ snapshots: optionalDependencies: enquirer: 2.3.6 - local-pkg@0.4.3: {} + local-pkg@0.5.0: + dependencies: + mlly: 1.5.0 + pkg-types: 1.0.3 locate-path@3.0.0: dependencies: @@ -20403,8 +21337,6 @@ snapshots: lodash.isarguments@3.1.0: {} - lodash.isequal@4.5.0: {} - lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} @@ -20443,6 +21375,8 @@ snapshots: lru-cache@10.2.2: {} + lru-cache@11.0.0: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -20472,9 +21406,11 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - magic-string@0.30.7: + magicast@0.3.4: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + source-map-js: 1.2.0 mailcheck@1.1.1: {} @@ -20499,7 +21435,7 @@ snapshots: cacache: 18.0.0 http-cache-semantics: 4.1.1 is-lambda: 1.0.1 - minipass: 7.0.4 + minipass: 7.1.2 minipass-fetch: 3.0.3 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 @@ -20523,7 +21459,7 @@ snapshots: markdown-table@3.0.3: {} - markdown-to-jsx@7.3.2(react@18.3.1): + markdown-to-jsx@7.4.7(react@18.3.1): dependencies: react: 18.3.1 @@ -20638,7 +21574,7 @@ snapshots: media-typer@0.3.0: {} - meilisearch@0.38.0(encoding@0.1.13): + meilisearch@0.41.0(encoding@0.1.13): dependencies: cross-fetch: 3.1.6(encoding@0.1.13) transitivePeerDependencies: @@ -20847,7 +21783,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.0 @@ -20866,9 +21802,9 @@ snapshots: transitivePeerDependencies: - supports-color - micromatch@4.0.5: + micromatch@4.0.7: dependencies: - braces: 3.0.2 + braces: 3.0.3 picomatch: 2.3.1 mime-db@1.52.0: {} @@ -20895,6 +21831,10 @@ snapshots: minimalistic-assert@1.0.1: {} + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@3.0.8: dependencies: brace-expansion: 1.1.11 @@ -20959,13 +21899,13 @@ snapshots: minipass@7.0.4: {} + minipass@7.1.2: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 yallist: 4.0.0 - mkdirp-classic@0.5.3: {} - mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -20976,7 +21916,7 @@ snapshots: mlly@1.5.0: dependencies: - acorn: 8.11.3 + acorn: 8.12.1 pathe: 1.1.2 pkg-types: 1.0.3 ufo: 1.3.2 @@ -21015,18 +21955,18 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.2 - msw-storybook-addon@2.0.1(msw@2.2.14(typescript@5.4.5)): + msw-storybook-addon@2.0.3(msw@2.3.4(typescript@5.5.4)): dependencies: is-node-process: 1.2.0 - msw: 2.2.14(typescript@5.4.5) + msw: 2.3.4(typescript@5.5.4) - msw@2.2.14(typescript@5.4.5): + msw@2.3.4(typescript@5.5.4): 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/cookies': 1.1.0 - '@mswjs/interceptors': 0.26.15 + '@mswjs/interceptors': 0.29.1 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.4 @@ -21037,10 +21977,10 @@ snapshots: outvariant: 1.4.2 path-to-regexp: 6.2.1 strict-event-emitter: 0.5.1 - type-fest: 4.9.0 + type-fest: 4.20.1 yargs: 17.7.2 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 muggle-string@0.4.1: {} @@ -21064,7 +22004,7 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.18.0: {} + nan@2.20.0: {} nanoid@3.3.7: {} @@ -21143,11 +22083,11 @@ snapshots: node-gyp-build@4.8.1: {} - node-gyp@10.0.1: + node-gyp@10.1.0: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.1 - glob: 10.3.12 + glob: 10.3.10 graceful-fs: 4.2.11 make-fetch-happen: 13.0.0 nopt: 7.2.0 @@ -21162,7 +22102,7 @@ snapshots: node-releases@2.0.14: {} - nodemailer@6.9.13: {} + nodemailer@6.9.14: {} nodemon@3.0.2: dependencies: @@ -21177,14 +22117,14 @@ snapshots: touch: 3.1.0 undefsafe: 2.0.5 - nodemon@3.1.0: + nodemon@3.1.4: dependencies: chokidar: 3.5.3 debug: 4.3.4(supports-color@5.5.0) ignore-by-default: 1.0.1 minimatch: 3.1.2 pstree.remy: 1.1.8 - semver: 7.5.4 + semver: 7.6.0 simple-update-notifier: 2.0.0 supports-color: 5.5.0 touch: 3.1.0 @@ -21224,6 +22164,8 @@ snapshots: normalize-url@8.0.0: {} + normalize-url@8.0.1: {} + npm-run-path@2.0.2: dependencies: path-key: 2.0.1 @@ -21236,11 +22178,15 @@ snapshots: dependencies: path-key: 4.0.0 + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 - nwsapi@2.2.9: {} + nwsapi@2.2.12: {} oauth2orize-pkce@0.1.2: {} @@ -21336,30 +22282,30 @@ snapshots: undici: 5.28.2 yargs-parser: 21.1.1 - opentelemetry-instrumentation-fetch-node@1.2.0: + opentelemetry-instrumentation-fetch-node@1.2.3(@opentelemetry/api@1.9.0): dependencies: - '@opentelemetry/api': 1.8.0 - '@opentelemetry/instrumentation': 0.43.0(@opentelemetry/api@1.8.0) - '@opentelemetry/semantic-conventions': 1.24.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.25.1 transitivePeerDependencies: - supports-color optional: true - optionator@0.9.3: + optionator@0.9.4: dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 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.7.0 + cli-spinners: 2.9.2 is-interactive: 1.0.0 is-unicode-supported: 0.1.0 log-symbols: 4.1.0 @@ -21374,9 +22320,9 @@ snapshots: ospath@1.2.2: {} - otpauth@9.2.3: + otpauth@9.3.1: dependencies: - jssha: 3.3.1 + '@noble/hashes': 1.4.0 outvariant@1.4.2: {} @@ -21396,7 +22342,7 @@ snapshots: dependencies: yocto-queue: 0.1.0 - p-limit@4.0.0: + p-limit@5.0.0: dependencies: yocto-queue: 1.0.0 @@ -21427,7 +22373,7 @@ snapshots: p-try@2.2.0: {} - pako@0.2.9: {} + package-json-from-dist@1.0.0: {} parent-module@1.0.1: dependencies: @@ -21435,7 +22381,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.24.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -21444,6 +22390,8 @@ snapshots: dependencies: xtend: 4.0.2 + parse-ms@4.0.0: {} + parse-srcset@1.0.2: {} parse5-htmlparser2-tree-adapter@6.0.1: @@ -21486,10 +22434,15 @@ snapshots: lru-cache: 10.2.2 minipass: 7.0.4 - path-scurry@1.10.2: + path-scurry@1.11.1: dependencies: lru-cache: 10.2.2 - minipass: 7.0.4 + minipass: 7.1.2 + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.0 + minipass: 7.1.2 path-to-regexp@0.1.7: {} @@ -21503,6 +22456,8 @@ snapshots: path-type@4.0.0: {} + path-type@5.0.0: {} + pathe@1.1.2: {} pathval@1.1.1: {} @@ -21513,11 +22468,7 @@ snapshots: peek-readable@5.0.0: {} - peek-stream@1.1.3: - dependencies: - buffer-from: 1.1.2 - duplexify: 3.7.1 - through2: 2.0.5 + peek-readable@5.1.3: {} pend@1.2.0: {} @@ -21532,11 +22483,9 @@ snapshots: pg-numeric@1.0.2: {} - pg-pool@3.6.2(pg@8.11.5): + pg-pool@3.6.2(pg@8.12.0): dependencies: - pg: 8.11.5 - - pg-protocol@1.6.0: {} + pg: 8.12.0 pg-protocol@1.6.1: {} @@ -21558,10 +22507,10 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.3 - pg@8.11.5: + pg@8.12.0: dependencies: pg-connection-string: 2.6.4 - pg-pool: 3.6.2(pg@8.11.5) + pg-pool: 3.6.2(pg@8.12.0) pg-protocol: 1.6.1 pg-types: 2.2.0 pgpass: 1.0.5 @@ -21572,10 +22521,12 @@ snapshots: dependencies: split2: 4.1.0 - photoswipe@5.4.3: {} + photoswipe@5.4.4: {} picocolors@1.0.0: {} + picocolors@1.0.1: {} + picomatch@2.3.1: {} pid-port@1.0.0: @@ -21586,26 +22537,26 @@ snapshots: pify@4.0.1: {} - pino-abstract-transport@1.1.0: + pino-abstract-transport@1.2.0: dependencies: readable-stream: 4.3.0 split2: 4.1.0 - pino-std-serializers@6.1.0: {} + pino-std-serializers@7.0.0: {} - pino@8.17.0: + pino@9.2.0: dependencies: atomic-sleep: 1.0.0 fast-redact: 3.1.2 on-exit-leak-free: 2.1.0 - pino-abstract-transport: 1.1.0 - pino-std-serializers: 6.1.0 - process-warning: 2.2.0 + pino-abstract-transport: 1.2.0 + pino-std-serializers: 7.0.0 + process-warning: 3.0.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.4.2 - sonic-boom: 3.7.0 - thread-stream: 2.3.0 + sonic-boom: 4.0.1 + thread-stream: 3.1.0 pirates@4.0.5: {} @@ -21647,140 +22598,140 @@ snapshots: dependencies: '@babel/runtime': 7.23.4 - postcss-calc@9.0.1(postcss@8.4.38): + postcss-calc@9.0.1(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-selector-parser: 6.0.15 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.4.38): + postcss-colormin@6.1.0(postcss@8.4.40): dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.4.38): + postcss-convert-values@6.1.0(postcss@8.4.40): dependencies: browserslist: 4.23.0 - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-discard-comments@6.0.2(postcss@8.4.38): + postcss-discard-comments@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 - postcss-discard-duplicates@6.0.3(postcss@8.4.38): + postcss-discard-duplicates@6.0.3(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 - postcss-discard-empty@6.0.3(postcss@8.4.38): + postcss-discard-empty@6.0.3(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 - postcss-discard-overridden@6.0.2(postcss@8.4.38): + postcss-discard-overridden@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 - postcss-merge-longhand@6.0.5(postcss@8.4.38): + postcss-merge-longhand@6.0.5(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.4.38) + stylehacks: 6.1.1(postcss@8.4.40) - postcss-merge-rules@6.1.1(postcss@8.4.38): + postcss-merge-rules@6.1.1(postcss@8.4.40): dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 4.0.2(postcss@8.4.40) + postcss: 8.4.40 postcss-selector-parser: 6.0.16 - postcss-minify-font-values@6.1.0(postcss@8.4.38): + postcss-minify-font-values@6.1.0(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.4.38): + postcss-minify-gradients@6.0.3(postcss@8.4.40): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 4.0.2(postcss@8.4.40) + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.4.38): + postcss-minify-params@6.1.0(postcss@8.4.40): dependencies: browserslist: 4.23.0 - cssnano-utils: 4.0.2(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 4.0.2(postcss@8.4.40) + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.4.38): + postcss-minify-selectors@6.0.4(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-selector-parser: 6.0.16 - postcss-normalize-charset@6.0.2(postcss@8.4.38): + postcss-normalize-charset@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 - postcss-normalize-display-values@6.0.2(postcss@8.4.38): + postcss-normalize-display-values@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.4.38): + postcss-normalize-positions@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.4.38): + postcss-normalize-repeat-style@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.4.38): + postcss-normalize-string@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.4.38): + postcss-normalize-timing-functions@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.4.38): + postcss-normalize-unicode@6.1.0(postcss@8.4.40): dependencies: browserslist: 4.23.0 - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.4.38): + postcss-normalize-url@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.4.38): + postcss-normalize-whitespace@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-ordered-values@6.0.2(postcss@8.4.38): + postcss-ordered-values@6.0.2(postcss@8.4.40): dependencies: - cssnano-utils: 4.0.2(postcss@8.4.38) - postcss: 8.4.38 + cssnano-utils: 4.0.2(postcss@8.4.40) + postcss: 8.4.40 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.4.38): + postcss-reduce-initial@6.1.0(postcss@8.4.40): dependencies: browserslist: 4.23.0 caniuse-api: 3.0.0 - postcss: 8.4.38 + postcss: 8.4.40 - postcss-reduce-transforms@6.0.2(postcss@8.4.38): + postcss-reduce-transforms@6.0.2(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 postcss-selector-parser@6.0.15: @@ -21793,15 +22744,15 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@6.0.3(postcss@8.4.38): + postcss-svgo@6.0.3(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-value-parser: 4.2.0 svgo: 3.2.0 - postcss-unique-selectors@6.0.4(postcss@8.4.38): + postcss-unique-selectors@6.0.4(postcss@8.4.40): dependencies: - postcss: 8.4.38 + postcss: 8.4.40 postcss-selector-parser: 6.0.16 postcss-value-parser@4.2.0: {} @@ -21812,6 +22763,12 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.2.0 + postcss@8.4.40: + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + postgres-array@2.0.0: {} postgres-array@3.0.2: {} @@ -21836,7 +22793,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.2.5: {} + prettier@3.3.3: {} pretty-bytes@5.6.0: {} @@ -21854,6 +22811,10 @@ snapshots: pretty-hrtime@1.0.3: {} + pretty-ms@9.0.0: + dependencies: + parse-ms: 4.0.0 + private-ip@2.3.3: dependencies: ip-regex: 4.3.0 @@ -21936,19 +22897,21 @@ snapshots: js-stringify: 1.0.2 pug-runtime: 3.0.1 - pug-code-gen@3.0.2: + pug-code-gen@3.0.3: dependencies: constantinople: 4.0.1 doctypes: 1.1.0 js-stringify: 1.0.2 pug-attrs: 3.0.0 - pug-error: 2.0.0 + pug-error: 2.1.0 pug-runtime: 3.0.1 void-elements: 3.1.0 with: 7.0.2 pug-error@2.0.0: {} + pug-error@2.1.0: {} + pug-filters@4.0.0: dependencies: constantinople: 4.0.1 @@ -21986,9 +22949,9 @@ snapshots: pug-walk@2.0.0: {} - pug@3.0.2: + pug@3.0.3: dependencies: - pug-code-gen: 3.0.2 + pug-code-gen: 3.0.3 pug-filters: 4.0.0 pug-lexer: 5.0.1 pug-linker: 4.0.0 @@ -21997,29 +22960,18 @@ snapshots: pug-runtime: 3.0.1 pug-strip-comments: 2.0.0 - pump@2.0.1: - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - pump@3.0.0: dependencies: end-of-stream: 1.4.4 once: 1.4.0 - pumpify@1.5.1: - dependencies: - duplexify: 3.7.1 - inherits: 2.0.4 - pump: 2.0.1 - punycode@2.3.1: {} pure-rand@6.0.0: {} pvtsutils@1.3.5: dependencies: - tslib: 2.6.2 + tslib: 2.6.3 pvutils@1.1.3: {} @@ -22066,13 +23018,6 @@ snapshots: ratelimiter@3.4.1: {} - raw-body@2.5.1: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - raw-body@2.5.2: dependencies: bytes: 3.1.2 @@ -22084,11 +23029,11 @@ snapshots: dependencies: setimmediate: 1.0.5 - re2@1.20.10: + re2@1.21.3: dependencies: install-artifact-from-github: 1.3.5 - nan: 2.18.0 - node-gyp: 10.0.1 + nan: 2.20.0 + node-gyp: 10.1.0 transitivePeerDependencies: - supports-color @@ -22097,15 +23042,15 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-docgen-typescript@2.2.2(typescript@5.4.5): + react-docgen-typescript@2.2.2(typescript@5.5.4): dependencies: - typescript: 5.4.5 + typescript: 5.5.4 react-docgen@7.0.1: dependencies: - '@babel/core': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 + '@babel/core': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.20.0 '@types/doctrine': 0.0.9 @@ -22192,21 +23137,13 @@ snapshots: real-require@0.2.0: {} - recast@0.23.4: - dependencies: - assert: 2.1.0 - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tslib: 2.6.2 - recast@0.23.6: dependencies: ast-types: 0.16.1 esprima: 4.0.1 source-map: 0.6.1 tiny-invariant: 1.3.3 - tslib: 2.6.2 + tslib: 2.6.3 reconnecting-websocket@4.4.0: {} @@ -22319,7 +23256,7 @@ snapshots: require-in-the-middle@7.3.0: dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -22343,11 +23280,6 @@ snapshots: resolve.exports@2.0.0: {} - resolve@1.19.0: - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - resolve@1.22.8: dependencies: is-core-module: 2.13.1 @@ -22383,30 +23315,32 @@ snapshots: dependencies: glob: 7.2.3 - rollup@4.17.2: + rollup@4.19.1: dependencies: '@types/estree': 1.0.5 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.17.2 - '@rollup/rollup-android-arm64': 4.17.2 - '@rollup/rollup-darwin-arm64': 4.17.2 - '@rollup/rollup-darwin-x64': 4.17.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.17.2 - '@rollup/rollup-linux-arm-musleabihf': 4.17.2 - '@rollup/rollup-linux-arm64-gnu': 4.17.2 - '@rollup/rollup-linux-arm64-musl': 4.17.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.17.2 - '@rollup/rollup-linux-riscv64-gnu': 4.17.2 - '@rollup/rollup-linux-s390x-gnu': 4.17.2 - '@rollup/rollup-linux-x64-gnu': 4.17.2 - '@rollup/rollup-linux-x64-musl': 4.17.2 - '@rollup/rollup-win32-arm64-msvc': 4.17.2 - '@rollup/rollup-win32-ia32-msvc': 4.17.2 - '@rollup/rollup-win32-x64-msvc': 4.17.2 + '@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 fsevents: 2.3.3 rrweb-cssom@0.6.0: {} + rrweb-cssom@0.7.1: {} + rss-parser@3.13.0: dependencies: entities: 2.2.0 @@ -22454,7 +23388,7 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.4.38 - sass@1.76.0: + sass@1.77.8: dependencies: chokidar: 3.5.3 immutable: 4.2.2 @@ -22536,14 +23470,14 @@ snapshots: dependencies: kind-of: 6.0.3 - sharp@0.33.3: + sharp@0.33.4: dependencies: color: 4.2.3 detect-libc: 2.0.3 semver: 7.6.0 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.3 - '@img/sharp-darwin-x64': 0.33.3 + '@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 @@ -22552,15 +23486,15 @@ snapshots: '@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.3 - '@img/sharp-linux-arm64': 0.33.3 - '@img/sharp-linux-s390x': 0.33.3 - '@img/sharp-linux-x64': 0.33.3 - '@img/sharp-linuxmusl-arm64': 0.33.3 - '@img/sharp-linuxmusl-x64': 0.33.3 - '@img/sharp-wasm32': 0.33.3 - '@img/sharp-win32-ia32': 0.33.3 - '@img/sharp-win32-x64': 0.33.3 + '@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 shebang-command@1.2.0: dependencies: @@ -22581,9 +23515,10 @@ snapshots: vscode-oniguruma: 1.7.0 vscode-textmate: 8.0.0 - shiki@1.4.0: + shiki@1.12.0: dependencies: - '@shikijs/core': 1.4.0 + '@shikijs/core': 1.12.0 + '@types/hast': 3.0.4 shimmer@1.2.1: {} @@ -22599,11 +23534,11 @@ snapshots: signal-exit@4.1.0: {} - simple-oauth2@5.0.0: + simple-oauth2@5.1.0: dependencies: - '@hapi/hoek': 10.0.1 + '@hapi/hoek': 11.0.4 '@hapi/wreck': 18.0.1 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) joi: 17.11.0 transitivePeerDependencies: - supports-color @@ -22684,6 +23619,8 @@ snapshots: slash@3.0.0: {} + slash@5.1.0: {} + slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -22701,7 +23638,7 @@ snapshots: socks-proxy-agent@8.0.2: dependencies: agent-base: 7.1.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -22711,7 +23648,7 @@ snapshots: ip: 2.0.1 smart-buffer: 4.2.0 - sonic-boom@3.7.0: + sonic-boom@4.0.1: dependencies: atomic-sleep: 1.0.0 @@ -22725,8 +23662,6 @@ snapshots: sortablejs@1.14.0: {} - source-map-js@1.0.2: {} - source-map-js@1.2.0: {} source-map-support@0.5.13: @@ -22767,7 +23702,7 @@ snapshots: sprintf-js@1.0.3: {} - sprintf-js@1.1.2: {} + sprintf-js@1.1.3: {} sshpk@1.17.0: dependencies: @@ -22793,16 +23728,16 @@ snapshots: standard-as-callback@2.1.0: {} - start-server-and-test@2.0.3: + start-server-and-test@2.0.4: dependencies: arg: 5.0.2 bluebird: 3.7.2 check-more-types: 2.24.0 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.5(supports-color@8.1.1) execa: 5.1.1 lazy-ass: 1.6.0 ps-tree: 1.2.0 - wait-on: 7.2.0(debug@4.3.4) + wait-on: 7.2.0(debug@4.3.5) transitivePeerDependencies: - supports-color @@ -22816,28 +23751,52 @@ snapshots: store2@2.14.2: {} - storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/components@8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/core-events@8.0.9)(@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/preview-api@8.0.9)(@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@storybook/types@8.0.9)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(3rvqj7p7l43ansgshs3zbslm7u): dependencies: - '@storybook/blocks': 8.0.9(@types/react@18.0.28)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/components': 8.0.9(@types/react@18.0.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/core-events': 8.0.9 - '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/preview-api': 8.0.9 - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/types': 8.0.9 + '@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)) optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook@8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.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): dependencies: - '@storybook/cli': 8.0.9(@babel/preset-env@7.23.5(@babel/core@7.24.0))(bufferutil@4.0.7)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@6.0.3) + '@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 transitivePeerDependencies: - '@babel/preset-env' - bufferutil - - encoding - - react - - react-dom - supports-color - utf-8-validate @@ -22856,8 +23815,6 @@ snapshots: transitivePeerDependencies: - supports-color - stream-shift@1.0.1: {} - stream-wormhole@1.1.0: {} streamsearch@1.1.0: {} @@ -22938,6 +23895,8 @@ snapshots: strip-final-newline@3.0.0: {} + strip-final-newline@4.0.0: {} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -22948,9 +23907,9 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@1.3.0: + strip-literal@2.1.0: dependencies: - acorn: 8.11.3 + js-tokens: 9.0.0 strip-outer@2.0.0: {} @@ -22961,10 +23920,15 @@ snapshots: '@tokenizer/token': 0.3.0 peek-readable: 5.0.0 - stylehacks@6.1.1(postcss@8.4.38): + strtok3@8.0.1: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.1.3 + + stylehacks@6.1.1(postcss@8.4.40): dependencies: browserslist: 4.23.0 - postcss: 8.4.38 + postcss: 8.4.40 postcss-selector-parser: 6.0.16 supports-color@5.5.0: @@ -23000,22 +23964,7 @@ snapshots: symbol-tree@3.2.4: {} - systeminformation@5.22.7: {} - - tar-fs@2.1.1: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.0 - tar-stream: 2.2.0 - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.4 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.0 + systeminformation@5.22.11: {} tar-stream@3.1.6: dependencies: @@ -23040,24 +23989,23 @@ snapshots: dependencies: memoizerific: 1.11.3 - temp-dir@2.0.0: {} + temp-dir@3.0.0: {} temp@0.8.4: dependencies: rimraf: 2.6.3 - tempy@1.0.1: + tempy@3.1.0: dependencies: - del: 6.1.1 - is-stream: 2.0.1 - temp-dir: 2.0.0 - type-fest: 0.16.0 - unique-string: 2.0.0 + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 - terser@5.30.3: + terser@5.31.3: dependencies: - '@jridgewell/source-map': 0.3.5 - acorn: 8.11.3 + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 commander: 2.20.3 source-map-support: 0.5.21 @@ -23081,25 +24029,18 @@ snapshots: dependencies: any-promise: 1.3.0 - thread-stream@2.3.0: + thread-stream@3.1.0: dependencies: real-require: 0.2.0 - three@0.164.1: {} + three@0.167.0: {} - throttle-debounce@5.0.0: {} + throttle-debounce@5.0.2: {} throttleit@1.0.0: {} - through2@2.0.5: - dependencies: - readable-stream: 2.3.7 - xtend: 4.0.2 - through@2.3.8: {} - tiny-invariant@1.3.1: {} - tiny-invariant@1.3.3: {} tiny-lru@10.0.1: {} @@ -23108,7 +24049,7 @@ snapshots: tinycolor2@1.6.0: {} - tinypool@0.7.0: {} + tinypool@0.8.4: {} tinyspy@2.2.0: {} @@ -23124,12 +24065,8 @@ snapshots: dependencies: is-number: 7.0.0 - toad-cache@3.3.0: {} - toad-cache@3.7.0: {} - tocbot@4.21.1: {} - toidentifier@1.0.1: {} token-stream@1.0.0: {} @@ -23139,11 +24076,16 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + token-types@6.0.0: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + touch@3.1.0: dependencies: nopt: 1.0.10 - tough-cookie@4.1.3: + tough-cookie@4.1.4: dependencies: psl: 1.9.0 punycode: 2.3.1 @@ -23174,19 +24116,19 @@ snapshots: dependencies: typescript: 5.3.3 - ts-api-utils@1.3.0(typescript@5.4.5): + ts-api-utils@1.3.0(typescript@5.5.4): dependencies: - typescript: 5.4.5 + typescript: 5.5.4 ts-case-convert@2.0.2: {} ts-dedent@2.2.0: {} - ts-jest@29.1.2(@babel/core@7.23.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.5))(esbuild@0.20.2)(jest@29.7.0(@types/node@20.12.7))(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.0)(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 - jest: 29.7.0(@types/node@20.12.7) + jest: 29.7.0(@types/node@20.14.12) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -23195,14 +24137,14 @@ snapshots: typescript: 5.1.6 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.23.5 + '@babel/core': 7.24.7 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.23.5) - esbuild: 0.20.2 + babel-jest: 29.7.0(@babel/core@7.24.7) + esbuild: 0.23.0 ts-map@1.0.3: {} - tsc-alias@1.8.8: + tsc-alias@1.8.10: dependencies: chokidar: 3.5.3 commander: 9.5.0 @@ -23224,9 +24166,9 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsd@0.30.7: + tsd@0.31.1: dependencies: - '@tsd/typescript': 5.3.3 + '@tsd/typescript': 5.4.5 eslint-formatter-pretty: 4.1.0 globby: 11.1.0 jest-diff: 29.7.0 @@ -23238,6 +24180,8 @@ snapshots: tslib@2.6.2: {} + tslib@2.6.3: {} + tsx@4.4.0: dependencies: esbuild: 0.18.20 @@ -23257,8 +24201,6 @@ snapshots: type-detect@4.0.8: {} - type-fest@0.16.0: {} - type-fest@0.18.1: {} type-fest@0.20.2: {} @@ -23269,9 +24211,11 @@ snapshots: type-fest@0.8.1: {} + type-fest@1.4.0: {} + type-fest@2.19.0: {} - type-fest@4.9.0: {} + type-fest@4.20.1: {} type-is@1.6.18: dependencies: @@ -23315,7 +24259,7 @@ snapshots: shiki: 0.14.7 typescript: 5.1.6 - typeorm@0.3.20(ioredis@5.4.1)(pg@8.11.5): + typeorm@0.3.20(ioredis@5.4.1)(pg@8.12.0): dependencies: '@sqltools/formatter': 1.2.5 app-root-path: 3.1.0 @@ -23323,7 +24267,7 @@ snapshots: chalk: 4.1.2 cli-highlight: 2.1.11 dayjs: 1.11.10 - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) dotenv: 16.0.3 glob: 10.3.10 mkdirp: 2.1.6 @@ -23334,7 +24278,7 @@ snapshots: yargs: 17.7.2 optionalDependencies: ioredis: 5.4.1 - pg: 8.11.5 + pg: 8.12.0 transitivePeerDependencies: - supports-color @@ -23344,7 +24288,7 @@ snapshots: typescript@5.4.2: {} - typescript@5.4.5: {} + typescript@5.5.4: {} ufo@1.3.2: {} @@ -23357,6 +24301,8 @@ snapshots: dependencies: '@lukeed/csprng': 1.0.1 + uint8array-extras@1.4.0: {} + ulid@2.3.0: {} unbox-primitive@1.0.2: @@ -23385,6 +24331,8 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} + unicorn-magic@0.1.0: {} + unified@11.0.4: dependencies: '@types/unist': 3.0.2 @@ -23403,9 +24351,9 @@ snapshots: dependencies: imurmurhash: 0.1.4 - unique-string@2.0.0: + unique-string@3.0.0: dependencies: - crypto-random-string: 2.0.0 + crypto-random-string: 4.0.0 unist-util-is@6.0.0: dependencies: @@ -23438,7 +24386,7 @@ snapshots: unplugin@1.4.0: dependencies: - acorn: 8.11.3 + acorn: 8.12.1 chokidar: 3.5.3 webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 @@ -23471,6 +24419,11 @@ snapshots: node-gyp-build: 4.6.0 optional: true + utf-8-validate@6.0.4: + dependencies: + node-gyp-build: 4.8.1 + optional: true + util-deprecate@1.0.2: {} util@0.12.5: @@ -23483,18 +24436,20 @@ snapshots: utils-merge@1.0.1: {} + uuid@10.0.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} - v-code-diff@1.11.0(vue@3.4.26(typescript@5.4.5)): + v-code-diff@1.12.0(vue@3.4.37(typescript@5.5.4)): dependencies: diff: 5.1.0 diff-match-patch: 1.0.5 highlight.js: 11.9.0 - vue: 3.4.26(typescript@5.4.5) - vue-demi: 0.14.7(vue@3.4.26(typescript@5.4.5)) - vue-i18n: 9.13.1(vue@3.4.26(typescript@5.4.5)) + 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)) v8-to-istanbul@9.2.0: dependencies: @@ -23507,8 +24462,6 @@ snapshots: spdx-correct: 3.1.1 spdx-expression-parse: 3.0.1 - validator@13.9.0: {} - vary@1.1.2: {} verror@1.10.0: @@ -23528,14 +24481,13 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 - vite-node@0.34.6(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3): + vite-node@1.6.0(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3): dependencies: cac: 6.7.14 - debug: 4.3.4(supports-color@8.1.1) - mlly: 1.5.0 + debug: 4.3.5(supports-color@8.1.1) pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) + vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3) transitivePeerDependencies: - '@types/node' - less @@ -23548,53 +24500,50 @@ snapshots: vite-plugin-turbosnap@1.0.3: {} - vite@5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3): + vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3): dependencies: - esbuild: 0.20.2 - postcss: 8.4.38 - rollup: 4.17.2 + esbuild: 0.21.5 + postcss: 8.4.40 + rollup: 4.19.1 optionalDependencies: - '@types/node': 20.12.7 + '@types/node': 20.14.12 fsevents: 2.3.3 - sass: 1.76.0 - terser: 5.30.3 + sass: 1.77.8 + terser: 5.31.3 - vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.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.77.8)(terser@5.31.3)): dependencies: cross-fetch: 3.1.6(encoding@0.1.13) - vitest: 0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.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.77.8)(terser@5.31.3) transitivePeerDependencies: - encoding - vitest@0.34.6(happy-dom@10.0.3)(jsdom@24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3))(sass@1.76.0)(terser@5.30.3): - dependencies: - '@types/chai': 4.3.11 - '@types/chai-subset': 1.3.5 - '@types/node': 20.12.7 - '@vitest/expect': 0.34.6 - '@vitest/runner': 0.34.6 - '@vitest/snapshot': 0.34.6 - '@vitest/spy': 0.34.6 - '@vitest/utils': 0.34.6 - acorn: 8.11.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.77.8)(terser@5.31.3): + 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 - cac: 6.7.14 chai: 4.3.10 - debug: 4.3.4(supports-color@8.1.1) - local-pkg: 0.4.3 + 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: 1.3.0 + strip-literal: 2.1.0 tinybench: 2.6.0 - tinypool: 0.7.0 - vite: 5.2.11(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) - vite-node: 0.34.6(@types/node@20.12.7)(sass@1.76.0)(terser@5.30.3) + 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) why-is-node-running: 2.2.2 optionalDependencies: + '@types/node': 20.14.12 happy-dom: 10.0.3 - jsdom: 24.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - less - lightningcss @@ -23631,98 +24580,102 @@ snapshots: vscode-textmate@8.0.0: {} - vue-component-meta@2.0.16(typescript@5.4.5): + vscode-uri@3.0.8: {} + + vue-component-meta@2.0.16(typescript@5.5.4): dependencies: '@volar/typescript': 2.2.0 - '@vue/language-core': 2.0.16(typescript@5.4.5) + '@vue/language-core': 2.0.16(typescript@5.5.4) path-browserify: 1.0.1 vue-component-type-helpers: 2.0.16 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 vue-component-type-helpers@1.8.4: {} vue-component-type-helpers@2.0.16: {} - vue-component-type-helpers@2.0.19: {} + vue-component-type-helpers@2.0.29: {} + + vue-component-type-helpers@2.1.2: {} - vue-demi@0.14.7(vue@3.4.26(typescript@5.4.5)): + vue-demi@0.14.7(vue@3.4.37(typescript@5.5.4)): dependencies: - vue: 3.4.26(typescript@5.4.5) + vue: 3.4.37(typescript@5.5.4) - vue-docgen-api@4.75.1(vue@3.4.26(typescript@5.4.5)): + vue-docgen-api@4.75.1(vue@3.4.37(typescript@5.5.4)): dependencies: - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 - '@vue/compiler-dom': 3.4.21 - '@vue/compiler-sfc': 3.4.26 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + '@vue/compiler-dom': 3.4.34 + '@vue/compiler-sfc': 3.4.37 ast-types: 0.16.1 hash-sum: 2.0.0 lru-cache: 8.0.4 - pug: 3.0.2 - recast: 0.23.4 + pug: 3.0.3 + recast: 0.23.6 ts-map: 1.0.3 - vue: 3.4.26(typescript@5.4.5) - vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.4.26(typescript@5.4.5)) + 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-eslint-parser@9.4.2(eslint@8.57.0): + vue-eslint-parser@9.4.3(eslint@9.8.0): dependencies: - debug: 4.3.4(supports-color@8.1.1) - eslint: 8.57.0 + debug: 4.3.4(supports-color@5.5.0) + eslint: 9.8.0 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 esquery: 1.4.2 lodash: 4.17.21 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - vue-i18n@9.13.1(vue@3.4.26(typescript@5.4.5)): + 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.26(typescript@5.4.5) + vue: 3.4.37(typescript@5.5.4) - vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.4.26(typescript@5.4.5)): + vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.4.37(typescript@5.5.4)): dependencies: - vue: 3.4.26(typescript@5.4.5) + vue: 3.4.37(typescript@5.5.4) vue-template-compiler@2.7.14: dependencies: de-indent: 1.0.2 he: 1.2.0 - vue-tsc@2.0.16(typescript@5.4.5): + vue-tsc@2.0.29(typescript@5.5.4): dependencies: - '@volar/typescript': 2.2.0 - '@vue/language-core': 2.0.16(typescript@5.4.5) + '@volar/typescript': 2.4.0-alpha.18 + '@vue/language-core': 2.0.29(typescript@5.5.4) semver: 7.6.0 - typescript: 5.4.5 + typescript: 5.5.4 - vue@3.4.26(typescript@5.4.5): + vue@3.4.37(typescript@5.5.4): dependencies: - '@vue/compiler-dom': 3.4.26 - '@vue/compiler-sfc': 3.4.26 - '@vue/runtime-dom': 3.4.26 - '@vue/server-renderer': 3.4.26(vue@3.4.26(typescript@5.4.5)) - '@vue/shared': 3.4.26 + '@vue/compiler-dom': 3.4.37 + '@vue/compiler-sfc': 3.4.37 + '@vue/runtime-dom': 3.4.37 + '@vue/server-renderer': 3.4.37(vue@3.4.37(typescript@5.5.4)) + '@vue/shared': 3.4.37 optionalDependencies: - typescript: 5.4.5 + typescript: 5.5.4 - vuedraggable@4.1.0(vue@3.4.26(typescript@5.4.5)): + vuedraggable@4.1.0(vue@3.4.37(typescript@5.5.4)): dependencies: sortablejs: 1.14.0 - vue: 3.4.26(typescript@5.4.5) + vue: 3.4.37(typescript@5.5.4) w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 - wait-on@7.2.0(debug@4.3.4): + wait-on@7.2.0(debug@4.3.5): dependencies: - axios: 1.6.2(debug@4.3.4) + axios: 1.6.2(debug@4.3.5) joi: 17.11.0 lodash: 4.17.21 minimist: 1.2.8 @@ -23730,6 +24683,8 @@ snapshots: transitivePeerDependencies: - debug + walk-up-path@3.0.1: {} + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -23755,6 +24710,9 @@ snapshots: web-streams-polyfill@3.2.1: {} + web-streams-polyfill@4.0.0: + optional: true + webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -23829,11 +24787,13 @@ snapshots: with@7.0.2: dependencies: - '@babel/parser': 7.24.5 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 assert-never: 1.2.1 babel-walk: 3.0.0-canary-5 + word-wrap@1.2.5: {} + wordwrap@1.0.0: {} wrap-ansi@6.2.0: @@ -23867,16 +24827,21 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@8.14.2(bufferutil@4.0.7)(utf-8-validate@6.0.3): + ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.4): optionalDependencies: - bufferutil: 4.0.7 - utf-8-validate: 6.0.3 + bufferutil: 4.0.8 + utf-8-validate: 6.0.4 - ws@8.17.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): + ws@8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): optionalDependencies: bufferutil: 4.0.7 utf-8-validate: 6.0.3 + ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 6.0.4 + xev@3.0.2: {} xml-js@1.6.11: @@ -23960,13 +24925,7 @@ snapshots: yocto-queue@1.0.0: {} - z-schema@5.0.5: - dependencies: - lodash.get: 4.4.2 - lodash.isequal: 4.5.0 - validator: 13.9.0 - optionalDependencies: - commander: 9.5.0 + yoctocolors@2.0.2: {} zip-stream@6.0.1: dependencies: diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs index c3c38cd9a653fc1f1026d98b3d14217557ee24fb..fcf29cef2209bf60d176dceb2be71a1d0cb2d327 100644 --- a/scripts/build-assets.mjs +++ b/scripts/build-assets.mjs @@ -13,7 +13,7 @@ import * as terser from 'terser'; import { build as buildLocales } from '../locales/index.js'; import generateDTS from '../locales/generateDTS.js'; -import meta from '../package.json' assert { type: "json" }; +import meta from '../package.json' with { type: "json" }; import buildTarball from './tarball.mjs'; const configDir = fileURLToPath(new URL('../.config', import.meta.url)); diff --git a/scripts/changelog-checker/eslint.config.js b/scripts/changelog-checker/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..813e96981ace311fa335394644e1531ec3df070d --- /dev/null +++ b/scripts/changelog-checker/eslint.config.js @@ -0,0 +1,17 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../packages/shared/eslint.config.js'; + +export default [ + ...sharedConfig, + { + files: ['src/**/*.ts', 'src/**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/scripts/tarball.mjs b/scripts/tarball.mjs index 707cd3e7e5fca7cdab20523cbdc1517a57c6dc5f..93cd78a06a5b9a2e855251f565d3b079e6617167 100644 --- a/scripts/tarball.mjs +++ b/scripts/tarball.mjs @@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url'; import glob from 'fast-glob'; import walk from 'ignore-walk'; import Pack from 'tar/lib/pack.js'; -import meta from '../package.json' assert { type: "json" }; +import meta from '../package.json' with { type: "json" }; const cwd = fileURLToPath(new URL('..', import.meta.url)); const ignore = [