diff --git a/.config/ci.yml b/.config/ci.yml index 8730ccab3a..b53ed0d354 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -167,18 +167,8 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 +# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) +maxNoteLength: 3000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -227,10 +217,9 @@ checkActivityPubGetSignature: false #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] +# Prefer these languages for remote notes with language-specific content +# Must be valid language codes according to BCP 47 +#langPref: ['en', 'ja'] + # Upload or download file size limits (bytes) #maxFileSize: 262144000 - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml deleted file mode 100644 index 342b0f43da..0000000000 --- a/.config/cypress-devcontainer.yml +++ /dev/null @@ -1,229 +0,0 @@ -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# Misskey configuration -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -# ┌─────┐ -#───┘ URL └───────────────────────────────────────────────────── - -# Final accessible URL seen by a user. -url: 'http://misskey.local' - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# URL SETTINGS AFTER THAT! - -# ┌───────────────────────┐ -#───┘ Port and TLS settings └─────────────────────────────────── - -# -# Misskey requires a reverse proxy to support HTTPS connections. -# -# +----- https://example.tld/ ------------+ -# +------+ |+-------------+ +----------------+| -# | User | ---> || Proxy (443) | ---> | Misskey (3000) || -# +------+ |+-------------+ +----------------+| -# +---------------------------------------+ -# -# You need to set up a reverse proxy. (e.g. nginx) -# An encrypted connection with HTTPS is highly recommended -# because tokens may be transferred in GET requests. - -# The port that your Misskey server should listen on. -port: 61812 - -# ┌──────────────────────────┐ -#───┘ PostgreSQL configuration └──────────────────────────────── - -db: - host: db - port: 5432 - - # Database name - db: misskey - - # Auth - user: postgres - pass: postgres - - # Whether disable Caching queries - #disableCache: true - - # Extra Connection options - #extra: - # ssl: true - -dbReplications: false - -# You can configure any number of replicas here -#dbSlaves: -# - -# host: -# port: -# db: -# user: -# pass: -# - -# host: -# port: -# db: -# user: -# pass: - -# ┌─────────────────────┐ -#───┘ Redis configuration └───────────────────────────────────── - -redis: - host: redis - port: 6379 - #family: 0 # 0=Both, 4=IPv4, 6=IPv6 - #pass: example-pass - #prefix: example-prefix - #db: 1 - -#redisForPubsub: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForJobQueue: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForTimelines: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - -# ┌───────────────────────────┐ -#───┘ MeiliSearch configuration └───────────────────────────── - -#meilisearch: -# host: meilisearch -# port: 7700 -# apiKey: '' -# ssl: true -# index: '' - -# ┌───────────────┐ -#───┘ ID generation └─────────────────────────────────────────── - -# You can select the ID generation method. -# You don't usually need to change this setting, but you can -# change it according to your preferences. - -# Available methods: -# aid ... Short, Millisecond accuracy -# aidx ... Millisecond accuracy -# meid ... Similar to ObjectID, Millisecond accuracy -# ulid ... Millisecond accuracy -# objectid ... This is left for backward compatibility - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# ID SETTINGS AFTER THAT! - -id: 'aidx' - -# ┌────────────────┐ -#───┘ Error tracking └────────────────────────────────────────── - -# Sentry is available for error tracking. -# See the Sentry documentation for more details on options. - -#sentryForBackend: -# enableNodeProfiling: true -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -#sentryForFrontend: -# options: -# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' - -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── - -# Whether disable HSTS -#disableHsts: true - -# Number of worker processes -#clusterLimit: 1 - -# Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 - -# Job rate limiter -# deliverJobPerSec: 128 -# inboxJobPerSec: 32 - -# Job attempts -# deliverJobMaxAttempts: 12 -# inboxJobMaxAttempts: 8 - -# IP address family used for outgoing request (ipv4, ipv6 or dual) -#outgoingAddressFamily: ipv4 - -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 - -# Proxy for HTTP/HTTPS -#proxy: http://127.0.0.1:3128 - -proxyBypassHosts: - - api.deepl.com - - api-free.deepl.com - - www.recaptcha.net - - hcaptcha.com - - challenges.cloudflare.com - -# Proxy for SMTP/SMTPS -#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT -#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 -#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 - -# Media Proxy -#mediaProxy: https://example.com/proxy - -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - -allowedPrivateNetworks: [ - '127.0.0.1/32' -] - -# Upload or download file size limits (bytes) -#maxFileSize: 262144000 - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.config/docker_example.yml b/.config/docker_example.yml index ce2daf3aec..36bcc8cb74 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -163,14 +163,6 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── @@ -250,18 +242,8 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 +# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) +maxNoteLength: 3000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -310,10 +292,9 @@ checkActivityPubGetSignature: false #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] +# Prefer these languages for remote notes with language-specific content +# Must be valid language codes according to BCP 47 +#langPref: ['en', 'ja'] + # Upload or download file size limits (bytes) #maxFileSize: 262144000 - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.config/example.yml b/.config/example.yml index 9debb3bf70..c08cb4c67f 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -99,10 +99,10 @@ db: port: 5432 # Database name - db: sharkey + db: misskey # Auth - user: sharkey + user: example-misskey-user pass: example-misskey-pass # Whether disable Caching queries @@ -172,16 +172,6 @@ redis: # # You can specify more ioredis options... # #username: example-username -#redisForReactions: -# host: localhost -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 -# # You can specify more ioredis options... -# #username: example-username - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── @@ -261,18 +251,8 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -#maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -#maxRemoteNoteLength: 100000 -# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) -#maxCwLength: 500 -# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) -#maxRemoteCwLength: 5000 -# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -#maxAltTextLength: 20000 -# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -#maxRemoteAltTextLength: 100000 +# Amount of characters that can be used when writing notes (maximum: 8192, minimum: 1) +maxNoteLength: 3000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 @@ -324,6 +304,10 @@ checkActivityPubGetSignature: false #customMOTD: ['Hello World', 'The sharks rule all', 'Shonks'] +# Prefer these languages for remote notes with language-specific content +# Must be valid language codes according to BCP 47 +#langPref: ['en', 'ja'] + # Upload or download file size limits (bytes) #maxFileSize: 262144000 @@ -334,8 +318,3 @@ checkActivityPubGetSignature: false # PID File of master process #pidFile: /tmp/misskey.pid - -# CHMod-style permission bits to apply to uploaded files. -# Permission bits are specified as a base-8 string representing User/Group/Other permissions. -# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. -#filePermissionBits: '644' diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 3eb4fc2879..beefcfd0a2 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -103,14 +103,6 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── diff --git a/.devcontainer/init.sh b/.devcontainer/init.sh index e02a533c15..55fb1e6fa6 100755 --- a/.devcontainer/init.sh +++ b/.devcontainer/init.sh @@ -3,8 +3,6 @@ set -xe sudo chown node node_modules -sudo apt-get update -sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb git config --global --add safe.directory /workspace git submodule update --init corepack install @@ -14,4 +12,3 @@ pnpm install --frozen-lockfile cp .devcontainer/devcontainer.yml .config/default.yml pnpm build pnpm migrate -pnpm exec cypress install diff --git a/.gitignore b/.gitignore index 7cc7354a4a..0aebf364bb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,9 +23,6 @@ packages/sw/.yarn/cache # pnpm .pnpm-store -# eslint -**/.eslintcache - # Cypress cypress/screenshots cypress/videos @@ -38,9 +35,6 @@ coverage !/.config/example.yml !/.config/docker_example.yml !/.config/docker_example.env -!/.config/cypress-devcontainer.yml -docker-compose.yml -compose.yml .devcontainer/compose.yml !/.devcontainer/compose.yml @@ -48,7 +42,6 @@ compose.yml /build built built-test -js-built /data /.cache-loader /db @@ -68,9 +61,8 @@ temp tsdoc-metadata.json misskey-assets -# Vite temporary files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* +# Sharkey +/packages/megalodon/lib # blender backups *.blend1 @@ -81,6 +73,3 @@ vite.config.ts.timestamp-* # VSCode addon .favorites.json - -# Sharkey -/packages/megalodon/lib diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4db8bda32e..2e773eddf9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,9 +20,9 @@ testCommit: - pnpm install --frozen-lockfile - pnpm run build - pnpm run migrate - - pnpm run --filter='!megalodon' test - - pnpm run --filter=backend --filter=misskey-js lint - - pnpm run --filter=frontend --filter=frontend-embed eslint + - pnpm run --filter='!megalodon' --workspace-concurrency=1 test + - pnpm run --filter=backend lint + - pnpm run --filter=frontend eslint cache: key: test policy: pull-push diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md index a909067269..6914647570 100644 --- a/.gitlab/issue_templates/bug.md +++ b/.gitlab/issue_templates/bug.md @@ -3,33 +3,27 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -# **What happened?** - +**What happened?** _(Please give us a brief description of what happened.)_ -# **What did you expect to happen?** - +**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_ -# **Version** - +**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ -# **Instance** - +**Instance** _(What instance of Sharkey are you using?)_ -# **What type of issue is this?** - +**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_ -# **What browser are you using? (Client-side issues only)** +**What browser are you using? (Client-side issues only)** -# **What operating system are you using? (Client-side issues only)** +**What operating system are you using? (Client-side issues only)** -# **How do you deploy Sharkey on your server? (Server-side issues only)** +**How do you deploy Sharkey on your server? (Server-side issues only)** -# **What operating system are you using? (Server-side issues only)** +**What operating system are you using? (Server-side issues only)** -# **Relevant log output** - +**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_ -# **Contribution Guidelines** +**Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md index a77f9335fe..d4235eb5a3 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -3,19 +3,15 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -# **What feature would you like implemented?** - +**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_ -# **Why should we add this feature?** - +**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_ -# **Version** - +**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ -# **Instance** - +**Instance** _(What instance of Sharkey are you using?)_ -# **Contribution Guidelines** +**Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index e6977def70..18bffa5419 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -1,12 +1,11 @@ -# **What does this MR do?** - +**What does this PR do?** _(Please give us a brief description of what this PR does.)_ -# **Contribution Guidelines** +**Contribution Guidelines** By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have made sure to test this merge request +- [ ] I have made sure to test this pull request diff --git a/CHANGELOG.md b/CHANGELOG.md index cf0437e51a..a7da62a4ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,94 +1,3 @@ -## 2024.9.0 - -### General -- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 - - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください -- Feat: パスキーでログインボタンを実装 (#14574) -- Feat: フォローされた際のメッセージを設定できるように -- Feat: 連合をホワイトリスト制にできるように -- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) -- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) -- Feat: データエクスポートが完了した際に通知を発行するように -- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように -- Enhance: 依存関係の更新 -- Enhance: l10nの更新 - -### Client -- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように -- Enhance: アイコンデコレーション管理画面にプレビューを追加 -- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく -- Enhance: ScratchpadにUIインスペクターを追加 -- Enhance: Play編集画面の項目の並びを少しリデザイン -- Enhance: 各種メニューをドロワー表示するかどうか設定可能に -- Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加 -- Enhance: CWでも絵文字をクリックしてメニューを表示できるように -- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 -- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正 -- Fix: 月の違う同じ日はセパレータが表示されないのを修正 -- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正 - (Cherry-picked from https://github.com/taiyme/misskey/pull/265) -- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正 - (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725) -- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正 -- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110) -- Fix: 一部画面のページネーションが動作しにくくなっていたのを修正 ( #12766 , #11449 ) - -### Server -- Feat: Misskey® Reactions Boost Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に -- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように - - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます -- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 -- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) -- Fix: Continue importing from file if single emoji import fails -- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正 - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624) -- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634) -- Fix: ``を追って照会するのはOKレスポンスが返却された場合のみに - (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633) -- Fix: メールにスタイルが適用されていなかった問題を修正 - -## 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2e48ec61d..4ac34b726f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -529,8 +529,7 @@ enumの列挙の内容の削除は、その値をもつレコードを全て削 ### Migration作成方法 packages/backendで: ```sh -pnpm run build -pnpm dlx typeorm migration:generate -d ormconfig.js -o migration/ +pnpm dlx typeorm migration:generate -d ormconfig.js -o ``` - 生成後、ファイルをmigration下に移してください @@ -574,26 +573,6 @@ marginはそのコンポーネントを使う側が設定する ### indexというファイル名を使うな ESMではディレクトリインポートは廃止されているのと、ディレクトリインポートせずともファイル名が index だと何故か一部のライブラリ?でディレクトリインポートだと見做されてエラーになる -## CSS Recipe - -### Lighten CSS vars - -``` css -color: hsl(from var(--accent) h s calc(l + 10)); -``` - -### Darken CSS vars - -``` css -color: hsl(from var(--accent) h s calc(l - 10)); -``` - -### Add alpha to CSS vars - -``` css -color: color(from var(--accent) srgb r g b / 0.5); -``` - ## Merging from Misskey into Sharkey Make sure you have both remotes in the same clone (`git remote add misskey @@ -611,11 +590,12 @@ seems to do a decent job) *after that commit*, do all the extra work, on the same branch: * copy all changes (commit after each step): - * in - `packages/backend/src/core/activitypub/models/ApNoteService.ts`, - from `createNote` to `updateNote` + * in `packages/backend/src/core/NoteCreateService.ts`, from `create` to + `import` (and vice versa if `git` got confused!) * 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 @@ -628,12 +608,10 @@ seems to do a decent job) `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 --filter=frontend-embed build` (the `development` - tells it to keep some of the original filenames in the built files) +* build the frontend: `rm -rf built/; NODE_ENV=development pnpm --filter=frontend + build` (the `development` tells it to keep some of the original + filenames in the built files) * make sure there aren't any new `ti-*` classes (Tabler Icons), and replace them with appropriate `ph-*` ones (Phosphor Icons): `grep -rP '["'\'']ti[ -](?!fw)' -- built/` should show you what to change. diff --git a/Dockerfile b/Dockerfile index abee7fb098..288e97481c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,10 +23,9 @@ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ RUN pnpm build RUN node scripts/trim-deps.mjs RUN mv packages/frontend/assets sharkey-assets -RUN mv packages/frontend-embed/assets sharkey-embed-assets RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm prune -RUN rm -r node_modules packages/frontend packages/frontend-shared packages/frontend-embed packages/sw +RUN rm -r node_modules packages/frontend packages/sw RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ pnpm i --prod --frozen-lockfile --aggregate-output RUN rm -rf .git @@ -40,8 +39,6 @@ RUN apk add ffmpeg tini jemalloc \ && corepack enable \ && addgroup -g "${GID}" sharkey \ && adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \ - && mkdir /sharkey/files \ - && chown sharkey:sharkey /sharkey/files \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; \ && find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \; @@ -67,7 +64,6 @@ COPY --chown=sharkey:sharkey --from=build /sharkey/packages/megalodon/lib ./pack COPY --chown=sharkey:sharkey --from=build /sharkey/fluent-emojis ./fluent-emojis COPY --chown=sharkey:sharkey --from=build /sharkey/tossface-emojis/dist ./tossface-emojis/dist COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-assets ./packages/frontend/assets -COPY --chown=sharkey:sharkey --from=build /sharkey/sharkey-embed-assets ./packages/frontend-embed/assets COPY --chown=sharkey:sharkey pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --chown=sharkey:sharkey packages/backend/package.json ./packages/backend/package.json diff --git a/README.md b/README.md index f9198c06c0..2407bee3f0 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ join the community - - donate + + donate --- diff --git a/UPGRADE_NOTES.md b/UPGRADE_NOTES.md deleted file mode 100644 index c941de6643..0000000000 --- a/UPGRADE_NOTES.md +++ /dev/null @@ -1,74 +0,0 @@ -# Upgrade Notes - -## 2024.10.0 - -### Hellspawns - -Sharkey versions before 2024.10 suffered from a bug in the "Mark instance as NSFW" feature. -When a user from such an instance boosted a note, the boost would be converted to a hellspawn (pure renote with Content Warning). -Hellspawns are buggy and do not properly federate, so it may be desirable to correct any that already exist in the database. -The following script will correct any local or remote hellspawns in the database. - -```postgresql -/* Remove "instance is marked as NSFW" hellspawns */ -UPDATE "note" -SET "cw" = null -WHERE - "renoteId" IS NOT NULL - AND "text" IS NULL - AND "cw" = 'Instance is marked as NSFW' - AND "replyId" IS NULL - AND "hasPoll" = false - AND "fileIds" = '{}'; - -/* Fix legacy / user-created hellspawns */ -UPDATE "note" -SET "text" = '.' -WHERE - "renoteId" IS NOT NULL - AND "text" IS NULL - AND "cw" IS NOT NULL - AND "replyId" IS NULL - AND "hasPoll" = false - AND "fileIds" = '{}'; -``` - -## 2024.9.0 - -### Following Feed - -When upgrading an existing instance to version 2024.9.0, the Following Feed will initially be empty. -The feed will gradually fill as new posts federate, but it may be desirable to back-fill the feed with existing data. -This database script will populate the feed with the latest post of each type for all users, ensuring that data is fully populated after the update. -Run this after migrations but before starting the instance. -Warning: the script may take a long time to execute! - -```postgresql -INSERT INTO latest_note (user_id, note_id, is_public, is_reply, is_quote) -SELECT - "userId" as user_id, - id as note_id, - visibility = 'public' AS is_public, - "replyId" IS NOT NULL AS is_reply, - ( - "renoteId" IS NOT NULL - AND ( - text IS NOT NULL - OR cw IS NOT NULL - OR "replyId" IS NOT NULL - OR "hasPoll" - OR "fileIds" != '{}' - ) - ) AS is_quote -FROM note -WHERE ( -- Exclude pure renotes (boosts) - "renoteId" IS NULL - OR text IS NOT NULL - OR cw IS NOT NULL - OR "replyId" IS NOT NULL - OR "hasPoll" - OR "fileIds" != '{}' - ) -ORDER BY id DESC -- This part is very important: it ensures that we only load the *latest* notes of each type. Do not remove it! -ON CONFLICT DO NOTHING; -- Any conflicts are guaranteed to be older notes that we can ignore. -``` diff --git a/chart/files/default.yml b/chart/files/default.yml index 97201aad66..aab7ed6ce1 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -124,14 +124,6 @@ redis: # #prefix: example-prefix # #db: 1 -#redisForReactions: -# host: redis -# port: 6379 -# #family: 0 # 0=Both, 4=IPv4, 6=IPv6 -# #pass: example-pass -# #prefix: example-prefix -# #db: 1 - # ┌───────────────────────────┐ #───┘ MeiliSearch configuration └───────────────────────────── diff --git a/compose_example.yml b/compose_example.yml index 0db8b04dc6..15df128eff 100644 --- a/compose_example.yml +++ b/compose_example.yml @@ -53,7 +53,7 @@ services: # restart: always # image: mcaptcha/mcaptcha:latest # networks: -# shonk: +# shonks: # aliases: # - localhost # ports: @@ -63,8 +63,6 @@ services: # environment: # PORT: 7493 # MCAPTCHA_redis_URL: "redis://mcaptcha_redis/" -# MCAPTCHA_allow_registration: true -# MCAPTCHA_server_DOMAIN: "example.tld" # depends_on: # db: # condition: service_healthy @@ -74,7 +72,7 @@ services: # mcaptcha_redis: # image: mcaptcha/cache:latest # networks: -# - shonk +# - shonks # healthcheck: # test: "redis-cli ping" # interval: 5s diff --git a/crowdin.yml b/crowdin.yml index 0525ac7b0b..774ddc7a63 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,4 +1,4 @@ files: - - source: /sharkey-locales/en-US.yml - translation: /sharkey-locales/%locale%.yml + - source: /locales/ja-JP.yml + translation: /locales/%locale%.yml update_option: update_as_unapproved diff --git a/eslint/locale.js b/eslint/locale.js deleted file mode 100644 index dbb807b714..0000000000 --- a/eslint/locale.js +++ /dev/null @@ -1,251 +0,0 @@ -/* - * SPDX-FileCopyrightText: dakkar and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only -*/ - -/* This is a ESLint rule to report use of the `i18n.ts` and `i18n.tsx` - * objects that reference translation items that don't actually exist - * in the lexicon (the `locale/` files) - */ - -/* given a MemberExpression node, collects all the member names - * - * e.g. for a bit of code like `foo=one.two.three`, `collectMembers` - * called on the node for `three` would return `['one', 'two', - * 'three']` - */ -function collectMembers(node) { - if (!node) return []; - if (node.type !== 'MemberExpression') return []; - // this is something like `foo[bar]` - if (node.computed) return []; - return [ node.property.name, ...collectMembers(node.parent) ]; -} - -/* given an object and an array of names, recursively descends the - * object via those names - * - * e.g. `walkDown({one:{two:{three:15}}},['one','two','three'])` would - * return 15 - */ -function walkDown(locale, path) { - if (!locale) return null; - if (!path || path.length === 0 || !path[0]) return locale; - return walkDown(locale[path[0]], path.slice(1)); -} - -/* given a MemberExpression node, returns its attached CallExpression - * node if present - * - * e.g. for a bit of code like `foo=one.two.three()`, - * `findCallExpression` called on the node for `three` would return - * the node for function call (which is the parent of the `one` and - * `two` nodes, and holds the nodes for the argument list) - * - * if the code had been `foo=one.two.three`, `findCallExpression` - * would have returned null, because there's no function call attached - * to the MemberExpressions - */ -function findCallExpression(node) { - if (!node.parent) return null; - - // the second half of this guard protects from cases like - // `foo(one.two.three)` where the CallExpression is parent of the - // MemberExpressions, but via `arguments`, not `callee` - if (node.parent.type === 'CallExpression' && node.parent.callee === node) return node.parent; - if (node.parent.type === 'MemberExpression') return findCallExpression(node.parent); - return null; -} - -// same, but for Vue expressions (``) -function findVueExpression(node) { - if (!node.parent) return null; - - if (node.parent.type.match(/^VExpr/) && node.parent.expression === node) return node.parent; - if (node.parent.type === 'MemberExpression') return findVueExpression(node.parent); - return null; -} - -function areArgumentsOneObject(node) { - return node.arguments.length === 1 && - node.arguments[0].type === 'ObjectExpression'; -} - -// only call if `areArgumentsOneObject(node)` is true -function getArgumentObjectProperties(node) { - return new Set(node.arguments[0].properties.map( - p => { - if (p.key && p.key.type === 'Identifier') return p.key.name; - return null; - }, - )); -} - -function getTranslationParameters(translation) { - return new Set(Array.from(translation.matchAll(/\{(\w+)\}/g)).map( m => m[1] )); -} - -function setDifference(a,b) { - const result = []; - for (const element of a.values()) { - if (!b.has(element)) { - result.push(element); - } - } - - return result; -} - -/* the actual rule body - */ -function theRuleBody(context,node) { - // we get the locale/translations via the options; it's the data - // that goes into a specific language's JSON file, see - // `scripts/build-assets.mjs` - const locale = context.options[0]; - - // sometimes we get MemberExpression nodes that have a - // *descendent* with the right identifier: skip them, we'll get - // the right ones as well - if (node.object?.name !== 'i18n') { - return; - } - - // `method` is going to be `'ts'` or `'tsx'`, `path` is going to - // be the various translation steps/names - const [ method, ...path ] = collectMembers(node); - const pathStr = `i18n.${method}.${path.join('.')}`; - - // does that path point to a real translation? - const translation = walkDown(locale, path); - if (!translation) { - context.report({ - node, - message: `translation missing for ${pathStr}`, - }); - return; - } - - // we hit something weird, assume the programmers know what - // they're doing (this is usually some complicated slicing of - // the translation structure) - if (typeof(translation) !== 'string') return; - - const callExpression = findCallExpression(node); - const vueExpression = findVueExpression(node); - - // some more checks on how the translation is called - if (method === 'ts') { - // the ` component gets parametric translations via - // `i18n.ts.*`, but we error out elsewhere - if (translation.match(/\{/) && !vueExpression) { - context.report({ - node, - message: `translation for ${pathStr} is parametric, but called via 'ts'`, - }); - return; - } - - if (callExpression) { - context.report({ - node, - message: `translation for ${pathStr} is not parametric, but is called as a function`, - }); - } - } - - if (method === 'tsx') { - if (!translation.match(/\{/)) { - context.report({ - node, - message: `translation for ${pathStr} is not parametric, but called via 'tsx'`, - }); - return; - } - - if (!callExpression && !vueExpression) { - context.report({ - node, - message: `translation for ${pathStr} is parametric, but not called as a function`, - }); - return; - } - - // we're not currently checking arguments when used via the - // `` component, because it's too complicated (also, it - // would have to be done inside the `if (method === 'ts')`) - if (!callExpression) return; - - if (!areArgumentsOneObject(callExpression)) { - context.report({ - node, - message: `translation for ${pathStr} should be called with a single object as argument`, - }); - return; - } - - const translationParameters = getTranslationParameters(translation); - const parameterCount = translationParameters.size; - const callArguments = getArgumentObjectProperties(callExpression); - const argumentCount = callArguments.size; - - if (parameterCount !== argumentCount) { - context.report({ - node, - message: `translation for ${pathStr} has ${parameterCount} parameters, but is called with ${argumentCount} arguments`, - }); - } - - // node 20 doesn't have `Set.difference`... - const extraArguments = setDifference(callArguments, translationParameters); - const missingArguments = setDifference(translationParameters, callArguments); - - if (extraArguments.length > 0) { - context.report({ - node, - message: `translation for ${pathStr} passes unused arguments ${extraArguments.join(' ')}`, - }); - } - - if (missingArguments.length > 0) { - context.report({ - node, - message: `translation for ${pathStr} does not pass arguments ${missingArguments.join(' ')}`, - }); - } - } -} - -function theRule(context) { - // we get the locale/translations via the options; it's the data - // that goes into a specific language's JSON file, see - // `scripts/build-assets.mjs` - const locale = context.options[0]; - - // for all object member access that have an identifier 'i18n'... - return context.getSourceCode().parserServices.defineTemplateBodyVisitor( - { - // this is for - - - - - - @@ -134,10 +125,9 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import MkColorInput from '@/components/MkColorInput.vue'; -import { host } from '@@/js/config.js'; +import { host } from '@/config.js'; const iconUrl = ref(null); -const sidebarLogoUrl = ref(null); const app192IconUrl = ref(null); const app512IconUrl = ref(null); const bannerUrl = ref(null); @@ -156,7 +146,6 @@ const manifestJsonOverride = ref('{}'); async function init() { const meta = await misskeyApi('admin/meta'); iconUrl.value = meta.iconUrl; - sidebarLogoUrl.value = meta.sidebarLogoUrl; app192IconUrl.value = meta.app192IconUrl; app512IconUrl.value = meta.app512IconUrl; bannerUrl.value = meta.bannerUrl; @@ -176,7 +165,6 @@ async function init() { function save() { os.apiWithDialog('admin/update-meta', { iconUrl: iconUrl.value, - sidebarLogoUrl: sidebarLogoUrl.value, app192IconUrl: app192IconUrl.value, app512IconUrl: app512IconUrl.value, bannerUrl: bannerUrl.value, diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue index ddfe5ae81f..4a858887f3 100644 --- a/packages/frontend/src/pages/admin/email-settings.vue +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -100,7 +100,7 @@ async function init() { async function testEmail() { const { canceled, result: destination } = await os.inputText({ - title: i18n.ts.emailDestination, + title: i18n.ts.destination, type: 'email', default: instance.maintainerEmail ?? '', placeholder: 'test@example.com', diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index 50e2c2dd51..e4308e6030 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -19,7 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only - @@ -28,12 +27,17 @@ SPDX-License-Identifier: AGPL-3.0-only - - Save
-
+
+ @@ -44,12 +48,12 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; +import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -import MkFolder from '@/components/MkFolder.vue'; const deeplAuthKey = ref(''); const deeplIsPro = ref(false); @@ -64,7 +68,7 @@ async function init() { deeplFreeInstance.value = meta.deeplFreeInstance; } -function save_deepl() { +function save() { os.apiWithDialog('admin/update-meta', { deeplAuthKey: deeplAuthKey.value, deeplIsPro: deeplIsPro.value, @@ -84,3 +88,10 @@ definePageMetadata(() => ({ icon: 'ph-arrow-square-out ph-bold ph-lg', })); + + diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index e8d123060a..794669d6b5 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
@@ -63,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index c94cd512f3..6cc19db127 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -94,23 +94,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - - - - - - - - - - - @@ -120,6 +103,15 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.flagAsBot }}
+ + + + + + + + +
@@ -161,7 +153,6 @@ const setMaxBirthDate = () => { const profile = reactive({ name: $i.name, description: $i.description, - followedMessage: $i.followedMessage, location: $i.location, birthday: $i.birthday, listenbrainz: $i.listenbrainz, @@ -218,8 +209,6 @@ function save() { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing description: profile.description || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - followedMessage: profile.followedMessage || null, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing location: profile.location || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing birthday: profile.birthday || null, diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue index e7aef55a53..ad07a6b539 100644 --- a/packages/frontend/src/pages/settings/theme.vue +++ b/packages/frontend/src/pages/settings/theme.vue @@ -88,9 +88,19 @@ import { uniqueBy } from '@/scripts/array.js'; import { fetchThemes, getThemes } from '@/theme-store.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { miLocalStorage } from '@/local-storage.js'; -import { reloadAsk } from '@/scripts/reload-ask.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; import * as os from '@/os.js'; +async function reloadAsk() { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting, + }); + if (canceled) return; + + unisonReload(); +} + const installedThemes = ref(getThemes()); const builtinThemes = getBuiltinThemesRef(); @@ -138,13 +148,13 @@ watch(syncDeviceDarkMode, () => { } }); -watch(wallpaper, async () => { +watch(wallpaper, () => { if (wallpaper.value == null) { miLocalStorage.removeItem('wallpaper'); } else { miLocalStorage.setItem('wallpaper', wallpaper.value); } - await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true }); + reloadAsk(); }); onActivated(() => { diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index adeaf8550c..058ef69c35 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -21,41 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only -
-
-
- {{ i18n.ts._webhookSettings._events.follow }} - -
-
- {{ i18n.ts._webhookSettings._events.followed }} - -
-
- {{ i18n.ts._webhookSettings._events.note }} - -
-
- {{ i18n.ts._webhookSettings._events.reply }} - -
-
- {{ i18n.ts._webhookSettings._events.renote }} - -
-
- {{ i18n.ts._webhookSettings._events.reaction }} - -
-
- {{ i18n.ts._webhookSettings._events.mention }} - -
-
- -
- {{ i18n.ts._webhookSettings.testRemarks }} -
+
+ {{ i18n.ts._webhookSettings._events.follow }} + {{ i18n.ts._webhookSettings._events.followed }} + {{ i18n.ts._webhookSettings._events.note }} + {{ i18n.ts._webhookSettings._events.reply }} + {{ i18n.ts._webhookSettings._events.renote }} + {{ i18n.ts._webhookSettings._events.reaction }} + {{ i18n.ts._webhookSettings._events.mention }}
@@ -70,7 +43,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue index 0d261b1af3..9b77392872 100644 --- a/packages/frontend/src/pages/tag.vue +++ b/packages/frontend/src/pages/tag.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 5a41100bf1..31911649ac 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; -import { host, version } from '@@/js/config.js'; +import { host, version } from '@/config.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { login } from '@/account.js'; diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue index ee8d4e1d62..252b1a2955 100644 --- a/packages/frontend/src/pages/welcome.timeline.note.vue +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -84,7 +84,7 @@ onUpdated(() => { left: 0; width: 100%; height: 64px; - background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0)); + background: linear-gradient(0deg, var(--panel), var(--X15)); } } diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 16d558cc91..045f424cda 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -24,7 +24,7 @@ import * as Misskey from 'misskey-js'; import { onUpdated, ref, shallowRef } from 'vue'; import XNote from '@/pages/welcome.timeline.note.vue'; import { misskeyApiGet } from '@/scripts/misskey-api.js'; -import { getScrollContainer } from '@@/js/scroll.js'; +import { getScrollContainer } from '@/scripts/scroll.js'; const notes = ref([]); const isScrolling = ref(false); diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue index 38d257506c..915fe35025 100644 --- a/packages/frontend/src/pages/welcome.vue +++ b/packages/frontend/src/pages/welcome.vue @@ -15,7 +15,7 @@ import { computed, ref } from 'vue'; import * as Misskey from 'misskey-js'; import XSetup from './welcome.setup.vue'; import XEntrance from './welcome.entrance.a.vue'; -import { instanceName } from '@@/js/config.js'; +import { instanceName } from '@/config.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { fetchInstance } from '@/instance.js'; diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index c0034d414c..81233a5a5e 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -6,10 +6,8 @@ import { ref } from 'vue'; import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js'; -import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; +import { inputText } from '@/os.js'; import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js'; -import { warningExternalWebsite } from '@/scripts/warning-external-website.js'; const parser = new Parser(); const pluginContexts = new Map(); @@ -94,7 +92,7 @@ function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record { utils.assertString(url); - warningExternalWebsite(url.value); + window.open(url.value, '_blank', 'noopener'); }), 'Plugin:config': values.OBJ(config), }; diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 686ac6920a..14110d1f9b 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -3,14 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { AsyncComponentLoader, defineAsyncComponent } from 'vue'; -import type { IRouter, RouteDef } from '@/nirax.js'; -import { Router } from '@/nirax.js'; +import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue'; +import type { RouteDef } from '@/nirax.js'; +import { IRouter, Router } from '@/nirax.js'; import { $i, iAmModerator } from '@/account.js'; import MkLoading from '@/pages/_loading_.vue'; import MkError from '@/pages/_error_.vue'; +import { setMainRouter } from '@/router/main.js'; -export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ +const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ loader: loader, loadingComponent: MkLoading, errorComponent: MkError, @@ -226,14 +227,6 @@ const routes: RouteDef[] = [{ path: '/explore', component: page(() => import('@/pages/explore.vue')), hash: 'initialTab', -}, { - path: '/following-feed', - component: page(() => import('@/pages/following-feed.vue')), - loginRequired: true, -}, { - path: '/following-feed/:userId', - component: page(() => import('@/pages/user/recent-notes.vue')), - loginRequired: true, }, { path: '/search', component: page(() => import('@/pages/search.vue')), @@ -470,14 +463,22 @@ const routes: RouteDef[] = [{ path: '/relays', name: 'relays', component: page(() => import('@/pages/admin/relays.vue')), + }, { + path: '/instance-block', + name: 'instance-block', + component: page(() => import('@/pages/admin/instance-block.vue')), + }, { + path: '/proxy-account', + name: 'proxy-account', + component: page(() => import('@/pages/admin/proxy-account.vue')), }, { path: '/external-services', name: 'external-services', component: page(() => import('@/pages/admin/external-services.vue')), }, { - path: '/performance', - name: 'performance', - component: page(() => import('@/pages/admin/performance.vue')), + path: '/other-settings', + name: 'other-settings', + component: page(() => import('@/pages/admin/other-settings.vue')), }, { path: '/server-rules', name: 'server-rules', @@ -600,6 +601,36 @@ const routes: RouteDef[] = [{ component: page(() => import('@/pages/not-found.vue')), }]; -export function createMainRouter(path: string): IRouter { +function createRouterImpl(path: string): IRouter { return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue'))); } + +/** + * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 + * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) + */ +export function setupRouter(app: App) { + app.provide('routerFactory', createRouterImpl); + + const mainRouter = createRouterImpl(location.pathname + location.search + location.hash); + + window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); + }); + + mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.addListener('same', () => { + window.scroll({ top: 0, behavior: 'smooth' }); + }); + + mainRouter.addListener('replace', ctx => { + window.history.replaceState({ key: ctx.key }, '', ctx.path); + }); + + mainRouter.init(); + + setMainRouter(mainRouter); +} diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts index 709c508741..7a3fde131e 100644 --- a/packages/frontend/src/router/main.ts +++ b/packages/frontend/src/router/main.ts @@ -3,41 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { ShallowRef } from 'vue'; import { EventEmitter } from 'eventemitter3'; import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; -import type { App, ShallowRef } from 'vue'; - -/** - * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。 - * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能) - */ -export function setupRouter(app: App, routerFactory: ((path: string) => IRouter)): void { - app.provide('routerFactory', routerFactory); - - const mainRouter = routerFactory(location.pathname + location.search + location.hash); - - window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); - }); - - mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.addListener('same', () => { - window.scroll({ top: 0, behavior: 'smooth' }); - }); - - mainRouter.addListener('replace', ctx => { - window.history.replaceState({ key: ctx.key }, '', ctx.path); - }); - - mainRouter.init(); - - setMainRouter(mainRouter); -} - function getMainRouter(): IRouter { const router = mainRouterHolder; if (!router) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 46aed49330..98a0c61752 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -4,13 +4,13 @@ */ import { utils, values } from '@syuilo/aiscript'; -import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; -import { url, lang } from '@@/js/config.js'; +import { url, lang } from '@/config.js'; +import { nyaize } from '@/scripts/nyaize.js'; export function aiScriptReadline(q: string): Promise { return new Promise(ok => { @@ -87,7 +87,7 @@ export function createAiScriptEnv(opts) { }), 'Mk:nyaize': values.FN_NATIVE(([text]) => { utils.assertString(text); - return values.STR(Misskey.nyaize(text.value)); + return values.STR(nyaize(text.value)); }), }; } diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 2b386bebb8..fa3fcac2e7 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -27,8 +27,6 @@ export type AsUiContainer = AsUiComponentBase & { font?: 'serif' | 'sans-serif' | 'monospace'; borderWidth?: number; borderColor?: string; - borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset'; - borderRadius?: number; padding?: number; rounded?: boolean; hidden?: boolean; @@ -175,10 +173,6 @@ function getContainerOptions(def: values.Value | undefined): Omit t.id === 'javascript'); - const highlighter = await createHighlighterCore({ + const highlighter = await getHighlighterCore({ themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), diff --git a/packages/frontend-shared/js/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts similarity index 86% rename from packages/frontend-shared/js/collapsed.ts rename to packages/frontend/src/scripts/collapsed.ts index af1f88cb73..4ec88a3c65 100644 --- a/packages/frontend-shared/js/collapsed.ts +++ b/packages/frontend/src/scripts/collapsed.ts @@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js'; export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { const collapsed = note.cw == null && ( - (note.text != null && ( + note.text != null && ( (note.text.includes('$[x2')) || (note.text.includes('$[x3')) || (note.text.includes('$[x4')) || @@ -15,7 +15,7 @@ export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): bo (note.text.split('\n').length > 9) || (note.text.length > 500) || (urls.length >= 4) - )) || (note.files != null && note.files.length >= 5) + ) || note.files.length >= 5 ); return collapsed; diff --git a/packages/frontend-shared/js/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts similarity index 91% rename from packages/frontend-shared/js/emoji-base.ts rename to packages/frontend/src/scripts/emoji-base.ts index 858cd801de..16a5a6aa5b 100644 --- a/packages/frontend-shared/js/emoji-base.ts +++ b/packages/frontend/src/scripts/emoji-base.ts @@ -20,8 +20,8 @@ export function char2fluentEmojiFilePath(char: string): string { // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x != null && x.length > 0); - const fileName = (codes as string[]).map(x => x.padStart(4, '0')).join('-'); + codes = codes.filter(x => x && x.length); + const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); return `${fluentEmojiPngBase}/${fileName}.png`; } diff --git a/packages/frontend-shared/js/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts similarity index 96% rename from packages/frontend-shared/js/emojilist.ts rename to packages/frontend/src/scripts/emojilist.ts index bde30a864f..6565feba97 100644 --- a/packages/frontend-shared/js/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -12,12 +12,12 @@ export type UnicodeEmojiDef = { } // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from './emojilist.json'; +import _emojilist from '../emojilist.json'; export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ name: x[1] as string, char: x[0] as string, - category: unicodeEmojiCategories[x[2] as number], + category: unicodeEmojiCategories[x[2]], })); const unicodeEmojisMap = new Map( diff --git a/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts similarity index 100% rename from packages/frontend-shared/js/extract-avg-color-from-blurhash.ts rename to packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts index fb7caea830..a5df36f520 100644 --- a/packages/frontend/src/scripts/focus-trap.ts +++ b/packages/frontend/src/scripts/focus-trap.ts @@ -16,57 +16,21 @@ function containsFocusTrappedElements(el: HTMLElement): boolean { }); } -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) - ) - ) { + if (siblingEl !== el && (focusTrapElements.has(siblingEl) || containsFocusTrappedElements(siblingEl) || focusTrapElements.size === 0)) { siblingEl.inert = false; } else if ( - highestZIndexElement != null && - siblingEl !== highestZIndexElement.el && - !siblingEl.contains(highestZIndexElement.el) && + focusTrapElements.size > 0 && + !containsFocusTrappedElements(siblingEl) && + !focusTrapElements.has(siblingEl) && !ignoreElements.includes(siblingEl.tagName.toLowerCase()) ) { siblingEl.inert = true; @@ -81,29 +45,9 @@ function releaseFocusTrap(el: HTMLElement): void { 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); diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index 81278b17ea..eb2da5ad86 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js'; +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; diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts deleted file mode 100644 index 39f17949d6..0000000000 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, Ref, WritableComputedRef } from 'vue'; -import { defaultStore } from '@/store.js'; -import { deepMerge } from '@/scripts/merge.js'; -import { PageHeaderItem } from '@/types/page-header.js'; -import { i18n } from '@/i18n.js'; -import { popupMenu } from '@/os.js'; -import { MenuItem } from '@/types/menu.js'; - -export const followingTab = 'following' as const; -export const mutualsTab = 'mutuals' as const; -export const followersTab = 'followers' as const; -export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const; -export type FollowingFeedTab = typeof followingFeedTabs[number]; - -export function followingTabName(tab: FollowingFeedTab): string; -export function followingTabName(tab: FollowingFeedTab | null | undefined): null; -export function followingTabName(tab: FollowingFeedTab | null | undefined): string | null { - if (tab === followingTab) return i18n.ts.following; - if (tab === followersTab) return i18n.ts.followers; - if (tab === mutualsTab) return i18n.ts.mutuals; - return null; -} - -export function followingTabIcon(tab: FollowingFeedTab | null | undefined): string { - if (tab === followersTab) return 'ph-user ph-bold ph-lg'; - if (tab === mutualsTab) return 'ph-user-switch ph-bold ph-lg'; - return 'ph-user-check ph-bold ph-lg'; -} - -export type FollowingFeedModel = { - [Key in keyof FollowingFeedState]: WritableComputedRef; -} - -export interface FollowingFeedState { - withNonPublic: boolean, - withQuotes: boolean, - withBots: boolean, - withReplies: boolean, - onlyFiles: boolean, - userList: FollowingFeedTab, - remoteWarningDismissed: boolean, -} - -export const defaultFollowingFeedState: FollowingFeedState = { - withNonPublic: false, - withQuotes: false, - withBots: true, - withReplies: false, - onlyFiles: false, - userList: followingTab, - remoteWarningDismissed: false, -}; - -interface StorageInterface = Partial> { - readonly state: Partial; - readonly reactiveState: Ref>; - save(updated: T): void; -} - -export function createHeaderItem(storage?: Ref): PageHeaderItem { - const menu = createOptionsMenu(storage); - return { - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: ev => popupMenu(menu, ev.currentTarget ?? ev.target), - }; -} - -export function createOptionsMenu(storage?: Ref): MenuItem[] { - const { - userList, - withNonPublic, - withQuotes, - withBots, - withReplies, - onlyFiles, - } = createModel(storage); - - return [ - { - type: 'switch', - text: i18n.ts.showNonPublicNotes, - ref: withNonPublic, - disabled: computed(() => userList.value === followersTab), - }, - { - type: 'switch', - text: i18n.ts.showQuotes, - ref: withQuotes, - }, - { - type: 'switch', - text: i18n.ts.showBots, - ref: withBots, - }, - { - type: 'switch', - text: i18n.ts.showReplies, - ref: withReplies, - disabled: onlyFiles, - }, - { - type: 'divider', - }, - { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: withReplies, - }, - ]; -} - -export function createModel(storage?: Ref): FollowingFeedModel { - // eslint-disable-next-line no-param-reassign - storage ??= createDefaultStorage(); - - // Based on timeline.saveTlFilter() - const saveFollowingFilter = (key: K, value: FollowingFeedState[K]) => { - const state = deepMerge(storage.value.state, defaultFollowingFeedState); - const out = deepMerge({ [key]: value }, state); - storage.value.save(out); - }; - - const userList: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.userList ?? defaultFollowingFeedState.userList, - set: value => saveFollowingFilter('userList', value), - }); - const withNonPublic: WritableComputedRef = computed({ - get: () => { - if (userList.value === 'followers') return false; - return storage.value.reactiveState.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic; - }, - set: value => saveFollowingFilter('withNonPublic', value), - }); - const withQuotes: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes, - set: value => saveFollowingFilter('withQuotes', value), - }); - const withBots: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots, - set: value => saveFollowingFilter('withBots', value), - }); - const withReplies: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies, - set: value => saveFollowingFilter('withReplies', value), - }); - const onlyFiles: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles, - set: value => saveFollowingFilter('onlyFiles', value), - }); - const remoteWarningDismissed: WritableComputedRef = computed({ - get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed, - set: value => saveFollowingFilter('remoteWarningDismissed', value), - }); - - return { - userList, - withNonPublic, - withQuotes, - withBots, - withReplies, - onlyFiles, - remoteWarningDismissed, - }; -} - -function createDefaultStorage() { - return computed(() => ({ - state: defaultStore.state.followingFeed, - reactiveState: defaultStore.reactiveState.followingFeed, - save(updated: typeof defaultStore.state.followingFeed) { - return defaultStore.set('followingFeed', updated); - }, - })); -} diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index a85ee01e26..60884d08d3 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { host as localHost } from '@@/js/config.js'; +import { host as localHost } from '@/config.js'; export async function genSearchQuery(v: any, q: string) { let host: string; diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/scripts/get-appear-note.ts deleted file mode 100644 index 40ce80eac9..0000000000 --- a/packages/frontend/src/scripts/get-appear-note.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index c8ab9238d3..108648d640 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -9,7 +9,7 @@ import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import type { MenuItem } from '@/types/menu.js'; +import { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; function rename(file: Misskey.entities.DriveFile) { @@ -87,10 +87,8 @@ async function deleteFile(file: Misskey.entities.DriveFile) { export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { const isImage = file.type.startsWith('image/'); - - const menuItems: MenuItem[] = []; - - menuItems.push({ + let menu; + menu = [{ type: 'link', to: `/my/drive/file/${file.id}`, text: i18n.ts._fileViewer.title, @@ -111,20 +109,14 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => describe(file), - }); - - if (isImage) { - menuItems.push({ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () => os.cropImage(file, { - aspectRatio: NaN, - uploadFolder: folder ? folder.id : folder, - }), - }); - } - - menuItems.push({ type: 'divider' }, { + }, ...isImage ? [{ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () => os.cropImage(file, { + aspectRatio: NaN, + uploadFolder: folder ? folder.id : folder, + }), + }] : [], { type: 'divider' }, { text: i18n.ts.createNoteFromTheFile, icon: 'ti ti-pencil', action: () => os.post({ @@ -146,17 +138,17 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss icon: 'ti ti-trash', danger: true, action: () => deleteFile(file), - }); + }]; if (defaultStore.state.devMode) { - menuItems.push({ type: 'divider' }, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, action: () => { copyToClipboard(file.id); }, - }); + }]); } - return menuItems; + return menu; } diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts deleted file mode 100644 index 158ab9c7f8..0000000000 --- a/packages/frontend/src/scripts/get-embed-code.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { defineAsyncComponent } from 'vue'; -import { v4 as uuid } from 'uuid'; -import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js'; -import { url } from '@@/js/config.js'; -import * as os from '@/os.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js'; - -const MOBILE_THRESHOLD = 500; - -/** - * パラメータを正規化する(埋め込みコード作成用) - * @param params パラメータ - * @returns 正規化されたパラメータ - */ -export function normalizeEmbedParams(params: EmbedParams): Record { - // paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す - const normalizedParams: Record = {}; - for (const key in params) { - // デフォルトの値と同じならparamsに含めない - if (params[key] == null || params[key] === defaultEmbedParams[key]) { - continue; - } - switch (typeof params[key]) { - case 'number': - normalizedParams[key] = params[key].toString(); - break; - case 'boolean': - normalizedParams[key] = params[key] ? 'true' : 'false'; - break; - default: - normalizedParams[key] = params[key]; - break; - } - } - return normalizedParams; -} - -/** - * 埋め込みコードを生成(iframe IDの発番もやる) - */ -export function getEmbedCode(path: string, params?: EmbedParams): string { - const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく - - let paramString = ''; - if (params) { - const searchParams = new URLSearchParams(normalizeEmbedParams(params)); - paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString(); - } - - const iframeCode = [ - ``, - ``, - ]; - return iframeCode.join('\n'); -} - -/** - * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) - * - * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください - */ -export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { - const _params = { ...params }; - - if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { - _params.maxHeight = 700; - } - - // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー - if (window.innerWidth < MOBILE_THRESHOLD) { - copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); - os.success(); - } else { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { - entity, - id, - params: _params, - }, { - closed: () => dispose(), - }); - } -} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 4f3fb65665..76a9400daa 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -12,16 +12,14 @@ import { instance } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { url } from '@@/js/config.js'; +import { url } from '@/config.js'; import { defaultStore, noteActions } from '@/store.js'; import { miLocalStorage } from '@/local-storage.js'; import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache, favoritedChannelsCache } from '@/cache.js'; -import type { MenuItem } from '@/types/menu.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'; -import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -36,7 +34,14 @@ export async function getNoteClipMenu(props: { } } - const appearNote = getAppearNote(props.note); + 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 clips = await clipsCache.fetch(); const menu: MenuItem[] = [...clips.map(clip => ({ @@ -67,11 +72,6 @@ export async function getNoteClipMenu(props: { }); if (props.currentClip?.id === clip.id) props.isDeleted.value = true; } - } else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') { - os.alert({ - type: 'error', - text: i18n.ts.clipNoteLimitExceeded, - }); } else { os.alert({ type: 'error', @@ -99,13 +99,11 @@ export async function getNoteClipMenu(props: { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - default: null, label: i18n.ts.name, }, description: { type: 'string', required: false, - default: null, multiline: true, label: i18n.ts.description, }, @@ -170,19 +168,6 @@ export function getCopyNoteOriginLinkMenu(note: misskey.entities.Note, text: str }; } -function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { - if (note.url != null || note.uri != null) return undefined; - if (['specified', 'followers'].includes(note.visibility)) return undefined; - - return { - icon: 'ti ti-code', - text, - action: (): void => { - genEmbedCode('notes', note.id); - }, - }; -} - export function getNoteMenu(props: { note: Misskey.entities.Note; translation: Ref; @@ -190,7 +175,14 @@ export function getNoteMenu(props: { isDeleted: Ref; currentClip?: Misskey.entities.Clip; }) { - const appearNote = getAppearNote(props.note); + 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 cleanups = [] as (() => void)[]; @@ -278,7 +270,6 @@ export function getNoteMenu(props: { } async function unclip(): Promise { - if (!props.currentClip) return; os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id }); props.isDeleted.value = true; } @@ -288,7 +279,7 @@ export function getNoteMenu(props: { title: i18n.ts.numberOfDays, }); - if (canceled || days == null) return; + if (canceled) return; os.apiWithDialog('admin/promo/create', { noteId: appearNote.id, @@ -298,8 +289,8 @@ export function getNoteMenu(props: { function share(): void { navigator.share({ - title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }), - text: appearNote.text ?? '', + title: i18n.tsx.noteOf({ user: appearNote.user.name }), + text: appearNote.text, url: `${url}/notes/${appearNote.id}`, }); } @@ -319,184 +310,170 @@ export function getNoteMenu(props: { props.translation.value = res; } - const menuItems: MenuItem[] = []; - + let menu: MenuItem[]; if ($i) { const statePromise = misskeyApi('notes/state', { noteId: appearNote.id, }); - if (props.currentClip?.userId === $i.id) { - menuItems.push({ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, { type: 'divider' }); - } - - menuItems.push({ - icon: 'ti ti-info-circle', - text: i18n.ts.details, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); - - if (appearNote.url || appearNote.uri) { - menuItems.push( + menu = [ + ...( + props.currentClip?.userId === $i.id ? [{ + icon: 'ti ti-backspace', + text: i18n.ts.unclip, + danger: true, + action: unclip, + }, { type: 'divider' }] : [] + ), { + icon: 'ti ti-info-circle', + text: i18n.ts.details, + action: openDetail, + }, { + icon: 'ti ti-copy', + text: i18n.ts.copyContent, + action: copyContent, + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) + , (appearNote.url || appearNote.uri) ? getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - ); - menuItems.push({ + : undefined, + (appearNote.url || appearNote.uri) ? { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - }); - } else { - menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); - } - - if (isSupportShare()) { - menuItems.push({ + } : undefined, + ...(isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, action: share, - }); - } - - if ($i.policies.canUseTranslator && instance.translatorAvailable) { - menuItems.push({ + }] : []), + $i && $i.policies.canUseTranslator && instance.translatorAvailable ? { icon: 'ti ti-language-hiragana', text: i18n.ts.translate, action: translate, - }); - } - - menuItems.push({ type: 'divider' }); - - menuItems.push(statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - })); - - menuItems.push({ - type: 'parent', - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - children: () => getNoteClipMenu(props), - }); - - menuItems.push(statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - })); - - if (appearNote.userId === $i.id) { - if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) { - menuItems.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - }); - } else { - menuItems.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - }); - } - } - - menuItems.push({ - type: 'parent', - icon: 'ti ti-user', - text: i18n.ts.user, - children: async () => { - const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); - const { menu, cleanup } = getUserMenu(user); - cleanups.push(cleanup); - return menu; + } : undefined, + { type: 'divider' }, + statePromise.then(state => state.isFavorited ? { + icon: 'ti ti-star-off', + text: i18n.ts.unfavorite, + action: () => toggleFavorite(false), + } : { + icon: 'ti ti-star', + text: i18n.ts.favorite, + action: () => toggleFavorite(true), + }), + { + type: 'parent' as const, + icon: 'ti ti-paperclip', + text: i18n.ts.clip, + children: () => getNoteClipMenu(props), }, - }); - - if (appearNote.userId !== $i.id) { - menuItems.push({ type: 'divider' }); - menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse)); - } - - if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) { - menuItems.push({ type: 'divider' }); - menuItems.push({ - type: 'parent', - icon: 'ti ti-device-tv', - text: i18n.ts.channel, + statePromise.then(state => state.isMutedThread ? { + icon: 'ti ti-message-off', + text: i18n.ts.unmuteThread, + action: () => toggleThreadMute(false), + } : { + icon: 'ti ti-message-off', + text: i18n.ts.muteThread, + action: () => toggleThreadMute(true), + }), + appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? { + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => togglePin(false), + } : { + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => togglePin(true), + } : undefined, + { + type: 'parent' as const, + icon: 'ti ti-user', + text: i18n.ts.user, children: async () => { - const channelChildMenu = [] as MenuItem[]; - - const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); - - if (channel.pinnedNoteIds.includes(appearNote.id)) { - channelChildMenu.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), - }), - }); - } else { - channelChildMenu.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], - }), - }); - } - return channelChildMenu; + const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); + const { menu, cleanup } = getUserMenu(user); + cleanups.push(cleanup); + return menu; }, - }); - } + }, + /* + ...($i.isModerator || $i.isAdmin ? [ + { type: 'divider' }, + { + icon: 'ti ti-speakerphone', + text: i18n.ts.promote, + action: promote + }] + : [] + ),*/ + ...(appearNote.userId !== $i.id ? [ + { type: 'divider' }, + appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined, + ] + : [] + ), + ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [ + { type: 'divider' }, + { + type: 'parent' as const, + icon: 'ti ti-device-tv', + text: i18n.ts.channel, + children: async () => { + const channelChildMenu = [] as MenuItem[]; - if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) { - menuItems.push({ type: 'divider' }); - if (appearNote.userId === $i.id) { - menuItems.push({ + const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); + + if (channel.pinnedNoteIds.includes(appearNote.id)) { + channelChildMenu.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), + }), + }); + } else { + channelChildMenu.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], + }), + }); + } + return channelChildMenu; + }, + }, + ] + : [] + ), + ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ + { type: 'divider' }, + appearNote.userId === $i.id ? { icon: 'ph-pencil-simple ph-bold ph-lg', text: i18n.ts.edit, action: edit, - }); - menuItems.push({ + } : undefined, + { icon: 'ti ti-edit', text: i18n.ts.deleteAndEdit, danger: true, action: delEdit, - }); - } - menuItems.push({ - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }); - } + }, + { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: del, + }] + : [] + )] + .filter(x => x !== undefined); } else { - menuItems.push({ + menu = [{ icon: 'ti ti-info-circle', text: i18n.ts.details, action: openDetail, @@ -504,45 +481,38 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); - - if (appearNote.url || appearNote.uri) { - menuItems.push( - getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') - ); - menuItems.push({ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); - }, - }); - } else { - menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); - } + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) + , (appearNote.url || appearNote.uri) ? + getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') + : undefined, + (appearNote.url || appearNote.uri) ? { + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); + }, + } : undefined] + .filter(x => x !== undefined); } if (noteActions.length > 0) { - menuItems.push({ type: 'divider' }); - - menuItems.push(...noteActions.map(action => ({ + menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(appearNote); }, - }))); + }))]); } if (defaultStore.state.devMode) { - menuItems.push({ type: 'divider' }, { + menu = menu.concat([{ type: "divider" }, { icon: 'ti ti-id', text: i18n.ts.copyNoteId, action: () => { copyToClipboard(appearNote.id); - os.success(); }, - }); + }]); } const cleanup = () => { @@ -553,7 +523,7 @@ export function getNoteMenu(props: { }; return { - menu: menuItems, + menu, cleanup, }; } @@ -573,7 +543,14 @@ export function getRenoteMenu(props: { renoteButton: ShallowRef; mock?: boolean; }) { - const appearNote = getAppearNote(props.note); + 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 channelRenoteItems: MenuItem[] = []; const normalRenoteItems: MenuItem[] = []; diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts index 58d486bf9b..6fd9947ac1 100644 --- a/packages/frontend/src/scripts/get-note-summary.ts +++ b/packages/frontend/src/scripts/get-note-summary.ts @@ -27,13 +27,13 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // 本文 if (note.cw != null) { - summary += `CW: ${note.cw}`; - } else if (note.text) { - summary += note.text; + summary += note.cw; + } else { + summary += note.text ? note.text : ''; } // ファイルが添付されているとき - if (note.files && note.files.length !== 0) { + if ((note.files || []).length !== 0) { summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`; } @@ -44,7 +44,7 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // 返信のとき if (note.replyId) { - if (note.reply && !note.cw) { + if (note.reply) { summary += `\n\nRE: ${getNoteSummary(note.reply)}`; } else { summary += '\n\nRE: ...'; @@ -53,7 +53,7 @@ export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { // Renoteのとき if (note.renoteId) { - if (note.renote && !note.cw) { + if (note.renote) { summary += `\n\nRN: ${getNoteSummary(note.renote)}`; } else { summary += '\n\nRN: ...'; diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 090cffe203..33f16a68aa 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -8,7 +8,7 @@ import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { host, url } from '@@/js/config.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'; @@ -17,8 +17,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; -import { genEmbedCode } from '@/scripts/get-embed-code.js'; -import type { MenuItem } from '@/types/menu.js'; +import { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; @@ -103,7 +102,8 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter async function getConfirmed(text: string): Promise { const confirm = await os.confirm({ - type: 'question', + type: 'warning', + title: 'confirm', text, }); @@ -147,154 +147,123 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - const menuItems: MenuItem[] = []; - - menuItems.push({ + let menu: MenuItem[] = [{ icon: 'ti ti-at', text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host ?? host}`); }, - }); - - if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) { - menuItems.push({ - icon: 'ti ti-search', - text: i18n.ts.searchThisUsersNotes, - action: () => { - router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); - }, - }); - } - - if (iAmModerator) { - menuItems.push({ - icon: 'ti ti-user-exclamation', - text: i18n.ts.moderation, - action: () => { - router.push(`/admin/user/${user.id}`); - }, - }); - } - - menuItems.push({ + }, ...( 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: 'ti ti-rss', text: i18n.ts.copyRSS, action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, - }); - - if (user.host != null && user.url != null) { - menuItems.push({ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - if (user.url == null) return; - window.open(user.url, '_blank', 'noopener'); - }, - }); - } else { - menuItems.push({ - icon: 'ti ti-code', - text: i18n.ts.genEmbedCode, - type: 'parent', - children: [{ - text: i18n.ts.noteOfThisUser, - action: () => { - genEmbedCode('user-timeline', user.id); - }, - }], // TODO: ユーザーカードの埋め込みなど - }); - } - - menuItems.push({ + }, ...(user.host != null && user.url != null ? [{ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + if (user.url == null) return; + window.open(user.url, '_blank', 'noopener'); + }, + }] : []), { icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }); - - if ($i) { - menuItems.push({ - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; - os.post({ specified: user, initialText: `${canonical} ` }); - }, - }, { type: 'divider' }, { - icon: 'ti ti-pencil', - text: i18n.ts.editMemo, - action: editMemo, - }, { - type: 'parent', - icon: 'ti ti-list', - text: i18n.ts.addToList, - children: async () => { - const lists = await userListsCache.fetch(); - return lists.map(list => { - const isListed = ref(list.userIds?.includes(user.id) ?? false); - cleanups.push(watch(isListed, () => { - if (isListed.value) { - os.apiWithDialog('users/lists/push', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds?.push(user.id); - }); - } else { - os.apiWithDialog('users/lists/pull', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds?.splice(list.userIds?.indexOf(user.id), 1); - }); - } - })); - - return { - type: 'switch', - text: list.name, - ref: isListed, - }; - }); - }, - }, { - type: 'parent', - icon: 'ti ti-antenna', - text: i18n.ts.addToAntenna, - children: async () => { - const antennas = await antennasCache.fetch(); - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; - return antennas.filter((a) => a.src === 'users').map(antenna => ({ - text: antenna.name, - action: async () => { - await os.apiWithDialog('antennas/update', { - antennaId: antenna.id, - name: antenna.name, - keywords: antenna.keywords, - excludeKeywords: antenna.excludeKeywords, - src: antenna.src, - userListId: antenna.userListId, - users: [...antenna.users, canonical], - caseSensitive: antenna.caseSensitive, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - notify: antenna.notify, + }, ...($i ? [{ + icon: 'ti ti-mail', + text: i18n.ts.sendMessage, + action: () => { + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; + os.post({ specified: user, initialText: `${canonical} ` }); + }, + }, { type: 'divider' }, { + icon: 'ti ti-pencil', + text: i18n.ts.editMemo, + action: () => { + editMemo(); + }, + }, { + type: 'parent', + icon: 'ti ti-list', + text: i18n.ts.addToList, + children: async () => { + const lists = await userListsCache.fetch(); + return lists.map(list => { + const isListed = ref(list.userIds.includes(user.id)); + cleanups.push(watch(isListed, () => { + if (isListed.value) { + os.apiWithDialog('users/lists/push', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds.push(user.id); }); - antennasCache.delete(); - }, + } else { + os.apiWithDialog('users/lists/pull', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds.splice(list.userIds.indexOf(user.id), 1); + }); + } })); - }, - }); - } + + return { + type: 'switch', + text: list.name, + ref: isListed, + }; + }); + }, + }, { + type: 'parent', + icon: 'ti ti-antenna', + text: i18n.ts.addToAntenna, + children: async () => { + const antennas = await antennasCache.fetch(); + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; + return antennas.filter((a) => a.src === 'users').map(antenna => ({ + text: antenna.name, + action: async () => { + await os.apiWithDialog('antennas/update', { + antennaId: antenna.id, + name: antenna.name, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + src: antenna.src, + userListId: antenna.userListId, + users: [...antenna.users, canonical], + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, + }); + antennasCache.delete(); + }, + })); + }, + }] : [])] as any; if ($i && meId !== user.id) { if (iAmModerator) { - menuItems.push({ + menu = menu.concat([{ type: 'parent', icon: 'ti ti-badges', text: i18n.ts.roles, @@ -332,14 +301,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }, })); }, - }); + }]); } // フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため //if (user.isFollowing) { - const withRepliesRef = ref(user.withReplies ?? false); - - menuItems.push({ + const withRepliesRef = ref(user.withReplies); + menu = menu.concat([{ type: 'switch', icon: 'ti ti-messages', text: i18n.ts.showRepliesToOthersInTimeline, @@ -348,8 +316,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, action: toggleNotify, - }); - + }]); watch(withRepliesRef, (withReplies) => { misskeyApi('following/update', { userId: user.id, @@ -360,7 +327,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); //} - menuItems.push({ type: 'divider' }, { + menu = menu.concat([{ type: 'divider' }, { icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, @@ -372,68 +339,70 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, - }); + }]); if (user.isFollowed) { - menuItems.push({ + menu = menu.concat([{ icon: 'ti ti-link-off', text: i18n.ts.breakFollow, action: invalidateFollow, - }); + }]); } - menuItems.push({ type: 'divider' }, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-exclamation-circle', text: i18n.ts.reportAbuse, action: reportAbuse, - }); + }]); } if (user.host !== null) { - menuItems.push({ type: 'divider' }, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-refresh', text: i18n.ts.updateRemoteUser, action: userInfoUpdate, - }); + }]); } if (defaultStore.state.devMode) { - menuItems.push({ type: 'divider' }, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyUserId, action: () => { copyToClipboard(user.id); }, - }); + }]); } if ($i && meId === user.id) { - menuItems.push({ type: 'divider' }, { + menu = menu.concat([{ type: 'divider' }, { icon: 'ti ti-pencil', text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); }, - }); + }]); } if (userActions.length > 0) { - menuItems.push({ type: 'divider' }, ...userActions.map(action => ({ + menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(user); }, - }))); + }))]); } + const cleanup = () => { + if (_DEV_) console.log('user menu cleanup', cleanups); + for (const cl of cleanups) { + cl(); + } + }; + return { - menu: menuItems, - cleanup: () => { - if (_DEV_) console.log('user menu cleanup', cleanups); - for (const cl of cleanups) { - cl(); - } - }, + menu, + cleanup, }; } diff --git a/packages/frontend-shared/js/i18n.ts b/packages/frontend/src/scripts/i18n.ts similarity index 78% rename from packages/frontend-shared/js/i18n.ts rename to packages/frontend/src/scripts/i18n.ts index 18232691fa..c2f44a33cc 100644 --- a/packages/frontend-shared/js/i18n.ts +++ b/packages/frontend/src/scripts/i18n.ts @@ -2,10 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ILocale, ParameterizedString } from '../../../locales/index.js'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type TODO = any; +import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; type FlattenKeys = keyof { [K in keyof T as T[K] extends ILocale @@ -35,18 +32,15 @@ type Tsx = { export class I18n { private tsxCache?: Tsx; - private devMode: boolean; - - constructor(public locale: T, devMode = false) { - this.devMode = devMode; + constructor(public locale: T) { //#region BIND this.t = this.t.bind(this); //#endregion } public get ts(): T { - if (this.devMode) { + if (_DEV_) { class Handler implements ProxyHandler { get(target: TTarget, p: string | symbol): unknown { const value = target[p as keyof TTarget]; @@ -78,7 +72,7 @@ export class I18n { } public get tsx(): Tsx { - if (this.devMode) { + if (_DEV_) { if (this.tsxCache) { return this.tsxCache; } @@ -119,7 +113,7 @@ export class I18n { return () => value; } - return (arg: TODO) => { + return (arg) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -143,6 +137,7 @@ export class I18n { return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this.tsxCache) { return this.tsxCache; } @@ -158,7 +153,7 @@ export class I18n { const value = target[k as keyof typeof target]; if (typeof value === 'object') { - (result as TODO)[k] = build(value as ILocale); + result[k] = build(value as ILocale); } else if (typeof value === 'string') { const quasis: string[] = []; const expressions: string[] = []; @@ -185,7 +180,7 @@ export class I18n { continue; } - (result as TODO)[k] = (arg: TODO) => { + result[k] = (arg) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -214,9 +209,9 @@ export class I18n { let str: string | ParameterizedString | ILocale = this.locale; for (const k of key.split('.')) { - str = (str as TODO)[k]; + str = str[k]; - if (this.devMode) { + if (_DEV_) { if (typeof str === 'undefined') { console.error(`Unexpected locale key: ${key}`); return key; @@ -225,7 +220,7 @@ export class I18n { } if (args) { - if (this.devMode) { + if (_DEV_) { const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); if (missing.length) { @@ -236,7 +231,7 @@ export class I18n { for (const [k, v] of Object.entries(args)) { const search = `{${k}}`; - if (this.devMode) { + if (_DEV_) { if (!(str as string).includes(search)) { console.error(`Unexpected locale parameter: ${k} at ${key}`); } @@ -249,3 +244,51 @@ export class I18n { return str; } } + +if (import.meta.vitest) { + const { describe, expect, it } = import.meta.vitest; + + describe('i18n', () => { + it('t', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.t('foo')).toBe('foo'); + expect(i18n.t('bar.baz')).toBe('baz'); + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + it('ts', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.ts.foo).toBe('foo'); + expect(i18n.ts.bar.baz).toBe('baz'); + }); + it('tsx', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + }); +} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 20f51660c7..6b511f2a5f 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,11 +10,10 @@ import { set as iset, del as idel, } from 'idb-keyval'; -import { miLocalStorage } from '@/local-storage.js'; -const PREFIX = 'idbfallback::'; +const fallbackName = (key: string) => `idbfallback::${key}`; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと @@ -39,15 +38,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); + return JSON.parse(window.localStorage.getItem(fallbackName(key))); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); + return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); } export async function del(key: string) { if (idbAvailable) return idel(key); - return miLocalStorage.removeItem(`${PREFIX}${key}`); + return window.localStorage.removeItem(fallbackName(key)); } diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts index 867ebf19ed..1517e4e1e8 100644 --- a/packages/frontend/src/scripts/initialize-sw.ts +++ b/packages/frontend/src/scripts/initialize-sw.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@@/js/config.js'; +import { lang } from '@/config.js'; export async function initializeSw() { if (!('serviceWorker' in navigator)) return; diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts index 385f59ec39..aaa4f0a86e 100644 --- a/packages/frontend/src/scripts/intl-const.ts +++ b/packages/frontend/src/scripts/intl-const.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { lang } from '@@/js/config.js'; +import { lang } from '@/config.js'; export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index e28e5725bc..406404c462 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 || $i.isAdmin || $i.isModerator)) return true; + if ($i && $i.id === user.id) 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 || $i.isAdmin || $i.isModerator)) return true; + if ($i && $i.id === user.id) return true; if (user.followersVisibility === 'private') return false; if (user.followersVisibility === 'followers' && !user.isFollowing) return false; diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/scripts/langmap.ts index b32de15963..f2a36c0fdd 100644 --- a/packages/frontend/src/scripts/langmap.ts +++ b/packages/frontend/src/scripts/langmap.ts @@ -35,6 +35,9 @@ export const langmap = { 'ar-SA': { nativeName: 'العربية (السعودية)', }, + 'ay': { + nativeName: 'Aymar aru', + }, 'ay-BO': { nativeName: 'Aymar aru', }, @@ -44,6 +47,9 @@ export const langmap = { 'az-AZ': { nativeName: 'Azərbaycan dili', }, + 'be': { + nativeName: 'Беларуская', + }, 'be-BY': { nativeName: 'Беларуская', }, @@ -65,6 +71,9 @@ export const langmap = { 'br': { nativeName: 'Brezhoneg', }, + 'bs': { + nativeName: 'Bosanski', + }, 'bs-BA': { nativeName: 'Bosanski', }, @@ -77,7 +86,7 @@ export const langmap = { 'cak': { nativeName: 'Maya Kaqchikel', }, - 'ck-US': { + 'chr': { nativeName: 'ᏣᎳᎩ (tsalagi)', }, 'cs': { @@ -152,9 +161,6 @@ export const langmap = { 'en-ZA': { nativeName: 'English (South Africa)', }, - 'en@pirate': { - nativeName: 'English (Pirate)', - }, 'eo': { nativeName: 'Esperanto', }, @@ -248,6 +254,9 @@ export const langmap = { 'fr-CH': { nativeName: 'Français (Suisse)', }, + 'fy': { + nativeName: 'Frysk', + }, 'fy-NL': { nativeName: 'Frysk', }, @@ -266,16 +275,22 @@ export const langmap = { 'gl-ES': { nativeName: 'Galego', }, + 'gn': { + nativeName: 'Avañe\'ẽ', + }, 'gn-PY': { nativeName: 'Avañe\'ẽ', }, + 'gu': { + nativeName: 'ગુજરાતી', + }, 'gu-IN': { nativeName: 'ગુજરાતી', }, 'gv': { nativeName: 'Gaelg', }, - 'gx-GR': { + 'grc': { nativeName: 'Ἑλληνική ἀρχαία', }, 'he': { @@ -338,12 +353,21 @@ export const langmap = { 'ja-JP': { nativeName: '日本語 (日本)', }, + 'jv': { + nativeName: 'Basa Jawa', + }, 'jv-ID': { nativeName: 'Basa Jawa', }, + 'ka': { + nativeName: 'ქართული', + }, 'ka-GE': { nativeName: 'ქართული', }, + 'kk': { + nativeName: 'Қазақша', + }, 'kk-KZ': { nativeName: 'Қазақша', }, @@ -371,6 +395,9 @@ export const langmap = { 'ko-KR': { nativeName: '한국어 (한국)', }, + 'ku': { + nativeName: 'Kurdî', + }, 'ku-TR': { nativeName: 'Kurdî', }, @@ -386,6 +413,9 @@ export const langmap = { 'lb': { nativeName: 'Lëtzebuergesch', }, + 'li': { + nativeName: 'Lèmbörgs', + }, 'li-NL': { nativeName: 'Lèmbörgs', }, @@ -404,6 +434,9 @@ export const langmap = { 'mai': { nativeName: 'मैथिली, মৈথিলী', }, + 'mg': { + nativeName: 'Malagasy', + }, 'mg-MG': { nativeName: 'Malagasy', }, @@ -419,6 +452,9 @@ export const langmap = { 'ml-IN': { nativeName: 'മലയാളം', }, + 'mn': { + nativeName: 'Монгол', + }, 'mn-MN': { nativeName: 'Монгол', }, @@ -443,6 +479,9 @@ export const langmap = { 'my': { nativeName: 'ဗမာစကာ', }, + 'nan': { + nativeName: '閩南語', + }, 'no': { nativeName: 'Norsk', }, @@ -467,12 +506,18 @@ export const langmap = { 'nl-NL': { nativeName: 'Nederlands (Nederland)', }, + 'nn': { + nativeName: 'Norsk (nynorsk)', + }, 'nn-NO': { nativeName: 'Norsk (nynorsk)', }, 'oc': { nativeName: 'Occitan', }, + 'or': { + nativeName: 'ଓଡ଼ିଆ', + }, 'or-IN': { nativeName: 'ଓଡ଼ିଆ', }, @@ -488,6 +533,9 @@ export const langmap = { 'pl-PL': { nativeName: 'Polski', }, + 'ps': { + nativeName: 'پښتو', + }, 'ps-AF': { nativeName: 'پښتو', }, @@ -500,9 +548,15 @@ export const langmap = { 'pt-PT': { nativeName: 'Português (Portugal)', }, + 'qu': { + nativeName: 'Qhichwa', + }, 'qu-PE': { nativeName: 'Qhichwa', }, + 'rm': { + nativeName: 'Rumantsch', + }, 'rm-CH': { nativeName: 'Rumantsch', }, @@ -518,15 +572,24 @@ export const langmap = { 'ru-RU': { nativeName: 'Русский', }, + 'sa': { + nativeName: 'संस्कृतम्', + }, 'sa-IN': { nativeName: 'संस्कृतम्', }, + 'se': { + nativeName: 'Davvisámegiella', + }, 'se-NO': { nativeName: 'Davvisámegiella', }, 'sh': { nativeName: 'српскохрватски', }, + 'si': { + nativeName: 'සිංහල', + }, 'si-LK': { nativeName: 'සිංහල', }, @@ -542,6 +605,9 @@ export const langmap = { 'sl-SI': { nativeName: 'Slovenščina', }, + 'so': { + nativeName: 'Soomaaliga', + }, 'so-SO': { nativeName: 'Soomaaliga', }, @@ -602,12 +668,18 @@ export const langmap = { 'tlh': { nativeName: 'tlhIngan-Hol', }, + 'tok': { + nativeName: 'Toki Pona', + }, 'tr': { nativeName: 'Türkçe', }, 'tr-TR': { nativeName: 'Türkçe', }, + 'tt': { + nativeName: 'татарча', + }, 'tt-RU': { nativeName: 'татарча', }, @@ -635,6 +707,9 @@ export const langmap = { 'vi-VN': { nativeName: 'Tiếng Việt', }, + 'xh': { + nativeName: 'isiXhosa', + }, 'xh-ZA': { nativeName: 'isiXhosa', }, @@ -644,6 +719,9 @@ export const langmap = { 'yi-DE': { nativeName: 'ייִדיש (German)', }, + 'yue': { + nativeName: '粵語', + }, 'zh': { nativeName: '中文', }, @@ -665,7 +743,16 @@ export const langmap = { 'zh-TW': { nativeName: '中文(台灣)', }, + 'zu': { + nativeName: 'isiZulu', + }, 'zu-ZA': { nativeName: 'isiZulu', }, }; + +export const langs: string[] = [ + ...(Object.keys(langmap).filter(tag => tag.indexOf('-') < 0)), + 'zh-Hans', + 'zh-Hant', +]; diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 78eba35ead..099a22163a 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -3,32 +3,51 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { MediaProxy } from '@@/js/media-proxy.js'; -import { url } from '@@/js/config.js'; +import { query } from '@/scripts/url.js'; +import { url } from '@/config.js'; import { instance } from '@/instance.js'; -let _mediaProxy: MediaProxy | null = null; +export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { + const localProxy = `${url}/proxy`; -export function getProxiedImageUrl(...args: Parameters): string { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); + if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { + // もう既にproxyっぽそうだったらurlを取り出す + imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; } - return _mediaProxy.getProxiedImageUrl(...args); + return `${mustOrigin ? localProxy : instance.mediaProxy}/${ + type === 'preview' ? 'preview.webp' + : 'image.webp' + }?${query({ + url: imageUrl, + ...(!noFallback ? { 'fallback': '1' } : {}), + ...(type ? { [type]: '1' } : {}), + ...(mustOrigin ? { origin: '1' } : {}), + })}`; } -export function getProxiedImageUrlNullable(...args: Parameters): string | null { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); +export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { + if (imageUrl == null) return null; + return getProxiedImageUrl(imageUrl, type); +} + +export function getStaticImageUrl(baseUrl: string): string { + const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); + + if (u.href.startsWith(`${url}/emoji/`)) { + // もう既にemojiっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; } - return _mediaProxy.getProxiedImageUrlNullable(...args); -} - -export function getStaticImageUrl(...args: Parameters): string { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); + if (u.href.startsWith(instance.mediaProxy + '/')) { + // もう既にproxyっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; } - return _mediaProxy.getStaticImageUrl(...args); + return `${instance.mediaProxy}/static.webp?${query({ + url: u.href, + static: '1', + })}`; } diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 89fdda0cbb..9794a300da 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -18,7 +18,7 @@ function isPureObject(value: unknown): value is Record(value: DeepPartial, def: X): X { +export function deepMerge>(value: DeepPartial, def: X): X { if (isPureObject(value) && isPureObject(def)) { const result = deepClone(value as Cloneable) as X; for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 2911469cdd..63acf9d3de 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -6,7 +6,7 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@@/js/const.js'; +import { MFM_TAGS } from '@/const.js'; import type { MenuItem } from '@/types/menu.js'; /** diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts index 1b1159fd01..49fb6f9e59 100644 --- a/packages/frontend/src/scripts/misskey-api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -5,7 +5,7 @@ import * as Misskey from 'misskey-js'; import { ref } from 'vue'; -import { apiUrl } from '@@/js/config.js'; +import { apiUrl } from '@/config.js'; import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); diff --git a/packages/misskey-js/src/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts similarity index 81% rename from packages/misskey-js/src/nyaize.ts rename to packages/frontend/src/scripts/nyaize.ts index d5ee54776f..5e6fa298d1 100644 --- a/packages/misskey-js/src/nyaize.ts +++ b/packages/frontend/src/scripts/nyaize.ts @@ -7,10 +7,10 @@ const koRegex1 = /[나-낳]/g; const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; -function ifAfter(prefix: string, fn: (x: string) => string) { +function ifAfter(prefix, fn) { const preLen = prefix.length; const regex = new RegExp(prefix, 'i'); - return (x: string, pos: number, string: string) => { + return (x, pos, string) => { return pos > 0 && string.substring(pos - preLen, pos).match(regex) ? fn(x) : x; }; } @@ -24,9 +24,9 @@ export function nyaize(text: string): string { .replace(/ing/gi, ifAfter('morn', x => x === 'ING' ? 'YAN' : 'yan')) .replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan')) // ko-KR - .replace(koRegex1, match => !isNaN(match.charCodeAt(0)) ? String.fromCharCode( + .replace(koRegex1, match => String.fromCharCode( match.charCodeAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0), - ) : match) + )) .replace(koRegex2, '다냥') .replace(koRegex3, '냥'); } diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts index 39c6df6500..53b2a9e441 100644 --- a/packages/frontend/src/scripts/player-url-transform.ts +++ b/packages/frontend/src/scripts/player-url-transform.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { hostname } from '@@/js/config.js'; +import { hostname } from '@/config.js'; export function transformPlayerUrl(url: string): string { const urlObj = new URL(url); diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 5b141222e8..1caa2dfc21 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from '@@/js/url.js'; -import * as config from '@@/js/config.js'; +import { appendQuery } from './url.js'; +import * as config from '@/config.js'; export function popout(path: string, w?: HTMLElement) { let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index be49532cf8..3dad41a8b3 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -15,8 +15,6 @@ export function calcPopupPosition(el: HTMLElement, props: { const contentWidth = el.offsetWidth; const contentHeight = el.offsetHeight; - const HORIZONTAL_MARGIN = 1; - let rect: DOMRect; if (props.anchorElement) { @@ -38,11 +36,9 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; + left = window.innerWidth - contentWidth + window.scrollX - 1; } - left = Math.max(HORIZONTAL_MARGIN, left); - return [left, top]; }; @@ -61,11 +57,9 @@ export function calcPopupPosition(el: HTMLElement, props: { left -= (el.offsetWidth / 2); if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - HORIZONTAL_MARGIN; + left = window.innerWidth - contentWidth + window.scrollX - 1; } - left = Math.max(HORIZONTAL_MARGIN, left); - return [left, top]; }; @@ -81,8 +75,6 @@ export function calcPopupPosition(el: HTMLElement, props: { top = props.y; } - left = Math.max(HORIZONTAL_MARGIN, left); - top -= (el.offsetHeight / 2); if (top + contentHeight - window.scrollY > window.innerHeight) { @@ -114,8 +106,6 @@ export function calcPopupPosition(el: HTMLElement, props: { top -= (el.offsetHeight / 2); } - left = Math.max(HORIZONTAL_MARGIN, left); - if (top + contentHeight - window.scrollY > window.innerHeight) { top = window.innerHeight - contentHeight + window.scrollY - 1; } diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 11b6f52ddd..31a9ac1ad9 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -18,7 +18,7 @@ export type MiPostMessageEvent = { * 親フレームにイベントを送信 */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.parent.postMessage({ + window.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts deleted file mode 100644 index 733d91b85a..0000000000 --- a/packages/frontend/src/scripts/reload-ask.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { i18n } from '@/i18n.js'; -import * as os from '@/os.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; - -let isReloadConfirming = false; - -export async function reloadAsk(opts: { - unison?: boolean; - reason?: string; -}) { - if (isReloadConfirming) { - return; - } - - isReloadConfirming = true; - - const { canceled } = await os.confirm(opts.reason == null ? { - type: 'info', - text: i18n.ts.reloadConfirm, - } : { - type: 'info', - title: i18n.ts.reloadConfirm, - text: opts.reason, - }).finally(() => { - isReloadConfirming = false; - }); - - if (canceled) return; - - if (opts.unison) { - unisonReload(); - } else { - location.reload(); - } -} diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts new file mode 100644 index 0000000000..6bfcef6c36 --- /dev/null +++ b/packages/frontend/src/scripts/safe-parse.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function safeParseFloat(str: unknown): number | null { + if (typeof str !== 'string' || str === '') return null; + const num = parseFloat(str); + if (isNaN(num)) return null; + return num; +} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts new file mode 100644 index 0000000000..0edf4e9eba --- /dev/null +++ b/packages/frontend/src/scripts/safe-uri-decode.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend/src/scripts/scroll.ts similarity index 82% rename from packages/frontend-shared/js/scroll.ts rename to packages/frontend/src/scripts/scroll.ts index 4f2e9105c3..f0274034b5 100644 --- a/packages/frontend-shared/js/scroll.ts +++ b/packages/frontend/src/scripts/scroll.ts @@ -36,27 +36,19 @@ export function getScrollPosition(el: HTMLElement | null): number { return container == null ? window.scrollY : container.scrollTop; } -export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknown, tolerance = 1, once = false) { +export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { // とりあえず評価してみる - const firstTopVisible = isTopVisible(el); - if (el.isConnected && firstTopVisible) { - cb(firstTopVisible); + if (el.isConnected && isTopVisible(el)) { + cb(); if (once) return null; } const container = getScrollContainer(el) ?? window; - // 以下のケースにおいて、cbが何度も呼び出されてしまって具合が悪いので1回呼んだら以降は無視するようにする - // - スクロールイベントは1回のスクロールで複数回発生することがある - // - toleranceの範囲内に収まる程度の微量なスクロールが発生した - let prevTopVisible = firstTopVisible; - const onScroll = () => { + const onScroll = ev => { if (!document.body.contains(el)) return; - - const topVisible = isTopVisible(el, tolerance); - if (topVisible !== prevTopVisible) { - prevTopVisible = topVisible; - cb(topVisible); + if (isTopVisible(el, tolerance)) { + cb(); if (once) removeListener(); } }; @@ -77,7 +69,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 } const containerOrWindow = container ?? window; - const onScroll = () => { + const onScroll = ev => { if (!document.body.contains(el)) return; if (isBottomVisible(el, 1, container)) { cb(); @@ -134,7 +126,6 @@ export function scrollToBottom( export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { const scrollTop = getScrollPosition(el); - if (_DEV_) console.log(scrollTop, tolerance, scrollTop <= tolerance); return scrollTop <= tolerance; } diff --git a/packages/frontend/src/scripts/show-system-account-dialog.ts b/packages/frontend/src/scripts/show-system-account-dialog.ts deleted file mode 100644 index 3c28d901fc..0000000000 --- a/packages/frontend/src/scripts/show-system-account-dialog.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; - -export function showSystemAccountDialog(): Promise { - return os.alert({ - type: 'error', - title: i18n.ts.systemAccountTitle, - text: i18n.ts.systemAccountDescription, - }); -} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts deleted file mode 100644 index cb0e607fcb..0000000000 --- a/packages/frontend/src/scripts/stream-mock.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { EventEmitter } from 'eventemitter3'; -import * as Misskey from 'misskey-js'; -import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; - -type AnyOf> = T[keyof T]; -type OmitFirst = T extends [any, ...infer R] ? R : never; - -/** - * Websocket無効化時に使うStreamのモック(なにもしない) - */ -export class StreamMock extends EventEmitter implements IStream { - public readonly state = 'initializing'; - - constructor(...args: ConstructorParameters) { - super(); - // do nothing - } - - public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock { - return new ChannelConnectionMock(this, channel, name); - } - - public removeSharedConnection(connection: any): void { - // do nothing - } - - public removeSharedConnectionPool(pool: any): void { - // do nothing - } - - public disconnectToChannel(): void { - // do nothing - } - - public send(typeOrPayload: string): void - public send(typeOrPayload: string, payload: any): void - public send(typeOrPayload: Record | any[]): void - public send(typeOrPayload: string | Record | any[], payload?: any): void { - // do nothing - } - - public ping(): void { - // do nothing - } - - public heartbeat(): void { - // do nothing - } - - public close(): void { - // do nothing - } -} - -class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection { - public id = ''; - public name?: string; // for debug - public inCount = 0; // for debug - public outCount = 0; // for debug - public channel: string; - - constructor(stream: IStream, ...args: OmitFirst>>) { - super(); - - this.channel = args[0]; - this.name = args[1]; - } - - public send(type: T, body: Channel['receives'][T]): void { - // do nothing - } - - public dispose(): void { - // do nothing - } -} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index bd3cddde67..e59643b09c 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,11 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; -import lightTheme from '@@/themes/_light.json5'; -import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { @@ -54,7 +54,7 @@ export const getBuiltinThemes = () => Promise.all( 'd-u0', 'rosepine', 'rosepine-dawn', - ].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), + ].map(name => import(`@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); export const getBuiltinThemesRef = () => { @@ -78,8 +78,6 @@ export function applyTheme(theme: Theme, persist = true) { const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; - document.documentElement.dataset.colorScheme = colorScheme; - // Deep copy const _theme = deepClone(theme); diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 22dce609c6..3e947183c9 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -9,11 +9,10 @@ import { v4 as uuid } from 'uuid'; import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; -import { apiUrl } from '@@/js/config.js'; +import { apiUrl } from '@/config.js'; import { $i } from '@/account.js'; import { alert } from '@/os.js'; import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; type Uploading = { id: string; @@ -40,15 +39,6 @@ export function uploadFile( if (folder && typeof folder === 'object') folder = folder.id; - if (file.size > instance.maxFileSize) { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, - }); - return Promise.reject(); - } - return new Promise((resolve, reject) => { const id = uuid(); diff --git a/packages/frontend-shared/js/url.ts b/packages/frontend/src/scripts/url.ts similarity index 70% rename from packages/frontend-shared/js/url.ts rename to packages/frontend/src/scripts/url.ts index eb830b1eea..5a8265af9e 100644 --- a/packages/frontend-shared/js/url.ts +++ b/packages/frontend/src/scripts/url.ts @@ -8,18 +8,18 @@ * 2. プロパティがundefinedの時はクエリを付けない * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) */ -export function query(obj: Record): string { +export function query(obj: Record): string { const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition - .reduce>((a, [k, v]) => (a[k] = v, a), {}); + .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) + .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); return Object.entries(params) .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) .join('&'); } -export function appendQuery(url: string, queryString: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`; +export function appendQuery(url: string, query: string): string { + return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; } export function extractDomain(url: string) { diff --git a/packages/frontend-shared/js/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts similarity index 85% rename from packages/frontend-shared/js/use-document-visibility.ts rename to packages/frontend/src/scripts/use-document-visibility.ts index b1197e68da..a8f4d5e03a 100644 --- a/packages/frontend-shared/js/use-document-visibility.ts +++ b/packages/frontend/src/scripts/use-document-visibility.ts @@ -3,8 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { onMounted, onUnmounted, ref } from 'vue'; -import type { Ref } from 'vue'; +import { onMounted, onUnmounted, ref, Ref } from 'vue'; export function useDocumentVisibility(): Ref { const visibility = ref(document.visibilityState); diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts deleted file mode 100644 index 0d505fe466..0000000000 --- a/packages/frontend/src/scripts/use-form.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, Reactive, reactive, watch } from 'vue'; - -function copy(v: T): T { - return JSON.parse(JSON.stringify(v)); -} - -function unwrapReactive(v: Reactive): T { - return JSON.parse(JSON.stringify(v)); -} - -export function useForm>(initialState: T, save: (newState: T) => Promise) { - const currentState = reactive(copy(initialState)); - const previousState = reactive(copy(initialState)); - - const modifiedStates = reactive>({} as any); - for (const key in currentState) { - modifiedStates[key] = false; - } - const modified = computed(() => Object.values(modifiedStates).some(v => v)); - const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length); - - watch([currentState, previousState], () => { - for (const key in modifiedStates) { - modifiedStates[key] = currentState[key] !== previousState[key]; - } - }, { deep: true }); - - async function _save() { - await save(unwrapReactive(currentState)); - for (const key in currentState) { - previousState[key] = copy(currentState[key]); - } - } - - function discard() { - for (const key in currentState) { - currentState[key] = copy(previousState[key]); - } - } - - return { - state: currentState, - savedState: previousState, - modifiedStates, - modified, - modifiedCount, - save: _save, - discard, - }; -} diff --git a/packages/frontend-shared/js/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts similarity index 100% rename from packages/frontend-shared/js/use-interval.ts rename to packages/frontend/src/scripts/use-interval.ts diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index 89aa023f23..1b3626bff5 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -13,16 +13,16 @@ import { misskeyApi } from './misskey-api.js'; export function useNoteCapture(props: { rootEl: ShallowRef; note: Ref; - pureNote?: Ref; + pureNote: Ref; isDeletedRef: Ref; - onReplyCallback?: (replyNote: Misskey.entities.Note) => void | Promise; - onDeleteCallback?: (id: Misskey.entities.Note['id']) => void | Promise; + onReplyCallback: (replyNote: Misskey.entities.Note) => void | undefined; + onDeleteCallback: (id: Misskey.entities.Note['id']) => void | undefined; }) { const note = props.note; const pureNote = props.pureNote !== undefined ? props.pureNote : props.note; const connection = $i ? useStream() : null; - async function onStreamNoteUpdated(noteData): Promise { + async function onStreamNoteUpdated(noteData): void { const { type, id, body } = noteData; if ((id !== note.value.id) && (id !== pureNote.value.id)) return; @@ -39,7 +39,7 @@ export function useNoteCapture(props: { await props.onReplyCallback(replyNote); } catch { /* empty */ } - + break; } @@ -81,7 +81,7 @@ export function useNoteCapture(props: { case 'pollVoted': { const choice = body.choice; - const choices = [...note.value.poll!.choices]; + const choices = [...note.value.poll.choices]; choices[choice] = { ...choices[choice], votes: choices[choice].votes + 1, @@ -90,7 +90,7 @@ export function useNoteCapture(props: { } : {}), }; - note.value.poll!.choices = choices; + note.value.poll.choices = choices; break; } @@ -106,7 +106,7 @@ export function useNoteCapture(props: { const editedNote = await misskeyApi('notes/show', { noteId: id, }); - + const keys = new Set(); Object.keys(editedNote) .concat(Object.keys(note.value)) diff --git a/packages/frontend/src/scripts/warning-external-website.ts b/packages/frontend/src/scripts/warning-external-website.ts deleted file mode 100644 index 67158c6438..0000000000 --- a/packages/frontend/src/scripts/warning-external-website.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { instance } from '@/instance.js'; -import { defaultStore } from '@/store.js'; -import * as os from '@/os.js'; -import MkUrlWarningDialog from '@/components/MkUrlWarningDialog.vue'; - -const extractDomain = /^(https?:\/\/|\/\/)?([^@/\s]+@)?(www\.)?([^:/\s]+)/i; -const isRegExp = /^\/(.+)\/(.*)$/; - -export async function warningExternalWebsite(url: string) { - const domain = extractDomain.exec(url)?.[4]; - - if (!domain) return false; - - const isTrustedByInstance = instance.trustedLinkUrlPatterns.some(expression => { - const r = isRegExp.exec(expression); - - if (r) { - return new RegExp(r[1], r[2]).test(url); - } else if (expression.includes(' ')) { - return expression.split(' ').every(keyword => url.includes(keyword)); - } else { - return domain.endsWith(expression); - } - }); - - const isTrustedByUser = defaultStore.reactiveState.trustedDomains.value.includes(domain); - const isDisabledByUser = !defaultStore.reactiveState.warnExternalUrl.value; - - if (!isTrustedByInstance && !isTrustedByUser && !isDisabledByUser) { - const confirm = await new Promise<{ canceled: boolean }>(resolve => { - const { dispose } = os.popup(MkUrlWarningDialog, { - url, - }, { - done: result => { - resolve(result ?? { canceled: true }); - }, - closed: () => dispose(), - }); - }); - - if (confirm.canceled) return false; - - return window.open(url, '_blank', 'nofollow noopener popup=false'); - } - - return window.open(url, '_blank', 'nofollow noopener popup=false'); -} diff --git a/packages/frontend-shared/js/worker-multi-dispatch.ts b/packages/frontend/src/scripts/worker-multi-dispatch.ts similarity index 84% rename from packages/frontend-shared/js/worker-multi-dispatch.ts rename to packages/frontend/src/scripts/worker-multi-dispatch.ts index 5d393ed1ed..6b3fcd9383 100644 --- a/packages/frontend-shared/js/worker-multi-dispatch.ts +++ b/packages/frontend/src/scripts/worker-multi-dispatch.ts @@ -3,18 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -function defaultUseWorkerNumber(prev: number) { +function defaultUseWorkerNumber(prev: number, totalWorkers: number) { return prev + 1; } -type WorkerNumberGetter = (prev: number, totalWorkers: number) => number; - -export class WorkerMultiDispatch { +export class WorkerMultiDispatch { private symbol = Symbol('WorkerMultiDispatch'); private workers: Worker[] = []; private terminated = false; private prevWorkerNumber = 0; - private getUseWorkerNumber: WorkerNumberGetter; + private getUseWorkerNumber = defaultUseWorkerNumber; private finalizationRegistry: FinalizationRegistry; constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { @@ -31,7 +29,7 @@ export class WorkerMultiDispatch { if (_DEV_) console.log('WorkerMultiDispatch: Created', this); } - public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: WorkerNumberGetter = this.getUseWorkerNumber) { + public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) { let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); @@ -48,14 +46,12 @@ export class WorkerMultiDispatch { return workerNumber; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public addListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { this.workers.forEach(worker => { worker.addEventListener('message', callback, options); }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any public removeListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { this.workers.forEach(worker => { worker.removeEventListener('message', callback, options); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index bbd9873ad8..44de9aa0a7 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -5,14 +5,11 @@ import { markRaw, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { hemisphere } from '@@/js/intl-const.js'; -import lightTheme from '@@/themes/l-cherry.json5'; -import darkTheme from '@@/themes/d-ice.json5'; import { miLocalStorage } from './local-storage.js'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; -import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; +import { hemisphere } from '@/scripts/intl-const.js'; interface PostFormAction { title: string, @@ -168,14 +165,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'account', default: 'public' as 'public' | 'home' | 'followers', }, - trustedDomains: { - where: 'account', - default: [] as string[], - }, - warnExternalUrl: { - where: 'account', - default: true, - }, menu: { where: 'deviceAccount', @@ -242,10 +231,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'deviceAccount', default: [] as Misskey.entities.UserList[], }, - followingFeed: { - where: 'account', - default: defaultFollowingFeedState, - }, overridedDeviceKind: { where: 'device', @@ -303,17 +288,13 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: window.matchMedia('(prefers-reduced-motion)').matches, }, - disableCatSpeak: { - where: 'account', - default: false, - }, emojiStyle: { where: 'device', default: 'twemoji', // twemoji / fluentEmoji / native }, - menuStyle: { + disableDrawer: { where: 'device', - default: 'auto' as 'auto' | 'popup' | 'drawer', + default: false, }, useBlurEffectForModal: { where: 'device', @@ -375,9 +356,9 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 2, }, - emojiPickerStyle: { + emojiPickerUseDrawerForMobile: { where: 'device', - default: 'auto' as 'auto' | 'popup' | 'drawer', + default: true, }, recentlyUsedEmojis: { where: 'device', @@ -475,14 +456,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: 'horizontal' as 'vertical' | 'horizontal', }, - notificationClickable: { + enableCondensedLineForAcct: { where: 'device', default: false, }, - enableCondensedLine: { - where: 'device', - default: true, - }, additionalUnicodeEmojiIndexes: { where: 'device', default: {} as Record>, @@ -547,10 +524,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - contextMenu: { + contextMenu: { where: 'device', default: 'app' as 'app' | 'appWithShift' | 'native', - }, + }, sound_masterVolume: { where: 'device', @@ -580,6 +557,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, }, + recentlyUsedPostLanguages: { + where: 'account', + default: [] as string[], + }, })); // TODO: 他のタブと永続化されたstateを同期 @@ -609,6 +590,8 @@ interface Watcher { /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ +import lightTheme from '@/themes/l-cherry.json5'; +import darkTheme from '@/themes/d-ice.json5'; export class ColdDeviceStorage { public static default = { @@ -645,7 +628,7 @@ export class ColdDeviceStorage { public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼び出し側のバグ等で undefined が来ることがある // undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index e63dac951c..0d5bd78b09 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -6,21 +6,18 @@ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; -import { wsOrigin } from '@@/js/config.js'; -// TODO: No WebsocketモードでStreamMockが使えそう -//import { StreamMock } from '@/scripts/stream-mock.js'; +import { wsOrigin } from '@/config.js'; // heart beat interval in ms const HEART_BEAT_INTERVAL = 1000 * 60; -let stream: Misskey.IStream | null = null; -let timeoutHeartBeat: number | null = null; +let stream: Misskey.Stream | null = null; +let timeoutHeartBeat: ReturnType | null = null; let lastHeartbeatCall = 0; -export function useStream(): Misskey.IStream { +export function useStream(): Misskey.Stream { if (stream) return stream; - // TODO: No Websocketモードもここで判定 stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index d990a706b3..62ba7a08d5 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -33,14 +33,21 @@ --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); --minBottomSpacing: var(--minBottomSpacingMobile); - //--ad: rgb(255 169 0 / 10%); - @media (max-width: 500px) { --margin: var(--marginHalf); } --avatar: 48px; --thread-width: 2px; + + //--ad: rgb(255 169 0 / 10%); + --eventFollow: #36aed2; + --eventRenote: #36d298; + --eventReply: #007aff; + --eventReactionHeart: #dd2e44; + --eventReaction: #e99a0b; + --eventAchievement: #cb9a11; + --eventOther: #88a6b7; } html.radius-misskey { @@ -282,11 +289,11 @@ rt { background: var(--accent); &:not(:disabled):hover { - background: hsl(from var(--accent) h s calc(l + 5)); + background: var(--X8); } &:not(:disabled):active { - background: hsl(from var(--accent) h s calc(l - 5)); + background: var(--X9); } } @@ -296,11 +303,11 @@ rt { background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); &:not(:disabled):hover { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } &:not(:disabled):active { - background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + background: linear-gradient(90deg, var(--X8), var(--X8)); } } @@ -405,16 +412,6 @@ rt { vertical-align: top; } -._modified { - margin-left: 0.7em; - font-size: 65%; - padding: 2px 3px; - color: var(--warn); - border: solid 1px var(--warn); - border-radius: 4px; - vertical-align: top; -} - ._table { > ._row { display: flex; @@ -484,7 +481,7 @@ rt { --fg: #693410; } -html[data-color-scheme=dark] ._woodenFrame { +html[data-color-mode=dark] ._woodenFrame { --bg: #1d0c02; --fg: #F1E8DC; --panel: #192320; diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 similarity index 89% rename from packages/frontend-shared/themes/_dark.json5 rename to packages/frontend/src/themes/_dark.json5 index e4649311c3..7b70aa1e09 100644 --- a/packages/frontend-shared/themes/_dark.json5 +++ b/packages/frontend/src/themes/_dark.json5 @@ -13,7 +13,6 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', - love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#000', acrylicBg: ':alpha<0.5<@bg', @@ -55,13 +54,11 @@ infoFg: '#fff', infoWarnBg: '#42321c', infoWarnFg: '#ffbd3e', - folderHeaderBg: 'rgba(255, 255, 255, 0.05)', - folderHeaderHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonBg: ':lighten<5<@panel', - buttonHoverBg: ':lighten<10<@panel', + switchBg: 'rgba(255, 255, 255, 0.15)', + buttonBg: 'rgba(255, 255, 255, 0.05)', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<20<@accent', - switchBg: 'rgba(255, 255, 255, 0.15)', switchOffBg: 'rgba(255, 255, 255, 0.1)', switchOffFg: ':alpha<0.8<@fg', switchOnBg: '@accentedBg', @@ -81,14 +78,22 @@ codeBoolean: '#c59eff', deckBg: '#000', htmlThemeColor: '@bg', + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.3)', X12: 'rgba(255, 255, 255, 0.1)', X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', }, codeHighlighter: { diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 similarity index 89% rename from packages/frontend-shared/themes/_light.json5 rename to packages/frontend/src/themes/_light.json5 index b6218a5f1d..d797aec734 100644 --- a/packages/frontend-shared/themes/_light.json5 +++ b/packages/frontend/src/themes/_light.json5 @@ -13,7 +13,6 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', accentedBg: ':alpha<0.15<@accent', - love: '#dd2e44', focus: ':alpha<0.3<@accent', bg: '#fff', acrylicBg: ':alpha<0.5<@bg', @@ -55,13 +54,11 @@ infoFg: '#72818a', infoWarnBg: '#fff0db', infoWarnFg: '#8f6e31', - folderHeaderBg: 'rgba(0, 0, 0, 0.05)', - folderHeaderHoverBg: 'rgba(0, 0, 0, 0.1)', - buttonBg: ':darken<5<@panel', - buttonHoverBg: ':darken<10<@panel', + switchBg: 'rgba(0, 0, 0, 0.15)', + buttonBg: 'rgba(0, 0, 0, 0.05)', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<20<@accent', - switchBg: 'rgba(0, 0, 0, 0.15)', switchOffBg: 'rgba(0, 0, 0, 0.1)', switchOffFg: '@panel', switchOnBg: '@accent', @@ -81,14 +78,22 @@ codeBoolean: '#62b70c', deckBg: ':darken<3<@bg', htmlThemeColor: '@bg', + X2: ':darken<2<@panel', X3: 'rgba(0, 0, 0, 0.05)', X4: 'rgba(0, 0, 0, 0.1)', X5: 'rgba(0, 0, 0, 0.05)', X6: 'rgba(0, 0, 0, 0.25)', X7: 'rgba(0, 0, 0, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.1)', X12: 'rgba(0, 0, 0, 0.1)', X13: 'rgba(0, 0, 0, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', }, codeHighlighter: { diff --git a/packages/frontend-shared/themes/d-astro.json5 b/packages/frontend/src/themes/d-astro.json5 similarity index 87% rename from packages/frontend-shared/themes/d-astro.json5 rename to packages/frontend/src/themes/d-astro.json5 index a674a5c5c9..fee25cc4a4 100644 --- a/packages/frontend-shared/themes/d-astro.json5 +++ b/packages/frontend/src/themes/d-astro.json5 @@ -25,6 +25,7 @@ mention: '#ffd152', modalBg: 'rgba(0, 0, 0, 0.5)', success: '#86b300', + buttonBg: 'rgba(255, 255, 255, 0.05)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', mentionMe: '#fb5d38', @@ -41,6 +42,7 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@accent', accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<-20<@accent', driveFolderBg: ':alpha<0.3<@accent', @@ -55,13 +57,20 @@ wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', panelHeaderDivider: 'rgba(0, 0, 0, 0)', scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.3)', X12: 'rgba(255, 255, 255, 0.1)', X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', }, } diff --git a/packages/frontend-shared/themes/d-botanical.json5 b/packages/frontend/src/themes/d-botanical.json5 similarity index 100% rename from packages/frontend-shared/themes/d-botanical.json5 rename to packages/frontend/src/themes/d-botanical.json5 diff --git a/packages/frontend-shared/themes/d-cherry.json5 b/packages/frontend/src/themes/d-cherry.json5 similarity index 100% rename from packages/frontend-shared/themes/d-cherry.json5 rename to packages/frontend/src/themes/d-cherry.json5 diff --git a/packages/frontend-shared/themes/d-dark.json5 b/packages/frontend/src/themes/d-dark.json5 similarity index 100% rename from packages/frontend-shared/themes/d-dark.json5 rename to packages/frontend/src/themes/d-dark.json5 diff --git a/packages/frontend-shared/themes/d-future.json5 b/packages/frontend/src/themes/d-future.json5 similarity index 100% rename from packages/frontend-shared/themes/d-future.json5 rename to packages/frontend/src/themes/d-future.json5 diff --git a/packages/frontend-shared/themes/d-green-lime.json5 b/packages/frontend/src/themes/d-green-lime.json5 similarity index 100% rename from packages/frontend-shared/themes/d-green-lime.json5 rename to packages/frontend/src/themes/d-green-lime.json5 diff --git a/packages/frontend-shared/themes/d-green-orange.json5 b/packages/frontend/src/themes/d-green-orange.json5 similarity index 100% rename from packages/frontend-shared/themes/d-green-orange.json5 rename to packages/frontend/src/themes/d-green-orange.json5 diff --git a/packages/frontend-shared/themes/d-ice.json5 b/packages/frontend/src/themes/d-ice.json5 similarity index 100% rename from packages/frontend-shared/themes/d-ice.json5 rename to packages/frontend/src/themes/d-ice.json5 diff --git a/packages/frontend-shared/themes/d-persimmon.json5 b/packages/frontend/src/themes/d-persimmon.json5 similarity index 100% rename from packages/frontend-shared/themes/d-persimmon.json5 rename to packages/frontend/src/themes/d-persimmon.json5 diff --git a/packages/frontend-shared/themes/d-u0.json5 b/packages/frontend/src/themes/d-u0.json5 similarity index 93% rename from packages/frontend-shared/themes/d-u0.json5 rename to packages/frontend/src/themes/d-u0.json5 index 32ac9ec5cf..3bd0b9483c 100644 --- a/packages/frontend-shared/themes/d-u0.json5 +++ b/packages/frontend/src/themes/d-u0.json5 @@ -3,11 +3,14 @@ base: 'dark', name: 'Mi U0 Dark', props: { + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', bg: '#172426', fg: '#dadada', X10: ':alpha<0.4<@accent', @@ -38,6 +41,7 @@ mention: '@accent', modalBg: 'rgba(0, 0, 0, 0.5)', success: '#86b300', + buttonBg: 'rgba(255, 255, 255, 0.05)', switchBg: 'rgba(255, 255, 255, 0.15)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', @@ -60,6 +64,7 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@indicator', accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', fgTransparent: ':alpha<0.5<@fg', diff --git a/packages/frontend-shared/themes/l-apricot.json5 b/packages/frontend/src/themes/l-apricot.json5 similarity index 100% rename from packages/frontend-shared/themes/l-apricot.json5 rename to packages/frontend/src/themes/l-apricot.json5 diff --git a/packages/frontend-shared/themes/l-botanical.json5 b/packages/frontend/src/themes/l-botanical.json5 similarity index 100% rename from packages/frontend-shared/themes/l-botanical.json5 rename to packages/frontend/src/themes/l-botanical.json5 diff --git a/packages/frontend-shared/themes/l-cherry.json5 b/packages/frontend/src/themes/l-cherry.json5 similarity index 100% rename from packages/frontend-shared/themes/l-cherry.json5 rename to packages/frontend/src/themes/l-cherry.json5 diff --git a/packages/frontend-shared/themes/l-coffee.json5 b/packages/frontend/src/themes/l-coffee.json5 similarity index 100% rename from packages/frontend-shared/themes/l-coffee.json5 rename to packages/frontend/src/themes/l-coffee.json5 diff --git a/packages/frontend-shared/themes/l-light.json5 b/packages/frontend/src/themes/l-light.json5 similarity index 100% rename from packages/frontend-shared/themes/l-light.json5 rename to packages/frontend/src/themes/l-light.json5 diff --git a/packages/frontend-shared/themes/l-rainy.json5 b/packages/frontend/src/themes/l-rainy.json5 similarity index 100% rename from packages/frontend-shared/themes/l-rainy.json5 rename to packages/frontend/src/themes/l-rainy.json5 diff --git a/packages/frontend-shared/themes/l-sushi.json5 b/packages/frontend/src/themes/l-sushi.json5 similarity index 100% rename from packages/frontend-shared/themes/l-sushi.json5 rename to packages/frontend/src/themes/l-sushi.json5 diff --git a/packages/frontend-shared/themes/l-u0.json5 b/packages/frontend/src/themes/l-u0.json5 similarity index 96% rename from packages/frontend-shared/themes/l-u0.json5 rename to packages/frontend/src/themes/l-u0.json5 index 0b952b003a..dbc777d493 100644 --- a/packages/frontend-shared/themes/l-u0.json5 +++ b/packages/frontend/src/themes/l-u0.json5 @@ -3,11 +3,14 @@ base: 'light', name: 'Mi U0 Light', props: { + X2: ':darken<2<@panel', X3: 'rgba(255, 255, 255, 0.05)', X4: 'rgba(255, 255, 255, 0.1)', X5: 'rgba(255, 255, 255, 0.05)', X6: 'rgba(255, 255, 255, 0.15)', X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', bg: '#e7e7eb', fg: '#5f5f5f', X10: ':alpha<0.4<@accent', diff --git a/packages/frontend-shared/themes/l-vivid.json5 b/packages/frontend/src/themes/l-vivid.json5 similarity index 86% rename from packages/frontend-shared/themes/l-vivid.json5 rename to packages/frontend/src/themes/l-vivid.json5 index f1c63dde6e..3368855b5e 100644 --- a/packages/frontend-shared/themes/l-vivid.json5 +++ b/packages/frontend/src/themes/l-vivid.json5 @@ -28,6 +28,7 @@ mention: '@accent', modalBg: 'rgba(0, 0, 0, 0.3)', success: '#86b300', + buttonBg: 'rgba(0, 0, 0, 0.05)', acrylicBg: ':alpha<0.5<@bg', indicator: '@accent', mentionMe: '@mention', @@ -44,6 +45,7 @@ acrylicPanel: ':alpha<0.5<@panel', navIndicator: '@accent', accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':darken<3<@fg', fgTransparent: ':alpha<0.5<@fg', @@ -58,13 +60,21 @@ fgTransparentWeak: ':alpha<0.75<@fg', panelHeaderDivider: '@divider', scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', + X2: ':darken<2<@panel', X3: 'rgba(0, 0, 0, 0.05)', X4: 'rgba(0, 0, 0, 0.1)', X5: 'rgba(0, 0, 0, 0.05)', X6: 'rgba(0, 0, 0, 0.25)', X7: 'rgba(0, 0, 0, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', X11: 'rgba(0, 0, 0, 0.1)', X12: 'rgba(0, 0, 0, 0.1)', X13: 'rgba(0, 0, 0, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', }, } diff --git a/packages/frontend-shared/themes/rosepine-dawn.json5 b/packages/frontend/src/themes/rosepine-dawn.json5 similarity index 100% rename from packages/frontend-shared/themes/rosepine-dawn.json5 rename to packages/frontend/src/themes/rosepine-dawn.json5 diff --git a/packages/frontend-shared/themes/rosepine.json5 b/packages/frontend/src/themes/rosepine.json5 similarity index 100% rename from packages/frontend-shared/themes/rosepine.json5 rename to packages/frontend/src/themes/rosepine.json5 diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index 8355ae3061..17079b3ddc 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -7,7 +7,7 @@ import { defineAsyncComponent } from 'vue'; import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { instance } from '@/instance.js'; -import { host } from '@@/js/config.js'; +import { host } from '@/config.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/account.js'; @@ -41,9 +41,7 @@ function toolsMenuItems(): MenuItem[] { } export function openInstanceMenu(ev: MouseEvent) { - const menuItems: MenuItem[] = []; - - menuItems.push({ + os.popupMenu([{ text: instance.name ?? host, type: 'label', }, { @@ -71,18 +69,12 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.ads, icon: 'ti ti-ad', to: '/ads', - }); - - if ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) { - menuItems.push({ - type: 'link', - to: '/invite', - text: i18n.ts.invite, - icon: 'ti ti-user-plus', - }); - } - - menuItems.push({ + }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? { + type: 'link', + to: '/invite', + text: i18n.ts.invite, + icon: 'ti ti-user-plus', + } : undefined, { type: 'parent', text: i18n.ts.tools, icon: 'ti ti-tool', @@ -92,80 +84,50 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.inquiry, icon: 'ti ti-help-circle', to: '/contact', - }); - - if (instance.impressumUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.impressum, - icon: 'ti ti-file-invoice', - href: instance.impressumUrl, - target: '_blank', - }); - } - - if (instance.tosUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.termsOfService, - icon: 'ti ti-notebook', - href: instance.tosUrl, - target: '_blank', - }); - } - - if (instance.privacyPolicyUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.privacyPolicy, - icon: 'ti ti-shield-lock', - href: instance.privacyPolicyUrl, - target: '_blank', - }); - } - - if (instance.donationUrl) { - menuItems.push({ - type: 'a', - text: i18n.ts.donation, - icon: 'ph-hand-coins ph-bold ph-lg', - href: instance.donationUrl, - target: '_blank', - }); - } - - if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) { - menuItems.push({ type: 'divider' }); - } - - menuItems.push({ + }, (instance.impressumUrl) ? { + type: 'a', + text: i18n.ts.impressum, + icon: 'ti ti-file-invoice', + href: instance.impressumUrl, + target: '_blank', + } : undefined, (instance.tosUrl) ? { + type: 'a', + text: i18n.ts.termsOfService, + icon: 'ti ti-notebook', + href: instance.tosUrl, + target: '_blank', + } : undefined, (instance.privacyPolicyUrl) ? { + type: 'a', + text: i18n.ts.privacyPolicy, + icon: 'ti ti-shield-lock', + href: instance.privacyPolicyUrl, + target: '_blank', + } : undefined, (instance.donationUrl) ? { + type: 'a', + text: i18n.ts.donation, + icon: 'ph-hand-coins ph-bold ph-lg', + href: instance.donationUrl, + target: '_blank', + } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl && !instance.donationUrl) ? undefined : { type: 'divider' }, { type: 'a', text: i18n.ts.document, icon: 'ti ti-bulb', href: 'https://misskey-hub.net/docs/for-users/', target: '_blank', - }); - - if ($i) { - menuItems.push({ - text: i18n.ts._initialTutorial.launchTutorial, - icon: 'ti ti-presentation', - action: () => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { - closed: () => dispose(), - }); - }, - }); - } - - menuItems.push({ + }, ($i) ? { + text: i18n.ts._initialTutorial.launchTutorial, + icon: 'ti ti-presentation', + action: () => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { + closed: () => dispose(), + }); + }, + } : undefined, { type: 'link', text: i18n.ts.aboutMisskey, icon: 'sk-icons sk-shark sk-icons-lg', to: '/about-sharkey', - }); - - os.popupMenu(menuItems, ev.currentTarget ?? ev.target, { + }], 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 a8ff2a4c8d..442b6479dd 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -30,11 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only :enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''" > -
+
@@ -43,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
DEV BUILD
+
DEV BUILD
{{ i18n.ts.loggedInAsBot }}
@@ -105,10 +101,6 @@ if ($i) { swInject(); } } - -function getPointerEvents() { - return defaultStore.state.notificationClickable ? undefined : 'none'; -} diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index f3244b5697..a3f9d6cf2c 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -82,8 +82,6 @@ function more() { diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index ccc9af8d12..eb587554b9 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -4,7 +4,7 @@ */ import { throttle } from 'throttle-debounce'; -import { computed, markRaw, Ref } from 'vue'; +import { markRaw } from 'vue'; import { notificationTypes } from 'misskey-js'; import type { BasicTimelineType } from '@/timelines.js'; import { Storage } from '@/pizzax.js'; @@ -29,7 +29,6 @@ export const columnTypes = [ 'mentions', 'direct', 'roleTimeline', - 'following', ] as const; export type ColumnType = typeof columnTypes[number]; @@ -113,8 +112,8 @@ export const loadDeck = async () => { }; // TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する -export const saveDeck = throttle(1000, async () => { - await misskeyApi('i/registry/set', { +export const saveDeck = throttle(1000, () => { + misskeyApi('i/registry/set', { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, value: { @@ -314,7 +313,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat saveDeck(); } -export async function updateColumn(id: Column['id'], column: Partial) { +export function updateColumn(id: Column['id'], column: Partial) { const columns = deepClone(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const currentColumn = deepClone(deckStore.state.columns[columnIndex]); @@ -323,18 +322,6 @@ export async function updateColumn(id: Column['id'], column: Partial(id: Column['id']): TColumn { - return deckStore.state.columns.find(c => c.id === id) as TColumn; -} - -export function getReactiveColumn(id: Column['id']): Ref { - return computed(() => { - return deckStore.reactiveState.columns.value.find(c => c.id === id) as TColumn; - }); + deckStore.set('columns', columns); + saveDeck(); } diff --git a/packages/frontend/src/ui/deck/following-column.vue b/packages/frontend/src/ui/deck/following-column.vue deleted file mode 100644 index 6b8c9db917..0000000000 --- a/packages/frontend/src/ui/deck/following-column.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 8762fb0cce..a0e318f7eb 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -22,7 +22,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.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'; diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index f8c712c371..79c9671917 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -26,8 +26,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@@/js/scroll.js'; -import { isLink } from '@@/js/is-link.js'; +import { getScrollContainer } from '@/scripts/scroll.js'; import { mainRouter } from '@/router/main.js'; defineProps<{ @@ -53,6 +52,12 @@ function back() { function onContextmenu(ev: MouseEvent) { if (!ev.target) return; + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + }; if (isLink(ev.target as HTMLElement)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index beb4237978..a375e9c574 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -21,7 +21,7 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import type { MenuItem } from '@/types/menu.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'; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index 8315f7fca5..17afa12551 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -115,41 +115,29 @@ function onNote() { sound.playMisskeySfxFile(soundSetting.value); } -const menu = computed(() => { - const menuItems: MenuItem[] = []; - - menuItems.push({ - icon: 'ti ti-pencil', - text: i18n.ts.timeline, - action: setType, - }, { - icon: 'ti ti-bell', - text: i18n.ts._deck.newNoteNotificationSettings, - action: () => soundSettingsButton(soundSetting), - }, { - type: 'switch', - text: i18n.ts.showRenotes, - ref: withRenotes, - }); - - if (hasWithReplies(props.column.tl)) { - menuItems.push({ - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, - }); - } - - menuItems.push({ - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: hasWithReplies(props.column.tl) ? withReplies : false, - }); - - return menuItems; -}); +const menu = computed(() => [{ + icon: 'ti ti-pencil', + text: i18n.ts.timeline, + action: setType, +}, { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), +}, { + type: 'switch', + text: i18n.ts.showRenotes, + ref: withRenotes, +}, hasWithReplies(props.column.tl) ? { + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles, +} : undefined, { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: hasWithReplies(props.column.tl) ? withReplies : false, +}]);