Compare commits

...

96 Commits

Author SHA1 Message Date
renovate[bot] c7ba767af6
chore(deps): update node.js to v22.20.0 2025-09-24 17:16:38 +00:00
かっこかり 218070eb13
fix(frontend): ビルド成果物のファイル名にlocalesのhashを含めるように (#16580) 2025-09-24 17:01:48 +09:00
syuilo 0f8c068e84
feat(frontend): Video compression (#16574)
* wip

* Update CHANGELOG.md

* wip

* wip

* wip

* wip

* Update use-uploader.ts

* Update use-uploader.ts
2025-09-24 09:01:06 +09:00
github-actions[bot] 69d66b89f2 Bump version to 2025.9.1-alpha.1 2025-09-22 10:52:25 +00:00
饺子w (Yumechi) 211365de64
enhance(backend): 設定ファイルにFastifyOptions.trustProxyを追加 (#16567)
* enhance(backend): 設定ファイルにFastifyOptions.trustProxyを追加

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* try harder

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-09-22 19:45:01 +09:00
syuilo 966127c63e enhance(frontend): 絵文字ピッカーのサイズをより大きくできるように 2025-09-22 19:43:31 +09:00
果物リン 54800971eb
プロフィールの「ユーザーのノートを検索」でローカルのユーザーを検索できないのを修正 (#16570)
* プロフィールの「ユーザーのノートを検索」でローカルのユーザーを検索できないのを修正

* fix lint
2025-09-22 19:21:01 +09:00
syuilo 13d5c6d2b2 refactor MkAnimBg 2025-09-22 19:00:47 +09:00
syuilo 2cff00eedd update pnpm 2025-09-22 18:27:39 +09:00
syuilo 3fc2261041 dev(pnpm): set minimumReleaseAge to 7days
to mitigate supply-chain attack
Resolve #16572
2025-09-22 18:23:43 +09:00
syuilo 18d66c0233 Update CHANGELOG.md 2025-09-20 14:21:51 +09:00
Copilot 2f52c20150
Implement professional-grade Gaussian-approximated blur effect with resolution independence and configurable quality for image effector system (#16571)
* Initial plan

* Implement blur effect for image effector system

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Improve blur quality with configurable sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Revert to simpler blur implementation with configurable sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Fix blur size independence from sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Make blur radius resolution-independent

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Update blur.ts

* Enhance blur quality with explicit diagonal sampling and fix parameter configuration

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Implement Gaussian-approximated blur with distance-based weighting for superior quality

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Revert "Implement Gaussian-approximated blur with distance-based weighting for superior quality"

This reverts commit c739e9f55b.

* Revert "Enhance blur quality with explicit diagonal sampling and fix parameter configuration"

This reverts commit ffbc6eeba7.

* wip

* tweak

* ellipse

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-20 14:19:35 +09:00
github-actions[bot] 9d70c9ad78 Bump version to 2025.9.1-alpha.0 2025-09-19 13:51:21 +00:00
tamaina 42b2aea533
feat(frontend): 自分のプロフィールページの二次元コード(QRコード)を表示し、他の人のコードを読み取りするページを追加 (#16456)
* wip (qr.show.vue)

* added to navbar

* qr.show.vue

* fix

* added to navbar

* fix size

* 🎨

* 🎨

* fix div warn

* fix

* use * 0.25

* fix??

* fix lint

* clean up

* ???

* ?

* fix

* 🎨

* 🎨

* refactor

* 🎨

* 🎨

* :ar:t

* 🎨

* iphone flip

* no lazy import

* 🎨

* 🎨

* 🎨

* ユーザー全部flipでいいや

* ✌️

* fix

* fix

* fix lint

* 🎨

* fix type

* fix: local user profile url cannot be resolved with ap/show

* fix: local user url with hostname could not be resolved

* chore: use common utility for checking self host

* wip

* 🎨

* 🎨

* fix imports

* fix

* fix

* fix

* 🎨

* fix...

* set spacer-w

* ✌️

* 全画面でQRを読むように

* fix

* 🎨

* modify navbar.ts

* start/stop on vue activation

* display raw content read from qr

* 端末のQRをスキャンするボタンを追加

* chore

* やっぱりmfmを先に表示する

* 🎨

* fix 18n

* QRの内容は/users/:userIdにする

* add spdx

* use cqh

* `defineProps` is a compiler macro and no longer needs to be imported.

* use MkUserName

* 🎨

* 🎨

* refactor

* clean up

* refactor

* 🎨

* Update qr.show.vue

* Misskeyロゴにdrop-shadowを追加

* clean up: do not use empty css

* fix os.select usage

* Update qr.vue

* Update qr.show.vue

* Update qr.show.vue

* Update get-user-menu.ts

* ✌️

* Update show.ts

* Update ja-JP.yml

* watermark

* Update CHANGELOG.md

* Update qr.read.vue

* Update qr.read.vue

* wip

* Update MkWatermarkEditorDialog.Layer.vue

---------

Co-authored-by: anatawa12 <anatawa12@icloud.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-19 21:02:30 +09:00
syuilo 97adf6f2cc 🎨 2025-09-19 14:23:34 +09:00
tamaina 93ff209c51
enhance(frontend): bootでonunhandledrejectionでrenderErrorする場合、PromiseRejectionEvent.reasonを渡すように (#16563) 2025-09-18 19:35:23 +09:00
syuilo 5fe08d0bbb fix(frontend): iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
Fix #16562
2025-09-18 19:18:31 +09:00
syuilo 8c413d01e6
enhance(frontend): マスクエフェクト (#16556)
* wip

* wip

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update fillSquare.ts

* Update CHANGELOG.md

* Update fillSquare.ts
2025-09-17 18:38:56 +09:00
syuilo b231da7c7c enhance(frontend): チャットの日本語名称をダイレクトメッセージに & ベータを外す 2025-09-16 16:24:10 +09:00
syuilo df3e44f62e enhance(backend): allow upload csv by default
Related #16541
2025-09-16 12:16:18 +09:00
かっこかり e504560477
fix: サーバー設定によりコンテンツの閲覧が制限されている場合のメッセージを区別するように (#16527) 2025-09-16 11:53:20 +09:00
syuilo bcb2073715 enhance(backend): 初期設定をスキップした場合の初期状態ポリシーでインポート系をオプトインに
Resolve #16540
2025-09-16 11:26:35 +09:00
syuilo 6a80c23a50
chore(frontend): enable enableFolderPageView by default 2025-09-15 14:33:32 +09:00
syuilo 2621f468ff enhance: 広告ごとにセンシティブフラグを設定できるように 2025-09-14 15:25:22 +09:00
かっこかり d4654dd7bd
refactor(frontend): os.select, MkSelectのitem指定をオブジェクトによる定義に統一し、型を狭める (#16475)
* refactor(frontend): MkSelectのitem指定をオブジェクトによる定義に統一

* fix

* spdx

* fix

* fix os.select

* fix lint

* add comment

* fix

* fix: os.select対応漏れを修正

* fix

* fix

* fix: MkSelectのmodelに対する型チェックを厳格化

* fix

* fix

* fix

* Update packages/frontend/src/components/MkEmbedCodeGenDialog.vue

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* fix

* fix types

* fix

* fix

* Update packages/frontend/src/pages/admin/roles.editor.vue

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* fix: MkSelectに直接配列を指定している場合に正常に型が解決されるように

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-13 21:00:33 +09:00
renovate[bot] b7da6cad87
fix(deps): update dependency vite [security] (#16535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 10:32:49 +09:00
かっこかり 5b4115e21a
refactor(frontend): フロントエンドの型エラー解消(途中まで) (#16539)
* fix(frontend): FormLinkをボタンとして使用した際にエラーが出る問題を修正

* refactor(frontend): フロントエンドの型エラー解消

* remove unused ts-expect-error

* migrate

* remove unrelated changes

* fix lint

* more type fixes
2025-09-13 08:33:14 +09:00
syuilo c174c5c144
Update CHANGELOG.md 2025-09-12 17:13:13 +09:00
かっこかり aebc3f781e
perf(frontend): 低精度な現在時刻を一か所で管理するように (#16479)
* perf(frontend): 低精度な現在時刻を一か所で管理するように

* lint

* fix

* remove unused imports

* fix

* Update Changelog

* [ci skip] typo

* enhance: カレンダーウィジェットの日付変更は時間通りに行うように

* [ci skip] fix
2025-09-12 17:12:50 +09:00
かっこかり f60b6291d7
chore(gh): add frontend-builder to renovate 2025-09-10 10:01:25 +09:00
taiy 7673874675
fix(eslint): add window prefix rules to frontend-embed & frontend-shared (#16531) 2025-09-10 09:22:12 +09:00
github-actions[bot] 6e3354f95d [skip ci] Update CHANGELOG.md (prepend template) 2025-09-08 12:29:30 +00:00
github-actions[bot] b9df928097 Release: 2025.9.0 2025-09-08 12:29:25 +00:00
github-actions[bot] 0754678144 Bump version to 2025.9.0-rc.0 2025-09-08 11:33:58 +00:00
tamaina a8cc51dc77
fix(frontend): Safari 26でモバイルUIが崩れる問題に対するhotfix (#16528) 2025-09-08 20:32:19 +09:00
github-actions[bot] 690edcef16 Bump version to 2025.9.0-beta.1 2025-09-08 11:21:12 +00:00
renovate[bot] 2ea784f345
fix(deps): update [backend] update dependencies (#16491)
* fix(deps): update [backend] update dependencies

* fix type error

* run pnpm dedupe

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-09-08 17:11:18 +09:00
renovate[bot] 20d257b562
chore(deps): update [misskey-js] update dependencies (#16489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 16:14:23 +09:00
renovate[bot] c215415613
fix(deps): update [root] update dependencies (#16490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 16:08:15 +09:00
github-actions[bot] 726c03d96a Bump version to 2025.9.0-beta.0 2025-09-08 06:32:15 +00:00
syuilo e65ddb546c
New Crowdin updates (#16526)
* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Russian)
2025-09-08 15:20:07 +09:00
renovate[bot] 85aea9077f
fix(deps): update [frontend] update dependencies (#16492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 15:16:22 +09:00
syuilo f3fffce6a9 fix type 2025-09-08 14:57:53 +09:00
syuilo eb7db5a3aa Update MkSuspense.vue 2025-09-08 14:56:58 +09:00
syuilo e33eb26863 Update CHANGELOG.md 2025-09-07 19:41:40 +09:00
かっこかり 430310f306
fix(frontend): ctrlキーを押しながらリンクをクリックしても新しいタブで開かない問題を修正 (#16453)
* fix(frontend): ctrlキーを押しながらクリックしても新しいタブで開かない問題を修正

* Update Changelog

* fix: 制御キーの場合を個別ハンドリングするのではなくブラウザ既定の挙動に任せるように

* fix
2025-09-07 09:32:32 +09:00
syuilo 1e1eea521e chore(frontend): add force cloud backup button for debugging 2025-09-07 09:16:25 +09:00
syuilo 86ad771221
New Crowdin updates (#16525)
* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Russian)
2025-09-07 09:01:12 +09:00
syuilo 057acf471e
New Crowdin updates (#16493)
* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Spanish)
2025-09-06 20:53:36 +09:00
github-actions[bot] 2bfe257879 Bump version to 2025.9.0-alpha.2 2025-09-06 08:54:34 +00:00
syuilo 6d75624aa8
Update CHANGELOG.md 2025-09-06 17:49:53 +09:00
tamaina 369f0ec88a
fix(backend): webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正 (#16523)
画像をnsfwjsにかける前にsharpで均一にするようにした
2025-09-06 17:48:53 +09:00
かっこかり 788c5660ba
enhance(frontend): フロントエンドのキャッシュクリア操作でブラウザの内部キャッシュも削除するように (#16522)
* enhance(frontend): フロントエンドのキャッシュクリア操作でブラウザの内部キャッシュも削除するように

* 削除するキャッシュを増やす

* Update Changelog

* fix: 何らかのエラーがあっても無視するように
2025-09-06 14:46:24 +09:00
github-actions[bot] 6cf1f86636 Bump version to 2025.9.0-alpha.1 2025-09-06 03:42:29 +00:00
syuilo 5b994b3e03 fix(frontend): プロファイルを復元後アカウントの切り替えができない問題を修正
Fix #16508
2025-09-06 12:41:27 +09:00
syuilo 7b2abb7577 enhance(frontend): クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
#16510
2025-09-06 11:18:08 +09:00
github-actions[bot] b681788315 Bump version to 2025.9.0-alpha.0 2025-09-06 02:11:36 +00:00
syuilo 279af1d72f Update CHANGELOG.md 2025-09-06 11:10:41 +09:00
syuilo 9e188ca3fa Revert "refactor"
This reverts commit aa85d701b9.
2025-09-06 11:09:24 +09:00
syuilo de1b2223ff enhance(frontend): AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように 2025-09-05 19:44:11 +09:00
なっかあ 9b565728e7
fix #16494 (#16509) 2025-09-05 15:26:39 +09:00
饺子w (Yumechi) a92fd8856a
feat(backend): Send Clear-Site-Data header on /flush (#16517)
* feat(backend): Send Clear-Site-Data header on /flush

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* simplify check on flush.pug

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-09-05 13:55:37 +09:00
takaion 047773341d
fix(frontend): エラー画像が横に引き伸ばされてしまう問題に対応 (#16502)
* fix(frontend): エラー画像が横に引き伸ばされてしまう問題に対応

Fix misskey-dev#15982

* Update CHANGELOG.md
2025-09-02 16:40:57 +09:00
yukineko 842670e100
fix(frontend): RSSティッカーウィジェットが正しく動作しない問題を修正 (#16498)
* fix: RSSティッカーウィジェットが正しく機能しない問題を修正

* chore: update CHANGELOG.md
2025-09-02 10:29:25 +09:00
anatawa12 ffc481a994
fix: 「自動でもっと見る」の設定ができない問題 (#16500) 2025-09-02 10:11:50 +09:00
syuilo 2ccf4f94cb refactor 2025-09-01 16:51:58 +09:00
syuilo 3566bc207f refactor 2025-09-01 16:36:15 +09:00
syuilo 4a0e968662 refactor 2025-09-01 16:23:05 +09:00
syuilo b1479ab1d8 Update misskey-js.api.md 2025-09-01 14:07:24 +09:00
syuilo 18a9ccf7af pnpm dedupe 2025-09-01 14:07:14 +09:00
syuilo 959e72b2b3 refactor 2025-09-01 14:02:14 +09:00
syuilo a3d78b2f08 refactor 2025-09-01 13:41:40 +09:00
syuilo 3c998e1f48 refactor 2025-09-01 12:59:53 +09:00
syuilo 782c9f9852 refactor 2025-09-01 12:33:44 +09:00
syuilo d27c740ab0 refactor 2025-09-01 12:31:27 +09:00
syuilo 08ecf7ca79 refactor 2025-09-01 10:19:14 +09:00
syuilo bdec4bf87a refactor 2025-09-01 10:16:33 +09:00
syuilo 7000095b44 refactor 2025-09-01 10:01:03 +09:00
syuilo 18e42cc83d refactoe 2025-09-01 09:53:38 +09:00
syuilo 11204eeb43 refactor 2025-09-01 09:50:36 +09:00
かっこかり c95092903a
refactor(frontend): フロントエンドの型エラー解消(途中まで) (#16477)
* refactor(frontend): フロントエンドの型エラー解消

* fix

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-08-31 19:53:38 +09:00
github-actions[bot] 21b2b9e5f8 [skip ci] Update CHANGELOG.md (prepend template) 2025-08-31 08:42:45 +00:00
github-actions[bot] 665ec2c43c Release: 2025.8.0 2025-08-31 08:42:39 +00:00
syuilo 34bd840525 Update CHANGELOG.md 2025-08-31 15:56:07 +09:00
syuilo 3d1cbcf094 Update CHANGELOG.md 2025-08-31 15:29:22 +09:00
github-actions[bot] 5f5d88036f Bump version to 2025.8.0-rc.0 2025-08-31 01:32:06 +00:00
syuilo 24739cd040
New Crowdin updates (#16483)
* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Italian)
2025-08-31 10:24:41 +09:00
syuilo b491432daa fix(frontend): タッチ操作時にマウスホバー時のユーザープレビューが開くことがある問題を修正
Fix #16464
2025-08-31 10:23:29 +09:00
syuilo ebe029458e enhance(frontend): アイコンのスクロール追従を無効化する際の適用範囲を強化
Resolve #16485
2025-08-31 10:18:48 +09:00
syuilo d127d82c5b Update CHANGELOG.md 2025-08-30 19:17:11 +09:00
syuilo aabda5a956 clean up 2025-08-30 19:15:52 +09:00
syuilo bd5b38c9d9 Update CHANGELOG.md 2025-08-30 19:12:53 +09:00
syuilo 647e03bf34 update locale 2025-08-30 19:10:19 +09:00
syuilo d16db7f311
New translations ja-jp.yml (English) (#16472) 2025-08-30 16:11:27 +09:00
github-actions[bot] ec4731dee4 Bump version to 2025.8.0-beta.6 2025-08-29 07:38:45 +00:00
syuilo 65a4d77a7f fix(backend): タイムラインAPIの withRenotes: false 時のレスポンスを修正 2025-08-29 16:03:03 +09:00
278 changed files with 6765 additions and 4082 deletions

View File

@ -105,6 +105,16 @@ port: 3000
# socket: /path/to/misskey.sock
# chmodSocket: '777'
# Proxy trust settings
#
# Changes how the server interpret the origin IP of the request.
#
# Any format supported by Fastify is accepted.
# Default: trust all proxies (i.e. trustProxy: true)
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
#
# trustProxy: 1
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────

View File

@ -1 +1 @@
22.15.0
22.20.0

View File

@ -1,3 +1,40 @@
## 2025.9.1
### NOTE
- pnpm 10.16.0 が必要です
### General
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
### Client
- Feat: アカウントのQRコードを表示・読み取りできるようになりました
- Feat: 動画を圧縮してアップロードできるようになりました
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし)を追加
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
### Server
- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました
## 2025.9.0
### Client
- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
- Enhance: /flushページでサイトキャッシュをクリアできるようになりました
- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
- Enhance: CtrlキーCommandキーを押下しながらリンクをクリックすると新しいタブで開くように
- Fix: プッシュ通知を有効にできない問題を修正
- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
- Fix: エラー画像が横に引き伸ばされてしまう問題に対応
### Server
- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正
## 2025.8.0
### Note
@ -6,14 +43,12 @@
### General
- ノートを削除した際、関連するノートが同時に削除されないようになりました
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning)
- 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります
- 定期的に古いリモートの投稿を削除する機能が実装されました
- コントロールパネル→パフォーマンス→Remote Notes Cleaning で有効化できます
- データベースの肥大化を防止することが可能です
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
- データベースサイズへの効果が見られない場合はautovacuumが有効になっているか確認してください
- ハイパーリンクによる参照は検知できないためリンク切れとなります。
- 現時点では、2023-10-01以前にクリップされたリモートのートは検知しないため削除対象となります。
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
- 実際の動作に即した名称になり、馴染みのない人でも理解しやすくなりました
@ -46,9 +81,11 @@
- Enhance: トルコ語 (tr-TR) に対応
- Enhance: 不必要な翻訳データを読み込まなくなり、パフォーマンスが向上しました
- Enhance: 画像エフェクトのパラメータ名の多言語対応
- Enhance: 依存ソフトウェアの更新
- Enhance: ートを非表示にする相対期間を1ヶ月単位で自由に指定できるように
- Enhance: メールアドレス確認画面のUIを改善
- Enhance: アイコンのスクロール追従を無効化する際の適用範囲を強化
- Enhance: レンダリングパフォーマンスの向上
- Enhance: 依存ソフトウェアの更新
- Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
@ -60,6 +97,7 @@
- Fix: ユーザーの前後ノートを閲覧する機能が動作しない問題を修正
- Fix: 照会ダイアログでap/showでローカルユーザーを解決した際@username@nullに飛ばされる問題を修正
- Fix: アイコンのデコレーションを付ける際にデコレーションが表示されなくなる問題を修正
- Fix: タッチ操作時にマウスホバー時のユーザープレビューが開くことがある問題を修正
- Fix: 管理中アカウント一覧で正しい表示が行われない問題を修正
- Fix: lookupページでリモートURLを指定した際に正しく動作しない問題を修正
@ -76,6 +114,7 @@
- Fix: SystemWebhook設定でsecretを空に出来ない問題を修正
- Fix: 削除されたユーザーがチャットメッセージにリアクションしている場合`chat/history`などでエラーになる問題を修正
- Fix: Pageのアイキャッチ画像をドライブから消してもPageごと消えないように
- Fix: タイムラインAPIの withRenotes: false 時のレスポンスを修正
## 2025.7.0

View File

@ -1644,7 +1644,7 @@ _serverSettings:
reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
remoteNotesCleaning: "Neteja automàtica de notes remotes"
remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se"
remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningMaxProcessingDuration: "Duració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes"
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."
@ -3120,7 +3120,6 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard."
remoteContentsCleaning: "Neteja automàtica del contingut rebut"
remoteContentsCleaning_description: "Quan es comença a federar es rep un munt de contingut, quan s'activa la neteja automàtica el contingut antic que no es consulta serà eliminat del servidor, el que permet estalviar espai d'emmagatzematge."
remoteContentsCleaning_description2: "Alguns mètodes de referència, com els enllaços, no poden ser detectats pel sistema."
adminInfo: "Informació de l'administrador "
adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes."
adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada."

View File

@ -1668,6 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Some current settings will be reset."
entrancePageStyle: "Entrance page style"
showTimelineForVisitor: "Show timeline"
showActivitiesForVisitor: "Show activities"
_userGeneratedContentsVisibilityForVisitor:
all: "Everything is public"
localOnly: "Only local content is published, remote content is kept private"
@ -3193,6 +3194,7 @@ _imageEffector:
mirror: "Mirror"
invert: "Invert Colors"
grayscale: "Grayscale"
blur: "Blur"
colorAdjust: "Color Correction"
colorClamp: "Color Compression"
colorClampAdvanced: "Color Compression (Advanced)"
@ -3208,6 +3210,8 @@ _imageEffector:
angle: "Angle"
scale: "Size"
size: "Size"
radius: "Radius"
samples: "Samples"
color: "Color"
opacity: "Opacity"
normalize: "Normalize"

View File

@ -2137,7 +2137,7 @@ _aboutMisskey:
_displayOfSensitiveMedia:
respect: "Esconder medios marcados como sensibles"
ignore: "Mostrar medios marcados como sensibles"
force: "Esconder todala multimedia"
force: "Esconder toda la multimedia"
_instanceTicker:
none: "No mostrar"
remote: "Mostrar a usuarios remotos"
@ -3120,7 +3120,6 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante."
remoteContentsCleaning: "Limpieza automática de los contenidos recibidos"
remoteContentsCleaning_description: "La federación puede dar lugar a un flujo continuo de contenido. Al habilitar la limpieza automática, se eliminará del servidor el contenido obsoleto y sin referencias para ahorrar espacio de almacenamiento."
remoteContentsCleaning_description2: "Ciertos métodos de referencia, como los hipervínculos, no pueden ser detectados por el sistema."
adminInfo: "Información del administrador"
adminInfo_description: "Establece la información del administrador para recibir consultas."
adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada."

244
locales/index.d.ts vendored
View File

@ -1030,6 +1030,10 @@ export interface Locale extends ILocale {
*
*/
"processing": string;
/**
*
*/
"preprocessing": string;
/**
*
*/
@ -1227,7 +1231,7 @@ export interface Locale extends ILocale {
*/
"noMoreHistory": string;
/**
*
*
*/
"startChat": string;
/**
@ -1927,7 +1931,7 @@ export interface Locale extends ILocale {
*/
"markAsReadAllUnreadNotes": string;
/**
*
*
*/
"markAsReadAllTalkMessages": string;
/**
@ -5390,6 +5394,14 @@ export interface Locale extends ILocale {
*
*/
"chat": string;
/**
*
*/
"directMessage": string;
/**
*
*/
"directMessage_short": string;
/**
*
*/
@ -5501,6 +5513,14 @@ export interface Locale extends ILocale {
* <br>
*/
"defaultImageCompressionLevel_description": string;
/**
*
*/
"defaultCompressionLevel": string;
/**
* <br>
*/
"defaultCompressionLevel_description": string;
/**
*
*/
@ -5529,6 +5549,40 @@ export interface Locale extends ILocale {
*
*/
"thankYouForTestingBeta": string;
/**
*
*/
"createUserSpecifiedNote": string;
"_compression": {
"_quality": {
/**
*
*/
"high": string;
/**
*
*/
"medium": string;
/**
*
*/
"low": string;
};
"_size": {
/**
*
*/
"large": string;
/**
*
*/
"medium": string;
/**
*
*/
"small": string;
};
};
"_order": {
/**
*
@ -5540,6 +5594,10 @@ export interface Locale extends ILocale {
"oldest": string;
};
"_chat": {
/**
*
*/
"messages": string;
/**
*
*/
@ -5549,36 +5607,36 @@ export interface Locale extends ILocale {
*/
"newMessage": string;
/**
*
*
*/
"individualChat": string;
/**
*
*
*/
"individualChat_description": string;
/**
*
*
*/
"roomChat": string;
/**
*
*
*
*
*/
"roomChat_description": string;
/**
*
*
*/
"createRoom": string;
/**
*
*
*/
"inviteUserToChat": string;
/**
*
*
*/
"yourRooms": string;
/**
*
*
*/
"joiningRooms": string;
/**
@ -5598,7 +5656,7 @@ export interface Locale extends ILocale {
*/
"noHistory": string;
/**
*
*
*/
"noRooms": string;
/**
@ -5618,7 +5676,7 @@ export interface Locale extends ILocale {
*/
"ignore": string;
/**
* 退
* 退
*/
"leave": string;
/**
@ -5642,35 +5700,35 @@ export interface Locale extends ILocale {
*/
"newline": string;
/**
*
*
*/
"muteThisRoom": string;
/**
*
*
*/
"deleteRoom": string;
/**
*
*
*/
"chatNotAvailableForThisAccountOrServer": string;
/**
*
*
*/
"chatIsReadOnlyForThisAccountOrServer": string;
/**
* 使
* 使
*/
"chatNotAvailableInOtherAccount": string;
/**
*
*
*/
"cannotChatWithTheUser": string;
/**
* 使
* 使
*/
"cannotChatWithTheUser_description": string;
/**
*
*
*/
"youAreNotAMemberOfThisRoomButInvited": string;
/**
@ -5678,31 +5736,31 @@ export interface Locale extends ILocale {
*/
"doYouAcceptInvitation": string;
/**
*
*
*/
"chatWithThisUser": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromFollowers": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromFollowing": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromMutualFollowing": string;
/**
*
*
*/
"thisUserNotAllowedChatAnyone": string;
/**
*
*
*/
"chatAllowedUsers": string;
/**
*
*
*/
"chatAllowedUsers_note": string;
"_chatAllowedUsers": {
@ -6531,7 +6589,7 @@ export interface Locale extends ILocale {
*/
"remoteNotesCleaning": string;
/**
* 稿
* 稿
*/
"remoteNotesCleaning_description": string;
/**
@ -7856,7 +7914,7 @@ export interface Locale extends ILocale {
*/
"canImportUserLists": string;
/**
*
*
*/
"chatAvailability": string;
/**
@ -8706,7 +8764,7 @@ export interface Locale extends ILocale {
*/
"badge": string;
/**
*
*
*/
"messageBg": string;
/**
@ -8733,7 +8791,7 @@ export interface Locale extends ILocale {
*/
"reaction": string;
/**
*
*
*/
"chatMessage": string;
};
@ -9017,11 +9075,11 @@ export interface Locale extends ILocale {
*/
"write:following": string;
/**
*
*
*/
"read:messaging": string;
/**
*
*
*/
"write:messaging": string;
/**
@ -9313,11 +9371,11 @@ export interface Locale extends ILocale {
*/
"write:report-abuse": string;
/**
*
*
*/
"write:chat": string;
/**
*
*
*/
"read:chat": string;
};
@ -9543,7 +9601,7 @@ export interface Locale extends ILocale {
*/
"birthdayFollowings": string;
/**
*
*
*/
"chat": string;
};
@ -10283,7 +10341,7 @@ export interface Locale extends ILocale {
*/
"roleAssigned": string;
/**
*
*
*/
"chatRoomInvitationReceived": string;
/**
@ -10396,7 +10454,7 @@ export interface Locale extends ILocale {
*/
"roleAssigned": string;
/**
*
*
*/
"chatRoomInvitationReceived": string;
/**
@ -10578,7 +10636,7 @@ export interface Locale extends ILocale {
*/
"roleTimeline": string;
/**
*
*
*/
"chat": string;
};
@ -10945,7 +11003,7 @@ export interface Locale extends ILocale {
*/
"deleteGalleryPost": string;
/**
*
*
*/
"deleteChatRoom": string;
/**
@ -12036,13 +12094,9 @@ export interface Locale extends ILocale {
*/
"remoteContentsCleaning": string;
/**
*
*
*/
"remoteContentsCleaning_description": string;
/**
*
*/
"remoteContentsCleaning_description2": string;
/**
*
*/
@ -12223,10 +12277,18 @@ export interface Locale extends ILocale {
*
*/
"text": string;
/**
*
*/
"qr": string;
/**
*
*/
"position": string;
/**
*
*/
"margin": string;
/**
*
*/
@ -12283,6 +12345,10 @@ export interface Locale extends ILocale {
*
*/
"polkadotSubDotDivisions": string;
/**
* URLになります
*/
"leaveBlankToAccountUrl": string;
};
"_imageEffector": {
/**
@ -12322,6 +12388,10 @@ export interface Locale extends ILocale {
*
*/
"grayscale": string;
/**
*
*/
"blur": string;
/**
* 調
*/
@ -12366,6 +12436,10 @@ export interface Locale extends ILocale {
*
*/
"tearing": string;
/**
*
*/
"fill": string;
};
"_fxProps": {
/**
@ -12380,6 +12454,18 @@ export interface Locale extends ILocale {
*
*/
"size": string;
/**
*
*/
"radius": string;
/**
*
*/
"samples": string;
/**
*
*/
"offset": string;
/**
*
*/
@ -12492,6 +12578,10 @@ export interface Locale extends ILocale {
*
*/
"zoomLinesBlack": string;
/**
*
*/
"circle": string;
};
};
/**
@ -12552,6 +12642,68 @@ export interface Locale extends ILocale {
*/
"listDrafts": string;
};
/**
*
*/
"qr": string;
"_qr": {
/**
*
*/
"showTabTitle": string;
/**
*
*/
"readTabTitle": string;
/**
* {name} {acct}
*/
"shareTitle": ParameterizedString<"name" | "acct">;
/**
* Fediverseで私をフォローしてください
*/
"shareText": string;
/**
*
*/
"chooseCamera": string;
/**
*
*/
"cannotToggleFlash": string;
/**
*
*/
"turnOnFlash": string;
/**
*
*/
"turnOffFlash": string;
/**
*
*/
"startQr": string;
/**
*
*/
"stopQr": string;
/**
* QRコードが見つかりません
*/
"noQrCodeFound": string;
/**
*
*/
"scanFile": string;
/**
*
*/
"raw": string;
/**
* MFM
*/
"mfm": string;
};
}
declare const locales: {
[lang: string]: Locale;

View File

@ -139,7 +139,7 @@ overwriteFromPinnedEmojis: "Sovrascrivi con le impostazioni globali"
reactionSettingDescription2: "Trascina per riorganizzare, clicca per cancellare, usa il pulsante \"+\" per aggiungere."
rememberNoteVisibility: "Ricordare le impostazioni di visibilità delle note"
attachCancel: "Rimuovi allegato"
deleteFile: "File da Drive eliminato"
deleteFile: "Elimina un file dal Drive"
markAsSensitive: "Segna come esplicito"
unmarkAsSensitive: "Non segnare come esplicito "
enterFileName: "Nome del file"
@ -2222,7 +2222,7 @@ _theme:
hashtag: "Hashtag"
mention: "Menzioni"
mentionMe: "Menzioni (di me)"
renote: "Renota"
renote: "Rinota"
modalBg: "Sfondo modale."
divider: "Interruzione di linea"
scrollbarHandle: "Maniglie della barra di scorrimento"
@ -2663,7 +2663,7 @@ _notification:
createToken: "È stato creato un token di accesso"
createTokenDescription: "In caso contrario, eliminare il token di accesso tramite ({text})."
_types:
all: "Tutto"
all: "Tutte"
note: "Nuove Note"
follow: "Follower"
mention: "Menzioni"
@ -2671,7 +2671,7 @@ _notification:
renote: "Rinota"
quote: "Cita"
reaction: "Reazioni"
pollEnded: "Sondaggio chiuso."
pollEnded: "Sondaggio terminato"
receiveFollowRequest: "Richieste di follow in arrivo"
followRequestAccepted: "Richieste di follow accettate"
roleAssigned: "Ruolo concesso"
@ -2679,7 +2679,7 @@ _notification:
achievementEarned: "Risultato raggiunto"
exportCompleted: "Esportazione completata"
login: "Accessi"
createToken: "Creare un token di accesso"
createToken: "Aggiunto un token di accesso"
test: "Notifiche di test"
app: "Notifiche da applicazioni"
_actions:
@ -2771,56 +2771,56 @@ _abuseReport:
notifiedWebhook: "Webhook da usare"
deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?"
_moderationLogTypes:
createRole: "Ruolo creato"
deleteRole: "Ruolo eliminato"
updateRole: "Ruolo aggiornato"
assignRole: "Ruolo assegnato"
unassignRole: "Ruolo disassegnato"
suspend: "Sospensione"
unsuspend: "Sospensione rimossa"
addCustomEmoji: "Emoji personalizzata aggiunta"
updateCustomEmoji: "Emoji personalizzata aggiornata"
deleteCustomEmoji: "Emoji personalizzata eliminata"
updateServerSettings: "Impostazioni del server aggiornate"
updateUserNote: "Promemoria di moderazione aggiornato"
deleteDriveFile: "File da Drive eliminato"
deleteNote: "Nota eliminata"
createGlobalAnnouncement: "Annuncio globale creato"
createUserAnnouncement: "Annuncio ai profili iscritti creato"
updateGlobalAnnouncement: "Annuncio globale aggiornato"
updateUserAnnouncement: "Annuncio ai profili iscritti aggiornato"
deleteGlobalAnnouncement: "Annuncio globale eliminato"
deleteUserAnnouncement: "Annuncio ai profili iscritti eliminato"
resetPassword: "Password azzerata"
suspendRemoteInstance: "Istanza remota sospesa"
unsuspendRemoteInstance: "Istanza remota riattivata"
updateRemoteInstanceNote: "Aggiornamento del promemoria di moderazione per il server remoto"
markSensitiveDriveFile: "File nel Drive segnato come esplicito"
unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito"
resolveAbuseReport: "Segnalazione risolta"
forwardAbuseReport: "Segnalazione inoltrata"
updateAbuseReportNote: "Ha aggiornato la segnalazione"
createInvitation: "Genera codice di invito"
createAd: "Banner creato"
deleteAd: "Banner eliminato"
updateAd: "Banner aggiornato"
createAvatarDecoration: "Creazione decorazione della foto profilo"
updateAvatarDecoration: "Aggiornamento decorazione foto profilo"
deleteAvatarDecoration: "Eliminazione decorazione della foto profilo"
unsetUserAvatar: "Rimossa foto profilo"
unsetUserBanner: "Rimossa intestazione profilo"
createSystemWebhook: "Crea un SystemWebhook"
updateSystemWebhook: "Modifica SystemWebhook"
deleteSystemWebhook: "Elimina SystemWebhook"
createRole: "Crea un Ruolo"
deleteRole: "Elimina un Ruolo"
updateRole: "Modifica un ruolo"
assignRole: "Assegna un Ruolo"
unassignRole: "Toglie un Ruolo al Profilo"
suspend: "Sospende"
unsuspend: "Solleva la sospensione"
addCustomEmoji: "Aggiunge Emoji personalizzata"
updateCustomEmoji: "Modifica Emoji personalizzata"
deleteCustomEmoji: "Elimina Emoji personalizzata"
updateServerSettings: "Modifica le impostazioni del server"
updateUserNote: "Modifica un promemoria di moderazione"
deleteDriveFile: "Elimina un file dal Drive"
deleteNote: "Elimina una Nota"
createGlobalAnnouncement: "Crea un annuncio globale"
createUserAnnouncement: "Crea un annuncio ai profili già iscritti"
updateGlobalAnnouncement: "Modifica un annuncio globale"
updateUserAnnouncement: "Modifica un annuncio ai profili già iscritti"
deleteGlobalAnnouncement: "Elimina un annuncio globale"
deleteUserAnnouncement: "Elimina un annuncio ai profili già iscritti"
resetPassword: "Azzera la password"
suspendRemoteInstance: "Sospende una istanza remota"
unsuspendRemoteInstance: "Riattiva una istanza remota"
updateRemoteInstanceNote: "Modifica il promemoria di moderazione per il server remoto"
markSensitiveDriveFile: "Aggiunge NSFW a un file nel Drive"
unmarkSensitiveDriveFile: "Toglie NSFW da un file nel Drive"
resolveAbuseReport: "Risolve una segnalazione"
forwardAbuseReport: "Inoltra una segnalazione"
updateAbuseReportNote: "Modifica una segnalazione"
createInvitation: "Genera un codice di invito"
createAd: "Aggiunge un Banner"
deleteAd: "Elimina un Banner"
updateAd: "Modifica un Banner"
createAvatarDecoration: "Crea una decorazione della foto profilo"
updateAvatarDecoration: "Modifica una decorazione della foto profilo"
deleteAvatarDecoration: "Elimina una decorazione della foto profilo"
unsetUserAvatar: "Toglie una foto profilo"
unsetUserBanner: "Toglie una immagine di intestazione profilo"
createSystemWebhook: "Aggiunge un System Webhook"
updateSystemWebhook: "Modifica un System Webhook"
deleteSystemWebhook: "Elimina un System Webhook"
createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni"
updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni"
deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni"
deleteAccount: "Quando viene eliminato un profilo"
deletePage: "Pagina eliminata"
deleteFlash: "Play eliminato"
deleteGalleryPost: "Eliminazione pubblicazione nella Galleria"
deleteChatRoom: "Elimina chat"
updateProxyAccountDescription: "Aggiornata la descrizione del profilo proxy"
updateAbuseReportNotificationRecipient: "Modifica un destinatario per le notifiche di segnalazioni"
deleteAbuseReportNotificationRecipient: "Elimina un destinatario per le notifiche di segnalazioni"
deleteAccount: "Elimina un profilo"
deletePage: "Elimina una Pagina"
deleteFlash: "Elimina un Play"
deleteGalleryPost: "Elimina pubblicazione nella Galleria"
deleteChatRoom: "Elimina una Chat"
updateProxyAccountDescription: "Aggiorna la descrizione del profilo proxy"
_fileViewer:
title: "Dettagli del file"
type: "Tipo di file"
@ -3120,7 +3120,6 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "Puoi svolgere la configurazione avanzata anche dopo. Ad esempio specificando quali server possono federarsi."
remoteContentsCleaning: "Pulizia automatica dei contenuti in arrivo"
remoteContentsCleaning_description: "Con la federazione funzionante, riceverai sempre più contenuti. Abilitando la pulizia automatica, i contenuti non referenziati e obsoleti verranno rimossi automaticamente dai tuoi server, risparmiando spazio di archiviazione."
remoteContentsCleaning_description2: "Alcuni metodi di riferimento, come i collegamenti ipertestuali, non possono essere rilevati sul sistema."
adminInfo: "Informazioni sull'amministratore"
adminInfo_description: "Imposta le informazioni dell'amministratore utilizzate per accettare le richieste."
adminInfo_mustBeFilled: "Questa operazione è necessaria su un server aperto o se è attiva la federazione."

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?"
pinLimitExceeded: "これ以上ピン留めできません"
done: "完了"
processing: "処理中"
preprocessing: "準備中"
preview: "プレビュー"
default: "デフォルト"
defaultValueIs: "デフォルト: {value}"
@ -302,7 +303,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
explore: "みつける"
messageRead: "既読"
noMoreHistory: "これより過去の履歴はありません"
startChat: "チャットを始める"
startChat: "メッセージを送る"
nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意"
agree: "同意する"
@ -477,7 +478,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
uploadFolder: "既定アップロード先"
markAsReadAllNotifications: "すべての通知を既読にする"
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
help: "ヘルプ"
inputMessageHere: "ここにメッセージを入力"
close: "閉じる"
@ -1343,6 +1344,8 @@ postForm: "投稿フォーム"
textCount: "文字数"
information: "情報"
chat: "チャット"
directMessage: "ダイレクトメッセージ"
directMessage_short: "メッセージ"
migrateOldSettings: "旧設定情報を移行"
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
compress: "圧縮"
@ -1370,6 +1373,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
hideAllTips: "全ての「ヒントとコツ」を非表示"
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
defaultCompressionLevel: "デフォルトの圧縮度"
defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。"
inMinutes: "分"
inDays: "日"
safeModeEnabled: "セーフモードが有効です"
@ -1377,53 +1382,65 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
createUserSpecifiedNote: "ユーザー指定ノートを作成"
_compression:
_quality:
high: "高品質"
medium: "中品質"
low: "低品質"
_size:
large: "サイズ大"
medium: "サイズ中"
small: "サイズ小"
_order:
newest: "新しい順"
oldest: "古い順"
_chat:
messages: "メッセージ"
noMessagesYet: "まだメッセージはありません"
newMessage: "新しいメッセージ"
individualChat: "個人チャット"
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
roomChat: "ルームチャット"
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
createRoom: "ルームを作成"
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
yourRooms: "作成したルーム"
joiningRooms: "参加中のルーム"
individualChat: "個"
individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
roomChat: "グループ"
roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
createRoom: "グループを作成"
inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
yourRooms: "作成したグループ"
joiningRooms: "参加中のグループ"
invitations: "招待"
noInvitations: "招待はありません"
history: "履歴"
noHistory: "履歴はありません"
noRooms: "ルームはありません"
noRooms: "グループはありません"
inviteUser: "ユーザーを招待"
sentInvitations: "送信した招待"
join: "参加"
ignore: "無視"
leave: "ルームから退出"
leave: "グループから退出"
members: "メンバー"
searchMessages: "メッセージを検索"
home: "ホーム"
send: "送信"
newline: "改行"
muteThisRoom: "このルームをミュート"
deleteRoom: "ルームを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
muteThisRoom: "このグループをミュート"
deleteRoom: "グループを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
doYouAcceptInvitation: "招待を承認しますか?"
chatWithThisUser: "チャットする"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
chatAllowedUsers: "チャットを許可する相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
chatWithThisUser: "ダイレクトメッセージ"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
chatAllowedUsers: "メッセージを許可する相手"
chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
_chatAllowedUsers:
everyone: "誰でも"
followers: "自分のフォロワーのみ"
@ -1660,7 +1677,7 @@ _serverSettings:
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
remoteNotesCleaning: "リモート投稿の自動クリーニング"
remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
remoteNotesCleaning_description: "有効にすると、一定期間経過したリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間"
remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数"
inquiryUrl: "問い合わせ先URL"
@ -2034,7 +2051,7 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
chatAvailability: "チャットを許可"
chatAvailability: "ダイレクトメッセージを許可"
uploadableFileTypes: "アップロード可能なファイル種別"
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
@ -2281,7 +2298,7 @@ _theme:
buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り"
badge: "バッジ"
messageBg: "チャットの背景"
messageBg: "メッセージの背景"
fgHighlighted: "強調された文字"
_sfx:
@ -2289,7 +2306,7 @@ _sfx:
noteMy: "ノート(自分)"
notification: "通知"
reaction: "リアクション選択時"
chatMessage: "チャットのメッセージ"
chatMessage: "ダイレクトメッセージ"
_soundSettings:
driveFile: "ドライブの音声を使用"
@ -2369,8 +2386,8 @@ _permissions:
"write:favorites": "お気に入りを操作する"
"read:following": "フォローの情報を見る"
"write:following": "フォロー・フォロー解除する"
"read:messaging": "チャットを見る"
"write:messaging": "チャットを操作する"
"read:messaging": "ダイレクトメッセージを見る"
"write:messaging": "ダイレクトメッセージを操作する"
"read:mutes": "ミュートを見る"
"write:mutes": "ミュートを操作する"
"write:notes": "ノートを作成・削除する"
@ -2443,8 +2460,8 @@ _permissions:
"read:clip-favorite": "クリップのいいねを見る"
"read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する"
"write:chat": "チャットを操作する"
"read:chat": "チャットを閲覧する"
"write:chat": "ダイレクトメッセージを操作する"
"read:chat": "ダイレクトメッセージを閲覧する"
_auth:
shareAccessTitle: "アプリへのアクセス許可"
@ -2507,7 +2524,7 @@ _widgets:
chooseList: "リストを選択"
clicker: "クリッカー"
birthdayFollowings: "今日誕生日のユーザー"
chat: "チャット"
chat: "ダイレクトメッセージ"
_cw:
hide: "隠す"
@ -2714,7 +2731,7 @@ _notification:
newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました"
chatRoomInvitationReceived: "チャットルームへ招待されました"
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
testNotification: "通知テスト"
@ -2744,7 +2761,7 @@ _notification:
receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された"
roleAssigned: "ロールが付与された"
chatRoomInvitationReceived: "チャットルームへ招待された"
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した"
login: "ログイン"
@ -2794,7 +2811,7 @@ _deck:
mentions: "メンション"
direct: "指名"
roleTimeline: "ロールタイムライン"
chat: "チャット"
chat: "ダイレクトメッセージ"
_dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
@ -2897,7 +2914,7 @@ _moderationLogTypes:
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"
deleteChatRoom: "チャットルームを削除"
deleteChatRoom: "ダイレクトメッセージのグループを削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
_fileViewer:
@ -3217,8 +3234,7 @@ _serverSetupWizard:
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
remoteContentsCleaning: "リモートコンテンツの自動クリーニング"
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
remoteContentsCleaning_description2: "ローカル内リモートコンテンツへのハイパーリンクはリンク切れとなります。"
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、一定期間経過したリモートコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
adminInfo: "管理者情報"
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"
@ -3272,7 +3288,9 @@ _watermarkEditor:
opacity: "不透明度"
scale: "サイズ"
text: "テキスト"
qr: "二次元コード"
position: "位置"
margin: "マージン"
type: "タイプ"
image: "画像"
advanced: "高度"
@ -3287,6 +3305,7 @@ _watermarkEditor:
polkadotSubDotOpacity: "サブドットの不透明度"
polkadotSubDotRadius: "サブドットの大きさ"
polkadotSubDotDivisions: "サブドットの数"
leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります"
_imageEffector:
title: "エフェクト"
@ -3300,6 +3319,7 @@ _imageEffector:
mirror: "ミラー"
invert: "色の反転"
grayscale: "白黒"
blur: "ぼかし"
colorAdjust: "色調補正"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"
@ -3311,11 +3331,15 @@ _imageEffector:
checker: "チェッカー"
blockNoise: "ブロックノイズ"
tearing: "ティアリング"
fill: "塗りつぶし"
_fxProps:
angle: "角度"
scale: "サイズ"
size: "サイズ"
radius: "半径"
samples: "サンプル数"
offset: "位置"
color: "色"
opacity: "不透明度"
normalize: "正規化"
@ -3344,6 +3368,7 @@ _imageEffector:
zoomLinesThreshold: "集中線の幅"
zoomLinesMaskSize: "中心径"
zoomLinesBlack: "黒色にする"
circle: "円形"
drafts: "下書き"
_drafts:
@ -3360,3 +3385,20 @@ _drafts:
restoreFromDraft: "下書きから復元"
restore: "復元"
listDrafts: "下書き一覧"
qr: "二次元コード"
_qr:
showTabTitle: "表示"
readTabTitle: "読み取る"
shareTitle: "{name} {acct}"
shareText: "Fediverseで私をフォローしてください"
chooseCamera: "カメラを選択"
cannotToggleFlash: "ライト選択不可"
turnOnFlash: "ライトをオンにする"
turnOffFlash: "ライトをオフにする"
startQr: "コードリーダーを再開"
stopQr: "コードリーダーを停止"
noQrCodeFound: "QRコードが見つかりません"
scanFile: "端末の画像をスキャン"
raw: "テキスト"
mfm: "MFM"

View File

@ -3120,7 +3120,6 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다."
remoteContentsCleaning: "리모트 콘텐츠 자동 정리"
remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다."
remoteContentsCleaning_description2: "로컬 내 원격 콘텐츠로의 하이퍼링크는 깨진 링크로 됩니다."
adminInfo: "관리자 정보"
adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다."
adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다."

View File

@ -1215,6 +1215,7 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль
tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности"
avatarDecorations: "Украшения для аватара"
attach: "Прикрепить"
detachAll: "Убрать всё"
angle: "Угол"
flip: "Переворот"
showAvatarDecorations: "Показать украшения для аватара"
@ -1253,7 +1254,7 @@ clipNoteLimitExceeded: "К этому клипу больше нельзя до
performance: "Производительность"
modified: "Изменено"
signinWithPasskey: "Войдите в систему, используя свой пароль"
unknownWebAuthnKey: "Не известный ключ "
unknownWebAuthnKey: "Неизвестный ключ"
passkeyVerificationFailed: "Ошибка проверка ключа доступа "
messageToFollower: "Сообщение подписчикам"
testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. <strong>Не использовать это в рабочей среде</strong>"
@ -1268,8 +1269,11 @@ availableRoles: "Доступные роли"
federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах."
draft: "Черновик"
markAsSensitiveConfirm: "Отметить контент как чувствительный?"
preferences: "Основное"
resetToDefaultValue: "Сбросить настройки до стандартных"
syncBetweenDevices: "Синхронизировать между устройствами"
postForm: "Форма отправки"
textCount: "Количество символов"
information: "Описание"
inMinutes: "мин"
inDays: "сут"
@ -1281,6 +1285,11 @@ _chat:
send: "Отправить"
_settings:
webhook: "Вебхук"
preferencesBanner: "Вы можете настроить общее поведение клиента по вашим предпочтениям"
timelineAndNote: "Лента и заметки"
_chat:
showSenderName: "Показывать имя отправителя"
sendOnEnter: "Использовать Enter для отправки"
_delivery:
stop: "Заморожено"
_type:
@ -1529,7 +1538,7 @@ _achievements:
description: "Нажато здесь"
_justPlainLucky:
title: "Чистая удача"
description: "Может достаться с вероятностью 0,01% каждые 10 секунд."
description: "Может достаться с вероятностью 0,005% каждые 10 секунд."
_setNameToSyuilo:
title: "Комплекс бога"
description: "Установлено «syuilo» в качестве имени"
@ -1557,6 +1566,12 @@ _achievements:
title: "Brain Diver"
description: "Опубликована ссылка на песню «Brain Diver»"
flavor: "Мисски-Мисски Ла-Ту-Ма"
_bubbleGameExplodingHead:
title: "🤯"
description: "Самый большой объект в Bubble game"
_bubbleGameDoubleExplodingHead:
title: "Двойной🤯"
description: "Два самых больших объекта в Bubble game одновременно!"
_role:
new: "Новая роль"
edit: "Изменить роль"

View File

@ -360,7 +360,7 @@ whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde"
disconnectedFromServer: "Sunucu bağlantısı kesildi"
reload: "Yenile"
doNothing: "Yoksay"
reloadConfirm: "Zaman çizelgesini yenilemek ister misin?"
reloadConfirm: "Panoyu yenilemek ister misin?"
watch: "İzle"
unwatch: "İzlemeyi bırak"
accept: "Kabul et"
@ -573,9 +573,9 @@ objectStorageSetPublicRead: "Yükleme sırasında \"genel-okuma\" ayarını yap
s3ForcePathStyleDesc: "s3ForcePathStyle etkinleştirilirse, kova adı URL'nin ana bilgisayar adı yerine URL yoluna eklenmelidir. Kendi kendine barındırılan bir Minio örneği gibi hizmetleri kullanırken bu ayarı etkinleştirmen gerekebilir."
serverLogs: "Sunucu log kayıtları"
deleteAll: "Tümünü sil"
showFixedPostForm: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle"
showFixedPostFormInChannel: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle (Kanallar)"
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak zaman çizelgesine dahil et"
showFixedPostForm: "Gönderi formunu pano üstünde görüntüle"
showFixedPostFormInChannel: "Gönderi formunu pano üstünde görüntüle (Kanallar)"
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak panoya dahil et"
newNoteRecived: "Yeni Not'lar var"
newNote: "Yeni Not"
sounds: "Sesler"
@ -1059,7 +1059,7 @@ achievements: "Başarılar"
gotInvalidResponseError: "Geçersiz sunucu yanıtı"
gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar dene."
thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir."
thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder"
thisPostMayBeAnnoyingHome: "Ana panoya gönder"
thisPostMayBeAnnoyingCancel: "İptal"
thisPostMayBeAnnoyingIgnore: "Yine de gönder"
collapseRenotes: "Daha önce görüntülenen Renote'lari kısaltılmış olarak göster"
@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Pano'da diğer kişilere verilen yanıtları
hideRepliesToOthersInTimeline: "Pano'dan diğer kişilerin yanıtlarını gizle"
showRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesin diğerlerine verdiği yanıtları göster"
hideRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesten diğer kişilere verilen yanıtları gizle"
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını zaman çizelgende diğer kullanıcılara göstermek istiyor musun?"
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını zaman tünelinde cidden göstermeyecek misin?"
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını panoda diğer kullanıcılara göstermek istiyor musun?"
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını panoda cidden göstermeyecek misin?"
externalServices: "Dış Hizmetler"
sourceCode: "Kaynak kodu"
sourceCodeIsNotYetProvided: "Kaynak kodu henüz mevcut değildir. Bu sorunu gidermek için yöneticiyle iletişime geçin."
@ -1570,9 +1570,9 @@ _initialTutorial:
description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsin."
_note:
title: "Not nedir?"
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar zaman çizelgesinde kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar panoda kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
reply: "Bir mesaja yanıt vermek için bu düğmeye tıklayın. Yanıtlara yanıt vermek de mümkündür, böylece konuşma bir konu başlığı gibi devam eder."
renote: "Bu notu kendi zaman çizelgende paylaşabilirsiniz. Ayrıca yorumlarınızla birlikte alıntı da yapabilirsin."
renote: "Bu notu kendi panonda paylaşabilirsin. Ayrıca yorumlarınla birlikte alıntı da yapabilirsin."
reaction: "Not'a tepkiler ekleyebilirsin. Daha fazla ayrıntı bir sonraki sayfada açıklanacak."
menu: "Not ayrıntılarını görüntüleyebilir, bağlantıları kopyalayabilir ve çeşitli diğer işlemleri gerçekleştirebilirsin."
_reaction:
@ -1640,7 +1640,7 @@ _serverSettings:
shortNameDescription: "Resmi adın uzun olması durumunda görüntülenebilen, örneğin adının kısaltması."
fanoutTimelineDescription: "Etkinleştirildiğinde Pano alma performansını büyük ölçüde artırır ve veritabanı yükünü azaltır. Bunun karşılığında Redis'in bellek kullanımı artacaktır. Sunucu belleği düşükse veya sunucu kararsızsa bunu devre dışı bırakmayı düşün."
fanoutTimelineDbFallback: "Veritabanına geri dön"
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek zaman çizelgelerinin aralığını sınırlar."
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek panoların aralığını sınırlar."
reactionsBufferingDescription: "Etkinleştirildiğinde, reaksiyon oluşturma sırasında performans büyük ölçüde artacak ve veritabanı üzerindeki yük azalacaktır. Ancak, Redis bellek kullanımı artacakt."
remoteNotesCleaning: "Uzak notların otomatik olarak temizlenmesi"
remoteNotesCleaning_description: "Etkinleştirildiğinde, kullanılmayan ve güncelliğini yitirmiş uzak notlar, veritabanının şişmesini önlemek için periyodik olarak temizlenecek."
@ -1668,6 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
entrancePageStyle: "Giriş sayfası stili"
showTimelineForVisitor: "Panoyu göster"
showActivitiesForVisitor: "Aktiviteleri göster"
_userGeneratedContentsVisibilityForVisitor:
all: "Her şey halka açıktır."
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."
@ -1876,7 +1877,7 @@ _achievements:
title: "Öz Referans"
description: "Kendi notunuzu alıntı yapın"
_htl20npm:
title: "Akış Zaman Çizelgesi"
title: "Akış Panosu"
description: "Ev zaman çizelgenizin hızı 20 npm'yi (dakika başına not sayısı) aşıyor mu?"
_viewInstanceChart:
title: "Analist"
@ -1965,7 +1966,7 @@ _role:
asBadge: "Rozet olarak göster"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
isExplorable: "Rolü keşfedilebilir hale getir"
descriptionOfIsExplorable: "Bu rolün zaman çizelgesi ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
descriptionOfIsExplorable: "Bu rolün panosu ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
displayOrder: "Pozisyon"
descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur."
preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun"
@ -1979,7 +1980,7 @@ _role:
high: "Yüksek"
_options:
gtlAvailable: "Global Pano'yu görüntüleyebilir"
ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir"
ltlAvailable: "Yerel panoyu görüntüleyebilir"
canPublicNote: "Halka açık notlar gönderebilir"
mentionMax: "Bir notta maksimum bahsetme sayısı"
canInvite: "Sunucu davet kodları oluşturabilir"
@ -2484,7 +2485,7 @@ _visibility:
public: "Halka açık"
publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır."
home: "Pano"
homeDescription: "Yalnızca ana zaman çizelgesine gönder"
homeDescription: "Yalnızca ana panoya gönder"
followers: "Takipçiler"
followersDescription: "Sadece takipçilerine görünür hale getir"
specified: "Doğrudan"
@ -2531,7 +2532,7 @@ _exportOrImport:
userLists: "Kullanıcı listeleri"
excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut"
excludeInactiveUsers: "Etkin olmayan kullanıcıları hariç tut"
withReplies: "İçe aktarılan kullanıcıların yanıtlarını zaman çizelgesine dahil edin"
withReplies: "İçe aktarılan kullanıcıların yanıtlarını panoya dahil edin"
_charts:
federation: "Federasyon"
apRequest: "Talepler"
@ -2925,7 +2926,7 @@ _reversi:
freeMatch: "Ücretsiz Eşleştirme"
lookingForPlayer: "Rakip aranıyor..."
gameCanceled: "Oyun iptal edildi."
shareToTlTheGameWhenStart: "Oyun başlatıldığında zaman çizelgesinde paylaş"
shareToTlTheGameWhenStart: "Oyun başlatıldığında panoda paylaş"
iStartedAGame: "Oyun başladı! #MisskeyReversi"
opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş."
allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)"
@ -3153,7 +3154,7 @@ _clientPerformanceIssueTip:
_clip:
tip: "Klip, notları gruplandırmanıza olanak tanıyan bir özelliktir."
_userLists:
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir zaman çizelgesi olarak görüntülenebilir."
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir pano olarak görüntülenebilir."
watermark: "Filigran"
defaultPreset: "Varsayılan Ön Ayar"
_watermarkEditor:

View File

@ -3120,7 +3120,6 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
remoteContentsCleaning: "自动清理传入内容"
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
remoteContentsCleaning_description2: "如超链接之类的某些引用方法无法被系统检测到。"
adminInfo: "管理员信息"
adminInfo_description: "设置用于接受询问的管理员信息。"
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"

View File

@ -3120,7 +3120,6 @@ _serverSetupWizard:
youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n"
remoteContentsCleaning: "自動清理接收的內容"
remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。"
remoteContentsCleaning_description2: "有些引用方式系統上無法檢測到,例如超連結。"
adminInfo: "管理員資訊"
adminInfo_description: "設定用於接收查詢的管理者資訊。\n"
adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n"

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.8.0-beta.5",
"version": "2025.9.1-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.15.0",
"packageManager": "pnpm@10.16.0",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@ -62,21 +62,22 @@
"js-yaml": "4.1.0",
"postcss": "8.5.6",
"tar": "7.4.3",
"terser": "5.43.1",
"terser": "5.44.0",
"typescript": "5.9.2"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/node": "22.17.2",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@types/js-yaml": "4.0.9",
"@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"cross-env": "7.0.3",
"cypress": "14.5.4",
"eslint": "9.34.0",
"eslint": "9.35.0",
"globals": "16.3.0",
"ncp": "2.0.0",
"pnpm": "10.15.0",
"start-server-and-test": "2.0.13"
"pnpm": "10.16.0",
"start-server-and-test": "2.1.0"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0"

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class SensitiveAd1757823175259 {
name = 'SensitiveAd1757823175259'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" ADD "isSensitive" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "isSensitive"`);
}
}

View File

@ -39,17 +39,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.13.4",
"@swc/core-darwin-x64": "1.13.4",
"@swc/core-darwin-arm64": "1.13.5",
"@swc/core-darwin-x64": "1.13.5",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.13.4",
"@swc/core-linux-arm64-gnu": "1.13.4",
"@swc/core-linux-arm64-musl": "1.13.4",
"@swc/core-linux-x64-gnu": "1.13.4",
"@swc/core-linux-x64-musl": "1.13.4",
"@swc/core-win32-arm64-msvc": "1.13.4",
"@swc/core-win32-ia32-msvc": "1.13.4",
"@swc/core-win32-x64-msvc": "1.13.4",
"@swc/core-linux-arm-gnueabihf": "1.13.5",
"@swc/core-linux-arm64-gnu": "1.13.5",
"@swc/core-linux-arm64-musl": "1.13.5",
"@swc/core-linux-x64-gnu": "1.13.5",
"@swc/core-linux-x64-musl": "1.13.5",
"@swc/core-win32-arm64-msvc": "1.13.5",
"@swc/core-win32-ia32-msvc": "1.13.5",
"@swc/core-win32-x64-msvc": "1.13.5",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@ -69,20 +69,20 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.873.0",
"@aws-sdk/lib-storage": "3.873.0",
"@aws-sdk/client-s3": "3.883.0",
"@aws-sdk/lib-storage": "3.883.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0",
"@fastify/express": "4.0.2",
"@fastify/http-proxy": "10.0.2",
"@fastify/multipart": "9.0.3",
"@fastify/multipart": "9.2.1",
"@fastify/static": "8.2.0",
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.3",
"@napi-rs/canvas": "0.1.77",
"@napi-rs/canvas": "0.1.79",
"@nestjs/common": "11.1.6",
"@nestjs/core": "11.1.6",
"@nestjs/testing": "11.1.6",
@ -93,7 +93,7 @@
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.8",
"@swc/core": "1.13.4",
"@swc/core": "1.13.5",
"@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
@ -103,7 +103,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.58.1",
"bullmq": "5.58.5",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.6.0",
@ -114,13 +114,13 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "5.5.0",
"fastify": "5.6.0",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4",
"got": "14.4.7",
"got": "14.4.8",
"happy-dom": "16.8.1",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
@ -141,7 +141,7 @@
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1",
"ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.5",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
@ -175,7 +175,7 @@
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.27.7",
"systeminformation": "5.27.8",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
@ -210,7 +210,7 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.17.2",
"@types/node": "22.18.1",
"@types/nodemailer": "6.4.19",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
@ -222,7 +222,7 @@
"@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7",
"@types/sanitize-html": "2.16.0",
"@types/semver": "7.7.0",
"@types/semver": "7.7.1",
"@types/simple-oauth2": "5.0.7",
"@types/sinonjs__fake-timers": "8.1.5",
"@types/supertest": "6.0.3",
@ -231,8 +231,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",

View File

@ -7,6 +7,7 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
import { type FastifyServerOptions } from 'fastify';
import type * as Sentry from '@sentry/node';
import type * as SentryVue from '@sentry/vue';
import type { RedisOptions } from 'ioredis';
@ -27,6 +28,7 @@ type Source = {
url?: string;
port?: number;
socket?: string;
trustProxy?: FastifyServerOptions['trustProxy'];
chmodSocket?: string;
disableHsts?: boolean;
db: {
@ -118,6 +120,7 @@ export type Config = {
url: string;
port: number;
socket: string | undefined;
trustProxy: FastifyServerOptions['trustProxy'];
chmodSocket: string | undefined;
disableHsts: boolean | undefined;
db: {
@ -266,6 +269,7 @@ export function loadConfig(): Config {
url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket,
trustProxy: config.trustProxy,
chmodSocket: config.chmodSocket,
disableHsts: config.disableHsts,
host,

View File

@ -29,7 +29,7 @@ export class AiService {
}
@bindThis
public async detectSensitive(path: string): Promise<nsfw.PredictionType[] | null> {
public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> {
try {
if (isSupportedCpu === undefined) {
isSupportedCpu = await this.computeIsSupportedCpu();
@ -51,7 +51,7 @@ export class AiService {
});
}
const buffer = await fs.promises.readFile(path);
const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source);
const image = await tf.node.decodeImage(buffer, 3) as any;
try {
const predictions = await this.model.classify(image);

View File

@ -21,6 +21,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { PredictionType } from 'nsfwjs';
import { isMimeImage } from '@/misc/is-mime-image.js';
export type FileInfo = {
size: number;
@ -204,16 +205,7 @@ export class FileInfoService {
return [sensitive, porn];
}
if ([
'image/jpeg',
'image/png',
'image/webp',
].includes(mime)) {
const result = await this.aiService.detectSensitive(source);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
} else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
const [outDir, disposeOutDir] = await createTempDir();
try {
const command = FFmpeg()
@ -281,6 +273,23 @@ export class FileInfoService {
} finally {
disposeOutDir();
}
} else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) {
/*
* tfjs-node sharp PNG
* 使299x299に事前にリサイズする
*/
const png = await (await sharpBmp(source, mime))
.resize(299, 299, {
withoutEnlargement: false,
})
.rotate()
.flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす
.png()
.toBuffer();
const result = await this.aiService.detectSensitive(png);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
}
return [sensitive, porn];

View File

@ -756,8 +756,8 @@ export class QueueService {
@bindThis
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
const job = await queue.getJob(jobId);
if (job != null) {
if (job.finishedOn != null) {
await job.retry();
} else {
@ -769,8 +769,8 @@ export class QueueService {
@bindThis
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
const job = await queue.getJob(jobId);
if (job != null) {
await job.remove();
}
}
@ -803,8 +803,8 @@ export class QueueService {
@bindThis
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
const job = await queue.getJob(jobId);
if (job != null) {
return this.packJobData(job);
} else {
throw new Error(`Job not found: ${jobId}`);

View File

@ -31,6 +31,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
// misskey-js の rolePolicies と同期すべし
export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
@ -100,14 +101,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
canImportAntennas: true,
canImportBlocking: true,
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
canImportAntennas: false,
canImportBlocking: false,
canImportFollowing: false,
canImportMuting: false,
canImportUserLists: false,
chatAvailability: 'available',
uploadableFileTypes: [
'text/plain',
'text/csv',
'application/json',
'image/*',
'video/*',

View File

@ -117,6 +117,7 @@ export class MetaEntityService {
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive ? true : undefined,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,

View File

@ -54,10 +54,17 @@ export class MiAd {
length: 8192, nullable: false,
})
public memo: string;
@Column('integer', {
default: 0, nullable: false,
})
public dayOfWeek: number;
@Column('boolean', {
default: false,
})
public isSensitive: boolean;
constructor(data: Partial<MiAd>) {
if (data == null) return;

View File

@ -10,6 +10,7 @@ import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js';
// misskey-js の notificationTypes と同期すべし
export type MiNotification = {
type: 'note';
id: string;

View File

@ -60,5 +60,10 @@ export const packedAdSchema = {
optional: false,
nullable: false,
},
isSensitive: {
type: 'boolean',
optional: false,
nullable: false,
},
},
} as const;

View File

@ -195,6 +195,10 @@ export const packedMetaLiteSchema = {
type: 'integer',
optional: false, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
},

View File

@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis
public async launch(): Promise<void> {
const fastify = Fastify({
trustProxy: true,
trustProxy: this.config.trustProxy ?? true,
logger: false,
});
this.#fastify = fastify;

View File

@ -176,6 +176,17 @@ export class ApiServerService {
}
});
fastify.all('/clear-browser-cache', (request, reply) => {
if (['GET', 'POST'].includes(request.method)) {
reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"');
reply.code(204);
reply.send();
} else {
reply.code(405);
reply.send();
}
});
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.

View File

@ -34,13 +34,22 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
ref: 'MeDetailed',
properties: {
token: {
type: 'string',
optional: false, nullable: false,
allOf: [
{
type: 'object',
ref: 'MeDetailed',
},
},
{
type: 'object',
optional: false, nullable: false,
properties: {
token: {
type: 'string',
optional: false, nullable: false,
},
},
}
],
},
} as const;

View File

@ -36,6 +36,7 @@ export const paramDef = {
startsAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 },
dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
},
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
} as const;
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,
@ -73,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
priority: ad.priority,

View File

@ -63,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
memo: ad.memo,

View File

@ -39,6 +39,7 @@ export const paramDef = {
expiresAt: { type: 'integer' },
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
},
required: ['id'],
} as const;
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
});
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });

View File

@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['federation'],

View File

@ -91,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
qb.orWhere(new Brackets(qb => {
qb.where('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}));
}

View File

@ -29,10 +29,16 @@ export const meta = {
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
},
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: '8e75455b-738c-471d-9f80-62693f33372e',
contentRestrictedByUser: {
message: 'Content restricted by user. Please sign in to view.',
code: 'CONTENT_RESTRICTED_BY_USER',
id: 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab',
},
contentRestrictedByServer: {
message: 'Content restricted by server settings. Please sign in to view.',
code: 'CONTENT_RESTRICTED_BY_SERVER',
id: '145f88d2-b03d-4087-8143-a78928883c4b',
},
},
} as const;
@ -61,15 +67,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
if (note.user!.requireSigninToViewContents && me == null) {
throw new ApiError(meta.errors.signinRequired);
throw new ApiError(meta.errors.contentRestrictedByUser);
}
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
throw new ApiError(meta.errors.signinRequired);
throw new ApiError(meta.errors.contentRestrictedByServer);
}
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
throw new ApiError(meta.errors.signinRequired);
throw new ApiError(meta.errors.contentRestrictedByServer);
}
return await this.noteEntityService.pack(note, me, {

View File

@ -242,6 +242,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}));
}

View File

@ -223,6 +223,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}));
}

View File

@ -22,17 +22,26 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
ref: 'UserList',
properties: {
likedCount: {
type: 'number',
optional: true, nullable: false,
allOf: [
{
type: 'object',
ref: 'UserList',
},
isLiked: {
type: 'boolean',
optional: true, nullable: false,
{
type: 'object',
optional: false, nullable: false,
properties: {
likedCount: {
type: 'number',
optional: true, nullable: false,
},
isLiked: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
},
],
},
errors: {

View File

@ -201,6 +201,8 @@ export class ClientServerService {
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const configUrl = new URL(this.config.url);
fastify.register(fastifyView, {
root: _dirname + '/views',
engine: {
@ -239,7 +241,6 @@ export class ClientServerService {
done();
});
} else {
const configUrl = new URL(this.config.url);
const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
const port = (process.env.VITE_PORT ?? '5173');
@ -887,6 +888,22 @@ export class ClientServerService {
[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
fastify.get('/flush', async (request, reply) => {
let sendHeader = true;
if (request.headers['origin']) {
const originURL = new URL(request.headers['origin']);
if (originURL.protocol !== 'https:') { // Clear-Site-Data only supports https
sendHeader = false;
}
if (originURL.host !== configUrl.host) {
sendHeader = false;
}
}
if (sendHeader) {
reply.header('Clear-Site-Data', '"*"');
}
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
return await reply.view('flush');
});

View File

@ -13,7 +13,7 @@
};
window.onunhandledrejection = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e);
};
let forceError = localStorage.getItem('forceError');

View File

@ -6,41 +6,45 @@ html
const msg = document.getElementById('msg');
const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
message('Start flushing.');
if (!document.cookie) {
message('Your site data is fully cleared by your browser.');
message(successText);
} else {
message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.');
(async function() {
try {
localStorage.clear();
message('localStorage cleared.');
(async function() {
try {
localStorage.clear();
message('localStorage cleared.');
const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
const delidb = indexedDB.deleteDatabase(name);
delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
delidb.onerror = e => rej(e)
}));
const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
const delidb = indexedDB.deleteDatabase(name);
delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
delidb.onerror = e => rej(e)
}));
await Promise.all(idbPromises);
await Promise.all(idbPromises);
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear');
await navigator.serviceWorker.getRegistrations()
.then(registrations => {
return Promise.all(registrations.map(registration => registration.unregister()));
})
.catch(e => { throw new Error(e) });
}
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear');
await navigator.serviceWorker.getRegistrations()
.then(registrations => {
return Promise.all(registrations.map(registration => registration.unregister()));
})
.catch(e => { throw new Error(e) });
message(successText);
} catch (e) {
message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
console.error(e);
setTimeout(() => {
location = '/';
}, 10000)
}
message(successText);
} catch (e) {
message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
console.error(e);
setTimeout(() => {
location = '/';
}, 10000)
}
})();
})();
}
function message(text) {
msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)

View File

@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
ADMIN_CACHE.set(host, {
id: res.id,
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
i: res.token,
});
return res as Misskey.entities.SignupResponse;

View File

@ -20,6 +20,6 @@
"dependencies": {
"estree-walker": "3.0.3",
"magic-string": "0.30.17",
"vite": "7.0.6"
"vite": "7.0.7"
}
}

View File

@ -46,9 +46,71 @@ export default [
allowSingleExtends: true,
}],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'],
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
// close ... window.closeと衝突 or 紛らわしい
// open ... window.openと衝突 or 紛らわしい
// fetch ... window.fetchと衝突 or 紛らわしい
// location ... window.locationと衝突 or 紛らわしい
// document ... window.documentと衝突 or 紛らわしい
// history ... window.historyと衝突 or 紛らわしい
// scroll ... window.scrollと衝突 or 紛らわしい
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
// setInterval ... window.setIntervalと衝突 or 紛らわしい
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
'no-restricted-globals': [
'error',
{
'name': 'open',
'message': 'Use `window.open`.',
},
{
'name': 'close',
'message': 'Use `window.close`.',
},
{
'name': 'fetch',
'message': 'Use `window.fetch`.',
},
{
'name': 'location',
'message': 'Use `window.location`.',
},
{
'name': 'document',
'message': 'Use `window.document`.',
},
{
'name': 'history',
'message': 'Use `window.history`.',
},
{
'name': 'scroll',
'message': 'Use `window.scroll`.',
},
{
'name': 'setTimeout',
'message': 'Use `window.setTimeout`.',
},
{
'name': 'setInterval',
'message': 'Use `window.setInterval`.',
},
{
'name': 'clearTimeout',
'message': 'Use `window.clearTimeout`.',
},
{
'name': 'clearInterval',
'message': 'Use `window.clearInterval`.',
},
{
'name': 'name',
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
},
],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,

View File

@ -13,10 +13,10 @@
"@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.19",
"@vue/compiler-sfc": "3.5.21",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@ -26,16 +26,16 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.48.0",
"sass": "1.90.0",
"shiki": "3.11.0",
"rollup": "4.50.1",
"sass": "1.92.1",
"shiki": "3.12.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"uuid": "11.1.0",
"vite": "7.1.3",
"vue": "3.5.19"
"vite": "7.1.5",
"vue": "3.5.21"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
@ -43,14 +43,14 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.17.2",
"@types/node": "22.18.1",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.19",
"@vue/runtime-core": "3.5.21",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"eslint-plugin-import": "2.32.0",
@ -59,11 +59,11 @@
"happy-dom": "18.0.1",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.10.5",
"msw": "2.11.1",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"start-server-and-test": "2.0.13",
"tsx": "4.20.4",
"start-server-and-test": "2.1.0",
"tsx": "4.20.5",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.0.6",
"vue-eslint-parser": "10.2.0",

View File

@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
console.log('Misskey Embed');
//#region Embedパラメータの取得・パース
const params = new URLSearchParams(location.search);
const params = new URLSearchParams(window.location.search);
const embedParams = parseEmbedParams(params);
if (_DEV_) console.log(embedParams);
//#endregion
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
//#endregion
// サイズの制限
document.documentElement.style.maxWidth = '500px';
window.document.documentElement.style.maxWidth = '500px';
// iframeIdの設定
function setIframeIdHandler(event: MessageEvent) {
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
const rootEl = ((): HTMLElement => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentRoot) {
console.warn('multiple import detected');
return currentRoot;
}
const root = document.createElement('div');
const root = window.document.createElement('div');
root.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(root);
window.document.body.appendChild(root);
return root;
})();
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
//#endregion
function removeSplash() {
const splash = document.getElementById('splash');
const splash = window.document.getElementById('splash');
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';

View File

@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
// Web Worker
if (import.meta.env.MODE === 'test') {
const canvas = document.createElement('canvas');
const canvas = window.document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
);
resolve(workers);
} else {
const canvas = document.createElement('canvas');
const canvas = window.document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);

View File

@ -29,7 +29,7 @@ const props = defineProps<{
// if no instance data is given, this is for the local instance
const instance = props.instance ?? {
name: serverMetadata.name,
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
themeColor: (window.document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
};
const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');

View File

@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
const url = `/${canonical}`;
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
</script>

View File

@ -134,7 +134,7 @@ const isBackTop = ref(false);
const empty = computed(() => items.value.size === 0);
const error = ref(false);
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body);
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body);
const visibility = useDocumentVisibility();
@ -353,7 +353,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible'
if (timerForSetPause) {
clearTimeout(timerForSetPause);
window.clearTimeout(timerForSetPause);
timerForSetPause = null;
} else {
isPausingUpdate = false;
@ -447,11 +447,11 @@ onBeforeMount(() => {
init().then(() => {
if (props.pagination.reversed) {
nextTick(() => {
setTimeout(toBottom, 800);
window.setTimeout(toBottom, 800);
// scrollToBottommoreFetching
// more = true
setTimeout(() => {
window.setTimeout(() => {
moreFetching.value = false;
}, 2000);
});
@ -461,11 +461,11 @@ onBeforeMount(() => {
onBeforeUnmount(() => {
if (timerForSetPause) {
clearTimeout(timerForSetPause);
window.clearTimeout(timerForSetPause);
timerForSetPause = null;
}
if (preventAppearFetchMoreTimer.value) {
clearTimeout(preventAppearFetchMoreTimer.value);
window.clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null;
}
scrollObserver.value?.disconnect();

View File

@ -4,7 +4,7 @@
*/
import * as Misskey from 'misskey-js';
const providedContextEl = document.getElementById('misskey_embedCtx');
const providedContextEl = window.document.getElementById('misskey_embedCtx');
export type ServerContext = {
clip?: Misskey.entities.Clip;

View File

@ -6,7 +6,7 @@
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/misskey-api.js';
const providedMetaEl = document.getElementById('misskey_meta');
const providedMetaEl = window.document.getElementById('misskey_meta');
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;

View File

@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout);
document.documentElement.classList.add('_themeChanging_');
window.document.documentElement.classList.add('_themeChanging_');
timeout = window.setTimeout(() => {
document.documentElement.classList.remove('_themeChanging_');
window.document.documentElement.classList.remove('_themeChanging_');
}, 1000);
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.colorScheme = colorScheme;
window.document.documentElement.dataset.colorScheme = colorScheme;
// Deep copy
const _theme = JSON.parse(JSON.stringify(theme));
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
const props = compile(_theme);
for (const tag of document.head.children) {
for (const tag of window.document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', props['htmlThemeColor']);
break;
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
}
for (const [k, v] of Object.entries(props)) {
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照

View File

@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
}
}
const page = location.pathname.split('/')[2];
const contentId = safeURIDecode(location.pathname.split('/')[3]);
const page = window.location.pathname.split('/')[2];
const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
if (_DEV_) console.log(page, contentId);
const embedParams = inject(DI.embedParams, defaultEmbedParams);

View File

@ -64,6 +64,8 @@ function toBase62(n: number): string {
}
export function getConfig(): UserConfig {
const localesHash = toBase62(hash(JSON.stringify(locales)));
return {
base: '/embed_vite/',
@ -148,9 +150,9 @@ export function getConfig(): UserConfig {
// dependencies of i18n.ts
'config': ['@@/js/config.js'],
},
entryFileNames: 'scripts/[hash:8].js',
chunkFileNames: 'scripts/[hash:8].js',
assetFileNames: 'assets/[hash:8][extname]',
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
paths(id) {
for (const p of externalPackages) {
if (p.match.test(id)) {

View File

@ -51,9 +51,71 @@ export default [
allowSingleExtends: true,
}],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'],
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
// close ... window.closeと衝突 or 紛らわしい
// open ... window.openと衝突 or 紛らわしい
// fetch ... window.fetchと衝突 or 紛らわしい
// location ... window.locationと衝突 or 紛らわしい
// document ... window.documentと衝突 or 紛らわしい
// history ... window.historyと衝突 or 紛らわしい
// scroll ... window.scrollと衝突 or 紛らわしい
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
// setInterval ... window.setIntervalと衝突 or 紛らわしい
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
'no-restricted-globals': [
'error',
{
'name': 'open',
'message': 'Use `window.open`.',
},
{
'name': 'close',
'message': 'Use `window.close`.',
},
{
'name': 'fetch',
'message': 'Use `window.fetch`.',
},
{
'name': 'location',
'message': 'Use `window.location`.',
},
{
'name': 'document',
'message': 'Use `window.document`.',
},
{
'name': 'history',
'message': 'Use `window.history`.',
},
{
'name': 'scroll',
'message': 'Use `window.scroll`.',
},
{
'name': 'setTimeout',
'message': 'Use `window.setTimeout`.',
},
{
'name': 'setInterval',
'message': 'Use `window.setInterval`.',
},
{
'name': 'clearTimeout',
'message': 'Use `window.clearTimeout`.',
},
{
'name': 'clearInterval',
'message': 'Use `window.clearInterval`.',
},
{
'name': 'name',
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
},
],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,

View File

@ -4,15 +4,15 @@
*/
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
export const host = address.host;
export const hostname = address.hostname;
export const url = address.origin;
export const port = address.port;
export const apiUrl = location.origin + '/api';
export const wsOrigin = location.origin;
export const apiUrl = window.location.origin + '/api';
export const wsOrigin = window.location.origin;
export const lang = localStorage.getItem('lang') ?? 'en-US';
export const langs = _LANGS_;
export const version = _VERSION_;

View File

@ -54,68 +54,6 @@ https://github.com/sindresorhus/file-type/blob/main/core.js
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
*/
export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
'chatRoomInvitationReceived',
'achievementEarned',
'exportCompleted',
'login',
'createToken',
'test',
'app',
] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'mentionLimit',
'canInvite',
'inviteLimit',
'inviteLimitCycle',
'inviteExpirationTime',
'canManageCustomEmojis',
'canManageAvatarDecorations',
'canSearchNotes',
'canSearchUsers',
'canUseTranslator',
'canHideAds',
'driveCapacityMb',
'maxFileSizeMb',
'alwaysMarkNsfw',
'canUpdateBioMedia',
'pinLimit',
'antennaLimit',
'wordMuteLimit',
'webhookLimit',
'clipLimit',
'noteEachClipsLimit',
'userListLimit',
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
'canImportAntennas',
'canImportBlocking',
'canImportFollowing',
'canImportMuting',
'canImportUserLists',
'chatAvailability',
'uploadableFileTypes',
'noteDraftLimit',
'watermarkAvailable',
] as const;
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
tada: ['speed=', 'delay='],

View File

@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
let prevTopVisible = firstTopVisible;
const onScroll = () => {
if (!document.body.contains(el)) return;
if (!window.document.body.contains(el)) return;
const topVisible = isHeadVisible(el, tolerance);
if (topVisible !== prevTopVisible) {
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
const containerOrWindow = container ?? window;
const onScroll = () => {
if (!document.body.contains(el)) return;
if (!window.document.body.contains(el)) return;
if (isTailVisible(el, 1, container)) {
cb();
if (once) removeListener();
@ -145,8 +145,8 @@ export function isTailVisible(el: HTMLElement, tolerance = 1, container = getScr
// https://ja.javascript.info/size-and-scroll-window#ref-932
export function getBodyScrollHeight() {
return Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight,
window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
window.document.body.clientHeight, window.document.documentElement.clientHeight,
);
}

View File

@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
import type { Ref } from 'vue';
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
const visibility = ref(document.visibilityState);
const visibility = ref(window.document.visibilityState);
const onChange = (): void => {
visibility.value = document.visibilityState;
visibility.value = window.document.visibilityState;
};
onMounted(() => {
document.addEventListener('visibilitychange', onChange);
window.document.addEventListener('visibilitychange', onChange);
});
onUnmounted(() => {
document.removeEventListener('visibilitychange', onChange);
window.document.removeEventListener('visibilitychange', onChange);
});
return visibility;

View File

@ -21,9 +21,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.17.2",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"esbuild": "0.25.9",
"eslint-plugin-vue": "10.4.0",
"nodemon": "3.1.10",
@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.19"
"vue": "3.5.21"
}
}

View File

@ -127,7 +127,7 @@ export function galleryPost(isSensitive = false) {
}
}
export function file(isSensitive = false) {
export function file(isSensitive = false): entities.DriveFile {
return {
id: 'somefileid',
createdAt: '2016-12-28T22:49:51.000Z',
@ -207,6 +207,7 @@ export function federationInstance(): entities.FederationInstance {
isSuspended: false,
suspensionState: 'none',
isBlocked: false,
isMediaSilenced: false,
softwareName: 'misskey',
softwareVersion: '2024.5.0',
openRegistrations: false,
@ -311,6 +312,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host: enti
alsoKnownAs: null,
notify: 'none',
memo: null,
canChat: true,
chatScope: 'everyone',
};
}
@ -378,6 +381,7 @@ export function role(params: {
asBadge: params.asBadge ?? true,
canEditMembersByModerator: params.canEditMembersByModerator ?? false,
usersCount: params.usersCount ?? 10,
preserveAssignmentOnMoveAccount: false,
condFormula: {
id: '',
type: 'or',

View File

@ -23,13 +23,13 @@
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@sentry/vue": "10.5.0",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.10.0",
"@syuilo/aiscript": "1.1.0",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.19",
"@vue/compiler-sfc": "3.5.21",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19",
"astring": "1.9.0",
@ -41,7 +41,7 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.1.3",
"chromatic": "13.1.4",
"compare-versions": "6.1.1",
"cropperjs": "2.0.1",
"date-fns": "4.1.0",
@ -57,27 +57,30 @@
"json5": "2.2.3",
"magic-string": "0.30.18",
"matter-js": "0.20.0",
"mediabunny": "1.15.1",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.48.0",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.50.1",
"sanitize-html": "2.17.0",
"sass": "1.90.0",
"shiki": "3.11.0",
"sass": "1.92.1",
"shiki": "3.12.2",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.179.1",
"three": "0.180.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"v-code-diff": "1.13.1",
"vite": "7.1.3",
"vue": "3.5.19",
"vite": "7.1.5",
"vue": "3.5.21",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@ -85,7 +88,7 @@
"@misskey-dev/summaly": "5.2.3",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "9.1.3",
"@storybook/addon-links": "9.1.5",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@ -93,31 +96,31 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
"@storybook/react": "9.1.3",
"@storybook/react-vite": "9.1.3",
"@storybook/react": "9.1.5",
"@storybook/react-vite": "9.1.5",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
"@storybook/vue3": "9.1.3",
"@storybook/vue3-vite": "9.1.3",
"@storybook/vue3": "9.1.5",
"@storybook/vue3-vite": "9.1.5",
"@tabler/icons-webfont": "3.34.1",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.0",
"@types/micromatch": "4.0.9",
"@types/node": "22.17.2",
"@types/node": "22.18.1",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.19",
"@vue/runtime-core": "3.5.19",
"@vue/compiler-core": "3.5.21",
"@vue/runtime-core": "3.5.21",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cypress": "14.5.4",
@ -128,17 +131,17 @@
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
"msw": "2.10.5",
"msw": "2.11.1",
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.13",
"storybook": "9.1.3",
"start-server-and-test": "2.1.0",
"storybook": "9.1.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.20.4",
"tsx": "4.20.5",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",

View File

@ -251,13 +251,30 @@ export async function openAccountMenu(opts: {
}
},
};
} else {
} else { // プロファイルを復元した場合などはアカウントのトークンや詳細情報はstoreにキャッシュされていない
return {
type: 'button' as const,
text: username,
active: opts.active != null ? opts.active === id : false,
action: async () => {
// TODO
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
initialUsername: username,
}, {
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
if (callback) {
fetchAccount(res.i, id).then(account => {
callback(account);
});
} else {
switchAccount(host, id);
}
},
closed: () => {
dispose();
},
});
},
};
}

View File

@ -86,7 +86,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
throw new errors.AiScriptRuntimeError('expected param');
}
utils.assertObject(param);
return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));

View File

@ -151,7 +151,21 @@ export async function common(createVue: () => Promise<App<Element>>) {
}
//#endregion
//#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', mql.matches);
}
});
//#endregion
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
// see: https://github.com/misskey-dev/misskey/issues/16562
watch(store.r.darkMode, (darkMode) => {
const theme = (() => {
if (darkMode) {
@ -183,18 +197,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
});
}
//#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', mql.matches);
}
});
//#endregion
if (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);

View File

@ -7,8 +7,8 @@ import * as Misskey from 'misskey-js';
import { Cache } from '@/utility/cache.js';
import { misskeyApi } from '@/utility/misskey-api.js';
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list', { limit: 30 }));
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list', { limit: 30 }));
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list', { limit: 30 }));
export const favoritedChannelsCache = new Cache<Misskey.entities.Channel[]>(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 }));

View File

@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import isChromatic from 'chromatic/isChromatic';
import { initShaderProgram } from '@/utility/webgl.js';
const canvasEl = useTemplateRef('canvasEl');
@ -21,47 +22,6 @@ const props = withDefaults(defineProps<{
focus: 1.0,
});
function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (shader == null) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
);
gl.deleteShader(shader);
return null;
}
return shader;
}
function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
if (vertexShader == null || fragmentShader == null) return null;
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert(
`failed to init shader: ${gl.getProgramInfoLog(
shaderProgram,
)}`,
);
return null;
}
return shaderProgram;
}
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => {
@ -71,7 +31,7 @@ onMounted(() => {
canvas.width = width;
canvas.height = height;
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true });
const maybeGl = canvas.getContext('webgl2', { premultipliedAlpha: true });
if (maybeGl == null) return;
const gl = maybeGl;
@ -82,18 +42,16 @@ onMounted(() => {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const shaderProgram = initShaderProgram(gl, `
attribute vec2 vertex;
const shaderProgram = initShaderProgram(gl, `#version 300 es
in vec2 position;
uniform vec2 u_scale;
varying vec2 v_pos;
out vec2 in_uv;
void main() {
gl_Position = vec4(vertex, 0.0, 1.0);
v_pos = vertex / u_scale;
gl_Position = vec4(position, 0.0, 1.0);
in_uv = position / u_scale;
}
`, `
`, `#version 300 es
precision mediump float;
vec3 mod289(vec3 x) {
@ -143,6 +101,7 @@ onMounted(() => {
return 130.0 * dot(m, g);
}
in vec2 in_uv;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_spread;
@ -150,8 +109,7 @@ onMounted(() => {
uniform float u_warp;
uniform float u_focus;
uniform float u_itensity;
varying vec2 v_pos;
out vec4 out_color;
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
float SPREAD = 0.7 * u_spread;
@ -182,13 +140,13 @@ onMounted(() => {
float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
vec3 color = vec3( 0.0 );
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
@ -198,10 +156,10 @@ onMounted(() => {
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
vec3 inverted = vec3( 1.0 ) - color;
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
out_color = vec4(color, max(max(color.x, color.y), color.z));
}
`);
if (shaderProgram == null) return;
@ -223,7 +181,7 @@ onMounted(() => {
gl.uniform1f(u_itensity, 0.5);
gl.uniform2fv(u_scale, [props.scale, props.scale]);
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
const vertex = gl.getAttribLocation(shaderProgram, 'position');
gl.enableVertexAttribArray(vertex);
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);

View File

@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkSelect v-model="src">
<MkSelect v-model="src" :items="antennaSourcesSelectDef">
<template #label>{{ i18n.ts.antennaSource }}</template>
<option value="all">{{ i18n.ts._antennaSources.all }}</option>
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
</MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId">
<MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
<template #label>{{ i18n.ts.userList }}</template>
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect>
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
<template #label>{{ i18n.ts.users }}</template>
@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { watch, ref } from 'vue';
import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import type { DeepPartial } from '@/utility/merge.js';
import MkButton from '@/components/MkButton.vue';
@ -64,6 +58,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { deepMerge } from '@/utility/merge.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
id?: string;
@ -99,9 +94,35 @@ const emit = defineEmits<{
(ev: 'deleted'): void,
}>();
const {
model: src,
def: antennaSourcesSelectDef,
} = useMkSelect({
items: [
{ value: 'all', label: i18n.ts._antennaSources.all },
//{ value: 'home', label: i18n.ts._antennaSources.homeTimeline },
{ value: 'users', label: i18n.ts._antennaSources.users },
//{ value: 'list', label: i18n.ts._antennaSources.userList },
{ value: 'users_blacklist', label: i18n.ts._antennaSources.userBlacklist },
],
initialValue: initialAntenna.src,
});
const {
model: userListId,
def: userListsSelectDef,
} = useMkSelect({
items: computed(() => {
if (userLists.value == null) return [];
return userLists.value.map(list => ({
value: list.id,
label: list.name,
}));
}),
initialValue: initialAntenna.userListId,
});
const name = ref<string>(initialAntenna.name);
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
const userListId = ref<string | null>(initialAntenna.userListId);
const users = ref<string>(initialAntenna.users.join('\n'));
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));

View File

@ -32,10 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" :items="selectDef" @update:modelValue="onSelectUpdate">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
</MkSelect>
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
@ -74,6 +73,7 @@ import MkSelect from '@/components/MkSelect.vue';
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
import MkFolder from '@/components/MkFolder.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{
component: AsUiComponent;
@ -130,7 +130,19 @@ function onSwitchUpdate(v: boolean) {
}
}
const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
const {
model: valueForSelect,
def: selectDef,
} = useMkSelect({
items: computed(() => {
if (c.type !== 'select') return [];
return (c.items ?? []).map(item => ({
value: item.value,
label: item.text,
}));
}),
initialValue: (c.type === 'select' && 'default' in c && typeof c.default !== 'boolean') ? c.default ?? null : null,
});
function onSelectUpdate(v) {
valueForSelect.value = v;

View File

@ -167,9 +167,13 @@ async function init() {
for (const user of usersRes) {
if (users.value.has(user.id)) continue;
const account = accounts.find(a => a.id === user.id);
if (!account || account.token == null) continue;
users.value.set(user.id, {
...user,
token: accounts.find(a => a.id === user.id)!.token,
token: account.token,
});
}
}

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
<span v-if="q != null && typeof q === 'string'" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
<span v-else v-text="emoji.name"></span>
<span v-if="emoji.aliasOf" :class="$style.emojiAlias">({{ emoji.aliasOf }})</span>
</li>
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</li>
</ol>
<ol v-else-if="type === 'mfmParam' && mfmParams.length > 0" ref="suggests" :class="$style.list">
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="completeMfmParam(param)" @keydown="onKeydown">
<span>{{ param }}</span>
</li>
</ol>
@ -194,6 +194,11 @@ const mfmParams = ref<string[]>([]);
const select = ref(-1);
const zIndex = os.claimZIndex('high');
function completeMfmParam(param: string) {
if (props.type !== 'mfmParam') throw new Error('Invalid type');
complete('mfmParam', props.q.params.toSpliced(-1, 1, param).join(','));
}
function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T]['payload']) {
emit('done', { type, value });
emit('closed');

View File

@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
onMounted(async () => {
users.value = await misskeyApi('users/show', {
userIds: props.userIds,
}) as unknown as Misskey.entities.UserLite[];
});
});
</script>

View File

@ -25,12 +25,12 @@ defineProps<{
showing: boolean;
x: number;
y: number;
title?: string;
title?: string | null;
series?: {
backgroundColor: string;
borderColor: string;
text: string;
}[];
}[] | null;
}>();
const emit = defineEmits<{

View File

@ -38,7 +38,7 @@ export const Default = {
};
},
args: {
file: file(),
imageFile: file(),
aspectRatio: NaN,
},
parameters: {

View File

@ -29,16 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
</template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">
<template v-for="item in select.items">
<optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
<option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
</optgroup>
<option v-else :value="item.value">{{ item.text }}</option>
</template>
</template>
</MkSelect>
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
@ -56,6 +47,8 @@ import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { i18n } from '@/i18n.js';
type Input = {
@ -67,17 +60,9 @@ type Input = {
maxLength?: number;
};
type SelectItem = {
value: any;
text: string;
};
type Select = {
items: (SelectItem | {
sectionTitle: string;
items: SelectItem[];
})[];
default: string | null;
items: MkSelectItem[];
default: OptionValue | null;
};
type Result = string | number | true | null;
@ -115,7 +100,6 @@ const emit = defineEmits<{
const modal = useTemplateRef('modal');
const inputValue = ref<string | number | null>(props.input?.default ?? null);
const selectedValue = ref(props.select?.default ?? null);
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) {
@ -134,6 +118,14 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
return null;
});
const {
def: selectDef,
model: selectedValue,
} = useMkSelect({
items: computed(() => props.select?.items ?? []),
initialValue: props.select?.default ?? null,
});
// overload function 使 lint
function done(canceled: true): void;
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare

View File

@ -699,7 +699,7 @@ useGlobalEvent('driveFoldersDeleted', (folders) => {
}
});
let connection: Misskey.ChannelConnection<Misskey.Channels['drive']> | null = null;
let connection: Misskey.IChannelConnection<Misskey.Channels['drive']> | null = null;
onMounted(() => {
if (store.s.realtimeMode) {

View File

@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #suffix>px</template>
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
</MkInput>
<MkSelect v-model="colorMode">
<MkSelect v-model="colorMode" :items="colorModeDef">
<template #label>{{ i18n.ts.theme }}</template>
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
<option value="light">{{ i18n.ts.light }}</option>
<option value="dark">{{ i18n.ts.dark }}</option>
</MkSelect>
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
@ -105,6 +102,7 @@ import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
@ -160,9 +158,20 @@ const embedPreviewUrl = computed(() => {
const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
const header = ref(props.params?.header ?? true);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
const {
model: colorMode,
def: colorModeDef,
} = useMkSelect({
items: [
{ value: 'auto', label: i18n.ts.syncDeviceDarkMode },
{ value: 'light', label: i18n.ts.light },
{ value: 'dark', label: i18n.ts.dark },
],
initialValue: props.params?.colorMode ?? 'auto',
});
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
const rounded = ref(props.params?.rounded ?? true);
const border = ref(props.params?.border ?? true);

View File

@ -530,6 +530,14 @@ defineExpose({
--eachSize: 50px;
}
&.s4 {
--eachSize: 55px;
}
&.s5 {
--eachSize: 60px;
}
&.w1 {
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
--columns: 1fr 1fr 1fr 1fr 1fr;

View File

@ -39,13 +39,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-text="v.label || k"></span>
<template v-if="v.description" #caption>{{ v.description }}</template>
</MkSwitch>
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.enum" :key="option.value" :value="option.value">{{ option.label }}</option>
</MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.options" :key="option.value" :value="option.value">{{ option.label }}</option>
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
</MkRadios>
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@ -77,7 +76,8 @@ import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue';
import XFile from './MkFormDialog.file.vue';
import type { Form } from '@/utility/form.js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
@ -99,7 +99,11 @@ const dialog = useTemplateRef('dialog');
const values = reactive({});
for (const item in props.form) {
values[item] = props.form[item].default ?? null;
if ('default' in props.form[item]) {
values[item] = props.form[item].default ?? null;
} else {
values[item] = null;
}
}
function ok() {
@ -115,4 +119,18 @@ function cancel() {
});
dialog.value?.close();
}
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
return def.enum.map((v) => {
if (typeof v === 'string') {
return { value: v, label: v };
} else {
return { value: v.value, label: v.label };
}
});
}
function getRadioKey(e: RadioFormItem['options'][number]) {
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
}
</script>

View File

@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.container">
<div :class="$style.preview">
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
<div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div class="_acrylic" :class="$style.editControls">
<button class="_button" :class="[$style.previewControlsButton, penMode != null ? $style.active : null]" @click="showPenMenu"><i class="ti ti-pencil"></i></button>
</div>
<div class="_acrylic" :class="$style.previewControls">
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
@ -212,6 +215,129 @@ watch(enabled, () => {
renderer.render();
}
});
const penMode = ref<'fill' | 'blur' | null>(null);
function showPenMenu(ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts._imageEffector._fxs.fill,
action: () => {
penMode.value = 'fill';
},
}, {
text: i18n.ts._imageEffector._fxs.blur,
action: () => {
penMode.value = 'blur';
},
}], ev.currentTarget ?? ev.target);
}
function onImagePointerdown(ev: PointerEvent) {
if (canvasEl.value == null || imageBitmap == null || penMode.value == null) return;
const AW = canvasEl.value.clientWidth;
const AH = canvasEl.value.clientHeight;
const BW = imageBitmap.width;
const BH = imageBitmap.height;
let xOffset = 0;
let yOffset = 0;
if (AW / AH < BW / BH) { //
yOffset = AH - BH * (AW / BW);
} else { //
xOffset = AW - BW * (AH / BH);
}
xOffset /= 2;
yOffset /= 2;
let startX = ev.offsetX - xOffset;
let startY = ev.offsetY - yOffset;
if (AW / AH < BW / BH) { //
startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
} else { //
startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
}
const id = genId();
if (penMode.value === 'fill') {
layers.push({
id,
fxId: 'fill',
params: {
offsetX: 0,
offsetY: 0,
scaleX: 0.1,
scaleY: 0.1,
angle: 0,
opacity: 1,
color: [1, 1, 1],
},
});
} else if (penMode.value === 'blur') {
layers.push({
id,
fxId: 'blur',
params: {
offsetX: 0,
offsetY: 0,
scaleX: 0.1,
scaleY: 0.1,
angle: 0,
radius: 3,
},
});
}
_move(ev.offsetX, ev.offsetY);
function _move(pointerX: number, pointerY: number) {
let x = pointerX - xOffset;
let y = pointerY - yOffset;
if (AW / AH < BW / BH) { //
x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
} else { //
x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
}
const scaleX = Math.abs(x - startX);
const scaleY = Math.abs(y - startY);
const layerIndex = layers.findIndex((l) => l.id === id);
const layer = layerIndex !== -1 ? layers[layerIndex] : null;
if (layer != null) {
layer.params.offsetX = (x + startX) - 1;
layer.params.offsetY = (y + startY) - 1;
layer.params.scaleX = scaleX;
layer.params.scaleY = scaleY;
layers[layerIndex] = layer;
}
}
function move(ev: PointerEvent) {
_move(ev.offsetX, ev.offsetY);
}
function up() {
canvasEl.value?.removeEventListener('pointermove', move);
canvasEl.value?.removeEventListener('pointerup', up);
canvasEl.value?.removeEventListener('pointercancel', up);
canvasEl.value?.releasePointerCapture(ev.pointerId);
penMode.value = null;
}
canvasEl.value.addEventListener('pointermove', move);
canvasEl.value.addEventListener('pointerup', up);
canvasEl.value.setPointerCapture(ev.pointerId);
}
</script>
<style module>
@ -251,6 +377,18 @@ watch(enabled, () => {
font-size: 85%;
}
.editControls {
position: absolute;
z-index: 100;
bottom: 8px;
left: 8px;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
}
.previewControls {
position: absolute;
z-index: 100;
@ -283,9 +421,13 @@ watch(enabled, () => {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 20px;
/* iOS
width: stretch;
height: stretch;
*/
width: calc(100% - 40px);
height: calc(100% - 40px);
margin: 20px;
box-sizing: border-box;
object-fit: contain;
}

View File

@ -8,7 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-for="v, k in paramDefs" :key="k">
<MkSwitch
v-if="v.type === 'boolean'"
v-model="params[k]">
v-model="params[k]"
>
<template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
</MkSwitch>
@ -53,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js';
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
defineProps<{
paramDefs: ImageEffectorFxParamDefs;

View File

@ -43,7 +43,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script lang="ts" setup>
<script lang="ts">
type SupportedTypes = 'text' | 'password' | 'email' | 'url' | 'tel' | 'number' | 'search' | 'date' | 'time' | 'datetime-local' | 'color';
type ModelValueType<T extends SupportedTypes> =
T extends 'number' ? number :
T extends 'text' | 'password' | 'email' | 'url' | 'tel' | 'search' | 'date' | 'time' | 'datetime-local' | 'color' ? string :
never;
</script>
<script lang="ts" setup generic="T extends SupportedTypes = 'text'">
import { onMounted, onUnmounted, nextTick, ref, useTemplateRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import { useInterval } from '@@/js/use-interval.js';
@ -55,8 +63,8 @@ import { Autocomplete } from '@/utility/autocomplete.js';
import { genId } from '@/utility/id.js';
const props = defineProps<{
modelValue: string | number | null;
type?: InputHTMLAttributes['type'];
modelValue: ModelValueType<T> | null;
type?: T;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@ -83,11 +91,11 @@ const emit = defineEmits<{
(ev: 'change', _ev: KeyboardEvent): void;
(ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter', _ev: KeyboardEvent): void;
(ev: 'update:modelValue', value: string | number): void;
(ev: 'update:modelValue', value: ModelValueType<T>): void;
}>();
const { modelValue, type, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const { modelValue } = toRefs(props);
const v = ref<ModelValueType<T> | null>(modelValue.value);
const id = genId();
const focused = ref(false);
const changed = ref(false);
@ -120,8 +128,8 @@ const onKeydown = (ev: KeyboardEvent) => {
const updated = () => {
changed.value = false;
if (type.value === 'number') {
emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0'));
if (props.type === 'number') {
emit('update:modelValue', typeof v.value === 'number' ? v.value as ModelValueType<T> : parseFloat(v.value ?? '0') as ModelValueType<T>);
} else {
emit('update:modelValue', v.value ?? '');
}
@ -167,7 +175,7 @@ useInterval(() => {
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
if (props.autofocus) {
focus();
}
});

View File

@ -9,31 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>Chart</template>
<div :class="$style.chart">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
<option value="federation">{{ i18n.ts._charts.federation }}</option>
<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
</optgroup>
<optgroup :label="i18n.ts.users">
<option value="users">{{ i18n.ts._charts.usersIncDec }}</option>
<option value="users-total">{{ i18n.ts._charts.usersTotal }}</option>
<option value="active-users">{{ i18n.ts._charts.activeUsers }}</option>
</optgroup>
<optgroup :label="i18n.ts.notes">
<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
<option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
</optgroup>
<optgroup :label="i18n.ts.drive">
<option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option>
<option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option>
</optgroup>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
<option value="hour">{{ i18n.ts.perHour }}</option>
<option value="day">{{ i18n.ts.perDay }}</option>
</MkSelect>
<MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
<MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
</div>
<div class="chart _panel">
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
@ -43,13 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection class="item">
<template #header>Active users heatmap</template>
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
<option value="active-users">Active users</option>
<option value="notes">Notes</option>
<option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
<option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
<option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
</MkSelect>
<MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
<div class="_panel" :class="$style.heatmap">
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
</div>
@ -84,11 +55,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, useTemplateRef } from 'vue';
import { onMounted, computed, useTemplateRef } from 'vue';
import { Chart } from 'chart.js';
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
import type { ChartSrc } from '@/components/MkChart.vue';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { $i } from '@/i.js';
import * as os from '@/os.js';
@ -100,15 +72,96 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/utility/init-chart.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
initChart();
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
const chartLimit = 500;
const chartSpan = ref<'hour' | 'day'>('hour');
const chartSrc = ref('active-users');
const heatmapSrc = ref<HeatmapSource>('active-users');
const {
model: chartSpan,
def: chartSpanDef,
} = useMkSelect({
items: [
{ value: 'hour', label: i18n.ts.perHour },
{ value: 'day', label: i18n.ts.perDay },
],
initialValue: 'hour',
});
const {
model: chartSrc,
def: chartSrcDef,
} = useMkSelect({
items: computed<MkSelectItem<ChartSrc>[]>(() => {
const items: MkSelectItem<ChartSrc>[] = [];
if (shouldShowFederation.value) {
items.push({
type: 'group',
label: i18n.ts.federation,
items: [
{ value: 'federation', label: i18n.ts._charts.federation },
{ value: 'ap-request', label: i18n.ts._charts.apRequest },
],
});
}
items.push({
type: 'group',
label: i18n.ts.users,
items: [
{ value: 'users', label: i18n.ts._charts.usersIncDec },
{ value: 'users-total', label: i18n.ts._charts.usersTotal },
{ value: 'active-users', label: i18n.ts._charts.activeUsers },
],
});
const notesItems: ItemOption<ChartSrc>[] = [
{ value: 'notes', label: i18n.ts._charts.notesIncDec },
{ value: 'local-notes', label: i18n.ts._charts.localNotesIncDec },
];
if (shouldShowFederation.value) notesItems.push({ value: 'remote-notes', label: i18n.ts._charts.remoteNotesIncDec });
notesItems.push(
{ value: 'notes-total', label: i18n.ts._charts.notesTotal },
);
items.push({
type: 'group',
label: i18n.ts.notes,
items: notesItems,
});
items.push({
type: 'group',
label: i18n.ts.drive,
items: [
{ value: 'drive-files', label: i18n.ts._charts.filesIncDec },
{ value: 'drive', label: i18n.ts._charts.storageUsageIncDec },
],
});
return items;
}),
initialValue: 'active-users',
});
const {
model: heatmapSrc,
def: heatmapSrcDef,
} = useMkSelect({
items: computed(() => [
{ value: 'active-users' as const, label: 'Active Users' },
{ value: 'notes' as const, label: 'Notes' },
...(shouldShowFederation.value ? [
{ value: 'ap-requests-inbox-received' as const, label: 'AP Requests: inboxReceived' },
{ value: 'ap-requests-deliver-succeeded' as const, label: 'AP Requests: deliverSucceeded' },
{ value: 'ap-requests-deliver-failed' as const, label: 'AP Requests: deliverFailed' },
] : []),
]),
initialValue: 'active-users',
});
const subDoughnutEl = useTemplateRef('subDoughnutEl');
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
tabindex="0"
>
<MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
<MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote?.reply ?? null" :class="$style.replyTo"/>
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
<div v-if="isRenote" :class="$style.renote">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="isEnabledUrlPreview">
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
</div>
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote?.renote ?? null" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div v-if="note" :class="$style.root">
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
<MkAvatar :class="[$style.avatar, prefer.s.useStickyIcons ? $style.useSticky : null]" :user="note.user" link preview/>
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div>
@ -31,6 +31,7 @@ import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
const props = defineProps<{
note: Misskey.entities.Note | null;
@ -54,9 +55,12 @@ const showContent = ref(false);
width: 34px;
height: 34px;
border-radius: 8px;
position: sticky !important;
top: calc(16px + var(--MI-stickyTop, 0px));
left: 0;
&.useSticky {
position: sticky !important;
top: calc(16px + var(--MI-stickyTop, 0px));
left: 0;
}
}
.main {

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import { notificationTypes } from 'misskey-js';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton>
<MkLoading v-else/>
</div>
<slot :items="unref(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
<slot :items="getValue(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
<div v-if="direction === 'down' || direction === 'both'" v-show="downButtonVisible">
<MkButton v-if="!downButtonLoading" :class="$style.more" primary rounded @click="downButtonClick">
{{ i18n.ts.loadMore }}
@ -90,6 +90,10 @@ function onContextmenu(ev: MouseEvent) {
}], ev);
}
function getValue(v: IPaginator['items']) {
return unref(v) as UnwrapRef<T['items']>;
}
if (props.autoLoad) {
onMounted(() => {
props.paginator.init();
@ -134,7 +138,7 @@ function downButtonClick() {
defineSlots<{
empty: () => void;
default: (props: { items: UnwrapRef<T['items']> }) => void;
default: (props: { items: UnwrapRef<T['items']>, fetching: boolean }) => void;
}>();
</script>

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<div :class="$style.control">
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
<MkSelect v-model="order" :class="$style.order" :items="orderDef">
<template #prefix><i class="ti ti-arrows-sort"></i></template>
</MkSelect>
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{
paginator: T;
@ -58,7 +59,16 @@ const props = withDefaults(defineProps<{
const searchOpened = ref(false);
const filterOpened = ref(props.filterOpened);
const order = ref<'newest' | 'oldest'>('newest');
const {
model: order,
def: orderDef,
} = useMkSelect({
items: [
{ label: i18n.ts._order.newest, value: 'newest' },
{ label: i18n.ts._order.oldest, value: 'oldest' },
],
initialValue: 'newest',
});
const date = ref<number | null>(null);
const q = ref<string | null>(null);

View File

@ -4,14 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root, accented ? $style.accented : null]"></div>
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
accented?: boolean;
revered?: boolean;
height?: number;
}>(), {
accented: false,
revered: false,
height: 200,
});
</script>
@ -27,14 +31,17 @@ const props = withDefaults(defineProps<{
--dot-size: 2px;
--gap-size: 40px;
--offset: calc(var(--gap-size) / 2);
--height: v-bind('props.height + "px"');
height: 200px;
margin-bottom: -200px;
height: var(--height);
background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size));
background-position: 0 0, 0 0, var(--offset) var(--offset);
background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size);
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
pointer-events: none;
&.revered {
mask-image: linear-gradient(to top, black 0%, transparent 100%);
}
}
</style>

View File

@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import { sum } from '@/utility/array.js';
import { pleaseLogin } from '@/utility/please-login.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useLowresTime } from '@/composables/use-lowres-time.js';
const props = defineProps<{
noteId: string;
@ -48,7 +48,21 @@ const props = defineProps<{
author?: Misskey.entities.UserLite;
}>();
const remaining = ref(-1);
const now = useLowresTime();
const expiresAtTime = computed(() => props.expiresAt ? new Date(props.expiresAt).getTime() : null);
const remaining = computed(() => {
if (expiresAtTime.value == null) return -1;
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
});
const remainingWatchStop = watch(remaining, (to) => {
if (to <= 0) {
showResult.value = true;
remainingWatchStop();
}
}, { immediate: true });
const total = computed(() => sum(props.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0);
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${props.noteId}`,
}));
//
if (props.expiresAt) {
const tick = () => {
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) {
showResult.value = true;
}
};
useInterval(tick, 3000, {
immediate: true,
afterMounted: false,
});
}
const vote = async (id) => {
const vote = async (id: number) => {
if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin({ openOnRemote: pleaseLoginContext.value });

View File

@ -22,11 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
<section>
<div>
<MkSelect v-model="expiration" small>
<MkSelect v-model="expiration" :items="expirationDef" small>
<template #label>{{ i18n.ts._poll.expiration }}</template>
<option value="infinite">{{ i18n.ts._poll.infinite }}</option>
<option value="at">{{ i18n.ts._poll.at }}</option>
<option value="after">{{ i18n.ts._poll.after }}</option>
</MkSelect>
<section v-if="expiration === 'at'">
<MkInput v-model="atDate" small type="date" class="input">
@ -40,12 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="after" small type="number" :min="1" class="input">
<template #label>{{ i18n.ts._poll.duration }}</template>
</MkInput>
<MkSelect v-model="unit" small>
<option value="second">{{ i18n.ts._time.second }}</option>
<option value="minute">{{ i18n.ts._time.minute }}</option>
<option value="hour">{{ i18n.ts._time.hour }}</option>
<option value="day">{{ i18n.ts._time.day }}</option>
</MkSelect>
<MkSelect v-model="unit" :items="unitDef" small></MkSelect>
</section>
</div>
</section>
@ -61,6 +53,7 @@ import MkButton from './MkButton.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
import { addTime } from '@/utility/time.js';
import { i18n } from '@/i18n.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
export type PollEditorModelValue = {
expiresAt: number | null;
@ -78,11 +71,32 @@ const emit = defineEmits<{
const choices = ref(props.modelValue.choices);
const multiple = ref(props.modelValue.multiple);
const expiration = ref('infinite');
const {
model: expiration,
def: expirationDef,
} = useMkSelect({
items: [
{ label: i18n.ts._poll.infinite, value: 'infinite' },
{ label: i18n.ts._poll.at, value: 'at' },
{ label: i18n.ts._poll.after, value: 'after' },
],
initialValue: 'infinite',
});
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
const atTime = ref('00:00');
const after = ref(0);
const unit = ref('second');
const {
model: unit,
def: unitDef,
} = useMkSelect({
items: [
{ label: i18n.ts._time.second, value: 'second' },
{ label: i18n.ts._time.minute, value: 'minute' },
{ label: i18n.ts._time.hour, value: 'hour' },
{ label: i18n.ts._time.day, value: 'day' },
],
initialValue: 'second',
});
if (props.modelValue.expiresAt) {
expiration.value = 'at';

View File

@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root]">
<div :class="$style.items">
<button class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-align-box-left-top"></i></button>
<button class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-align-box-center-top"></i></button>
<button class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-align-box-right-top"></i></button>
<button class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-align-box-left-middle"></i></button>
<button class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-align-box-center-middle"></i></button>
<button class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-align-box-right-middle"></i></button>
<button class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-align-box-left-bottom"></i></button>
<button class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-align-box-center-bottom"></i></button>
<button class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-align-box-right-bottom"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-arrow-up-left"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-arrow-up"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-arrow-up-right"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-arrow-left"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-focus-2"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-arrow-right"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-arrow-down-left"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-arrow-down"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-arrow-down-right"></i></button>
</div>
</div>
</template>

View File

@ -105,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue';
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@ -218,6 +218,10 @@ const uploader = useUploader({
multiple: true,
});
onUnmounted(() => {
uploader.dispose();
});
uploader.events.on('itemUploaded', ctx => {
files.value.push(ctx.item.uploaded!);
uploader.removeItem(ctx.item);
@ -567,11 +571,11 @@ async function toggleReactionAcceptance() {
const select = await os.select({
title: i18n.ts.reactionAcceptance,
items: [
{ value: null, text: i18n.ts.all },
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
{ value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
{ value: null, label: i18n.ts.all },
{ value: 'likeOnlyForRemote' as const, label: i18n.ts.likeOnlyForRemote },
{ value: 'nonSensitiveOnly' as const, label: i18n.ts.nonSensitiveOnly },
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, label: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
{ value: 'likeOnly' as const, label: i18n.ts.likeOnly },
],
default: reactionAcceptance.value,
});
@ -823,17 +827,15 @@ async function saveServerDraft(clearLocal = false) {
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
text: text.value,
useCw: useCw.value,
cw: cw.value,
cw: useCw.value ? cw.value || null : null,
visibility: visibility.value,
localOnly: localOnly.value,
hashtag: hashtags.value,
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
poll: poll.value,
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : undefined,
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined,
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
quoteId: quoteId.value,
channelId: targetChannel.value ? targetChannel.value.id : undefined,
reactionAcceptance: reactionAcceptance.value,
}).then(() => {
@ -1302,6 +1304,7 @@ async function canClose() {
defineExpose({
clear,
abortUploader: () => uploader.abortAll(),
canClose,
});
</script>

View File

@ -54,6 +54,7 @@ function onPosted() {
async function _close() {
const canClose = await form.value?.canClose();
if (!canClose) return;
form.value?.abortUploader();
modal.value?.close();
}

View File

@ -78,7 +78,7 @@ function subscribe() {
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
return promiseDialog(registration.value.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToBase64(instance.swPublickey),
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
})
.then(async subscription => {
pushSubscription.value = subscription;
@ -90,7 +90,7 @@ function subscribe() {
publickey: encode(subscription.getKey('p256dh')),
});
}, async err => { // When subscribe failed
//
//
if (err?.name === 'NotAllowedError') {
console.info('User denied the notification permission request.');
return;
@ -114,14 +114,13 @@ async function unsubscribe() {
if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', {
i: $i.token,
endpoint,
});
}, $i.token);
} else {
pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', {
endpoint,
});
}, null);
pushSubscription.value = null;
}
}
@ -131,16 +130,22 @@ function encode(buffer: ArrayBuffer | null) {
}
/**
* Convert the URL safe base64 string to a base64 string
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToBase64(base64String: string): string {
function urlBase64ToUint8Array(base64String: string): BufferSource {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
return base64;
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
if (navigator.serviceWorker == null) {

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<template v-if="forModeration">
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
<i v-if="'isPublic' in role && role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
</template>
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</span>
<span :class="$style.bodyName">{{ role.name }}</span>
<template v-if="detailed">
<template v-if="detailed && 'target' in role && 'usersCount' in role">
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
</template>
@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
role: Misskey.entities.Role;
role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
forModeration: boolean;
detailed?: boolean;
}>(), {

View File

@ -102,12 +102,12 @@ async function addRole() {
const items = roles.value
.filter(r => r.isPublic)
.filter(r => !selectedRoleIds.value.includes(r.id))
.map(r => ({ text: r.name, value: r }));
.map(r => ({ label: r.name, value: r.id }));
const { canceled, result: role } = await os.select({ items });
if (canceled || role == null) return;
const { canceled, result: roleId } = await os.select({ items });
if (canceled || roleId == null) return;
selectedRoleIds.value.push(role.id);
selectedRoleIds.value.push(roleId);
}
async function removeRole(roleId: string) {

View File

@ -40,46 +40,41 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
type ItemOption = {
export type OptionValue = string | number | null;
export type ItemOption<T extends OptionValue = OptionValue> = {
type?: 'option';
value: string | number | null;
value: T;
label: string;
};
type ItemGroup = {
export type ItemGroup<T extends OptionValue = OptionValue> = {
type: 'group';
label: string;
items: ItemOption[];
label?: string;
items: ItemOption<T>[];
};
export type MkSelectItem = ItemOption | ItemGroup;
export type MkSelectItem<T extends OptionValue = OptionValue> = ItemOption<T> | ItemGroup<T>;
type ValuesOfItems<T> = T extends (infer U)[]
? U extends { type: 'group'; items: infer V }
? V extends (infer W)[]
? W extends { value: infer X }
? X
: never
: never
: U extends { value: infer Y }
? Y
: never
export type GetMkSelectValueType<T extends MkSelectItem> = T extends ItemGroup
? T['items'][number]['value']
: T extends ItemOption
? T['value']
: never;
export type GetMkSelectValueTypesFromDef<T extends MkSelectItem[]> = T[number] extends MkSelectItem
? GetMkSelectValueType<T[number]>
: never;
</script>
<script lang="ts" setup generic="T extends MkSelectItem[]">
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
<script lang="ts" setup generic="const ITEMS extends MkSelectItem[], MODELT extends OptionValue">
import { onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import { useInterval } from '@@/js/use-interval.js';
import type { VNode, VNodeChild } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
// TODO: itemsslotoption(props.items)
// see: https://github.com/misskey-dev/misskey/issues/15558
//
const props = defineProps<{
modelValue: ValuesOfItems<T>;
items: ITEMS;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@ -88,16 +83,17 @@ const props = defineProps<{
inline?: boolean;
small?: boolean;
large?: boolean;
items?: T;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', value: ValuesOfItems<T>): void;
}>();
type ModelTChecked = MODELT & (
MODELT extends GetMkSelectValueTypesFromDef<ITEMS>
? unknown
: 'Error: The type of model does not match the type of items.'
);
const slots = useSlots();
const model = defineModel<ModelTChecked>({ required: true });
const { modelValue, autofocus } = toRefs(props);
const { autofocus } = toRefs(props);
const focused = ref(false);
const opening = ref(false);
const currentValueText = ref<string | null>(null);
@ -140,52 +136,26 @@ onMounted(() => {
});
});
watch([modelValue, () => props.items], () => {
if (props.items) {
let found: ItemOption | null = null;
for (const item of props.items) {
if (item.type === 'group') {
for (const option of item.items) {
if (option.value === modelValue.value) {
found = option;
break;
}
}
} else {
if (item.value === modelValue.value) {
found = item;
watch([model, () => props.items], () => {
let found: ItemOption | null = null;
for (const item of props.items) {
if (item.type === 'group') {
for (const option of item.items) {
if (option.value === model.value) {
found = option;
break;
}
}
} else {
if (item.value === model.value) {
found = item;
break;
}
}
if (found) {
currentValueText.value = found.label;
}
return;
}
const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') {
const optgroup = vnode;
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { //
const fragment = vnode;
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if false
// nop?
} else {
const option = vnode;
if (option.props?.value === modelValue.value) {
currentValueText.value = option.children as string;
break;
}
}
}
};
scanOptions(slots.default!());
if (found) {
currentValueText.value = found.label;
}
}, { immediate: true, deep: true });
function show() {
@ -196,68 +166,32 @@ function show() {
const menu: MenuItem[] = [];
if (props.items) {
for (const item of props.items) {
if (item.type === 'group') {
for (const item of props.items) {
if (item.type === 'group') {
if (item.label != null) {
menu.push({
type: 'label',
text: item.label,
});
for (const option of item.items) {
menu.push({
text: option.label,
active: computed(() => modelValue.value === option.value),
action: () => {
emit('update:modelValue', option.value);
},
});
}
} else {
}
for (const option of item.items) {
menu.push({
text: item.label,
active: computed(() => modelValue.value === item.value),
text: option.label,
active: computed(() => model.value === option.value),
action: () => {
emit('update:modelValue', item.value);
model.value = option.value as ModelTChecked;
},
});
}
}
} else {
let options = slots.default!();
const pushOption = (option: VNode) => {
} else {
menu.push({
text: option.children as string,
active: computed(() => modelValue.value === option.props?.value),
text: item.label,
active: computed(() => model.value === item.value),
action: () => {
emit('update:modelValue', option.props?.value);
model.value = item.value as ModelTChecked;
},
});
};
const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') {
const optgroup = vnode;
menu.push({
type: 'label',
text: optgroup.props?.label,
});
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { //
const fragment = vnode;
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if false
// nop?
} else {
const option = vnode;
pushOption(option);
}
}
};
scanOptions(options);
}
}
os.popupMenu(menu, container.value, {

Some files were not shown because too many files have changed in this diff Show More