Compare commits

...

130 Commits

Author SHA1 Message Date
github-actions[bot] 899273554a Bump version to 2025.6.4-alpha.1 2025-06-26 04:38:12 +00:00
syuilo bf57557ba3 refactor(frontend): refactor uploader image editing features and menu
Replaces separate 'effect' and 'crop' features with a unified 'imageEditing' feature in the uploader. Groups crop and effect actions under a new parent 'editImage' menu item, adds localization for 'editImage', and updates supported types accordingly.
2025-06-26 12:10:15 +09:00
syuilo 8fda4fefaf refactor 2025-06-26 12:06:53 +09:00
syuilo f1983d1aa5 Update type from UploaderDialogFeatures to UploaderFeatures
Replaces the UploaderDialogFeatures type with UploaderFeatures in the select function and SelectFileOptions type to ensure consistency and correct type usage.
2025-06-26 12:02:25 +09:00
syuilo 60649f4d66 add note 2025-06-26 12:01:56 +09:00
syuilo 525a330637 tweak eslint config 2025-06-26 10:49:41 +09:00
taichan b455e63da7 chore(frontend): 開発モード時に言語ファイルの変更を自動で反映するように (#16215)
* chore(frontend): 開発モード時に言語ファイルの変更を自動で反映するように

* fix message

* naming

* SPDX
2025-06-26 08:26:44 +09:00
syuilo 5626677e86 Update CHANGELOG.md 2025-06-25 20:30:49 +09:00
keito c424554d4a ジョブキューのProgressの値を正しく計算する (#16218)
* fix: ジョブキューのProgressの値の範囲を 0~100 に統一

* fix(backend): ジョブキューのProgressの計算に用いる総数を最初に一度だけ取得する
2025-06-25 20:30:17 +09:00
syuilo eee9a5f853 enhance(frontend): ページネーションの並び順を逆にできるように 2025-06-25 20:26:20 +09:00
github-actions[bot] 4d72d6caf4 Bump version to 2025.6.4-alpha.0 2025-06-25 08:50:37 +00:00
taichan b752dc72e5 feat: ノートの下書き(draft of note) (#15298)
* WIp (backend)

* Remove unused

* 下書きbackend 続き

* fix(backedn): visibilityが下書きに反映されない

* Update packages/backend/src/postgres.ts

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>

* Fix : import order

* fix(backend) : createでcwが効かない

* FIX FOREGIN KEY

* wip: frontend(既存の下書きを挿入)

まだ:チャンネル表示、下書きの作成、削除

* WIP: ノート選択ダイアログ
投稿時に下書きを削除

* Promiseに変更

* 連合なし、チャンネルも表示

* Hashtagの値抜け漏れ

* hasthagを0文字でも作成可能に

* 下書きの保存機構

* chore(misskey-js): build types

* localOnly抜け漏れ

* チャンネル情報の書き換え

* enhance(frontend): ヘッダ部の表示改善

* fix(frontend): ファイル添付できない

* fix: no file

* fix(frontend): 投票が反映されない

* ハッシュタグの展開(コメントアウト外し忘れ)

* fix: visibleUserIdsが反映されない

* enhance: APIの型を整備

* refactor: 型が整備できたのでasを削除

* Add userhost

* fix

* enhance: paginationを使う

* fix

* fix: 自分のアカウントでの投稿でしか下書きを利用できないように

完全に塞ぐことはできないが一応

* 🎨

* APIのエラーIDを追加

* enhance: スタイル調整

* remove unused code

* 🎨

* fix: ロールポリシーの型

* ロールの編集画面

* ダイアログの挙動改善

* 下書き機能が利用できない場合は表示しないように

* refactor

* fix: ダブルクリックが効かない問題を修正

* add comments

* fix

* fix: 保存時のエラーの種別にかかわらずmodalを閉じないように

* fix()backend: NoteDraftのreply, renoteの型が間違ってたので修正 (migtrationはあってた)

* fix: 投稿フォームを空白にして通常リノートできるやつは下書きとしては弾くように

* fix(backend): テキストが0文字でも下書きは保存できるように

* Fix(backend): replyIdの型定義がミスっているのを修正

* chore(misskey-js): update types

* Add CHANGELOG

* lint

* 常にサーバー下書きに保存し、上限を超えた場合のみ尋ねるように

* NoteDraftServiceにcreate, updateの処理を移譲

* Fix typeerror

* remove tooltip

* Remove Mkbutton:short and use iconOnly

* 不要なコメントの削除

* Remove Short Completely

* wip

* escキーまわりの挙動を改善

* 下書き選択時に下書き可能数と現在の量が分かるように

* cleanUp

* wip

* wi

* wip

* Update MkPostForm.vue

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-06-25 17:09:23 +09:00
syuilo 06d31c0b78 fix(frontend): ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正
Fix #16206
2025-06-25 10:55:30 +09:00
syuilo 32d4c312ef enhance(frontend): ファイルアップロード前にキャプション設定を行えるように
Resolve #16210
2025-06-25 10:49:58 +09:00
syuilo 36fde67992 enhance(frontend): improve theme settings 2025-06-25 10:23:39 +09:00
syuilo 43abbce2af enhance(frontend): 全てのページネーションにおいてコンテキストメニューからリロードできるように 2025-06-25 10:08:44 +09:00
syuilo 684424f26a enhance(frontend): improve useScrollPositionKeeper 2025-06-24 20:30:32 +09:00
syuilo 36989e0cd3 Update about-misskey.vue 2025-06-24 20:24:34 +09:00
syuilo d518682e73 add note 2025-06-24 11:44:16 +09:00
syuilo 0ada970337 enhance(frontend): 設定の自動バックアップをオンにした直後に自動バックアップするように 2025-06-23 17:12:25 +09:00
github-actions[bot] a812dfe853 [skip ci] Update CHANGELOG.md (prepend template) 2025-06-16 11:13:27 +00:00
github-actions[bot] 2baec208f5 Release: 2025.6.3 2025-06-16 11:13:22 +00:00
github-actions[bot] 4093616e23 Bump version to 2025.6.3-alpha.0 2025-06-16 10:52:09 +00:00
syuilo 062d5170df fix(frontend): キャッシュを削除しないとクライアントが使用できないことがある問題を修正
Fix #16196
2025-06-16 19:51:26 +09:00
github-actions[bot] a279bd4d49 [skip ci] Update CHANGELOG.md (prepend template) 2025-06-16 08:58:37 +00:00
github-actions[bot] 978ae706eb Release: 2025.6.2 2025-06-16 08:58:31 +00:00
syuilo 824643a44e [skip ci] Update CHANGELOG.md 2025-06-16 17:57:09 +09:00
github-actions[bot] 213c569242 Bump version to 2025.6.2-alpha.0 2025-06-16 05:08:24 +00:00
syuilo a1cf2d3074 New Crowdin updates (#16192)
* New translations ja-jp.yml (Chinese Traditional)

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

* New translations ja-jp.yml (Catalan)

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

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Chinese Simplified)
2025-06-16 14:07:48 +09:00
syuilo 4ea7c76c02 fix(frontend): キャッシュを削除しないとクライアントが使用できないことがある問題を修正 2025-06-16 13:55:27 +09:00
github-actions[bot] 1782a353d3 [skip ci] Update CHANGELOG.md (prepend template) 2025-06-16 02:33:19 +00:00
github-actions[bot] c69a13b592 Release: 2025.6.1 2025-06-16 02:33:14 +00:00
syuilo 40e35c051a Update CHANGELOG.md 2025-06-15 11:10:03 +09:00
syuilo b93717be33 Update CHANGELOG.md
Co-authored-by: おさむのひと <46447427+samunohito@users.noreply.github.com>
2025-06-15 11:08:53 +09:00
syuilo fe805fb7f0 enhance(frontend/image-effector): tweak fxs 2025-06-15 11:06:46 +09:00
syuilo e9af9d4451 enhance(frontend/image-effector): tweak fxs 2025-06-15 10:57:29 +09:00
syuilo ce90fee586 enhance(frontend/image-effector): add blockNoise fx 2025-06-15 10:55:11 +09:00
syuilo 5bec8ba6b0 enhance(frontend/image-effector): tweak fxs 2025-06-15 10:19:42 +09:00
syuilo 3dbfd80d65 enhance(frontend/image-effector): tweak colorAdjust fx 2025-06-15 09:25:57 +09:00
syuilo b33eeb1366 enhance(frontend/image-effector): tweak distort fx 2025-06-15 08:47:59 +09:00
syuilo 420756d744 Update CHANGELOG.md 2025-06-14 19:50:56 +09:00
かっこかり 32d721abf1 refactor(frontend): checkWordMuteの返り値が誤っている問題を修正 (#16188)
* refactor(frontend): checkWordMuteの返り値が誤っている問題を修正

* fix lint
2025-06-14 16:08:14 +09:00
github-actions[bot] 8ea6aa2ef3 Bump version to 2025.6.1-rc.0 2025-06-14 03:16:12 +00:00
syuilo bc07b79a23 fix(frontend): デッキのタイムラインカラムで新着ノート時のサウンドが再生されない問題を修正
Fix #16164
2025-06-14 11:36:42 +09:00
syuilo aae7961540 New Crowdin updates (#16187)
* New translations ja-jp.yml (Chinese Simplified)

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

* New translations ja-jp.yml (Chinese Simplified)
2025-06-13 10:31:14 +09:00
github-actions[bot] 1ad32990cb Bump version to 2025.6.1-beta.2 2025-06-12 10:07:07 +00:00
zyoshoka 89db7b3fa8 fix(frontend): display reactions correctly in welcome timeline (#16186) 2025-06-12 19:05:24 +09:00
syuilo 151121a567 New Crowdin updates (#16183)
* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Italian)
2025-06-12 17:34:12 +09:00
mochi33i 966e0812f5 fix(misskey-js) : misskey-jsの drive/file/create でファイルアップロードができない問題を修正 (#16028)
* fix(misskey-js): drive/file/createが動かない問題を修正

* CHANGELOG.mdに修正内容を追加
2025-06-12 16:54:59 +09:00
syuilo d378156212 Update CHANGELOG.md 2025-06-12 16:08:07 +09:00
renovate[bot] 568021498f fix(deps): update [root] update dependencies (#16142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 14:39:49 +09:00
github-actions[bot] 8f66ffc14d Bump version to 2025.6.1-beta.1 2025-06-11 03:45:02 +00:00
syuilo 63e8935c86 fix(frontend): disable note_view_interruptor temporary to prevent rendering glitch 2025-06-11 12:42:49 +09:00
renovate[bot] b16a05b9a7 fix(deps): update [backend] update dependencies (#16143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 09:56:29 +09:00
おさむのひと 090262f3c6 fix: pnpm-lock.yamlの再生成 (#16182) 2025-06-11 08:57:42 +09:00
renovate[bot] bc5a33d87f chore(deps): update [misskey-js] update dependencies (#16140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 16:08:04 +09:00
renovate[bot] 0ffd9e267a fix(deps): update [frontend] update dependencies (#16144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 16:07:34 +09:00
renovate[bot] 81bc27d804 chore(deps): update [tools] update dependencies (#16141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-10 15:53:04 +09:00
github-actions[bot] f50abed98d Bump version to 2025.6.1-beta.0 2025-06-10 04:43:58 +00:00
syuilo 8ab574a31a New Crowdin updates (#16163)
* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

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

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Italian)

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

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

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

* New translations ja-jp.yml (English)

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

* New translations ja-jp.yml (Korean)

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

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

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (German)
2025-06-10 13:42:09 +09:00
syuilo 9a3219f12e fix(frontend): Plugin:register_note_view_interruptor()によるノートの書き換えが機能しない問題を修正
Fix #16180
2025-06-10 09:51:45 +09:00
zyoshoka b5767c315a fix(backend): correct outbox pagination (#16176) 2025-06-08 09:12:59 +09:00
github-actions[bot] ac9206f192 Bump version to 2025.6.1-alpha.4 2025-06-07 10:52:03 +00:00
かっこかり e2b38edb3a deps(misskey-js): Update openapi-typescript to v7 (#15491)
* deps(misskey-js): Update openapi-typescript to v7

* update openapi-typescript to v7.7.3

* generate misskey-js types

* bump openapi-typescript

* enhance: 生成物からnever型を除去するように

* regenerate api types

* refactor: 処理共通化

---------

Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-06-07 19:36:00 +09:00
syuilo c5dc0fd51b Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2025-06-07 09:26:51 +09:00
syuilo dda2ad6bcd fix(frontend): support non-image files 2025-06-07 09:26:49 +09:00
github-actions[bot] a5429ebeee Bump version to 2025.6.1-alpha.3 2025-06-06 23:36:20 +00:00
syuilo 1c3604c7fb 🎨 2025-06-07 08:15:31 +09:00
syuilo 4906f1f45c 🎨 2025-06-07 08:07:23 +09:00
syuilo 6506429f09 enhance(frontend): アップローダー内でセンシティブフラグを設定可能に 2025-06-07 07:57:23 +09:00
syuilo 9bd5f887de enhance(frontend): 投稿フォームにアップローダーを埋め込み (#16173)
* wip

* Update MkPostForm.vue

* wip

* wip

* Update MkPostForm.vue

* wip

* wip

* add tip

* Update tips.ts

* Update MkPostForm.vue
2025-06-07 07:47:43 +09:00
syuilo be35fe468b refactor(frontend): refactor tips 2025-06-06 21:03:35 +09:00
syuilo 4b9b3ced01 enhance(frontend): improve MkTip usability 2025-06-06 20:59:01 +09:00
syuilo 20b8148ddf chore(frontend): tweak ui 2025-06-06 09:02:47 +09:00
github-actions[bot] 019dfbdc1c Bump version to 2025.6.1-alpha.2 2025-06-05 13:27:28 +00:00
かっこかり 95ea62f222 enhance(frontend): 画像エフェクトの操作でRangeをダブルクリックしたらデフォルトの値に戻るように (#16171)
* enhance(frontend): エフェクトの操作でRangeをダブルクリックしたらデフォルトの値に戻るように

* fix: trackの計算方法を修正

* remove unnecessary async
2025-06-05 22:25:49 +09:00
syuilo fde67dca74 enhance(frontend): tweak server setup wizard 2025-06-05 21:05:11 +09:00
かっこかり a603a4970e enhance(frontend): 画像エフェクト「色調補正」を追加 (#16170) 2025-06-05 20:29:02 +09:00
zyoshoka f37a1e84bd chore: fix failure to publish misskey-js to npm registry (#16169) 2025-06-05 19:21:15 +09:00
syuilo 6c9e055aae add note 2025-06-05 15:05:00 +09:00
syuilo a971e44cee refactor(frontend): refactor ImageEffector 2025-06-05 15:00:17 +09:00
syuilo c6808f1eb6 refactor(frontend): refactor ImageEffector 2025-06-05 12:58:32 +09:00
syuilo 2a78360588 refactor(frontend): refactor ImageEffector 2025-06-05 12:25:22 +09:00
zyoshoka 65ba33867b fix(backend): avoid deadlock when deleting account (#16162) 2025-06-04 19:14:11 +09:00
syuilo b55cc03621 New Crowdin updates (#16155)
* New translations ja-jp.yml (Korean)

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

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Catalan)

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

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

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Portuguese)

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

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Japanese, Kansai)
2025-06-04 18:25:39 +09:00
かっこかり 80f73c6712 enhance(gh): renovateのicons-subsetterをfrontendグループに統合 (#16161) 2025-06-04 16:39:06 +09:00
syuilo 60fc9a5195 🎨 2025-06-04 16:33:55 +09:00
かっこかり b43dfa260b fix/refactor(frontend): 画像編集機能の修正・型強化 (#16156)
* enhance: refine uploadFile

* fix: missing locale

* refactor: harden types

* refactor: シェーダーファイルをlazy-loadingできるように

* fix(frontend): omit console.log in production environment

* fix: glslのバージョン表記は最初の行になければならない

* fix: シェーダーの読み込みが完了してからレンダリングを行うように

* fix merge failure

* fix: ウォーターマークのプリセットがない場合にdividerが2重に表示される問題を修正

* fix: アップローダーダイアログの機能設定でウォーターマークが無効な場合でもデフォルトのプリセットが適用されてしまう問題を修正

* fix lint

* Revert "fix: シェーダーの読み込みが完了してからレンダリングを行うように"

This reverts commit e06f37a7d4.

* Revert "fix: glslのバージョン表記は最初の行になければならない"

This reverts commit afcc37d886.

* Revert "refactor: シェーダーファイルをlazy-loadingできるように"

This reverts commit a1ab2fa38c.

* fix: ウォーターマークのFX定義を分ける

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

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

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

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-06-04 16:22:09 +09:00
syuilo e3b57a118d 🎨 2025-06-04 16:15:06 +09:00
syuilo fdcb6a09a9 fix(frontend): タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正 2025-06-04 16:10:57 +09:00
syuilo 218df19d9c enhance(frontend): スマホでのMkModalWindowの表示を最適化 2025-06-04 12:28:50 +09:00
syuilo b766589c00 enhance(frontend): ミュートした絵文字をデバイス間で同期できるように
Resolve #16158
2025-06-04 12:01:33 +09:00
かっこかり d94bdff639 fix(misskey-js): build misskey-js with types (#16159) 2025-06-04 11:49:29 +09:00
github-actions[bot] 3a4288fe24 Bump version to 2025.6.1-alpha.1 2025-06-03 12:13:26 +00:00
syuilo 12c71de4c7 New Crowdin updates (#16153)
* New translations ja-jp.yml (Romanian)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Greek)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Dutch)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Swedish)

* New translations ja-jp.yml (Turkish)

* New translations ja-jp.yml (Ukrainian)

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

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

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)

* New translations ja-jp.yml (Uzbek)

* New translations ja-jp.yml (Lao)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Korean (Gyeongsang))
2025-06-03 21:07:02 +09:00
syuilo b7aa013a41 feat: 全てのチャットメッセージを既読にできるように 2025-06-03 21:06:37 +09:00
github-actions[bot] 3ff2e6b299 Bump version to 2025.6.1-alpha.0 2025-06-03 10:19:12 +00:00
syuilo cd9322a824 feat(frontend): 画像編集機能 (#16121)
* wip

* wip

* wip

* wip

* Update watermarker.ts

* wip

* wip

* Update watermarker.ts

* Update MkUploaderDialog.vue

* wip

* Update ImageEffector.ts

* Update ImageEffector.ts

* wip

* wip

* wip

* wip

* wip

* wip

* Update MkRange.vue

* Update MkRange.vue

* wip

* wip

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.Layer.vue

* wip

* Update zoomLines.ts

* Update zoomLines.ts

* wip

* wip

* Update ImageEffector.ts

* wip

* Update ImageEffector.ts

* wip

* Update ImageEffector.ts

* swip

* wip

* Update ImageEffector.ts

* wop

* Update MkUploaderDialog.vue

* Update ImageEffector.ts

* wip

* wip

* wip

* Update def.ts

* Update def.ts

* test

* test

* Update manager.ts

* Update manager.ts

* Update manager.ts

* Update manager.ts

* Update MkImageEffectorDialog.vue

* wip

* use WEBGL_lose_context

* wip

* Update MkUploaderDialog.vue

* Update drive.vue

* wip

* Update MkUploaderDialog.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2025-06-03 19:18:29 +09:00
syuilo 9fdc3c5def chore(frontend): remove duplicate declarations 2025-06-03 18:44:51 +09:00
かっこかり 4af8c7f8b0 enhance(frontend): リアクションビューワーで使用可能なリアクションを優先して表示するオプション (#16149)
* enhance(frontend): リアクションビューワーで使用可能なリアクションを優先して表示するオプション

* Update Changelog

* tweak

* fix

* enhance: リアクティブじゃなくする

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-06-03 18:44:01 +09:00
syuilo ed29a3613b fix(frontend): fix user search pagination 2025-06-03 17:57:51 +09:00
かっこかり 810a609df4 fix(frontend): 一部のグリッドのページネーションが正しく表示されない問題を修正 (#16147)
* fix(frontend): 一部のグリッドのページネーションが正しく表示されない問題を修正

* update changelog

* fix
2025-06-03 17:20:11 +09:00
かっこかり 5537558fa7 fix(frontend): ドライブのファイル選択が不安定な問題を修正 (#16148)
* fix(frontend): ドライブのファイル選択が不安定になっているのを修正

* Update Changelog
2025-06-03 15:45:26 +09:00
syuilo 26215b7466 🎨 2025-06-03 10:35:02 +09:00
syuilo 0a0247a678 enhance(frontend): add config button to emoji picker to improve usability 2025-06-03 08:04:15 +09:00
syuilo 93d17aff6c refactor(frontend): ID生成処理を統一 2025-06-03 07:37:08 +09:00
syuilo 3bc81522c6 enhance(frontend): IDにUUIDを使うのをやめる (#16138)
* wip

* Update flash-edit.vue
2025-06-03 07:31:19 +09:00
github-actions[bot] 2a077de148 [skip ci] Update CHANGELOG.md (prepend template) 2025-06-02 00:58:35 +00:00
github-actions[bot] 3eb64ccf7a Release: 2025.6.0 2025-06-02 00:58:25 +00:00
syuilo 47fede5e5c Update CHANGELOG.md 2025-06-02 09:43:27 +09:00
renovate[bot] b48a25ea77 fix(deps): update [frontend] update dependencies (#16086)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 09:13:06 +09:00
github-actions[bot] 51e7081c9d Bump version to 2025.6.0-beta.1 2025-06-01 23:49:28 +00:00
syuilo 982ae9238d New Crowdin updates (#16136)
* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)
2025-06-02 08:43:56 +09:00
syuilo d40c083233 enhance(frontend): 非同期的なコンポーネントの読み込み時のハンドリングを強化 2025-06-02 08:43:04 +09:00
syuilo 8f025c447b fix(backend): ログインできなくなるため、ugcVisibilityForVisitorがnoneでもusers/showは許可するように 2025-06-02 08:37:22 +09:00
syuilo 9dc423afd4 fix(frontend): tweak MkFolderPage z-index 2025-06-02 08:32:22 +09:00
github-actions[bot] 5893a95c99 Bump version to 2025.6.0-beta.0 2025-06-01 01:49:22 +00:00
syuilo f4167ae7f1 enhance(frontend): 非同期的なコンポーネントの読み込み時のハンドリングを強化 2025-06-01 10:44:45 +09:00
syuilo 63db879bcc fix(frontend): remove unused text 2025-06-01 10:22:32 +09:00
syuilo bd17b465c3 New Crowdin updates (#16129)
* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Korean)
2025-06-01 09:34:08 +09:00
syuilo 6b1fbf6819 Update about-misskey.vue 2025-06-01 09:24:49 +09:00
github-actions[bot] 021c332097 Bump version to 2025.6.0-alpha.0 2025-06-01 00:22:01 +00:00
syuilo 5bdbff19ae fix(frontend): リアクションの一部の絵文字が重複して表示されることがある問題を修正
Fix #16130
2025-06-01 08:10:49 +09:00
syuilo 070a4516fc 🎨 2025-06-01 07:57:22 +09:00
syuilo c5d33661b2 chore(frontend): improve type def 2025-06-01 07:57:18 +09:00
github-actions[bot] 777ca15083 [skip ci] Update CHANGELOG.md (prepend template) 2025-05-31 12:37:08 +00:00
github-actions[bot] 21344f7695 Release: 2025.5.1 2025-05-31 12:37:02 +00:00
syuilo 4254f52ced New Crowdin updates (#16126)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Catalan)

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

* New translations ja-jp.yml (English)
2025-05-31 18:06:59 +09:00
syuilo f14787bd10 fix(frontend): ベースロールの、「アップロード可能なファイル種別」を変更しても正しく保存されない
Fix #16128
2025-05-31 18:04:00 +09:00
syuilo 14cbc78031 Update packages/frontend/src/pages/admin/custom-emojis-manager.register.vue
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
2025-05-31 17:41:04 +09:00
266 changed files with 48146 additions and 33513 deletions
+2
View File
@@ -26,6 +26,8 @@ jobs:
with:
node-version-file: '.node-version'
cache: 'pnpm'
# see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry
registry-url: 'https://registry.npmjs.org'
- name: Publish package
run: |
pnpm i --frozen-lockfile
+63
View File
@@ -1,3 +1,66 @@
## 2025.6.4
### General
- Feat: ノートの下書き機能
### Client
- Enhance: 設定の自動バックアップをオンにした直後に自動バックアップするように
- Enhance: ファイルアップロード前にキャプション設定を行えるように
- Enhance: ページネーションの並び順を逆にできるように
- Fix: ファイルがドライブの既定アップロード先に指定したフォルダにアップロードされない問題を修正
### Server
- Fix: ジョブキューのProgressの値を正しく計算する
## 2025.6.3
### Client
- Fix: キャッシュを削除しないとクライアントが使用できないことがある問題を修正
## 2025.6.2
### Client
- Fix: キャッシュを削除しないとクライアントが使用できないことがある問題を修正
- 翻訳の更新
## 2025.6.1
### Note
- AiScript Misskey拡張APIMisskey Webプラグイン)の[note_view_interruptor](https://misskey-hub.net/ja/docs/for-developers/plugin/plugin-api-reference/#pluginregister_note_view_interruptorfn)は不具合の影響により現在一時的に無効化されています。
- Misskey Web投稿フォームのプレビュー切り替えは「...」メニュー内に配置されました
### Client
- Feat: 画像にウォーターマークを付与できるようになりました
- Feat: 画像の加工ができるようになりました(実験的)
- Enhance: ノートのリアクション一覧で、押せるリアクションを優先して表示できるようにするオプションを追加
- Enhance: 全てのチャットメッセージを既読にできるように(設定→その他)
- Enhance: ミュートした絵文字をデバイス間で同期できるように
- Fix: ドライブファイルの選択が不安定な問題を修正
- Fix: コントロールパネルのファイル欄などのデザインが崩れている問題を修正
- Fix: ユーザーの検索結果を追加で読み込むことができない問題を修正
- Fix: タッチ操作時にチャートのツールチップが消えなくなる場合がある問題を修正
- Fix: ウェルカムタイムラインでリアクションが表示されない問題を修正
- Fix: デッキのタイムラインカラムで新着ノート時のサウンドが再生されない問題を修正
### Server
- Feat: 全てのチャットメッセージを既読にするAPIを追加(chat/read-all)
- Fix: アカウント削除が正常に行われないことがあった問題を修正
- Fix: outboxのページネーションが正しく行われない問題を修正
### Misskey.js
- Fix: misskey-jsの drive/file/create でファイルアップロードができない問題を修正
## 2025.6.0
### Client
- Enhance: 非同期的なコンポーネントの読み込み時のハンドリングを強化
- Fix: リアクションの一部の絵文字が重複して表示されることがある問題を修正
- Fix: 非利用者に対するユーザー作成コンテンツの公開範囲が全て非公開になっている場合にログインできない問題を修正
### Server
- Fix: 非利用者に対するユーザー作成コンテンツの公開範囲が全て非公開になっている場合でもusers/showを許可するように
## 2025.5.1
### Note
-1
View File
@@ -34,7 +34,6 @@ describe('Before setup instance', () => {
cy.intercept('POST', '/api/admin/update-meta').as('update-meta');
cy.get('[data-cy-next]').click();
cy.get('[data-cy-next]').click();
cy.get('[data-cy-server-name] input').type('Testskey');
cy.get('[data-cy-server-setup-wizard-apply]').click();
+8
View File
@@ -1589,3 +1589,11 @@ _search:
searchScopeAll: "الكل"
searchScopeLocal: "المحلي"
searchScopeUser: "مستخدم محدد"
_watermarkEditor:
opacity: "الشفافية"
scale: "الحجم"
text: "نص"
position: "الموضع"
type: "نوع"
image: "صور"
advanced: "متقدم"
+6
View File
@@ -1349,3 +1349,9 @@ _remoteLookupErrors:
_search:
searchScopeAll: "সবগুলো"
searchScopeLocal: "স্থানীয়"
_watermarkEditor:
opacity: "অস্বচ্ছতা"
scale: "আকার"
text: "লেখা"
image: "ছবি"
advanced: "উন্নত"
+55
View File
@@ -1330,6 +1330,7 @@ restore: "Restaurar "
syncBetweenDevices: "Sincronització entre dispositius"
preferenceSyncConflictTitle: "Els valors de la configuració ja existeixen al dispositiu"
preferenceSyncConflictText: "Un element de la configuració amb sincronització activada desa els seus valors al servidor, però s'ha trobat un valor a la configuració desat al servidor per aquest element de la configuració. Quin valor us sobreescriure?"
preferenceSyncConflictChoiceMerge: "Integració "
preferenceSyncConflictChoiceServer: "Valors de configuració del servidor"
preferenceSyncConflictChoiceDevice: "Punts d'ajustos del dispositiu "
preferenceSyncConflictChoiceCancel: "Cancel·lar l'activació de la sincronització "
@@ -1364,6 +1365,8 @@ abort: "Cancel·lar"
tip: "Trucs i consells"
redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
hideAllTips: "Amagar tots els trucs i consells"
defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte"
defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran. <br>Alta, redueix la mida de l'arxiu però també la qualitat de la imatge."
_chat:
noMessagesYet: "Encara no tens missatges "
newMessage: "Missatge nou"
@@ -1451,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "Com més alt sigui l'adquisició de contingut en temps real, més baixa el rendiment i més consum de dades i bateria."
contentsUpdateFrequency_description2: "Quan s'activa el mode en temps real, el contingut s'actualitza en temps real, independentment d'aquesta configuració."
showUrlPreview: "Mostrar vista prèvia d'URL"
showAvailableReactionsFirstInNote: "Mostra les reacciones que pots fer servir al damunt"
_chat:
showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar"
@@ -2461,6 +2465,8 @@ _visibility:
disableFederation: "Sense federar"
disableFederationDescription: "No enviar a altres servidors"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "Hi ha arxius que no s'han carregat, vols descartar-los i tancar el formulari?"
uploaderTip: "L'arxiu encara no s'ha carregat. Des del menú arxiu pots canviar el nom, retallar imatges, posar marques d'aigua i comprimir o no l'arxiu. Els arxius es carreguen automàticament quan públiques una nota."
replyPlaceholder: "Contestar..."
quotePlaceholder: "Citar..."
channelPlaceholder: "Publicar a un canal..."
@@ -3116,3 +3122,52 @@ _clip:
tip: "Clip és una funció que permet organitzar les teves notes."
_userLists:
tip: "Es poden crear llistes amb qualsevol usuari. La llista creada es pot mostrar com una línia de temps."
watermark: "Marca d'aigua "
defaultPreset: "Per defecte"
_watermarkEditor:
tip: "A la imatge es pot afegir una marca d'aigua com informació sobre drets."
quitWithoutSaveConfirm: "Sortir sense desar?"
driveFileTypeWarn: "Aquest arxiu no és compatible"
driveFileTypeWarnDescription: "Selecciona un arxiu d'imatge "
title: "Editar la marca d'aigua "
cover: "Cobrir-ho tot"
repeat: "Repetir"
opacity: "Opacitat"
scale: "Mida"
text: "Text"
position: "Posició "
type: "Tipus"
image: "Imatges"
advanced: "Avançat"
stripe: "Bandes"
stripeWidth: "Amplada de la banda"
stripeFrequency: "Freqüència de la banda"
angle: "Angle"
polkadot: "Lunars"
checker: "Escacs"
polkadotMainDotOpacity: "Opacitat del lunar principal"
polkadotMainDotRadius: "Mida del lunar principal"
polkadotSubDotOpacity: "Opacitat del lunar secundari"
polkadotSubDotRadius: "Mida del lunar secundari"
polkadotSubDotDivisions: "Nombre de punts secundaris"
_imageEffector:
title: "Efecte"
addEffect: "Afegeix un efecte"
discardChangesConfirm: "Vols descartar els canvis i sortir?"
_fxs:
chromaticAberration: "Aberració cromàtica"
glitch: "Glitch"
mirror: "Mirall"
invert: "Inversió cromàtica "
grayscale: "Monocrom "
colorAdjust: "Correcció de color"
colorClamp: "Compressió cromàtica "
colorClampAdvanced: "Compressió de cromàtica avançada "
distort: "Distorsió "
threshold: "Binarització"
zoomLines: "Saturació de línies "
stripe: "Bandes"
polkadot: "Lunars"
checker: "Escacs"
blockNoise: "Bloqueig de soroll"
tearing: "Trencament d'imatge "
+8
View File
@@ -2043,3 +2043,11 @@ _search:
searchScopeAll: "Vše"
searchScopeLocal: "Místní"
searchScopeUser: "Upřesnit uživatele"
_watermarkEditor:
opacity: "Průhlednost"
scale: "Velikost"
text: "Text"
position: "Pozice"
type: "Typ"
image: "Obrázky"
advanced: "Pokročilé"
+132
View File
@@ -298,6 +298,7 @@ uploadFromUrl: "Von einer URL hochladen"
uploadFromUrlDescription: "URL der hochzuladenden Datei"
uploadFromUrlRequested: "Upload angefordert"
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis das Hochladen abgeschlossen ist."
uploadNFiles: "Lade {n} Dateien hoch"
explore: "Erkunden"
messageRead: "Gelesen"
noMoreHistory: "Kein weiterer Verlauf vorhanden"
@@ -326,6 +327,7 @@ dark: "Dunkel"
lightThemes: "Helle Farbschemata"
darkThemes: "Dunkle Farbschemata"
syncDeviceDarkMode: "Einstellung deines Geräts übernehmen"
switchDarkModeManuallyWhenSyncEnabledConfirm: "\"{x}\" ist eingeschaltet. Möchtest du die Synchronisation ausschalten und den Modus manuell wechseln?"
drive: "Drive"
fileName: "Dateiname"
selectFile: "Datei auswählen"
@@ -575,8 +577,10 @@ showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik an
showFixedPostFormInChannel: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen (Kanäle)"
withRepliesByDefaultForNewlyFollowed: "Standardmäßig Antworten von neu gefolgten Benutzern in der Chronik anzeigen"
newNoteRecived: "Es gibt neue Notizen"
newNote: "Neue Notiz"
sounds: "Töne"
sound: "Töne"
notificationSoundSettings: "Benachrichtigungston festlegen"
listen: "Anhören"
none: "Nichts"
showInPage: "In einer Seite anzeigen"
@@ -791,6 +795,7 @@ wide: "Breit"
narrow: "Schmal"
reloadToApplySetting: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft. Jetzt aktualisieren?"
needReloadToApply: "Diese Einstellung tritt nach einer Aktualisierung der Seite in Kraft."
needToRestartServerToApply: "Diese Einstellung tritt nach einem Neustart des Servers in Kraft."
showTitlebar: "Titelleiste anzeigen"
clearCache: "Cache leeren"
onlineUsersCount: "{n} Benutzer sind online"
@@ -997,6 +1002,7 @@ failedToUpload: "Hochladen fehlgeschlagen"
cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile der Datei als möglicherweise unangebracht festgestellt wurden."
cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein Drive-Speicherplatz aufgebraucht ist."
cannotUploadBecauseExceedsFileSizeLimit: "Diese Datei kann wegen Überschreitung der Maximalgröße nicht hochgeladen werden."
cannotUploadBecauseUnallowedFileType: "Hochladen nicht möglich wegen unzulässigem Dateityp."
beta: "Beta"
enableAutoSensitive: "Automarkierung sensibler Medien"
enableAutoSensitiveDescription: "Setzt soweit möglich durch Verwendung von Machine Learning automatisch Markierungen für sensible Medien. Auch wenn du diese Option deaktiviert hast, ist sie möglicherweise auf Instanzebene aktiviert."
@@ -1324,6 +1330,7 @@ restore: "Wiederherstellen"
syncBetweenDevices: "Zwischen Geräten synchronisieren"
preferenceSyncConflictTitle: "Der konfigurierte Wert ist auf dem Server bereits vorhanden."
preferenceSyncConflictText: "Die Einstellungen mit aktivierter Synchronisierung werden ihre Werte auf dem Server speichern. Es gibt jedoch bereits Werte auf dem Server. Welche Einstellungswerte sollen überschrieben werden?"
preferenceSyncConflictChoiceMerge: "Zusammenführen"
preferenceSyncConflictChoiceServer: "Konfigurierte Werte auf dem Server"
preferenceSyncConflictChoiceDevice: "Konfigurierte Werte auf dem Gerät"
preferenceSyncConflictChoiceCancel: "Einrichten der Synchronisierung abbrechen"
@@ -1346,6 +1353,20 @@ goToDeck: "Zurück zum Deck"
federationJobs: "Föderation Jobs"
driveAboutTip: "In Drive sehen Sie eine Liste der Dateien, die Sie in der Vergangenheit hochgeladen haben. <br>\nSie können diese Dateien wiederverwenden um sie zu beispiel an Notizen anzuhängen, oder sie können Dateien vorab hochzuladen, um sie später zu versenden! <br>\n<b>Wenn Sie eine Datei löschen, verschwindet sie auch von allen Stellen, an denen Sie sie verwendet haben (Notizen, Seiten, Avatare, Banner usw.).</b><br>\nSie können auch Ordner erstellen, um sie zu organisieren."
scrollToClose: "Zum Schließen scrollen"
advice: "Tipps"
realtimeMode: "Echtzeit-Modus"
turnItOn: "Einschalten"
turnItOff: "Ausschalten"
emojiMute: "Emoji stummschalten"
emojiUnmute: "Emoji-Stummschaltung aufheben"
muteX: "{x} stummschalten"
unmuteX: "Stummschaltung von {x} aufheben"
abort: "Abbrechen"
tip: "Tipps und Tricks"
redisplayAllTips: "Alle „Tipps und Tricks“ wieder anzeigen"
hideAllTips: "Alle „Tipps und Tricks“ ausblenden"
defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe"
defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität."
_chat:
noMessagesYet: "Noch keine Nachrichten"
newMessage: "Neue Nachricht"
@@ -1379,6 +1400,8 @@ _chat:
chatNotAvailableInOtherAccount: "Die Chatfunktion wurde vom anderen Benutzer deaktiviert."
cannotChatWithTheUser: "Starten eines Chats mit diesem Benutzer nicht möglich"
cannotChatWithTheUser_description: "Der Chat ist entweder nicht verfügbar oder die andere Seite hat den Chat nicht aktiviert."
youAreNotAMemberOfThisRoomButInvited: "Du bist kein Teilnehmer in diesem Raum, aber du hast eine Einladung erhalten. Bitte nimm die Einladung an, um beizutreten."
doYouAcceptInvitation: "Nimmst du die Einladung an?"
chatWithThisUser: "Mit dem Benutzer chatten"
thisUserAllowsChatOnlyFromFollowers: "Dieser Benutzer nimmt nur Chats von Followern an."
thisUserAllowsChatOnlyFromFollowing: "Dieser Benutzer nimmt nur Chats von Benutzern an, denen er folgt."
@@ -1418,12 +1441,20 @@ _settings:
makeEveryTextElementsSelectable: "Alle Textelemente auswählbar machen"
makeEveryTextElementsSelectable_description: "Die Aktivierung kann in manchen Situationen die Benutzerfreundlichkeit beeinträchtigen."
useStickyIcons: "Icons beim Scrollen folgen lassen"
enableHighQualityImagePlaceholders: "Zeige Platzhalter für Bilder in hoher Qualität an"
uiAnimations: "Animationen der Benutzeroberfläche"
showNavbarSubButtons: "Unterschaltflächen in der Navigationsleiste anzeigen"
ifOn: "Wenn eingeschaltet"
ifOff: "Wenn ausgeschaltet"
enableSyncThemesBetweenDevices: "Synchronisierung von installierten Themen auf verschiedenen Endgeräten"
enablePullToRefresh: "Ziehen zum Aktualisieren"
enablePullToRefresh_description: "Bei Benutzung einer Maus, mit gedrücktem Mausrad ziehen"
realtimeMode_description: "Stellt eine Verbindung mit dem Server her und aktualisiert die Inhalte in Echtzeit. Kann zu mehr Datenverkehr einem höheren Akkuverbrauch führen."
contentsUpdateFrequency: "Häufigkeit des Abrufs von Inhalten"
contentsUpdateFrequency_description: "Je höher der Wert, desto häufiger werden die Inhalte aktualisiert, aber die Leistung sinkt und der Datenverkehr und der Akkuverbrauch steigen."
contentsUpdateFrequency_description2: "Wenn der Echtzeitmodus aktiviert ist, werden die Inhalte unabhängig von dieser Einstellung in Echtzeit aktualisiert."
showUrlPreview: "URL-Vorschau anzeigen"
showAvailableReactionsFirstInNote: "Zeige die verfügbaren Reaktionen im oberen Bereich an."
_chat:
showSenderName: "Name des Absenders anzeigen"
sendOnEnter: "Eingabetaste sendet Nachricht"
@@ -1604,6 +1635,20 @@ _serverSettings:
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
deliverSuspendedSoftware: "Software, die nicht mehr beliefert wird"
deliverSuspendedSoftwareDescription: "Sie können eine Auswahl von Namen und Versionen verschiedener Serversoftware angeben, um die Zustellung zu stoppen, z. B. aufgrund von Sicherheitslücken. Diese Versionsinformationen werden vom Server bereitgestellt und ihre Zuverlässigkeit ist nicht garantiert. Es wird jedoch empfohlen, eine Vorabversion anzugeben, wie z. B. >= 2024.3.1-0, da die Angabe >= 2024.3.1 keine benutzerdefinierten Versionen wie 2024.3.1-custom.0 einschließt."
singleUserMode: "Einzelbenutzermodus"
singleUserMode_description: "Wenn du der einzige Benutzer dieses Servers bist, optimiert die Aktivierung dieses Modus die Leistung des Servers."
signToActivityPubGet: "ActivityPub-GET-Anfragen signieren"
signToActivityPubGet_description: "Normalerweise sollte diese Option aktiviert sein. Die Deaktivierung kann Probleme im Zusammenhang mit der Föderation beheben, aber andererseits könnte sie die Föderation mit einigen anderen Servern deaktivieren."
proxyRemoteFiles: "Proxy für Dateien fremder Instanzen"
proxyRemoteFiles_description: "Wenn diese Einstellung aktiviert ist, werden fremde Dateien über einen Proxyserver übertragen und bereitgestellt. Dies hilft bei der Erstellung von Vorschaubildern und schützt die Privatsphäre der Benutzer."
allowExternalApRedirect: "Weiterleitungen für Anfragen über ActivityPub zulassen"
allowExternalApRedirect_description: "Wenn diese Option aktiviert ist, können andere Server Inhalte von Drittanbietern über diesen Server abfragen, was jedoch zu Content-Spoofing führen kann."
userGeneratedContentsVisibilityForVisitor: "Sichtbarkeit von nutzergenerierten Inhalten für Gäste"
userGeneratedContentsVisibilityForVisitor_description: "Dies ist nützlich, um zu verhindern, dass unangemessene Inhalte, die nicht gut moderiert sind, ungewollt über deinen eigenen Server im Internet veröffentlicht werden."
_userGeneratedContentsVisibilityForVisitor:
all: "Alles ist öffentlich"
localOnly: "Nur lokale Inhalte werden veröffentlicht, fremde Inhalte bleiben privat"
none: "Alles ist privat"
_accountMigration:
moveFrom: "Von einem anderen Konto zu diesem migrieren"
moveFromSub: "Alias für ein anderes Konto erstellen"
@@ -1944,6 +1989,9 @@ _role:
canImportMuting: "Importieren von Stummgeschalteten zulassen"
canImportUserLists: "Importieren von Listen erlauben"
chatAvailability: "Chatten erlauben"
uploadableFileTypes: "Hochladbare Dateitypen"
uploadableFileTypes_caption: "Gibt die zulässigen MIME-/Dateitypen an. Mehrere MIME-Typen können durch einen Zeilenumbruch getrennt angegeben werden, und Platzhalter können mit einem Sternchen (*) angegeben werden. (z. B. image/*)"
uploadableFileTypes_caption2: "Bei manchen Dateien ist es nicht möglich, den Typ zu bestimmen. Um solche Dateien zuzulassen, füge {x} der Spezifikation hinzu."
_condition:
roleAssignedTo: "Manuellen Rollen zugewiesen"
isLocal: "Lokaler Benutzer"
@@ -2796,6 +2844,8 @@ _dataSaver:
_avatar:
title: "Animierte Profilbilder deaktivieren"
description: "Die Animation von Profilbildern wird angehalten. Da animierte Bilder eine größere Dateigröße haben können als normale Bilder, kann dies den Datenverkehr weiter reduzieren."
_disableUrlPreview:
title: "URL-Vorschau deaktivieren"
_code:
title: "Code-Hervorhebungen ausblenden"
description: "Wenn Code-Hervorhebungen in MFM usw. verwendet werden, werden sie erst geladen, wenn sie angetippt werden. Die Syntaxhervorhebung erfordert das Herunterladen der Definitionsdateien für jede Programmiersprache. Es ist daher zu erwarten, dass die Deaktivierung des automatischen Ladens dieser Dateien die Menge des Datenverkehrs reduziert."
@@ -3001,3 +3051,85 @@ _search:
pleaseEnterServerHost: "Gib den Server-Host ein"
pleaseSelectUser: "Benutzer auswählen"
serverHostPlaceholder: "Beispiel: misskey.example.com"
_serverSetupWizard:
installCompleted: "Die Installation von Misskey ist abgeschlossen!"
firstCreateAccount: "Erstelle zunächst ein Administratorkonto."
accountCreated: "Ein Administratorkonto wurde angelegt!"
serverSetting: "Servereinstellungen"
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Mit diesem Assistenten lässt sich die optimale Serverkonfiguration leicht einrichten."
settingsYouMakeHereCanBeChangedLater: "Die Einstellungen hier können später geändert werden."
howWillYouUseMisskey: "Wie wirst du Misskey verwenden?"
_use:
single: "Ein-Personen-Server"
single_description: "Verwende den Server alleine als deinen eigenen."
single_youCanCreateMultipleAccounts: "Bei Bedarf können mehrere Konten eingerichtet werden, auch wenn es sich um einen Ein-Personen-Server handelt."
group: "Gruppenserver"
open: "Offener Server"
open_description: "Registrierung für alle öffnen."
howManyUsersDoYouExpect: "Mit wie vielen Benutzern rechnest du?"
_scale:
small: "Weniger als 100 (kleiner Maßstab)"
medium: "Mehr als 100 und weniger als 1000 Benutzer (mittelgroß)"
large: "Mehr als 1000 (großer Maßstab)"
largeScaleServerAdvice: "Für große Server sind unter Umständen fortgeschrittene Kenntnisse erforderlich, z. B. Lastverteilung und Datenbankreplikation."
doYouConnectToFediverse: "Mit dem Fediverse verbinden?"
doYouConnectToFediverse_description1: "Bei Anschluss an ein Netz von verteilten Servern (Fediverse) können Inhalte mit anderen Servern ausgetauscht werden."
doYouConnectToFediverse_description2: "Die Verbindung mit dem Fediverse wird auch als „Föderation“ bezeichnet."
youCanConfigureMoreFederationSettingsLater: "Erweiterte Einstellungen, wie z. B. die Angabe von föderierbaren Servern, können später vorgenommen werden."
adminInfo_mustBeFilled: "Dies ist auf einem offenen Server oder bei aktivierter Föderation erforderlich."
followingSettingsAreRecommended: "Die folgenden Einstellungen werden empfohlen"
applyTheseSettings: "Diese Einstellungen anwenden"
skipSettings: "Konfiguration überspringen"
settingsCompleted: "Einrichtung abgeschlossen!"
settingsCompleted_description: "Vielen Dank für deine Zeit. Jetzt, wo alles fertig ist, kannst du den Server sofort benutzen."
settingsCompleted_description2: "Detaillierte Servereinstellungen können über die „Systemsteuerung“ vorgenommen werden."
donationRequest: "Spendenaufruf"
_donationRequest:
text1: "Misskey ist eine freie Software, die von Freiwilligen entwickelt wird."
text2: "Wir würden uns über deine Unterstützung freuen, damit wir dieses Projekt auch in Zukunft weiterentwickeln können."
text3: "Für Unterstützer gibt es auch besondere Vorteile!"
_uploader:
compressedToX: "Komprimiert zu {x}"
savedXPercent: "{x}% gespart"
abortConfirm: "Einige Dateien wurden nicht hochgeladen. Möchtest du den Vorgang abbrechen?"
doneConfirm: "Einige Dateien wurden nicht hochgeladen. Möchtest du den Vorgang fortsetzen?"
maxFileSizeIsX: "Die maximale Dateigröße, die hochgeladen werden kann, beträgt {x}."
allowedTypes: "Hochladbare Dateitypen"
tip: "Die Datei ist noch nicht hochgeladen worden. In diesem Dialog kannst du die Datei vor dem Hochladen anzeigen, umbenennen, komprimieren und zuschneiden. Wenn du fertig bist, klicke auf „Hochladen“, um den Upload zu starten."
_clientPerformanceIssueTip:
makeSureDisabledAdBlocker: "Deaktiviere deinen Adblocker"
makeSureDisabledAdBlocker_description: "Adblocker können die Leistung beeinträchtigen; vergewissere dich, ob in deinem Betriebssystem, Browser oder deinen Add-ons Adblocker aktiviert sind."
makeSureDisabledCustomCss: "Benutzerdefiniertes CSS deaktivieren"
makeSureDisabledCustomCss_description: "Das Überschreiben von Stilen kann die Leistung beeinträchtigen. Stelle daher sicher, dass du kein benutzerdefiniertes CSS oder Erweiterungen aktiviert hast, die Stile überschreiben."
makeSureDisabledAddons: "Erweiterungen deaktivieren"
makeSureDisabledAddons_description: "Einige Erweiterungen können das Verhalten des Clients stören und die Leistung beeinträchtigen. Deaktiviere die Browser-Erweiterungen und prüfe, ob sich die Situation dadurch verbessert."
_clip:
tip: "Clips sind eine Funktion, mit der du Notizen gruppieren kannst."
_userLists:
tip: "Es können Listen mit beliebigen Benutzern erstellt werden. Die erstellte Liste kann als eigene Chronik angezeigt werden."
watermark: "Wasserzeichen"
defaultPreset: "Standard-Voreinstellungen"
_watermarkEditor:
quitWithoutSaveConfirm: "Nicht gespeicherte Änderungen verwerfen?"
driveFileTypeWarn: "Diese Datei wird nicht unterstützt"
driveFileTypeWarnDescription: "Bilddatei auswählen"
title: "Wasserzeichen bearbeiten"
opacity: "Transparenz"
scale: "Größe"
text: "Text"
position: "Position"
type: "Art"
image: "Bilder"
advanced: "Fortgeschritten"
angle: "Winkel"
_imageEffector:
title: "Effekte"
addEffect: "Effekte hinzufügen"
discardChangesConfirm: "Änderungen verwerfen und beenden?"
_fxs:
chromaticAberration: "Chromatische Abweichung"
glitch: "Glitch"
mirror: "Spiegeln"
invert: "Farben umkehren"
grayscale: "Schwarzweiß"
colorAdjust: "Farbkorrektur"
+2
View File
@@ -403,3 +403,5 @@ _reversi:
total: "Σύνολο"
_search:
searchScopeLocal: "Τοπικό"
_watermarkEditor:
image: "Εικόνες"
+56 -1
View File
@@ -1330,6 +1330,7 @@ restore: "Restore"
syncBetweenDevices: "Sync between devices"
preferenceSyncConflictTitle: "The configured value exists on the server."
preferenceSyncConflictText: "The sync enabled settings will save their values to the server. However, there are existing values on the server. Which set of values would you like to overwrite?"
preferenceSyncConflictChoiceMerge: "Merge"
preferenceSyncConflictChoiceServer: "Configured value on server"
preferenceSyncConflictChoiceDevice: "Configured value on device"
preferenceSyncConflictChoiceCancel: "Cancel enabling sync"
@@ -1364,6 +1365,8 @@ abort: "Abort"
tip: "Tips & Tricks"
redisplayAllTips: "Show all “Tips & Tricks” again"
hideAllTips: "Hide all \"Tips & Tricks\""
defaultImageCompressionLevel: "Default image compression level"
defaultImageCompressionLevel_description: "High, reduces the file size but also the image quality. <br>High, reduces the file size but also the image quality."
_chat:
noMessagesYet: "No messages yet"
newMessage: "New message"
@@ -1451,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "The higher the value the more the content updates but it lowers the performance and increases the traffic and memory consumption."
contentsUpdateFrequency_description2: "When real-time mode is on, content is updated in real time regardless of this setting."
showUrlPreview: "Show URL preview"
showAvailableReactionsFirstInNote: "Show available reactions at the top."
_chat:
showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send"
@@ -2461,6 +2465,8 @@ _visibility:
disableFederation: "Defederate"
disableFederationDescription: "Don't transmit to other instances"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "There are files that have not been uploaded, do you want to discard them and close the form?"
uploaderTip: "The file has not yet been uploaded. From the file menu, you can rename, crop images, watermark and compress or uncompress the file. Files are automatically uploaded when you publish a note."
replyPlaceholder: "Reply to this note..."
quotePlaceholder: "Quote this note..."
channelPlaceholder: "Post to a channel..."
@@ -2967,7 +2973,7 @@ _customEmojisManager:
markAsDeleteTargetRanges: "Mark rows in the selection as a target to delete"
alertUpdateEmojisNothingDescription: "There are no updated Emojis."
alertDeleteEmojisNothingDescription: "There are no Emojis to be deleted."
confirmMovePage: ""
confirmMovePage: "Would you like to move pages?"
confirmChangeView: ""
confirmUpdateEmojisDescription: "Update {count} Emoji(s). Are you sure to continue?"
confirmDeleteEmojisDescription: "Delete checked {count} Emoji(s). Are you sure to continue?"
@@ -3116,3 +3122,52 @@ _clip:
tip: "Clip is a feature that allows you to organize your notes."
_userLists:
tip: "Lists can contain any user you specify when creating, the created list can then be displayed as a timeline showing only the specified users."
watermark: "Watermark"
defaultPreset: "Default Preset"
_watermarkEditor:
tip: "A watermark, such as credit information, can be added to the image."
quitWithoutSaveConfirm: "Discard unsaved changes?"
driveFileTypeWarn: "This file is not supported"
driveFileTypeWarnDescription: "Choose an image file"
title: "Edit Watermark"
cover: "Cover everything"
repeat: "spread all over"
opacity: "Opacity"
scale: "Size"
text: "Text"
position: "Position"
type: "Type"
image: "Images"
advanced: "Advanced"
stripe: "Stripes"
stripeWidth: "Line width"
stripeFrequency: "Lines count"
angle: "Angle"
polkadot: "Polkadot"
checker: "Checker"
polkadotMainDotOpacity: "Opacity of the main dot"
polkadotMainDotRadius: "Size of the main dot"
polkadotSubDotOpacity: "Opacity of the secondary dot"
polkadotSubDotRadius: "Size of the secondary dot"
polkadotSubDotDivisions: "Number of sub-dots."
_imageEffector:
title: "Effects"
addEffect: "Add Effects"
discardChangesConfirm: "Are you sure you want to leave? You have unsaved changes."
_fxs:
chromaticAberration: "Chromatic Aberration"
glitch: "Glitch"
mirror: "Mirror"
invert: "Invert Colors"
grayscale: "white-black"
colorAdjust: "Colour Correction"
colorClamp: "Color Compression"
colorClampAdvanced: "Color Compression (Advanced)"
distort: "Distortion"
threshold: "Binarize"
zoomLines: "Saturated lines"
stripe: "Stripes"
polkadot: "Polkadot"
checker: "Checker"
blockNoise: "Block Noise"
tearing: "Tearing"
+375
View File
@@ -1330,6 +1330,7 @@ restore: "Restaurar"
syncBetweenDevices: "Sincronizar entre dispositivos"
preferenceSyncConflictTitle: "Los valores configurados existen en el servidor."
preferenceSyncConflictText: "Los ajustes de sincronización activados guardarán sus valores en el servidor. Sin embargo, hay valores existentes en el servidor. ¿Qué conjunto de valores desea sobrescribir?"
preferenceSyncConflictChoiceMerge: "Fusionar"
preferenceSyncConflictChoiceServer: "Valores de configuración del servidor"
preferenceSyncConflictChoiceDevice: "Valor configurado en el dispositivo"
preferenceSyncConflictChoiceCancel: "Cancelar la activación de la sincronización"
@@ -1364,6 +1365,8 @@ abort: "Abortar"
tip: "Consejos y trucos"
redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\""
hideAllTips: "Ocultar todos los \"Trucos y consejos\""
defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto"
defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande. <br>Alta, reduce la medida del archivo pero también la calidad de la imagen."
_chat:
noMessagesYet: "Aún no hay mensajes"
newMessage: "Mensajes nuevos"
@@ -1451,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "Cuanto mayor sea el valor, más se actualiza el contenido, pero disminuye el rendimiento y aumenta el tráfico y el consumo de memoria."
contentsUpdateFrequency_description2: "Cuando el modo en tiempo real está activado, el contenido se actualiza en tiempo real independientemente de esta configuración."
showUrlPreview: "Mostrar la vista previa de la URL"
showAvailableReactionsFirstInNote: "Mostrar las reacciones disponibles en la parte superior."
_chat:
showSenderName: "Mostrar el nombre del remitente"
sendOnEnter: "Intro para enviar"
@@ -1626,6 +1630,26 @@ _serverSettings:
reactionsBufferingDescription: "Cuando se activa, el rendimiento durante la creación de reacciones mejorará considerablemente, reduciendo la carga de la base de datos. Sin embargo, aumentará el uso de memoria de Redis."
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Especifica una URL para el formulario de consulta al responsable del servidor o una página web para la información de contacto."
openRegistration: "Registros Abiertos"
openRegistrationWarning: "Abrir registros conlleva riesgos. Se recomienda solo habilitarlos si tienes un sistema en el cual puedes monitorear continuamente el servidor y respondes inmediatamente en caso de que haya cualquier problema."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no se ha detectado por un tiempo actividad de un moderador, este ajuste será automáticamente desactivado para prevenir el spam. "
deliverSuspendedSoftware: "Software suspendido."
deliverSuspendedSoftwareDescription: "Puede especificar un rango de nombres y versiones del software del servidor para detener la entrega, por ejemplo, debido a vulnerabilidades. Esta información sobre la versión la proporciona el servidor y su fiabilidad no está garantizada. Se puede utilizar una especificación de rango para especificar una versión, pero se recomienda especificar una versión previa, como >= 2024.3.1-0, ya que especificar >= 2024.3.1 no incluirá versiones personalizadas como 2024.3.1-custom.0."
singleUserMode: "Modo de usuario único"
singleUserMode_description: "Si eres el único usuario de este servidor, activar este modo optimizará su rendimiento."
signToActivityPubGet: "Firmar solicitudes GET de Activitypub."
signToActivityPubGet_description: "Normalmente, debería estar activada. Deshabilitarlo puede mejorar los problemas relacionados con la federación, pero por otro lado podría deshabilitar la federación hacia otros servidores."
proxyRemoteFiles: "Proxy de archivos remotos"
proxyRemoteFiles_description: "Cuando se activa, el servidor proxy sirve archivos remotos. Esto es útil para generar miniaturas de imágenes y proteger la privacidad del usuario."
allowExternalApRedirect: "Permitir redirecciones para consultas vía ActivityPub"
allowExternalApRedirect_description: "Si se activa, otros servidores pueden consultar contenidos de terceros a través de este servidor, pero esto puede dar lugar a la suplantación de contenidos."
userGeneratedContentsVisibilityForVisitor: "Visibilidad de contenido generado por un usuario a invitados"
userGeneratedContentsVisibilityForVisitor_description: "Esto es útil para evitar problemas causados por contenidos remotos inapropiados que no estén bien moderados y que se publiquen involuntariamente en Internet a través de su propio servidor."
userGeneratedContentsVisibilityForVisitor_description2: "Publicar incondicionalmente todo el contenido del servidor en Internet, incluido el contenido remoto recibido por el servidor, es arriesgado. Esto es especialmente importante para los invitados que desconocen la naturaleza distribuida del contenido, ya que pueden creer erróneamente que incluso el contenido remoto es contenido creado por usuarios en el servidor."
_userGeneratedContentsVisibilityForVisitor:
all: "Todo es público."
localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado"
none: "Todo es privado"
_accountMigration:
moveFrom: "Trasladar de otra cuenta a ésta"
moveFromSub: "Crear un alias para otra cuenta."
@@ -1922,6 +1946,8 @@ _role:
descriptionOfIsExplorable: "La línea de tiempo de éste rol y la lista de usuarios serán públicos si se activa.."
displayOrder: "Posición"
descriptionOfDisplayOrder: "Entre más alto el número, mayor es la posición en la interfaz."
preserveAssignmentOnMoveAccount: "Preservar los roles asignados durante la migración"
preserveAssignmentOnMoveAccount_description: "Si está activada, este rol se transferirá a la cuenta de destino cuando se migre una cuenta con este rol."
canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
priority: "Prioridad"
@@ -1941,7 +1967,9 @@ _role:
canManageCustomEmojis: "Administrar emojis personalizados"
canManageAvatarDecorations: "Administrar decoraciones de avatar"
driveCapacity: "Capacidad del drive"
maxFileSize: "Tamaño máximo de archivo que se puede cargar."
alwaysMarkNsfw: "Siempre marcar archivos como NSFW"
canUpdateBioMedia: "Puede editar un icono o una imagen de fondo (banner)"
pinMax: "Máximo de notas fijadas"
antennaMax: "Máximo de antenas"
wordMuteMax: "Máximo de caracteres en palabras silenciadas"
@@ -1956,6 +1984,15 @@ _role:
canSearchNotes: "Uso de la búsqueda de notas"
canUseTranslator: "Uso de traductor"
avatarDecorationLimit: "Número máximo de decoraciones de avatar"
canImportAntennas: "Permitir la importación de antenas"
canImportBlocking: "Permitir la importación de bloqueos"
canImportFollowing: "Permitir la importación de seguidos"
canImportMuting: "Permitir la importación de silenciados"
canImportUserLists: "Permitir la importación de listas"
chatAvailability: "Permitir Chats"
uploadableFileTypes: "Tipos de archivos que se pueden cargar."
uploadableFileTypes_caption: "Especifica los tipos MIME/archivos permitidos. Se pueden especificar varios tipos MIME separándolos con una nueva línea, y se pueden especificar comodines con un asterisco (*). (por ejemplo, image/*)"
uploadableFileTypes_caption2: "Es posible que no se detecten algunos tipos de archivos. Para permitir estos archivos, añade {x} a la especificación."
_condition:
roleAssignedTo: "Asignado a roles manuales"
isLocal: "Usuario local"
@@ -1964,6 +2001,7 @@ _role:
isBot: "Usuarios Bot"
isSuspended: "Usuario suspendido"
isLocked: "Cuentas privadas"
isExplorable: "Hacer que la cuenta sea visible en las búsquedas"
createdLessThan: "Menos de X han pasado desde la creación de la cuenta"
createdMoreThan: "Más de X han pasado desde la creación de la cuenta"
followersLessThanOrEq: "Tiene X o menos seguidores"
@@ -2118,6 +2156,7 @@ _theme:
installed: "{name} ha sido instalado"
installedThemes: "Temas instalados"
builtinThemes: "Temas integrados"
instanceTheme: "Tema del servidor (o también denominado: tema de la instancia)"
alreadyInstalled: "Este tema ya está instalado"
invalid: "El formato del tema no es válido"
make: "Crear tema"
@@ -2179,6 +2218,7 @@ _sfx:
noteMy: "Nota (a mí mismo)"
notification: "Notificaciones"
reaction: "Al seleccionar una reacción"
chatMessage: "Mensajes del Chat"
_soundSettings:
driveFile: "Usar un archivo de audio en Drive"
driveFileWarn: "Selecciona un archivo de audio en Drive."
@@ -2186,6 +2226,7 @@ _soundSettings:
driveFileTypeWarnDescription: "Selecciona un archivo de audio"
driveFileDurationWarn: "La duración del audio es demasiado larga."
driveFileDurationWarnDescription: "Usar un audio de larga duración puede llegar a molestar mientras usas Misskey. ¿Quieres continuar?"
driveFileError: "No puedo cargar el sonido. Por favor cambia la configuración."
_ago:
future: "Futuro"
justNow: "Justo ahora"
@@ -2325,6 +2366,7 @@ _permissions:
"read:federation": "Ver instancias federadas"
"write:report-abuse": "Crear reportes de usuario"
"write:chat": "Administrar chat"
"read:chat": "Explorar Chats"
_auth:
shareAccessTitle: "Permisos de la aplicación"
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
@@ -2333,8 +2375,11 @@ _auth:
permissionAsk: "Esta aplicación requiere los siguientes permisos"
pleaseGoBack: "Por favor, vuelve a la aplicación"
callback: "Volviendo a la aplicación"
accepted: "Acceso concedido."
denied: "Acceso denegado"
scopeUser: "Operar como el siguiente usuario"
pleaseLogin: "Se requiere un inicio de sesión para darle permisos a la aplicación"
byClickingYouWillBeRedirectedToThisUrl: "Cuando el acceso es concedido, serás automáticamente redireccionado a la siguiente URL"
_antennaSources:
all: "Todas las notas"
homeTimeline: "Notas de los usuarios que sigues"
@@ -2420,6 +2465,8 @@ _visibility:
disableFederation: "No federado"
disableFederationDescription: "No enviar a otras instancias"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "Hay archivos que no se han cargado, ¿deseas descartarlos y cerrar el formulario?"
uploaderTip: "El archivo aún no se ha cargado. Desde el menú Archivo, puedes cambiar el nombre, recortar imágenes, poner marcas de agua y comprimir o no el archivo. Los archivos se cargan automáticamente al publicar una nota."
replyPlaceholder: "Responder a esta nota"
quotePlaceholder: "Citar esta nota"
channelPlaceholder: "Publicar en el canal"
@@ -2444,6 +2491,9 @@ _profile:
changeBanner: "Cambiar banner"
verifiedLinkDescription: "Introduciendo una URL que contiene un enlace a tu perfil, se puede mostrar un icono de verificación de propiedad al lado del campo."
avatarDecorationMax: "Puedes añadir un máximo de {max} decoraciones de avatar."
followedMessage: "Mensaje cuando te han seguido"
followedMessageDescription: "Puedes establecer un mensaje de bienvenida para nuevos seguidores."
followedMessageDescriptionForLockedAccount: "Si apruebas manualmente seguidores, el mensaje se mostrará al seguidor en el momento de la aprobación."
_exportOrImport:
allNotes: "Todas las notas"
favoritedNotes: "Notas favoritas"
@@ -2533,6 +2583,7 @@ _pages:
eyeCatchingImageSet: "Elegir imagen llamativa"
eyeCatchingImageRemove: "Borrar imagen llamativa"
chooseBlock: "Agregar bloque"
enterSectionTitle: "Escribe el título de la sección"
selectType: "Elegir tipo"
contentBlocks: "Contenido"
inputBlocks: "Entrada"
@@ -2567,6 +2618,7 @@ _notification:
newNote: "Nueva nota"
unreadAntennaNote: "Antena {name}"
roleAssigned: "Rol asignado"
chatRoomInvitationReceived: "Invitado a la sala de chat."
emptyPushNotificationMessage: "Se han actualizado las notificaciones push"
achievementEarned: "Logro desbloqueado"
testNotification: "Notificación de prueba"
@@ -2574,8 +2626,13 @@ _notification:
sendTestNotification: "Enviar notificación de prueba"
notificationWillBeDisplayedLikeThis: "Las notificaciones tendrán este aspecto"
reactedBySomeUsers: "{n} usuarios han reaccionado"
likedBySomeUsers: "{n} usuarios les gustó tu nota"
renotedBySomeUsers: "{n} usuarios han renotado"
followedBySomeUsers: "Seguido por {n} usuarios"
flushNotification: "Limpiar notificaciones"
exportOfXCompleted: "La exportación de {x} ha sido completada."
login: "Alguien ha iniciado sesión"
createToken: "Token de acceso creado"
createTokenDescription: "Si no tienes ni idea, elimina el token de acceso a través de \"{text}\"."
_types:
all: "Todo"
@@ -2673,10 +2730,18 @@ _webhookSettings:
_abuseReport:
_notificationRecipient:
createRecipient: "Añadir destinatario a los informes"
modifyRecipient: "Editar un destinatario en el informe de moderación\n"
recipientType: "Tipo de notificación"
_recipientType:
mail: "Correo"
webhook: "Webhook"
_captions:
mail: "Enviar un correo electrónico a todos los moderadores cuando reciban un informe de moderación"
webhook: "Enviar una notificación al SystemWebhook cuando se reciba o se resuelva un informe de moderación"
keywords: "Palabras Clave"
notifiedUser: "Usuarios a notificar"
notifiedWebhook: "Webhook a utilizar"
deleteConfirm: "¿Estás seguro de que deseas borrar el destinatario del informe de moderación?"
_moderationLogTypes:
createRole: "Rol creado"
deleteRole: "Rol eliminado"
@@ -2701,9 +2766,12 @@ _moderationLogTypes:
resetPassword: "Resetear contraseña"
suspendRemoteInstance: "Instancia remota suspendida"
unsuspendRemoteInstance: "Suspensión de instancia remota retirada"
updateRemoteInstanceNote: "Nota de moderación de una instancia remota actualizada"
markSensitiveDriveFile: "Archivo marcado como sensible"
unmarkSensitiveDriveFile: "Archivo marcado como no sensible"
resolveAbuseReport: "Reporte resuelto"
forwardAbuseReport: "Informe reenviado"
updateAbuseReportNote: "Nota de moderación de un informe actualizada"
createInvitation: "Generar invitación"
createAd: "Anuncio creado"
deleteAd: "Anuncio eliminado"
@@ -2713,6 +2781,18 @@ _moderationLogTypes:
deleteAvatarDecoration: "Decoración de avatar eliminada"
unsetUserAvatar: "Quitar decoración de avatar de este usuario"
unsetUserBanner: "Quitar banner de este usuario"
createSystemWebhook: "Crear un SystemWebhook"
updateSystemWebhook: "Actualizar SystemWebhook "
deleteSystemWebhook: "Borrar SystemWebHook"
createAbuseReportNotificationRecipient: "Crear un destinatario para el informe de moderación"
updateAbuseReportNotificationRecipient: "Destinatario de los informes actualizados"
deleteAbuseReportNotificationRecipient: "Destinatario de los informes borrado"
deleteAccount: "Cuenta Borrada"
deletePage: "Página borrada"
deleteFlash: "Juego borrado"
deleteGalleryPost: "Publicación de la galería, eliminada"
deleteChatRoom: "Borrar sala del chat"
updateProxyAccountDescription: "Actualizar la descripción de la cuenta proxy"
_fileViewer:
title: "Detalles del archivo"
type: "Tipo de archivo"
@@ -2767,32 +2847,327 @@ _dataSaver:
_avatar:
title: "Avatares animados"
description: "Desactiva la animación de los avatares. Las imágenes animadas pueden llegar a ser de mayor tamaño que las normales, por lo que al desactivarlas puedes reducir el consumo de datos."
_urlPreviewThumbnail:
title: "Ocultar las miniaturas de las vistas previas de URL"
description: "Las imágenes en miniatura de la vista previa de URL no se pueden cargar "
_disableUrlPreview:
title: "Desactivar la vista previa de las URL"
description: "Desactiva la función de previsualización de la URL. A diferencia de solo las imágenes en miniatura, esta función reduce la carga de la propia información vinculada."
_code:
title: "Resaltar código"
description: "Si se usa resaltado de código en MFM, etc., no se cargará hasta pulsar en ello. El resaltado de sintaxis requiere la descarga de archivos de definición para cada lenguaje de programación. Debido a esto, al deshabilitar la carga automática de estos archivos reducirás el consumo de datos."
_hemisphere:
N: "Hemisferio norte"
S: "Hemisferio sur"
caption: "Usado en algunos clientes para determinar la estación del año"
_reversi:
reversi: "Reversi"
gameSettings: "Configuración del juego"
chooseBoard: "Elegir tablero"
blackOrWhite: "Negras/Blancas"
blackIs: "{name} juega con negras"
rules: "Reglas"
thisGameIsStartedSoon: "El juego comenzará en breve"
waitingForOther: "Esperando el turno del adversario"
waitingForMe: "Esperando tu turno"
waitingBoth: "Prepárate"
ready: "Listo"
cancelReady: "No estoy listo"
opponentTurn: "Turno del oponente"
myTurn: "¡Tu turno!"
turnOf: "Le toca a {name}"
pastTurnOf: "Turno de {name}"
surrender: "Rendirse"
surrendered: "Te has rendido"
timeout: "Se acabó el tiempo"
drawn: "Empate"
won: "{name} ha ganado"
black: "Negras"
white: "Blancas"
total: "Total"
turnCount: "Turno {count}"
myGames: "Mis rondas"
allGames: "Todos los juegos"
ended: "Finalizado"
playing: "Jugando actualmente"
isLlotheo: "El que tenga menos fichas gana (LLoTheO)"
loopedMap: "Mapa en bucle"
canPutEverywhere: "Las fichas se pueden poner a cualquier lugar\n"
timeLimitForEachTurn: "Tiempo límite por jugada."
freeMatch: "Partida libre."
lookingForPlayer: "Buscando oponente"
gameCanceled: "La partida ha sido cancelada."
shareToTlTheGameWhenStart: "Compartir la partida en la línea de tiempo cuando comience "
iStartedAGame: "¡La partida ha comenzado!"
opponentHasSettingsChanged: "El oponente ha cambiado su configuración"
allowIrregularRules: "Reglas irregulares (completamente libre)"
disallowIrregularRules: "Sin reglas irregulares "
showBoardLabels: "Mostrar el número de línea y de columna en el tablero de juego."
useAvatarAsStone: "Usar los avatares de los usuarios como fichas\n"
_offlineScreen:
title: "Fuera de línea. No se puede conectar con el servidor"
header: "Incapaz de conectar con el servidor"
_urlPreviewSetting:
title: "Configuración para la previsualización de la URL"
enable: "Activar la vista previa de URL"
allowRedirect: "Permitir la redirección de la visualización previa"
allowRedirectDescription: "Si una URL tiene una redirección establecida, puede activar esta función para seguir la redirección y mostrar una vista previa del contenido redirigido. Si se desactiva, se ahorrarán recursos del servidor, pero no se mostrará el contenido redirigido."
timeout: "Timeout de la carga de vista previa de las URLs (ms)"
timeoutDescription: "Si se tarda más de este valor en obtener la vista previa, ésta no se generará."
maximumContentLength: "Content-Length Máximo (bytes)"
maximumContentLengthDescription: "Si Content-Length es superior a este valor, no se generará la vista previa."
requireContentLength: "Genere la vista previa sólo si puede obtener Content-Length"
requireContentLengthDescription: "Si el otro servidor no devuelve Content-Length, no se generará la vista previa."
userAgent: "User-Agent"
userAgentDescription: "Establece el User-Agent que se utilizará al recuperar vistas previas. Si se deja en blanco, se utilizará el User-Agent por defecto."
summaryProxy: "Proxy endpoints para generar vistas previas"
summaryProxyDescription: "La vista previa se genera usando Summaly proxy, no la genera el mismo Misskey."
summaryProxyDescription2: "Los siguientes parámetros se vinculan al proxy como cadena de consulta (query string). Si el proxy no los admite, los valores se ignoran."
_mediaControls:
pip: "Picture in Picture"
playbackRate: "Velocidad de reproducción"
loop: "Reproducción en bucle"
_contextMenu:
title: "Menú contextual"
app: "Aplicación"
appWithShift: "Aplicación con la tecla shift"
native: "Interfaz nativa (del navegador web)"
_gridComponent:
_error:
requiredValue: "Este valor es obligatorio"
columnTypeNotSupport: "La validación con expresión regular sólo se admite para columnas de tipo:texto."
patternNotMatch: "Este valor no coincide con el patrón en {pattern}"
notUnique: "Este valor debe ser único"
_roleSelectDialog:
notSelected: "No seleccionado"
_customEmojisManager:
_gridCommon:
copySelectionRows: "Copiar filas seleccionadas"
copySelectionRanges: "Copiar selección"
deleteSelectionRows: "Borrar las líneas seleccionadas"
deleteSelectionRanges: "Borrar las filas de la selección"
searchSettings: "Ajustes de búsqueda"
searchSettingCaption: "Establecer criterios de búsqueda detallados."
searchLimit: "Límite de resultados"
sortOrder: "Ordenar"
registrationLogs: "Log de registros "
registrationLogsCaption: "Los registros se mostrarán al actualizar o borrar Emojis. Desaparecerán después de actualizarlos o eliminarlos, pasar a una nueva página o recargar."
alertEmojisRegisterFailedDescription: "No se ha podido actualizar o borrar el emoji. Por favor comprueba el log del registro para más detalles."
_logs:
showSuccessLogSwitch: "Mostrar registro de éxito"
failureLogNothing: "No hay log de fallos"
logNothing: "No hay logs"
_remote:
selectionRowDetail: "Detalle de la línea seleccionada"
importSelectionRows: "Importar las líneas seleccionadas"
importSelectionRangesRows: "Importar las filas seleccionadas"
importEmojisButton: "Importar los Emojis marcados"
confirmImportEmojisTitle: "Importar Emojis"
confirmImportEmojisDescription: "Importar {count} Emoji(s) recibidos del servidor remoto. Por favor, presta mucha atención a la licencia del Emoji. ¿Estás seguro de continuar?"
_local:
tabTitleList: "Lista de emojis registrados"
tabTitleRegister: "Registro de Emojis"
_list:
emojisNothing: "No hay Emojis registrados"
markAsDeleteTargetRows: "Marcar las filas seleccionadas como objetivo a eliminar"
markAsDeleteTargetRanges: "Selección de filas para su eliminación"
alertUpdateEmojisNothingDescription: "No hay Emojis actualizados"
alertDeleteEmojisNothingDescription: "No hay Emojis para borrar"
confirmMovePage: "¿Quieres cambiar de página?"
confirmChangeView: "¿De verdad quieres cambiar la vista?"
confirmUpdateEmojisDescription: "Actualizar {count} Emoji(s). ¿Deseas continuar?"
confirmDeleteEmojisDescription: "Borrar {count} Emoji(s) seleccionados. ¿Deseas continuar?"
confirmResetDescription: "Se restablecerán todos los cambios hechos hasta ahora."
confirmMovePageDesciption: "Se han realizado cambios en los Emojis de esta página.\nSi abandonas la página sin guardar, se descartarán todos los cambios realizados en esta página."
dialogSelectRoleTitle: "Buscar Emojis por rol"
_register:
uploadSettingTitle: "Ajustes de carga"
uploadSettingDescription: "En esta pantalla, puedes configurar el comportamiento al cargar Emojis."
directoryToCategoryLabel: "Introduce el nombre del directorio en el campo \"categoría\""
directoryToCategoryCaption: "Cuando arrastres y sueltes un directorio, introduce el nombre del directorio en el campo \"categoría\"."
confirmRegisterEmojisDescription: "Registra los Emojis de la lista como nuevos Emojis personalizados. ¿Estás seguro de continuar? (Para evitar sobrecargas, sólo {count} Emoji(s) en una sola operación)"
confirmClearEmojisDescription: "Descartar las ediciones y borrar los Emojis de la lista. ¿Estás seguro de continuar?"
confirmUploadEmojisDescription: "Cargar los {count} archivo(s) arrastrado(s) y soltado(s) en la unidad. ¿Estás seguro de continuar?"
_embedCodeGen:
title: "Personalizar el código de incrustación"
header: "Mostrar encabezados"
autoload: "Cargar más automáticamente (no recomendado)"
maxHeight: "Altura máxima"
maxHeightDescription: "0 desactiva el ajuste del valor máximo. Para evitar que el widget siga creciendo verticalmente, especifica algún valor."
maxHeightWarn: "El límite de altura máxima está desactivado (0). Si esto no estaba previsto, establece la altura máxima en algún valor."
previewIsNotActual: "La visualización difiere de la incrustación real porque excede el rango mostrado en la pantalla de vista previa."
rounded: "Bordes Redondeados"
border: "Añadir un borde al marco exterior"
applyToPreview: "Aplicar a la vista previa"
generateCode: "Crear el código para incrustar"
codeGenerated: "El código ha sido generado"
codeGeneratedDescription: "Pegue el código generado en su sitio web para incrustar el contenido."
_selfXssPrevention:
warning: "Advertencia"
title: "\"Pegar algo en esta pantalla\" es un timo."
description1: "Si pegas algo aquí, un usuario malintencionado podría secuestrar tu cuenta o robar tu información personal."
description2: "Si no entiendes que estás pegando exactamente, %cdetente ahora mismo y cierra esta ventana"
description3: "Para más información visita esto {link}"
_followRequest:
recieved: "Petición de seguimiento recibida"
sent: "Petición de seguimiento enviada"
_remoteLookupErrors:
_federationNotAllowed:
title: "Incapaz de comunicarse con este servidor."
description: "Es posible que se haya desactivado la comunicación con este servidor o que haya sido bloqueado.\nPonte en contacto con el administrador del servidor.."
_uriInvalid:
title: "La URI es inválida"
description: "Ha habido un problema con la dirección introducida. Comprueba que no hayas escrito caracteres que no pueden ser usados en la URI"
_requestFailed:
title: "Solicitud fallida."
description: "Ha fallado la comunicación con este servidor. Es posible que el servidor no funcione. Asegúrese también de que no ha introducido un URI no válido o inexistente."
_responseInvalid:
title: "La respuesta no es válida"
description: "Has podido comunicarte con este servidor, pero los datos obtenidos eran incorrectos. Si estás consultando contenidos remotos a través de un servidor de terceros, vuelve a realizar la consulta utilizando un URI que pueda obtenerse del servidor de origen."
_noSuchObject:
title: "No se encuentra"
description: "No se ha encontrado el recurso solicitado, por favor, vuelve a comprobar el URI."
_captcha:
verify: "Por favor verifica el CAPTCHA"
testSiteKeyMessage: "Puedes comprobar la vista previa introduciendo los valores de prueba para el sitio y las claves secretas.\nPara más detalles, consulta la página siguiente.\n"
_error:
_requestFailed:
title: "Ha fallado la solicitud del CAPTCHA"
text: "Por favor, ejecútalo después de un rato o comprueba los ajustes de nuevo."
_verificationFailed:
title: "Ha fallado la validación del CAPTCHA"
text: "Comprueba que los ajustes son los correctos."
_unknown:
title: "Error en el CAPTCHA."
text: "Se ha producido un error inesperado."
_bootErrors:
title: "Fallo al cargar"
serverError: "Si el problema persiste después de esperar un momento y volver a cargar, póngase en contacto con el administrador del servidor con el siguiente ID de error."
solution: "Lo siguiente puede resolver el problema."
solution1: "Actualiza tu navegador web y el sistema operativo a la última versión"
solution2: "Desactiva el AdBlocker"
solution3: "Borra la memoria caché del navegador web "
solution4: "(Navegador Tor) configura dom.webaudio.enabled a true"
otherOption: "Otras opciones"
otherOption1: "Borra la configuración y la memoria caché del cliente"
otherOption2: "Iniciar el cliente simple"
otherOption3: "Iniciar la herramienta de reparación"
_search:
searchScopeAll: "Todo"
searchScopeLocal: "Local"
searchScopeServer: "Especifica el servidor (Instancia)"
searchScopeUser: "Especificar usuario"
pleaseEnterServerHost: "Introduce la dirección del servidor/Instancia"
pleaseSelectUser: "Selecciona un usuario, por favor"
serverHostPlaceholder: "Ejemplo: misskey.example.com"
_serverSetupWizard:
installCompleted: "¡La instalación de Misskey se ha completado!"
firstCreateAccount: "Para comenzar, crea una cuenta de administrador"
accountCreated: "¡La cuenta de administrador se ha creado! "
serverSetting: "Configuración del servidor"
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "Este asistente te facilita una configuración óptima del servidor."
settingsYouMakeHereCanBeChangedLater: "Los ajustes que han sido cambiados a través de este asistente pueden ser modificados más tarde."
howWillYouUseMisskey: "¿Cómo vas a usar Misskey?"
_use:
single: "Servidor para un único usuario."
single_description: "Utilízalo como tu propio servidor dedicado."
single_youCanCreateMultipleAccounts: "Se pueden crear múltiples cuentas según sea necesario, incluso cuando se opera como servidor unipersonal."
group: "Servidor de grupo"
group_description: "Invita otros usuarios de confianza y úsalo con más de una persona.\n"
open: "Servidor público"
open_description: "Permite a cualquiera registrarse"
openServerAdvice: "Aceptar un número no determinado de usuarios comporta algunos riesgos. Se recomienda operar con un sistema de moderación fiable para hacer frente a los problemas."
openServerAntiSpamAdvice: "Para evitar que su servidor se convierta en un trampolín para el spam, también debe prestar mucha atención a la seguridad habilitando funciones anti-bot como reCAPTCHA."
howManyUsersDoYouExpect: "¿Cuántas personas esperas?"
_scale:
small: "Menos de 100 (escala pequeña)"
medium: "Más de 100 y menos de 1000 (escala media)\n"
large: "Más de 1000(escala grande)"
largeScaleServerAdvice: "Los grandes servidores pueden requerir conocimientos avanzados de infraestructura, como equilibrio de carga y replicación de bases de datos."
doYouConnectToFediverse: "¿Quieres conectarte al Fediverso?"
doYouConnectToFediverse_description1: "Cuando se conecta a una red de servidores distribuidos (Fediverso), el contenido puede intercambiarse con otros servidores."
doYouConnectToFediverse_description2: "Conectarse con el Fediverso también se conoce como \"federación\"."
youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante."
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."
followingSettingsAreRecommended: "Se recomienda los siguientes ajustes"
applyTheseSettings: "Aplicar estos ajustes"
skipSettings: "Omitir configuración"
settingsCompleted: "¡Configuración inicial del servidor completada!"
settingsCompleted_description: "Gracias por tu tiempo. Ahora que está todo listo puedes empezar a utilizar el servidor inmediatamente."
settingsCompleted_description2: "La configuración avanzada del servidor pueden realizarse a través del \"Panel de control\"."
donationRequest: "Por favor Dona"
_donationRequest:
text1: "Misskey es un software libre desarrollado por voluntarios."
text2: "Agradeceríamos su apoyo para que podamos seguir desarrollando este software en el futuro."
text3: "También hay beneficios especiales para los donantes"
_uploader:
compressedToX: "Comprimir a {x}"
savedXPercent: "Guardando {x}%"
abortConfirm: "Algunos archivos no se han cargado, ¿deseas cancelar?"
doneConfirm: "Algunos archivos no se han cargado, ¿deseas continuar de todos modos?"
maxFileSizeIsX: "El tamaño máximo de archivo que se puede cargar es de {x}"
allowedTypes: "Tipos de archivos que se pueden cargar."
tip: "El archivo aún no se ha cargado, por lo que este cuadro de diálogo te permite confirmar, renombrar, comprimir y recortar el archivo antes de cargarlo. Cuando esté listo, puedes iniciar la carga pulsando el botón \"Cargar\"."
_clientPerformanceIssueTip:
title: "Si crees que el consumo de batería es demasiado alto"
makeSureDisabledAdBlocker: "Por favor, desactive el bloqueador de publicidad."
makeSureDisabledAdBlocker_description: "Los bloqueadores de anuncios pueden afectar al rendimiento. Asegúrate de que no están activados en tu sistema o en las funciones/extensiones de tu navegador."
makeSureDisabledCustomCss: "Desactiva el CSS personalizado"
makeSureDisabledCustomCss_description: "Anular estilos puede afectar al rendimiento. Asegúrate de que el CSS personalizado o las extensiones que sobrescriben estilos no están activados."
makeSureDisabledAddons: "Desactiva las extensiones "
makeSureDisabledAddons_description: "Algunas extensiones pueden interferir con el comportamiento del cliente y afectar al rendimiento. Por favor, deshabilita las extensiones de tu navegador y comprueba si esto mejora la situación."
_clip:
tip: "Clip es una función que permite organizar varias notas."
_userLists:
tip: "Las listas pueden contener cualquier usuario que especifiques al crearlas, la lista creada puede mostrarse entonces como una línea de tiempo mostrando solo los usuarios especificados."
watermark: "Marca de Agua"
defaultPreset: "Por defecto"
_watermarkEditor:
tip: "Se puede añadir a la imagen una marca de agua, como información crediticia."
quitWithoutSaveConfirm: "¿Descartar cambios no guardados?"
driveFileTypeWarn: "Este archivo es incompatible"
driveFileTypeWarnDescription: "Elegir una imagen"
title: "Editar la marca de agua"
cover: "Cubrir todo"
repeat: "Repetir"
opacity: "Opacidad"
scale: "Tamaño"
text: "Texto"
position: "Posición"
type: "Tipo"
image: "Imágenes"
advanced: "Avanzado"
stripe: "Rayas"
stripeWidth: "Anchura de línea"
stripeFrequency: "Número de líneas."
angle: "Ángulo"
polkadot: "Lunares"
checker: "verificador"
polkadotMainDotOpacity: "Opacidad del círculo principal"
polkadotMainDotRadius: "Tamaño del círculo principal."
polkadotSubDotOpacity: "Opacidad del círculo secundario"
polkadotSubDotRadius: "Tamaño del círculo secundario."
polkadotSubDotDivisions: "Número de subpuntos."
_imageEffector:
title: "Efecto"
addEffect: "Añadir Efecto"
discardChangesConfirm: "¿Ignorar cambios y salir?"
_fxs:
chromaticAberration: "Aberración Cromática"
glitch: "Glitch"
mirror: "Espejo"
invert: "Invertir colores"
grayscale: "Blanco y negro"
colorAdjust: "Corrección de Color"
colorClamp: "Compresión cromática"
colorClampAdvanced: "Compresión cromática avanzada"
distort: "Distorsión"
threshold: "umbral"
zoomLines: "Saturación de Líneas"
stripe: "Rayas"
polkadot: "Lunares"
checker: "Corrector"
blockNoise: "Bloquear Ruido"
tearing: "Rasgado de Imagen (Tearing)"
+10
View File
@@ -2360,3 +2360,13 @@ _search:
searchScopeAll: "Tous"
searchScopeLocal: "Local"
searchScopeUser: "Spécifier l'utilisateur·rice"
_watermarkEditor:
driveFileTypeWarn: "Ce fichier n'est pas pris en charge"
opacity: "Transparence"
scale: "Taille"
text: "Texte"
position: "Position"
type: "Type"
image: "Images"
advanced: "Avancé"
angle: "Angle"
+10
View File
@@ -2608,3 +2608,13 @@ _search:
searchScopeAll: "Semua"
searchScopeLocal: "Lokal"
searchScopeUser: "Pengguna spesifik"
_watermarkEditor:
driveFileTypeWarn: "Berkas ini tidak didukung"
opacity: "Opasitas"
scale: "Ukuran"
text: "Teks"
position: "Posisi"
type: "Tipe"
image: "Gambar"
advanced: "Tingkat lanjut"
angle: "Sudut"
+294
View File
@@ -5270,6 +5270,10 @@ export interface Locale extends ILocale {
*
*/
"federationDisabled": string;
/**
*
*/
"draft": string;
/**
*
*/
@@ -5481,6 +5485,24 @@ export interface Locale extends ILocale {
*
*/
"hideAllTips": string;
/**
*
*/
"defaultImageCompressionLevel": string;
/**
* <br>
*/
"defaultImageCompressionLevel_description": string;
"_order": {
/**
*
*/
"newest": string;
/**
*
*/
"oldest": string;
};
"_chat": {
/**
*
@@ -5821,6 +5843,10 @@ export interface Locale extends ILocale {
* URLプレビューを表示する
*/
"showUrlPreview": string;
/**
*
*/
"showAvailableReactionsFirstInNote": string;
"_chat": {
/**
*
@@ -7765,6 +7791,10 @@ export interface Locale extends ILocale {
* {x}
*/
"uploadableFileTypes_caption2": ParameterizedString<"x">;
/**
*
*/
"noteDraftLimit": string;
};
"_condition": {
/**
@@ -8354,6 +8384,10 @@ export interface Locale extends ILocale {
*
*/
"code": string;
/**
*
*/
"copyThemeCode": string;
/**
*
*/
@@ -9572,6 +9606,14 @@ export interface Locale extends ILocale {
"disableFederationDescription": string;
};
"_postForm": {
/**
*
*/
"quitInspiteOfThereAreUnuploadedFilesConfirm": string;
/**
* 稿
*/
"uploaderTip": string;
/**
* ...
*/
@@ -11949,6 +11991,10 @@ export interface Locale extends ILocale {
};
};
"_uploader": {
/**
*
*/
"editImage": string;
/**
* {x}
*/
@@ -12020,6 +12066,254 @@ export interface Locale extends ILocale {
*/
"tip": string;
};
/**
*
*/
"watermark": string;
/**
*
*/
"defaultPreset": string;
"_watermarkEditor": {
/**
*
*/
"tip": string;
/**
*
*/
"quitWithoutSaveConfirm": string;
/**
*
*/
"driveFileTypeWarn": string;
/**
*
*/
"driveFileTypeWarnDescription": string;
/**
*
*/
"title": string;
/**
*
*/
"cover": string;
/**
*
*/
"repeat": string;
/**
*
*/
"opacity": string;
/**
*
*/
"scale": string;
/**
*
*/
"text": string;
/**
*
*/
"position": string;
/**
*
*/
"type": string;
/**
*
*/
"image": string;
/**
*
*/
"advanced": string;
/**
*
*/
"stripe": string;
/**
*
*/
"stripeWidth": string;
/**
*
*/
"stripeFrequency": string;
/**
*
*/
"angle": string;
/**
*
*/
"polkadot": string;
/**
*
*/
"checker": string;
/**
*
*/
"polkadotMainDotOpacity": string;
/**
*
*/
"polkadotMainDotRadius": string;
/**
*
*/
"polkadotSubDotOpacity": string;
/**
*
*/
"polkadotSubDotRadius": string;
/**
*
*/
"polkadotSubDotDivisions": string;
};
"_imageEffector": {
/**
*
*/
"title": string;
/**
*
*/
"addEffect": string;
/**
*
*/
"discardChangesConfirm": string;
"_fxs": {
/**
*
*/
"chromaticAberration": string;
/**
*
*/
"glitch": string;
/**
*
*/
"mirror": string;
/**
*
*/
"invert": string;
/**
*
*/
"grayscale": string;
/**
* 調
*/
"colorAdjust": string;
/**
*
*/
"colorClamp": string;
/**
* ()
*/
"colorClampAdvanced": string;
/**
*
*/
"distort": string;
/**
*
*/
"threshold": string;
/**
*
*/
"zoomLines": string;
/**
*
*/
"stripe": string;
/**
*
*/
"polkadot": string;
/**
*
*/
"checker": string;
/**
*
*/
"blockNoise": string;
/**
*
*/
"tearing": string;
};
};
/**
*
*/
"drafts": string;
"_drafts": {
/**
*
*/
"select": string;
/**
*
*/
"cannotCreateDraftAnymore": string;
/**
*
*/
"cannotCreateDraftOfRenote": string;
/**
*
*/
"delete": string;
/**
*
*/
"deleteAreYouSure": string;
/**
*
*/
"noDrafts": string;
/**
* {user}
*/
"replyTo": ParameterizedString<"user">;
/**
* {user}
*/
"quoteOf": ParameterizedString<"user">;
/**
* {channel}稿
*/
"postTo": ParameterizedString<"channel">;
/**
*
*/
"saveToDraft": string;
/**
*
*/
"restoreFromDraft": string;
/**
*
*/
"restore": string;
/**
*
*/
"listDrafts": string;
};
}
declare const locales: {
[lang: string]: Locale;
+59 -2
View File
@@ -327,6 +327,7 @@ dark: "Scuro"
lightThemes: "Tema Chiaro"
darkThemes: "Tema Scuro"
syncDeviceDarkMode: "Sincronizza il tema scuro con le impostazioni del dispositivo"
switchDarkModeManuallyWhenSyncEnabledConfirm: "({x}) è attiva. Vuoi disattivare la sincronizzazione e passare alla modalità manuale?"
drive: "Drive"
fileName: "Nome dell'allegato"
selectFile: "Scelta allegato"
@@ -581,7 +582,7 @@ sounds: "Impostazioni suoni"
sound: "Suono"
notificationSoundSettings: "Preferenze di notifica"
listen: "Ascolta"
none: "Nessuno"
none: "Nessuna"
showInPage: "Visualizza in pagina"
popout: "Finestra pop-out"
volume: "Volume"
@@ -1329,6 +1330,7 @@ restore: "Ripristina"
syncBetweenDevices: "Sincronizzazione tra i dispositivi"
preferenceSyncConflictTitle: "Sul server esiste già il valore impostato"
preferenceSyncConflictText: "Le impostazione sincronizzata salverà il valore sul server. Però, bada che esiste già un valore sul server. Quale vorresti sovrascrivere?"
preferenceSyncConflictChoiceMerge: "Integra"
preferenceSyncConflictChoiceServer: "Valore del server"
preferenceSyncConflictChoiceDevice: "Valore del dispositivo"
preferenceSyncConflictChoiceCancel: "Annulla la sincronizzazione"
@@ -1340,7 +1342,7 @@ information: "Informazioni"
chat: "Chat"
migrateOldSettings: "Migrare le vecchie impostazioni"
migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali."
compress: "Comprimi"
compress: "Compressione"
right: "Destra"
bottom: "Sotto"
top: "Sopra"
@@ -1349,6 +1351,7 @@ settingsMigrating: "Migrazione delle impostazioni. Attendere prego ... (Puoi anc
readonly: "Sola lettura"
goToDeck: "Torna al Deck"
federationJobs: "Coda di federazione"
driveAboutTip: "Il Drive mostra l'elenco di file caricati in passato. Puoi organizzarli in cartelle, riusarli allegandoli ad altre note, o caricarli in anticipo e poi pubblicarli in un secondo momento. Tieni presente che se elimini un file, non sarà più visibile in nessuno degli oggetti a cui è allegato (Note, pagine, avatar, banner, ecc.)"
scrollToClose: "Scorri per chiudere"
advice: "Consiglio"
realtimeMode: "Modalità in tempo reale"
@@ -1362,6 +1365,8 @@ abort: "Annulla"
tip: "Suggerimento"
redisplayAllTips: "Mostra tutti i suggerimenti"
hideAllTips: "Nascondi tutti i suggerimenti"
defaultImageCompressionLevel: "Livello predefinito di compressione immagini"
defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine."
_chat:
noMessagesYet: "Ancora nessun messaggio"
newMessage: "Nuovo messaggio"
@@ -1449,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "Se l'impostazione è alta, verranno aggiornati più frequentemente, consumando più dati e più batteria."
contentsUpdateFrequency_description2: "Quando la modalità è in tempo reale, arriveranno a prescindere."
showUrlPreview: "Mostra anteprima dell'URL"
showAvailableReactionsFirstInNote: "Mostra le reazioni disponibili in alto"
_chat:
showSenderName: "Mostra il nome del mittente"
sendOnEnter: "Invio spedisce"
@@ -2459,6 +2465,8 @@ _visibility:
disableFederation: "Senza federazione"
disableFederationDescription: "Non spedire attività alle altre istanze remote"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?"
uploaderTip: "Il file non è ancora stato caricato. Nel menu file (tre puntini), puoi ritagliare l'immagine, mettere la filigrana, decidere la presenza o l'assenza di compressione... Il file verrà caricato automaticamente quando pubblichi la Nota."
replyPlaceholder: "Rispondi a questa nota..."
quotePlaceholder: "Cita questa nota..."
channelPlaceholder: "Pubblica sul canale..."
@@ -2902,6 +2910,8 @@ _offlineScreen:
_urlPreviewSetting:
title: "Impostazioni per l'anteprima delle URL"
enable: "Attiva l'anteprima delle URL"
allowRedirect: "Segui i reindirizzamenti per visualizzare le anteprime"
allowRedirectDescription: "Se la URL inserita contiene un reindirizzamento, decidi di seguire il reindirizzamento fino alla destinazione, visualizzandone l'anteprima. Disabilitando questa opzione si risparmiano risorse del server, ma il contenuto effettivo dal reindirizzamento, non verrà visualizzato."
timeout: "Timeout dell'anteprima in millisecondi"
timeoutDescription: "Impegna al massimo il tempo indicato, altrimenti ignora l'anteprima"
maximumContentLength: "Grandezza del contenuto (Content-Length in byte)"
@@ -3112,3 +3122,50 @@ _clip:
tip: "Le clip sono una funzionalità che consente di raggruppare le Note."
_userLists:
tip: "Puoi creare un elenco di Note create da qualsiasi profilo. L'elenco è visualizzato come una sequenza temporale."
watermark: "Filigrana"
defaultPreset: "Impostazioni predefinite"
_watermarkEditor:
tip: "Puoi aggiungere una filigrana, ad esempio con i crediti alle tue immagini."
quitWithoutSaveConfirm: "Uscire senza salvare?"
driveFileTypeWarn: "Formato file non supportato"
driveFileTypeWarnDescription: "Per favore seleziona un file immagine"
title: "Modifica la filigrana"
cover: "Coprire tutto"
repeat: "Disposizione"
opacity: "Opacità"
scale: "Dimensioni"
text: "Testo"
position: "Posizione"
type: "Tipo"
image: "Immagini"
advanced: "Avanzato"
stripe: "Strisce"
stripeWidth: "Larghezza della linea"
stripeFrequency: "Il numero di linee"
angle: "Angolo"
polkadot: "A pallini"
checker: "revisore"
polkadotMainDotOpacity: "Opacità del punto principale"
polkadotMainDotRadius: "Dimensione del punto principale"
polkadotSubDotOpacity: "Opacità del punto secondario"
polkadotSubDotRadius: "Dimensione del punto secondario"
polkadotSubDotDivisions: "Quantità di punti secondari"
_imageEffector:
title: "Effetto"
addEffect: "Aggiungi effetto"
discardChangesConfirm: "Scarta le modifiche ed esci?"
_fxs:
chromaticAberration: "Aberrazione cromatica"
glitch: "Glitch"
mirror: "Specchio"
invert: "Inversione colore"
grayscale: "Bianco e nero"
colorAdjust: "Correzione Colore"
colorClamp: "Compressione del colore"
colorClampAdvanced: "Compressione del colore (avanzata)"
distort: "Distorsione"
threshold: "Soglia"
zoomLines: "Linea di saturazione"
stripe: "Strisce"
polkadot: "A pallini"
checker: "revisore"
+81
View File
@@ -1313,6 +1313,7 @@ availableRoles: "利用可能なロール"
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
draft: "下書き"
confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
@@ -1365,6 +1366,12 @@ abort: "中止"
tip: "ヒントとコツ"
redisplayAllTips: "全ての「ヒントとコツ」を再表示"
hideAllTips: "全ての「ヒントとコツ」を非表示"
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
_order:
newest: "新しい順"
oldest: "古い順"
_chat:
noMessagesYet: "まだメッセージはありません"
@@ -1455,6 +1462,7 @@ _settings:
contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。"
contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。"
showUrlPreview: "URLプレビューを表示する"
showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示"
_chat:
showSenderName: "送信者の名前を表示"
@@ -2010,6 +2018,7 @@ _role:
uploadableFileTypes: "アップロード可能なファイル種別"
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
@@ -2190,6 +2199,7 @@ _theme:
install: "テーマのインストール"
manage: "テーマの管理"
code: "テーマコード"
copyThemeCode: "テーマコードをコピー"
description: "説明"
installed: "{name}をインストールしました"
installedThemes: "インストールされたテーマ"
@@ -2519,6 +2529,8 @@ _visibility:
disableFederationDescription: "他サーバーへの配信を行いません"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされていないファイルがありますが、破棄してフォームを閉じますか?"
uploaderTip: "ファイルはまだアップロードされていません。ファイルのメニューから、リネームや画像のクロップ、ウォーターマークの付与、圧縮の有無などを設定できます。ファイルはノート投稿時に自動でアップロードされます。"
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..."
@@ -3196,6 +3208,7 @@ _serverSetupWizard:
text3: "支援者向け特典もあります!"
_uploader:
editImage: "画像の編集"
compressedToX: "{x}に圧縮"
savedXPercent: "{x}%節約"
abortConfirm: "アップロードされていないファイルがありますが、中止しますか?"
@@ -3218,3 +3231,71 @@ _clip:
_userLists:
tip: "任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。"
watermark: "ウォーターマーク"
defaultPreset: "デフォルトのプリセット"
_watermarkEditor:
tip: "画像にクレジット情報などのウォーターマークを追加することができます。"
quitWithoutSaveConfirm: "保存せずに終了しますか?"
driveFileTypeWarn: "このファイルは対応していません"
driveFileTypeWarnDescription: "画像ファイルを選択してください"
title: "ウォーターマークの編集"
cover: "全体に被せる"
repeat: "敷き詰める"
opacity: "不透明度"
scale: "サイズ"
text: "テキスト"
position: "位置"
type: "タイプ"
image: "画像"
advanced: "高度"
stripe: "ストライプ"
stripeWidth: "ラインの幅"
stripeFrequency: "ラインの数"
angle: "角度"
polkadot: "ポルカドット"
checker: "チェッカー"
polkadotMainDotOpacity: "メインドットの不透明度"
polkadotMainDotRadius: "メインドットの大きさ"
polkadotSubDotOpacity: "サブドットの不透明度"
polkadotSubDotRadius: "サブドットの大きさ"
polkadotSubDotDivisions: "サブドットの数"
_imageEffector:
title: "エフェクト"
addEffect: "エフェクトを追加"
discardChangesConfirm: "変更を破棄して終了しますか?"
_fxs:
chromaticAberration: "色収差"
glitch: "グリッチ"
mirror: "ミラー"
invert: "色の反転"
grayscale: "白黒"
colorAdjust: "色調補正"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"
distort: "歪み"
threshold: "二値化"
zoomLines: "集中線"
stripe: "ストライプ"
polkadot: "ポルカドット"
checker: "チェッカー"
blockNoise: "ブロックノイズ"
tearing: "ティアリング"
drafts: "下書き"
_drafts:
select: "下書きを選択"
cannotCreateDraftAnymore: "下書きの作成可能数を超えています。"
cannotCreateDraftOfRenote: "リノートの下書きは作成できません。"
delete: "下書きを削除"
deleteAreYouSure: "下書きを削除しますか?"
noDrafts: "下書きはありません"
replyTo: "{user}への返信"
quoteOf: "{user}のノートへの引用"
postTo: "{channel}への投稿"
saveToDraft: "下書きへ保存"
restoreFromDraft: "下書きから復元"
restore: "復元"
listDrafts: "下書き一覧"
+10
View File
@@ -2848,3 +2848,13 @@ _search:
searchScopeAll: "みんな"
searchScopeLocal: "ローカル"
searchScopeUser: "ユーザー指定"
_watermarkEditor:
driveFileTypeWarn: "このファイルは対応しとらへん"
opacity: "不透明度"
scale: "大きさ"
text: "テキスト"
position: "位置"
type: "タイプ"
image: "画像"
advanced: "高度"
angle: "角度"
+2
View File
@@ -848,3 +848,5 @@ _remoteLookupErrors:
_search:
searchScopeAll: "말캉"
searchScopeUser: "사용자 지정"
_watermarkEditor:
image: "이미지"
+170
View File
@@ -298,6 +298,7 @@ uploadFromUrl: "URL 업로드"
uploadFromUrlDescription: "업로드하려는 파일의 URL"
uploadFromUrlRequested: "업로드를 요청했습니다"
uploadFromUrlMayTakeTime: "업로드가 완료될 때까지 시간이 소요될 수 있습니다."
uploadNFiles: "{n}개의 파일을 업로"
explore: "둘러보기"
messageRead: "읽음"
noMoreHistory: "이것보다 과거의 기록이 없습니다"
@@ -326,6 +327,7 @@ dark: "다크"
lightThemes: "밝은 테마"
darkThemes: "어두운 테마"
syncDeviceDarkMode: "디바이스의 다크 모드 설정과 동기화"
switchDarkModeManuallyWhenSyncEnabledConfirm: "'{x}'가 켜져 있습니다. 동기화를 끄고 수동으로 모드를 변경하겠습니까?"
drive: "드라이브"
fileName: "파일명"
selectFile: "파일 선택"
@@ -575,8 +577,10 @@ showFixedPostForm: "타임라인 상단에 글 입력란을 표시"
showFixedPostFormInChannel: "채널 타임라인 상단에 글 입력란을 표시"
withRepliesByDefaultForNewlyFollowed: "팔로우 할 때 기본적으로 답글을 타임라인에 나오게 하기"
newNoteRecived: "새 노트가 있습니다"
newNote: "새로운 노트"
sounds: "소리"
sound: "소리"
notificationSoundSettings: "알림 설정"
listen: "듣기"
none: "없음"
showInPage: "페이지로 보기"
@@ -791,6 +795,7 @@ wide: "넓게"
narrow: "좁게"
reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?"
needReloadToApply: "변경 사항은 새로고침하면 적용됩니다."
needToRestartServerToApply: "변경 사항은 새로고침이 필요합니다."
showTitlebar: "타이틀 바를 표시하기"
clearCache: "캐시 비우기"
onlineUsersCount: "{n}명이 접속 중"
@@ -997,6 +1002,7 @@ failedToUpload: "업로드 실패"
cannotUploadBecauseInappropriate: "이 파일은 부적절한 내용을 포함한다고 판단되어 업로드할 수 없습니다."
cannotUploadBecauseNoFreeSpace: "드라이브 용량이 부족하여 업로드할 수 없습니다."
cannotUploadBecauseExceedsFileSizeLimit: "파일 크기가 너무 크기 때문에 업로드할 수 없습니다."
cannotUploadBecauseUnallowedFileType: "허가되지 않은 유형의 파일이기에 업로드할 수 없습니다."
beta: "베타"
enableAutoSensitive: "자동 NSFW 탐지"
enableAutoSensitiveDescription: "이용 가능할 경우 기계학습을 통해 자동으로 미디어 NSFW를 설정합니다. 이 기능을 해제하더라도, 서버 정책에 따라 자동으로 설정될 수 있습니다."
@@ -1324,6 +1330,7 @@ restore: "복원"
syncBetweenDevices: "장치간 동기화"
preferenceSyncConflictTitle: "서버에 설정값이 존재합니다."
preferenceSyncConflictText: "동기화를 활성화 한 항목의 설정 값은 서버에 저장되지만, 해당 항목은 이미 서버에 설정 값이 저장되어져 있습니다. 어느 쪽의 설정 값을 덮어씌울까요?"
preferenceSyncConflictChoiceMerge: "병합"
preferenceSyncConflictChoiceServer: "서버 설정값"
preferenceSyncConflictChoiceDevice: "장치 설정값"
preferenceSyncConflictChoiceCancel: "동기화 취소"
@@ -1346,6 +1353,20 @@ goToDeck: "덱으로 돌아가기"
federationJobs: "연합 작업"
driveAboutTip: "드라이브는 이전에 업로드한 파일 목록을 표시해요. <br>\n노트에 첨부할 때 다시 사용하거나 나중에 게시할 파일을 미리 업로드할 수 있어요. <br>\n<b>파일을 삭제하면, 지금까지 그 파일을 사용한 모든 장소(노트, 페이지, 아바타, 배너 등)에서도 보이지 않게 되므로 주의해 주세요. 폴더를 만들고 정리할 수도 있어요.</b><br>"
scrollToClose: "스크롤하여 닫기"
advice: "참고"
realtimeMode: "실시간 모드"
turnItOn: "켜기"
turnItOff: "끄기"
emojiMute: "이모티콘 뮤트"
emojiUnmute: "이모티콘 뮤트 해제"
muteX: "{x}를 뮤트"
unmuteX: "{x}의 뮤트를 해제"
abort: "중지"
tip: "팁과 유용한 정보"
redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시"
hideAllTips: "모든 '팁과 유용한 정보'를 비표시"
defaultImageCompressionLevel: "기본 이미지 압축 정도"
defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다."
_chat:
noMessagesYet: "아직 메시지가 없습니다"
newMessage: "새로운 메시지"
@@ -1379,6 +1400,8 @@ _chat:
chatNotAvailableInOtherAccount: "상대방 계정에서 채팅 기능을 사용할 수 없는 상태입니다."
cannotChatWithTheUser: "이 유저와 채팅을 시작할 수 없습니다"
cannotChatWithTheUser_description: "채팅을 사용할 수 없는 상태이거나 상대방이 채팅을 열지 않은 상태입니다."
youAreNotAMemberOfThisRoomButInvited: "당신은 이 룸의 참가자가 아닙니다만 초대 신청을 받으셨습니다. 참가하려면 초대를 수락해주십시오."
doYouAcceptInvitation: "초대를 수락하시겠습니까?"
chatWithThisUser: "채팅하기"
thisUserAllowsChatOnlyFromFollowers: "이 유저는 팔로워만 채팅을 할 수 있습니다."
thisUserAllowsChatOnlyFromFollowing: "이 유저는 이 유저가 팔로우하는 유저만 채팅을 허용합니다."
@@ -1418,12 +1441,20 @@ _settings:
makeEveryTextElementsSelectable: "모든 텍스트 요소를 선택할 수 있도록 함"
makeEveryTextElementsSelectable_description: "활성화 시, 일부 동작에서 유저의 접근성이 나빠질 수도 있습니다."
useStickyIcons: "아이콘이 스크롤을 따라가도록 하기"
enableHighQualityImagePlaceholders: "고화질 이미지의 플레이스홀더를 표시"
uiAnimations: "UI 애니메이션"
showNavbarSubButtons: "내비게이션 바에 보조 버튼 표시"
ifOn: "켜져 있을 때"
ifOff: "꺼져 있을 때"
enableSyncThemesBetweenDevices: "기기 간 설치한 테마 동기화"
enablePullToRefresh: "계속해서 갱신"
enablePullToRefresh_description: "마우스에서 휠을 누르면서 드래그해요."
realtimeMode_description: "서버에 접속하고 실시간으로 콘텐츠를 업데이트합니다. 데이터 사용량과 배터리의 소비가 증가할 수 있습니다."
contentsUpdateFrequency: "콘텐츠의 업데이트 빈도"
contentsUpdateFrequency_description: "높을수록 실시간으로 콘텐츠가 업데이트됩니다만, 성능이 저하되고 데이터 사용량과 배터리의 소비가 증가합니다."
contentsUpdateFrequency_description2: "실시간 모드가 켜져 있을 때는 이 설정과 상관없이 실시간으로 콘텐츠가 업데이트됩니다."
showUrlPreview: "URL 미리보기 표시"
showAvailableReactionsFirstInNote: "이용 가능한 리액션을 선두로 표시"
_chat:
showSenderName: "발신자 이름 표시"
sendOnEnter: "엔터로 보내기"
@@ -1604,6 +1635,21 @@ _serverSettings:
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
deliverSuspendedSoftware: "전달 정지 중인 소프트웨어"
deliverSuspendedSoftwareDescription: "취약성 등의 이유로 서버의 소프트웨어 이름 및 버전 범위를 지정하여 전달을 정지할 수 있어요. 이 버전 정보는 서버가 제공한 것이며 신뢰성은 보장되지 않아요. 버전 지정에는 semver의 범위 지정을 사용할 수 있지만, >= 2024.3.1로 지정하면 2024.3.1-custom.0과 같은 custom.0과 같은 custom 버전이 포함되지 않기 때문에 >= 2024.3.1-0과 같이 prerelease를 지정하는 것이 좋아요."
singleUserMode: "1인 모드"
singleUserMode_description: "이 서버의 이용자가 자신 뿐인 경우, 이 모드를 활성화하면 동작이 최적화됩니다."
signToActivityPubGet: "GET 요청에 사인"
signToActivityPubGet_description: "보통의 경우 활성화해 주십시오. 연합의 통신에 관한 문제가 있는 경우, 비활성화하면 개선되는 경우도 있습니다만, 서버에 따라서는 통신이 불가능해지는 경우도 있습니다."
proxyRemoteFiles: "리모트 파일 프록시"
proxyRemoteFiles_description: "활성화하면 리모트 파일을 프록시로 제공합니다. 이미지의 섬네일 생성이나 유저의 개인정보 보호에 도움을 줍니다."
allowExternalApRedirect: "ActivityPub 경유 조회에 리디렉션 허가"
allowExternalApRedirect_description: "활성화하면 다른 서버가 이 서버를 통해 제3자의 콘텐츠를 조회할 수 있습니다만, 콘텐츠의 사칭 문제가 생길 수 있습니다."
userGeneratedContentsVisibilityForVisitor: "비이용자에 대한 유저 작성 콘텐츠의 공개 범위"
userGeneratedContentsVisibilityForVisitor_description: "조정을 하기 힘든 부적절한 리모트 콘텐츠 등이 자신의 서버 경유로 의도치 않게 인터넷에 공개되는 문제의 방지 등에 도움을 줍니다."
userGeneratedContentsVisibilityForVisitor_description2: "서버에서 받은 리모트 콘텐츠를 포함해 서버 내의 모든 콘텐츠를 무조건 인터넷에 공개하는 것에는 위험이 따릅니다. 특히, 분산형 특성에 대해 모르는 열람자에게는 리모트 콘텐츠여도 서버 내에서 작성된 콘텐츠라고 잘못 인식할 수 있기에 주의가 필요합니다."
_userGeneratedContentsVisibilityForVisitor:
all: "모두 공개"
localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개"
none: "모두 비공개"
_accountMigration:
moveFrom: "다른 계정에서 이 계정으로 이사"
moveFromSub: "다른 계정에 대한 별칭을 생성"
@@ -1944,6 +1990,9 @@ _role:
canImportMuting: "뮤트 목록 가져오기 허용"
canImportUserLists: "리스트 목록 가져오기 허용"
chatAvailability: "채팅을 허락"
uploadableFileTypes: "업로드 가능한 파일 유형"
uploadableFileTypes_caption: "MIME 유형을 "
uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오."
_condition:
roleAssignedTo: "수동 역할에 이미 할당됨"
isLocal: "로컬 유저"
@@ -2416,6 +2465,8 @@ _visibility:
disableFederation: "연합에 보내지 않기"
disableFederationDescription: "다른 서버로 보내지 않습니다"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "업로드되지 않은 파일이 있습니다만, 없애고 폼을 닫겠습니까?"
uploaderTip: "파일이 아직 업로드돼있지 않습니다. 파일 메뉴에서 이름 바꾸기나 이미지의 자르기, 워터마크 넣기, 압축의 유무 등을 설정할 수 있습니다. 파일은 노트 게시 시 자동으로 업로드됩니다."
replyPlaceholder: "이 노트에 답글..."
quotePlaceholder: "이 노트를 인용..."
channelPlaceholder: "채널에 게시하기..."
@@ -2796,6 +2847,12 @@ _dataSaver:
_avatar:
title: "아이콘 이미지"
description: "아이콘 이미지의 애니메이션을 멈춥니다. 애니메이션 이미지는 일반 이미지보다 파일 크기가 클 수 있으므로 데이터 사용량을 더 줄일 수 있습니다."
_urlPreviewThumbnail:
title: "URL 미리보기의 섬네일을 비표시"
description: "URL 미리보기의 섬네일 이미지를 불러올 수 없게 됩니다."
_disableUrlPreview:
title: "URL 미리보기 비활성화"
description: "URL 미리보기 기능을 비활성화합니다. 섬네일 이미지와 달리 링크 정보 불러오기 자체를 줄일 수 있습니다."
_code:
title: "문자열 강조"
description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다."
@@ -2853,6 +2910,8 @@ _offlineScreen:
_urlPreviewSetting:
title: "URL 미리보기 설정"
enable: "URL 미리보기 활성화"
allowRedirect: "미리보기 위치의 리디렉션 허가"
allowRedirectDescription: "입력된 URL이 리디렉션될 경우, 그 리디렉션 위치를 따라 미리보기를 표시할 것인지 설정합니다. 비활성화하면 서버 리소스를 절약할 수 있습니다만, 리디렉션 위치의 내용은 표시되지 않습니다."
timeout: "미리보기를 불러올 때의 타임아웃 (ms)"
timeoutDescription: "미리보기를 로딩하는데 걸리는 시간이 정한 시간보다 오래 걸리는 경우, 미리보기를 생성하지 않습니다."
maximumContentLength: "Content-Length의 최대치 (byte)"
@@ -3001,3 +3060,114 @@ _search:
pleaseEnterServerHost: "서버의 호스트를 입력해 주세요."
pleaseSelectUser: "유저를 선택해주세요"
serverHostPlaceholder: "예: misskey.example.com"
_serverSetupWizard:
installCompleted: "Misskey의 설치가 완료됐습니다!"
firstCreateAccount: "먼저 관리자 계정을 만듭시다."
accountCreated: "관리자 계정이 만들어졌습니다!"
serverSetting: "서버 설정"
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "이 위자드로 쉽게 최적화된 서버의 설정을 할 수 있습니다."
settingsYouMakeHereCanBeChangedLater: "이 설정은 나중에 변경 가능합니다."
howWillYouUseMisskey: "Misskey를 어떻게 사용하십니까?"
_use:
single: "1인 서버"
single_description: "자신 전용 서버로 혼자서 사용"
single_youCanCreateMultipleAccounts: "1인 서버로 운영하는 경우에도 계정은 필요에 따라 여러 개 만들 수 있습니다."
group: "그룹 서버"
group_description: "신뢰 가능한 다른 유저를 초대해 여러 명이 사용"
open: "오픈 서버"
open_description: "불특정 다수의 유저를 받아들이는 운영을 함"
openServerAdvice: "불특정 다수의 유저를 받아들이는 것에는 위험이 따릅니다. 문제에 대처할 수 있도록 확실한 조정 체제로 운영하는 것을 권장합니다."
openServerAntiSpamAdvice: "자신의 서버가 스팸으로 사용되지 않게끔 reCAPTCHA라는 안티 봇 기능을 활성화하는 등 보안에 대해서도 세심한 주의가 필요합니다."
howManyUsersDoYouExpect: "어느 정도의 인원으로 생각 중이십니까?"
_scale:
small: "100명 이하(소규모)"
medium: "100명 이상 1000명 이하(중간 규모)"
large: "1000명 이상(대규모)"
largeScaleServerAdvice: "대규모 서버에서는 부하분산이나 데이터베이스의 복제 등 높은 인프라스트럭처 지식이 필요할 수 있습니다."
doYouConnectToFediverse: "Fediverse에 접속하시겠습니까?"
doYouConnectToFediverse_description1: "분산형 서버로 구성된 네트워크(Fediverse)에 접속하면 다른 서버와 서로 콘텐츠의 주고받기를 할 수 있습니다."
doYouConnectToFediverse_description2: "Fediverse에 접속하는 것을 '연합'이라고도 부릅니다."
youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다."
adminInfo: "관리자 정보"
adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다."
adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다."
followingSettingsAreRecommended: "아래의 설정이 권장됩니다."
applyTheseSettings: "이 설정을 적용"
skipSettings: "설정 건너뛰기"
settingsCompleted: "설정이 완료됐습니다!"
settingsCompleted_description: "수고하셨습니다. 준비를 마쳤으므로 바로 서버의 이용을 시작하실 수 있습니다."
settingsCompleted_description2: "상세한 서버 설정은 '제어판'에서 하실 수 있습니다."
donationRequest: "기부 요청"
_donationRequest:
text1: "Misskey는 자원봉사자들에 의해 개발되는 무료 소프트웨어입니다."
text2: "앞으로도 계속해서 개발을 할 수 있도록 괜찮으시다면 부디 기부를 부탁드립니다."
text3: "지원자 대상 특전도 있습니다!"
_uploader:
compressedToX: "{x}로 압축"
savedXPercent: "{x}% 절약"
abortConfirm: "업로드되지 않은 파일이 있습니다만, 그만 두시겠습니까?"
doneConfirm: "업로드되지 않은 파일이 있습니다만, 완료하시겠습니까?"
maxFileSizeIsX: "업로드 가능한 최대 파일 크기는 {x}입니다."
allowedTypes: "업로드 가능한 파일 유형"
tip: "파일은 아직 업로드되지 않았습니다. 이 다이얼로그에서 업로드 전의 확인, 이름 바꾸기, 압축, 자르기 등을 하실 수 있습니다. 준비가 되셨다면 '업로드' 버튼을 클릭해 업로드를 시작하실 수 있습니다."
_clientPerformanceIssueTip:
title: "배터리 소비가 심하다고 생각되시면"
makeSureDisabledAdBlocker: "광고 차단을 비활성화해 주십시오."
makeSureDisabledAdBlocker_description: "광고 차단은 성능에 영향을 미칠 수 있습니다. OS의 기능이나 브라우저의 기능, 애드온 등으로 광고 차단이 활성화돼있지 않은지 확인해 주십시오."
makeSureDisabledCustomCss: "커스텀 CSS를 무효로 해주십시오."
makeSureDisabledCustomCss_description: "스타일을 덮어쓰기하면 성능에 영향을 미칠 수 있습니다. 커스텀 CSS나 스타일을 덮어쓰기하는 확장 기능이 유효로 돼있는지 확인해주십시오."
makeSureDisabledAddons: "확장 기능을 비활성화해 주십시오."
makeSureDisabledAddons_description: "일부 확장 기능은 클라이언트의 동작에 간섭해 성능에 영향을 미칠 수 있습니다. 브라우저의 확장 기능을 비활성화해 개선할지 확인해주십시오."
_clip:
tip: "클립은 노트를 정리할 수 있는 기능입니다."
_userLists:
tip: "임의의 유저가 포함된 리스트를 작성할 수 있습니다. 작성한 리스트는 타임라인으로 표시가 가능합니다."
watermark: "워터마크"
defaultPreset: "기본 프리셋"
_watermarkEditor:
tip: "이미지에 크레딧 정보 등의 워터마크를 추가할 수 있습니다."
quitWithoutSaveConfirm: "보존하지 않고 종료하시겠습니까?"
driveFileTypeWarn: "이 파이"
driveFileTypeWarnDescription: "이미지 파일을 선택해주십시오."
title: "워터마크 편집"
cover: "전체에 붙이기"
repeat: "전면에 깔기"
opacity: "불투명도"
scale: "크기"
text: "텍스트"
position: "위치"
type: "종류"
image: "이미지"
advanced: "고급"
stripe: "줄무늬"
stripeWidth: "라인의 폭"
stripeFrequency: "라인의 수"
angle: "각도"
polkadot: "물방울 무늬"
checker: "체크 무늬"
polkadotMainDotOpacity: "주요 물방울의 불투명도"
polkadotMainDotRadius: "주요 물방울의 크기"
polkadotSubDotOpacity: "서브 물방울의 불투명도"
polkadotSubDotRadius: "서브 물방울의 크기"
polkadotSubDotDivisions: "서브 물방울의 수"
_imageEffector:
title: "이펙트"
addEffect: "이펙트를 추가"
discardChangesConfirm: "변경을 취소하고 종료하시겠습니까?"
_fxs:
chromaticAberration: "색수차"
glitch: "글리치"
mirror: "미러"
invert: "색 반전"
grayscale: "흑백"
colorAdjust: "색조 보정"
colorClamp: "색 압축"
colorClampAdvanced: "색 압축(고급)"
distort: "뒤틀림"
threshold: "이진화"
zoomLines: "집중선"
stripe: "줄무늬"
polkadot: "물방울 무늬"
checker: "체크 무늬"
blockNoise: "노이즈 방지"
tearing: "티어링"
+2
View File
@@ -483,3 +483,5 @@ _remoteLookupErrors:
title: "ບໍ່ພົບ"
_search:
searchScopeAll: "ທັງໝົດ"
_watermarkEditor:
image: "ຮູບພາບ"
+3
View File
@@ -1078,3 +1078,6 @@ _remoteLookupErrors:
title: "Niet gevonden"
_search:
searchScopeAll: "Alle"
_watermarkEditor:
image: "Afbeeldingen"
advanced: "Geavanceerd"
+5
View File
@@ -735,3 +735,8 @@ _remoteLookupErrors:
title: "Ikke funnet"
_search:
searchScopeAll: "Alle"
_watermarkEditor:
scale: "Størrelse"
text: "Tekst"
type: "Type"
image: "Bilder"
+7
View File
@@ -1584,3 +1584,10 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Wszystkie"
searchScopeLocal: "Lokalne"
_watermarkEditor:
opacity: "Przezroczystość"
scale: "Rozmiar"
text: "Tekst"
type: "Typ"
image: "Zdjęcia"
advanced: "Zaawansowane"
+10
View File
@@ -3084,3 +3084,13 @@ _clientPerformanceIssueTip:
makeSureDisabledCustomCss_description: "Substituir o estilo da página pode afetar o desempenho. Certifique-se que o CSS personalizado ou extensões que modifiquem o estilo da página estejam desabilitados."
makeSureDisabledAddons: "Desabilite extensões"
makeSureDisabledAddons_description: "Algumas extensões podem afetar comportamentos do cliente e afetar o desempenho. Por favor, desative as extensões do seu navegador e veja se isso melhora a situação."
_watermarkEditor:
driveFileTypeWarn: "Esse arquivo não é compatível"
opacity: "Opacidade"
scale: "Tamanho"
text: "Texto"
position: "Posição"
type: "Tipo"
image: "imagem"
advanced: "Avançado"
angle: "Ângulo"
+7
View File
@@ -1391,3 +1391,10 @@ _search:
searchScopeLocal: "Local"
searchScopeUser: "Utilizator specific"
serverHostPlaceholder: "Exemplu: misskey.example.com"
_watermarkEditor:
scale: "Dimensiune"
text: "Text"
position: "Poziție"
type: "Tip"
image: "Imagini"
advanced: "Avansat"
+9
View File
@@ -2191,3 +2191,12 @@ _search:
searchScopeAll: "Все"
searchScopeLocal: "Местная"
searchScopeUser: "Указанный пользователь"
_watermarkEditor:
opacity: "Непрозрачность"
scale: "Размер"
text: "Текст"
position: "Позиция"
type: "Тип"
image: "Изображения"
advanced: "Для продвинутых"
angle: "Угол"
+7
View File
@@ -1450,3 +1450,10 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Všetko"
searchScopeLocal: "Lokálne"
_watermarkEditor:
opacity: "Priehľadnosť"
scale: "Veľkosť"
text: "Text"
type: "Typ"
image: "Obrázky"
advanced: "Rozšírené"
+3
View File
@@ -711,3 +711,6 @@ _selfXssPrevention:
warning: "VARNING"
_search:
searchScopeAll: "Allt"
_watermarkEditor:
scale: "Storlek"
image: "Bilder"
+16
View File
@@ -220,6 +220,7 @@ silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี
mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้"
operations: "ดำเนินการ"
software: "ซอฟต์แวร์"
softwareName: "ชื่อซอฟต์แวร์"
version: "เวอร์ชั่น"
metadata: "Metadata"
withNFiles: "{n} ไฟล์"
@@ -1293,6 +1294,10 @@ federationDisabled: "เซิร์ฟเวอร์นี้ปิดกา
reactAreYouSure: "คุณต้องการที่จะตอบสนองต่อ \" {emoji}\" หรือไม่?"
markAsSensitiveConfirm: "คุณต้องการทำเครื่องหมายสื่อนี้ว่าละเอียดอ่อนหรือไม่?"
unmarkAsSensitiveConfirm: "คุณต้องการลบการกำหนดความไวของสื่อนี้หรือไม่?"
preferences: "การตั้งค่าสภาพแวดล้อม"
preferencesProfile: "โปรไฟล์การกำหนดค่า"
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
preferenceSyncConflictText: "รายการการตั้งค่าที่เปิดใช้งานการซิงโครไนซ์จะจัดเก็บค่าไว้บนเซิร์ฟเวอร์ และพบค่าที่จัดเก็บบนเซิร์ฟเวอร์สำหรับรายการการตั้งค่านี้ คุณต้องการทำอย่างไร?"
postForm: "แบบฟอร์มการโพสต์"
information: "เกี่ยวกับ"
right: "ขวา"
@@ -1305,6 +1310,7 @@ _chat:
send: "ส่ง"
_settings:
webhook: "Webhook"
preferencesBanner: "คุณสามารถกำหนดค่าพฤติกรรมโดยรวมของไคลเอนต์ได้ตามความต้องการของคุณ"
_accountSettings:
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
requireSigninToViewContentsDescription1: "ต้องเข้าสู่ระบบเพื่อดูบันทึกและเนื้อหาอื่น ๆ ทั้งหมดที่คุณสร้าง คาดว่าจะมีประสิทธิผลในการป้องกันไม่ให้ข้อมูลถูกเก็บรวบรวมโดยโปรแกรมรวบรวมข้อมูล"
@@ -2722,3 +2728,13 @@ _search:
searchScopeAll: "ทั้งหมด"
searchScopeLocal: "ท้องถิ่น"
searchScopeUser: "ผู้ใช้เฉพาะ"
_watermarkEditor:
driveFileTypeWarn: "ไม่รองรับไฟล์นี้"
opacity: "ความทึบแสง"
scale: "ขนาด"
text: "ข้อความ"
position: "ตำแหน่ง"
type: "รูปแบบ"
image: "รูปภาพ"
advanced: "ขั้นสูง"
angle: "แองเกิล"
+2
View File
@@ -460,3 +460,5 @@ _moderationLogTypes:
resetPassword: "Şifre sıfırlama"
_search:
searchScopeAll: "Tümü"
_watermarkEditor:
image: "Görseller"
+7
View File
@@ -1625,3 +1625,10 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Всі"
searchScopeLocal: "Локальна"
_watermarkEditor:
opacity: "Непрозорість"
scale: "Розмір"
text: "Текст"
type: "Тип"
image: "Зображення"
advanced: "Розширені"
+5
View File
@@ -1097,3 +1097,8 @@ _remoteLookupErrors:
_search:
searchScopeAll: "Barcha"
searchScopeLocal: "Mahalliy"
_watermarkEditor:
text: "Matn"
type: "turi"
image: "Rasmlar"
advanced: "Murakkab"
+15
View File
@@ -220,6 +220,7 @@ silenceThisInstance: "Máy chủ im lặng"
mediaSilenceThisInstance: "Tắt nội dung đa phương tiện từ máy chủ này"
operations: "Vận hành"
software: "Phần mềm"
softwareName: "Tên phần mềm"
version: "Phiên bản"
metadata: "Metadata"
withNFiles: "{n} tập tin"
@@ -1211,6 +1212,9 @@ federationDisabled: "Liên kết bị vô hiệu hóa trên máy chủ này. B
reactAreYouSure: "Bạn có muốn phản hồi với \" {emoji} \" không?"
preferences: "Thiết lập môi trường"
accessibility: "Khả năng tiếp cận"
preferencesProfile: "Hồ sơ sở thích"
preferenceSyncConflictTitle: "Cài đặt tồn tại trên máy chủ"
preferenceSyncConflictText: "Các thiết lập đồng bộ hóa được bật sẽ lưu các giá trị của chúng vào máy chủ. Tuy nhiên, có những giá trị hiện có trên máy chủ. Bạn muốn ghi đè lên bộ giá trị nào?"
paste: "dán"
postForm: "Mẫu đăng"
information: "Giới thiệu"
@@ -1223,6 +1227,8 @@ _chat:
members: "Thành viên"
home: "Trang chính"
send: "Gửi"
_settings:
preferencesBanner: "Bạn có thể cấu hình hành vi chung của máy khách theo sở thích của mình."
_accountSettings:
requireSigninToViewContents: "Yêu cầu đăng nhập để xem nội dung"
requireSigninToViewContentsDescription1: "Yêu cầu đăng nhập để xem tất cả ghi chú và nội dung khác mà bạn tạo. Điều này được kỳ vọng sẽ có hiệu quả trong việc ngăn chặn thông tin bị thu thập bởi các trình thu thập thông tin."
@@ -2074,3 +2080,12 @@ _search:
searchScopeAll: "Tất cả"
searchScopeLocal: "Máy chủ này"
searchScopeUser: "Người dùng chỉ định"
_watermarkEditor:
opacity: "Độ trong suốt"
scale: "Kích thước"
text: "Văn bản"
position: "Vị trí"
type: "Loại"
image: "Hình ảnh"
advanced: "Nâng cao"
angle: "Góc"
+59 -4
View File
@@ -1330,6 +1330,7 @@ restore: "恢复"
syncBetweenDevices: "设备间同步"
preferenceSyncConflictTitle: "服务器上已存在设定值"
preferenceSyncConflictText: "服务器上已有此设置的设定值。要覆盖哪个设定值?"
preferenceSyncConflictChoiceMerge: "合并"
preferenceSyncConflictChoiceServer: "服务器上的设定值"
preferenceSyncConflictChoiceDevice: "设备上的设定值"
preferenceSyncConflictChoiceCancel: "取消同步"
@@ -1364,6 +1365,8 @@ abort: "中止"
tip: "提示和技巧"
redisplayAllTips: "重新显示所有的提示和技巧"
hideAllTips: "隐藏所有的提示和技巧"
defaultImageCompressionLevel: "默认图像压缩等级"
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
_chat:
noMessagesYet: "还没有消息"
newMessage: "新消息"
@@ -1451,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "设置越高,内容更新越实时,但性能会降低,并且会消耗更多的流量和电池。"
contentsUpdateFrequency_description2: "当实时模式开启时,无论此设置如何,内容都会实时更新。"
showUrlPreview: "显示 URL 预览"
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
_chat:
showSenderName: "显示发送者的名字"
sendOnEnter: "回车键发送"
@@ -1627,7 +1631,7 @@ _serverSettings:
inquiryUrl: "联络地址"
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
openRegistration: "开放注册"
openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器并在出现问题时能够立即响应时才打开它。"
openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器并在出现问题时能够立即响应时才打开它。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
deliverSuspendedSoftware: "停止投递的软件"
deliverSuspendedSoftwareDescription: "可因安全漏洞之类的原因,停止向指定的服务器及服务器版本送信。版本信息由服务器提供,不保证可靠性。可使用 semver 范围来指定版本,但指定 >= 2024.3.1 将不包括如 2024.3.1-custom.0 等自定义版本,因此建议像 >= 2024.3.1-0 这样指定 prerelease 版本。"
@@ -1640,7 +1644,7 @@ _serverSettings:
allowExternalApRedirect: "允许通过 ActivityPub 重定向查询"
allowExternalApRedirect_description: "启用时,将允许其它服务器通过此服务器查询第三方内容,但有可能导致内容欺骗。"
userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性"
userGeneratedContentsVisibilityForVisitor_description: "对于防止诸如难以审核的不适当的远程内容通过自己的服务器无意中在互联网上公开等问题很有用。"
userGeneratedContentsVisibilityForVisitor_description: "对于防止难以审核的不适当的远程内容等,通过自己的服务器无意中在互联网上公开等问题很有用。"
userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。"
_userGeneratedContentsVisibilityForVisitor:
all: "全部公开"
@@ -2139,7 +2143,7 @@ _wordMute:
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
muteWordsDescription2: "正则表达式用斜线包裹"
_instanceMute:
instanceMuteDescription: "隐藏服务器中所有帖子和转帖,包括这些服务器上用户回复。"
instanceMuteDescription: "隐藏服务器中所有帖子和转帖,包括这些服务器上用户回复。"
instanceMuteDescription2: "一行一个"
title: "下面实例中的帖子将被隐藏。"
heading: "已隐藏的服务器"
@@ -2461,6 +2465,8 @@ _visibility:
disableFederation: "不参与联合"
disableFederationDescription: "不发送到其他服务器"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "还有未上传的文件,要丢弃并关闭窗口吗?"
uploaderTip: "文件还未上传。可以在文件菜单中进行重命名、裁剪、添加水印、设置是否压缩等操作。文件将在发帖时自动上传。"
replyPlaceholder: "回复这个帖子..."
quotePlaceholder: "引用这个帖子..."
channelPlaceholder: "发布到频道…"
@@ -2487,7 +2493,7 @@ _profile:
avatarDecorationMax: "最多可添加 {max} 个挂件"
followedMessage: "被关注时显示的消息"
followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息在请求被批准后显示。"
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息在请求被批准后显示。"
_exportOrImport:
allNotes: "所有帖子"
favoritedNotes: "收藏的帖子"
@@ -3116,3 +3122,52 @@ _clip:
tip: "便签功能可以将帖子合并在一起。"
_userLists:
tip: "可创建包含任意用户的列表。已创建的列表可作为时间线查看。"
watermark: "水印"
defaultPreset: "默认预设"
_watermarkEditor:
tip: "可在图像内增加包含作者等信息的水印。"
quitWithoutSaveConfirm: "不保存就退出吗?"
driveFileTypeWarn: "不支持此文件"
driveFileTypeWarnDescription: "请选择图像文件"
title: "编辑水印"
cover: "覆盖全体"
repeat: "平铺"
opacity: "不透明度"
scale: "大小"
text: "文本"
position: "位置"
type: "类型"
image: "图片"
advanced: "高级"
stripe: "条纹"
stripeWidth: "线条宽度"
stripeFrequency: "线条数量"
angle: "角度"
polkadot: "波点"
checker: "检查"
polkadotMainDotOpacity: "主波点的不透明度"
polkadotMainDotRadius: "主波点的大小"
polkadotSubDotOpacity: "副波点的不透明度"
polkadotSubDotRadius: "副波点的大小"
polkadotSubDotDivisions: "副波点的数量"
_imageEffector:
title: "效果"
addEffect: "添加效果"
discardChangesConfirm: "丢弃当前设置并退出?"
_fxs:
chromaticAberration: "色差"
glitch: "故障"
mirror: "镜像"
invert: "反转颜色"
grayscale: "黑白"
colorAdjust: "色彩校正"
colorClamp: "颜色限制"
colorClampAdvanced: "颜色限制(高级)"
distort: "失真"
threshold: "二值化"
zoomLines: "集中线"
stripe: "条纹"
polkadot: "波点"
checker: "检查"
blockNoise: "块状噪点"
tearing: "撕裂"
+56
View File
@@ -327,6 +327,7 @@ dark: "深色"
lightThemes: "淺色佈景主題"
darkThemes: "深色佈景主題"
syncDeviceDarkMode: "與裝置的深色模式同步"
switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」已開啟。要關閉同步並手動切換模式嗎?\n"
drive: "雲端硬碟"
fileName: "檔案名稱"
selectFile: "選擇檔案"
@@ -1329,6 +1330,7 @@ restore: "還原"
syncBetweenDevices: "裝置之間的同步化"
preferenceSyncConflictTitle: "伺服器上存在設定值"
preferenceSyncConflictText: "已啟用同步的設定項目會將設定值儲存至伺服器,並已找到該設定項目在伺服器上儲存的設定值。請選擇要使用哪個設定值進行覆寫。"
preferenceSyncConflictChoiceMerge: "合併至"
preferenceSyncConflictChoiceServer: "伺服器設定值"
preferenceSyncConflictChoiceDevice: "裝置的設定值"
preferenceSyncConflictChoiceCancel: "取消啟用同步"
@@ -1363,6 +1365,8 @@ abort: "取消"
tip: "提示與技巧"
redisplayAllTips: "重新顯示所有「提示與技巧」"
hideAllTips: "隱藏所有「提示與技巧」"
defaultImageCompressionLevel: "預設的影像壓縮程度"
defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。"
_chat:
noMessagesYet: "尚無訊息"
newMessage: "新訊息"
@@ -1450,6 +1454,7 @@ _settings:
contentsUpdateFrequency_description: "頻率越高,內容更新越即時,但可能會降低效能,並增加資料傳輸量與電池消耗。\n"
contentsUpdateFrequency_description2: "當即時模式開啟時,不論此設定為何,內容都會即時更新。"
showUrlPreview: "顯示網址預覽"
showAvailableReactionsFirstInNote: "將可用的反應顯示在頂部"
_chat:
showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息"
@@ -2460,6 +2465,8 @@ _visibility:
disableFederation: "停用聯邦"
disableFederationDescription: "不發送到其他伺服器"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "尚有未上傳的檔案,確定要放棄並關閉表單嗎?"
uploaderTip: "檔案尚未上傳。您可以從檔案選單中設定重新命名、裁切圖片、加上浮水印、是否壓縮等選項。檔案會在發布貼文時自動上傳。\n"
replyPlaceholder: "回覆此貼文..."
quotePlaceholder: "引用此貼文..."
channelPlaceholder: "發佈到頻道"
@@ -3115,3 +3122,52 @@ _clip:
tip: "摘錄是一項可以用來整理貼文的功能。"
_userLists:
tip: "您可以建立包含任意使用者的清單。建立後的清單可以作為時間軸顯示。\n"
watermark: "浮水印"
defaultPreset: "預設值"
_watermarkEditor:
tip: "可以在圖片中以浮水印加上出處等資訊。"
quitWithoutSaveConfirm: "不儲存就退出嗎?"
driveFileTypeWarn: "不支援此檔案"
driveFileTypeWarnDescription: "請選擇圖片檔案"
title: "編輯浮水印"
cover: "覆蓋整體"
repeat: "佈局"
opacity: "透明度"
scale: "大小"
text: "文字"
position: "位置"
type: "類型"
image: "圖片"
advanced: "進階"
stripe: "條紋"
stripeWidth: "線條寬度"
stripeFrequency: "線條數量"
angle: "角度"
polkadot: "波卡圓點"
checker: "棋盤格"
polkadotMainDotOpacity: "主圓點的不透明度"
polkadotMainDotRadius: "主圓點的尺寸"
polkadotSubDotOpacity: "子圓點的不透明度"
polkadotSubDotRadius: "子圓點的尺寸"
polkadotSubDotDivisions: "子圓點的數量"
_imageEffector:
title: "特效"
addEffect: "新增特效"
discardChangesConfirm: "捨棄更改並退出嗎?"
_fxs:
chromaticAberration: "色差"
glitch: "異常雜訊效果"
mirror: "鏡像"
invert: "反轉色彩"
grayscale: "黑白"
colorAdjust: "色彩校正"
colorClamp: "壓縮色彩"
colorClampAdvanced: "壓縮色彩(進階)"
distort: "變形"
threshold: "閾值"
zoomLines: "速度線"
stripe: "條紋"
polkadot: "波卡圓點"
checker: "棋盤格"
blockNoise: "阻擋雜訊"
tearing: "撕裂"
+13 -13
View File
@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.5.1-beta.6",
"version": "2025.6.4-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.11.0",
"packageManager": "pnpm@10.12.1",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@@ -53,28 +53,28 @@
},
"dependencies": {
"cssnano": "7.0.7",
"esbuild": "0.25.4",
"execa": "9.5.3",
"esbuild": "0.25.5",
"execa": "9.6.0",
"fast-glob": "3.3.3",
"glob": "11.0.2",
"ignore-walk": "7.0.0",
"js-yaml": "4.1.0",
"postcss": "8.5.3",
"postcss": "8.5.4",
"tar": "7.4.3",
"terser": "5.39.2",
"terser": "5.42.0",
"typescript": "5.8.3"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/node": "22.15.21",
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"cross-env": "7.0.3",
"cypress": "14.4.0",
"eslint": "9.27.0",
"globals": "16.1.0",
"cypress": "14.4.1",
"eslint": "9.28.0",
"globals": "16.2.0",
"ncp": "2.0.0",
"pnpm": "10.11.0",
"pnpm": "10.12.1",
"start-server-and-test": "2.0.12"
},
"optionalDependencies": {
+1
View File
@@ -25,6 +25,7 @@ export default [
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'import/order': ['warn', {
groups: [
'builtin',
@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class CreateNoteDraft1736686850345 {
name = 'CreateNoteDraft1736686850345'
async up(queryRunner) {
await queryRunner.query(`
CREATE TABLE "note_draft" (
"id" varchar NOT NULL,
"replyId" varchar NULL,
"renoteId" varchar NULL,
"text" text NULL,
"cw" varchar(512) NULL,
"userId" varchar NOT NULL,
"localOnly" boolean DEFAULT false,
"reactionAcceptance" varchar(64) NULL,
"visibility" varchar NOT NULL,
"fileIds" varchar[] DEFAULT '{}',
"visibleUserIds" varchar[] DEFAULT '{}',
"hashtag" varchar(128) NULL,
"channelId" varchar NULL,
"hasPoll" boolean DEFAULT false,
"pollChoices" varchar(256)[] DEFAULT '{}',
"pollMultiple" boolean NULL,
"pollExpiresAt" TIMESTAMP WITH TIME ZONE NULL,
"pollExpiredAfter" bigint NULL,
PRIMARY KEY ("id")
)`);
await queryRunner.query(`
CREATE INDEX "IDX_NOTE_DRAFT_REPLY_ID" ON "note_draft" ("replyId")
`);
await queryRunner.query(`
CREATE INDEX "IDX_NOTE_DRAFT_RENOTE_ID" ON "note_draft" ("renoteId")
`);
await queryRunner.query(`
CREATE INDEX "IDX_NOTE_DRAFT_USER_ID" ON "note_draft" ("userId")
`);
await queryRunner.query(`
CREATE INDEX "IDX_NOTE_DRAFT_FILE_IDS" ON "note_draft" USING GIN ("fileIds")
`);
await queryRunner.query(`
CREATE INDEX "IDX_NOTE_DRAFT_VISIBLE_USER_IDS" ON "note_draft" USING GIN ("visibleUserIds")
`);
await queryRunner.query(`
CREATE INDEX "IDX_NOTE_DRAFT_CHANNEL_ID" ON "note_draft" ("channelId")
`);
await queryRunner.query(`
ALTER TABLE "note_draft"
ADD CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE
`);
await queryRunner.query(`
ALTER TABLE "note_draft"
ADD CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE
`);
await queryRunner.query(`
ALTER TABLE "note_draft"
ADD CONSTRAINT "FK_NOTE_DRAFT_USER_ID" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE
`);
await queryRunner.query(`
ALTER TABLE "note_draft"
ADD CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE
`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_CHANNEL_ID"`);
await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_USER_ID"`);
await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_RENOTE_ID"`);
await queryRunner.query(`ALTER TABLE "note_draft" DROP CONSTRAINT "FK_NOTE_DRAFT_REPLY_ID"`);
await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_CHANNEL_ID"`);
await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_VISIBLE_USER_IDS"`);
await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_FILE_IDS"`);
await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_USER_ID"`);
await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_RENOTE_ID"`);
await queryRunner.query(`DROP INDEX "IDX_NOTE_DRAFT_REPLY_ID"`);
await queryRunner.query(`DROP TABLE "note_draft"`);
}
}
+29 -29
View File
@@ -37,17 +37,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.11.29",
"@swc/core-darwin-x64": "1.11.29",
"@swc/core-darwin-arm64": "1.12.0",
"@swc/core-darwin-x64": "1.12.0",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.11.29",
"@swc/core-linux-arm64-gnu": "1.11.29",
"@swc/core-linux-arm64-musl": "1.11.29",
"@swc/core-linux-x64-gnu": "1.11.29",
"@swc/core-linux-x64-musl": "1.11.29",
"@swc/core-win32-arm64-msvc": "1.11.29",
"@swc/core-win32-ia32-msvc": "1.11.29",
"@swc/core-win32-x64-msvc": "1.11.29",
"@swc/core-linux-arm-gnueabihf": "1.12.0",
"@swc/core-linux-arm64-gnu": "1.12.0",
"@swc/core-linux-arm64-musl": "1.12.0",
"@swc/core-linux-x64-gnu": "1.12.0",
"@swc/core-linux-x64-musl": "1.12.0",
"@swc/core-win32-arm64-msvc": "1.12.0",
"@swc/core-win32-ia32-msvc": "1.12.0",
"@swc/core-win32-x64-msvc": "1.12.0",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@@ -67,8 +67,8 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.817.0",
"@aws-sdk/lib-storage": "3.817.0",
"@aws-sdk/client-s3": "3.826.0",
"@aws-sdk/lib-storage": "3.826.0",
"@discordapp/twemoji": "15.1.0",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
@@ -80,10 +80,10 @@
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.1",
"@napi-rs/canvas": "0.1.70",
"@nestjs/common": "11.1.2",
"@nestjs/core": "11.1.2",
"@nestjs/testing": "11.1.2",
"@napi-rs/canvas": "0.1.71",
"@nestjs/common": "11.1.3",
"@nestjs/core": "11.1.3",
"@nestjs/testing": "11.1.3",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.55.0",
"@sentry/profiling-node": "8.55.0",
@@ -91,7 +91,7 @@
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.7",
"@swc/core": "1.11.29",
"@swc/core": "1.12.0",
"@twemoji/parser": "15.1.1",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
@@ -101,7 +101,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.53.0",
"bullmq": "5.53.2",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.4.1",
@@ -117,7 +117,7 @@
"feed": "4.2.2",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.2",
"form-data": "4.0.3",
"got": "14.4.7",
"happy-dom": "16.8.1",
"hpagent": "1.2.0",
@@ -133,9 +133,9 @@
"jsonld": "8.3.3",
"jsrsasign": "11.1.0",
"juice": "11.0.1",
"meilisearch": "0.50.0",
"meilisearch": "0.51.0",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"microformats-parser": "2.0.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
@@ -188,16 +188,16 @@
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.18",
"@sentry/vue": "9.22.0",
"@nestjs/platform-express": "10.4.19",
"@sentry/vue": "9.28.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.38",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.3",
"@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5",
"@types/body-parser": "1.19.6",
"@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.8",
"@types/content-disposition": "0.5.9",
"@types/fluent-ffmpeg": "2.1.27",
"@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.7",
@@ -208,12 +208,12 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.15.21",
"@types/node": "22.15.31",
"@types/nodemailer": "6.4.17",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.15.2",
"@types/pg": "8.15.4",
"@types/pug": "2.0.10",
"@types/qrcode": "1.5.5",
"@types/random-seed": "0.3.5",
@@ -229,8 +229,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",
+10
View File
@@ -331,6 +331,16 @@ export class ChatService {
await redisPipeline.exec();
}
@bindThis
public async readAllChatMessages(
readerId: MiUser['id'],
): Promise<void> {
const redisPipeline = this.redisClient.pipeline();
// TODO: newUserChatMessageExists とか newRoomChatMessageExists も消したい(けどキーの列挙が必要になって面倒)
redisPipeline.del(`newChatMessagesExists:${readerId}`);
await redisPipeline.exec();
}
@bindThis
public findMessageById(messageId: MiChatMessage['id']) {
return this.chatMessagesRepository.findOneBy({ id: messageId });
+12
View File
@@ -44,6 +44,7 @@ import { ModerationLogService } from './ModerationLogService.js';
import { NoteCreateService } from './NoteCreateService.js';
import { NoteDeleteService } from './NoteDeleteService.js';
import { NotePiningService } from './NotePiningService.js';
import { NoteDraftService } from './NoteDraftService.js';
import { NotificationService } from './NotificationService.js';
import { PollService } from './PollService.js';
import { PushNotificationService } from './PushNotificationService.js';
@@ -118,6 +119,7 @@ import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.
import { NoteEntityService } from './entities/NoteEntityService.js';
import { NoteFavoriteEntityService } from './entities/NoteFavoriteEntityService.js';
import { NoteReactionEntityService } from './entities/NoteReactionEntityService.js';
import { NoteDraftEntityService } from './entities/NoteDraftEntityService.js';
import { NotificationEntityService } from './entities/NotificationEntityService.js';
import { PageEntityService } from './entities/PageEntityService.js';
import { PageLikeEntityService } from './entities/PageLikeEntityService.js';
@@ -185,6 +187,7 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
const $NoteDraftService: Provider = { provide: 'NoteDraftService', useExisting: NoteDraftService };
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService };
@@ -266,6 +269,7 @@ const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityServi
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
const $NoteFavoriteEntityService: Provider = { provide: 'NoteFavoriteEntityService', useExisting: NoteFavoriteEntityService };
const $NoteReactionEntityService: Provider = { provide: 'NoteReactionEntityService', useExisting: NoteReactionEntityService };
const $NoteDraftEntityService: Provider = { provide: 'NoteDraftEntityService', useExisting: NoteDraftEntityService };
const $NotificationEntityService: Provider = { provide: 'NotificationEntityService', useExisting: NotificationEntityService };
const $PageEntityService: Provider = { provide: 'PageEntityService', useExisting: PageEntityService };
const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', useExisting: PageLikeEntityService };
@@ -335,6 +339,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService,
NoteDeleteService,
NotePiningService,
NoteDraftService,
NotificationService,
PollService,
SystemAccountService,
@@ -416,6 +421,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteEntityService,
NoteFavoriteEntityService,
NoteReactionEntityService,
NoteDraftEntityService,
NotificationEntityService,
PageEntityService,
PageLikeEntityService,
@@ -481,6 +487,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService,
$NoteDeleteService,
$NotePiningService,
$NoteDraftService,
$NotificationService,
$PollService,
$SystemAccountService,
@@ -562,6 +569,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteEntityService,
$NoteFavoriteEntityService,
$NoteReactionEntityService,
$NoteDraftEntityService,
$NotificationEntityService,
$PageEntityService,
$PageLikeEntityService,
@@ -628,6 +636,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteCreateService,
NoteDeleteService,
NotePiningService,
NoteDraftService,
NotificationService,
PollService,
SystemAccountService,
@@ -708,6 +717,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
NoteEntityService,
NoteFavoriteEntityService,
NoteReactionEntityService,
NoteDraftEntityService,
NotificationEntityService,
PageEntityService,
PageLikeEntityService,
@@ -773,6 +783,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteCreateService,
$NoteDeleteService,
$NotePiningService,
$NoteDraftService,
$NotificationService,
$PollService,
$SystemAccountService,
@@ -852,6 +863,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$NoteEntityService,
$NoteFavoriteEntityService,
$NoteReactionEntityService,
$NoteDraftEntityService,
$NotificationEntityService,
$PageEntityService,
$PageLikeEntityService,
+3 -3
View File
@@ -803,14 +803,14 @@ export class DriveService {
await Promise.all(promises);
}
this.deletePostProcess(file, isExpired, deleter);
await this.deletePostProcess(file, isExpired, deleter);
}
@bindThis
private async deletePostProcess(file: MiDriveFile, isExpired = false, deleter?: MiUser) {
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
this.driveFilesRepository.update(file.id, {
await this.driveFilesRepository.update(file.id, {
isLink: true,
url: file.uri,
thumbnailUrl: null,
@@ -822,7 +822,7 @@ export class DriveService {
webpublicAccessKey: 'webpublic-' + randomUUID(),
});
} else {
this.driveFilesRepository.delete(file.id);
await this.driveFilesRepository.delete(file.id);
}
this.driveChart.update(file, false);
@@ -0,0 +1,314 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import type { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { DI } from '@/di-symbols.js';
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { IPoll } from '@/models/Poll.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRenote, isQuote } from '@/misc/is-renote.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
export type NoteDraftOptions = {
replyId?: MiNote['id'] | null;
renoteId?: MiNote['id'] | null;
text?: string | null;
cw?: string | null;
localOnly?: boolean | null;
reactionAcceptance?: typeof noteReactionAcceptances[number];
visibility?: typeof noteVisibilities[number];
fileIds?: MiDriveFile['id'][];
visibleUserIds?: MiUser['id'][];
hashtag?: string;
channelId?: MiChannel['id'] | null;
poll?: (IPoll & { expiredAfter?: number | null }) | null;
};
@Injectable()
export class NoteDraftService {
constructor(
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.noteDraftsRepository)
private noteDraftsRepository: NoteDraftsRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
private roleService: RoleService,
private idService: IdService,
private noteEntityService: NoteEntityService,
) {
}
@bindThis
public async get(me: MiLocalUser, draftId: MiNoteDraft['id']): Promise<MiNoteDraft | null> {
const draft = await this.noteDraftsRepository.findOneBy({
id: draftId,
userId: me.id,
});
return draft;
}
@bindThis
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
//#region check draft limit
const currentCount = await this.noteDraftsRepository.countBy({
userId: me.id,
});
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) {
throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts');
}
//#endregion
if (data.poll) {
if (typeof data.poll.expiresAt === 'number') {
if (data.poll.expiresAt < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
}
} else if (typeof data.poll.expiredAfter === 'number') {
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
}
}
const appliedDraft = await this.checkAndSetDraftNoteOptions(me, this.noteDraftsRepository.create(), data);
appliedDraft.id = this.idService.gen();
appliedDraft.userId = me.id;
const draft = this.noteDraftsRepository.save(appliedDraft);
return draft;
}
@bindThis
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise<MiNoteDraft> {
const draft = await this.noteDraftsRepository.findOneBy({
id: draftId,
userId: me.id,
});
if (draft == null) {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
}
if (data.poll) {
if (typeof data.poll.expiresAt === 'number') {
if (data.poll.expiresAt < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
}
} else if (typeof data.poll.expiredAfter === 'number') {
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
}
}
const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data);
return await this.noteDraftsRepository.save(appliedDraft);
}
@bindThis
public async delete(me: MiLocalUser, draftId: MiNoteDraft['id']): Promise<void> {
const draft = await this.noteDraftsRepository.findOneBy({
id: draftId,
userId: me.id,
});
if (draft == null) {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
}
await this.noteDraftsRepository.delete(draft.id);
}
@bindThis
public async getDraft(me: MiLocalUser, draftId: MiNoteDraft['id']): Promise<MiNoteDraft> {
const draft = await this.noteDraftsRepository.findOneBy({
id: draftId,
userId: me.id,
});
if (draft == null) {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
}
return draft;
}
// 関連エンティティを取得し紐づける部分を共通化する
@bindThis
public async checkAndSetDraftNoteOptions(
me: MiLocalUser,
draft: MiNoteDraft,
data: NoteDraftOptions,
): Promise<MiNoteDraft> {
data.visibility ??= 'public';
data.localOnly ??= false;
if (data.reactionAcceptance === undefined) data.reactionAcceptance = null;
if (data.channelId != null) {
data.visibility = 'public';
data.visibleUserIds = [];
data.localOnly = true;
}
let appliedDraft = draft;
//#region visibleUsers
let visibleUsers: MiUser[] = [];
if (data.visibleUserIds != null) {
visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds),
});
}
//#endregion
//#region files
let files: MiDriveFile[] = [];
const fileIds = data.fileIds ?? null;
if (fileIds != null) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id,
fileIds: fileIds,
})
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
if (files.length !== fileIds.length) {
throw new IdentifiableError('b6992544-63e7-67f0-fa7f-32444b1b5306', 'No such drive file');
}
}
//#endregion
//#region renote
let renote: MiNote | null = null;
if (data.renoteId != null) {
renote = await this.notesRepository.findOneBy({ id: data.renoteId });
if (renote == null) {
throw new IdentifiableError('64929870-2540-4d11-af41-3b484d78c956', 'No such renote');
} else if (isRenote(renote) && !isQuote(renote)) {
throw new IdentifiableError('76cc5583-5a14-4ad3-8717-0298507e32db', 'Cannot renote');
}
// Check blocking
if (renote.userId !== me.id) {
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: renote.userId,
blockeeId: me.id,
},
});
if (blockExist) {
throw new IdentifiableError('075ca298-e6e7-485a-b570-51a128bb5168', 'You have been blocked by the user');
}
}
if (renote.visibility === 'followers' && renote.userId !== me.id) {
// 他人のfollowers noteはreject
throw new IdentifiableError('81eb8188-aea1-4e35-9a8f-3334a3be9855', 'Cannot Renote Due to Visibility');
} else if (renote.visibility === 'specified') {
// specified / direct noteはreject
throw new IdentifiableError('81eb8188-aea1-4e35-9a8f-3334a3be9855', 'Cannot Renote Due to Visibility');
}
if (renote.channelId && renote.channelId !== data.channelId) {
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
// リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
if (renoteChannel == null) {
// リノートしたいノートが書き込まれているチャンネルがない
throw new IdentifiableError('6815399a-6f13-4069-b60d-ed5156249d12', 'No such channel');
} else if (!renoteChannel.allowRenoteToExternal) {
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
throw new IdentifiableError('ed1952ac-2d26-4957-8b30-2deda76bedf7', 'Cannot Renote to External');
}
}
}
//#endregion
//#region reply
let reply: MiNote | null = null;
if (data.replyId != null) {
// Fetch reply
reply = await this.notesRepository.findOneBy({ id: data.replyId });
if (reply == null) {
throw new IdentifiableError('c4721841-22fc-4bb7-ad3d-897ef1d375b5', 'No such reply');
} else if (isRenote(reply) && !isQuote(reply)) {
throw new IdentifiableError('e6c10b57-2c09-4da3-bd4d-eda05d51d140', 'Cannot reply To Pure Renote');
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
throw new IdentifiableError('593c323c-6b6a-4501-a25c-2f36bd2a93d6', 'Cannot reply To Invisible Note');
} else if (reply.visibility === 'specified' && data.visibility !== 'specified') {
throw new IdentifiableError('215dbc76-336c-4d2a-9605-95766ba7dab0', 'Cannot reply To Specified Note With Extended Visibility');
}
// Check blocking
if (reply.userId !== me.id) {
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: me.id,
},
});
if (blockExist) {
throw new IdentifiableError('075ca298-e6e7-485a-b570-51a128bb5168', 'You have been blocked by the user');
}
}
}
//#endregion
//#region channel
let channel: MiChannel | null = null;
if (data.channelId != null) {
channel = await this.channelsRepository.findOneBy({ id: data.channelId, isArchived: false });
if (channel == null) {
throw new IdentifiableError('6815399a-6f13-4069-b60d-ed5156249d12', 'No such channel');
}
}
//#endregion
appliedDraft = {
...appliedDraft,
visibility: data.visibility,
cw: data.cw ?? null,
fileIds: fileIds ?? [],
replyId: data.replyId ?? null,
renoteId: data.renoteId ?? null,
channelId: data.channelId ?? null,
text: data.text ?? null,
hashtag: data.hashtag ?? null,
hasPoll: data.poll != null,
pollChoices: data.poll ? data.poll.choices : [],
pollMultiple: data.poll ? data.poll.multiple : false,
pollExpiresAt: data.poll ? data.poll.expiresAt : null,
pollExpiredAfter: data.poll ? data.poll.expiredAfter ?? null : null,
visibleUserIds: data.visibleUserIds ?? [],
localOnly: data.localOnly,
reactionAcceptance: data.reactionAcceptance,
} satisfies MiNoteDraft;
return appliedDraft;
}
}
+3
View File
@@ -66,6 +66,7 @@ export type RolePolicies = {
canImportUserLists: boolean;
chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[];
noteDraftLimit: number;
};
export const DEFAULT_POLICIES: RolePolicies = {
@@ -109,6 +110,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
'video/*',
'audio/*',
],
noteDraftLimit: 10,
};
@Injectable()
@@ -430,6 +432,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
return [...set];
}),
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
};
}
@@ -0,0 +1,177 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { Packed } from '@/misc/json-schema.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiUser, MiNote, MiNoteDraft } from '@/models/_.js';
import type { NoteDraftsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
@Injectable()
export class NoteDraftEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private driveFileEntityService: DriveFileEntityService;
private idService: IdService;
private noteEntityService: NoteEntityService;
private noteDraftLoader = new DebounceLoader(this.findNoteDraftOrFail);
constructor(
private moduleRef: ModuleRef,
@Inject(DI.noteDraftsRepository)
private noteDraftsRepository: NoteDraftsRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
) {
}
onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
this.idService = this.moduleRef.get('IdService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
}
@bindThis
public async packAttachedFiles(fileIds: MiNote['fileIds'], packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>): Promise<Packed<'DriveFile'>[]> {
const missingIds = [];
for (const id of fileIds) {
if (!packedFiles.has(id)) missingIds.push(id);
}
if (missingIds.length) {
const additionalMap = await this.driveFileEntityService.packManyByIdsMap(missingIds);
for (const [k, v] of additionalMap) {
packedFiles.set(k, v);
}
}
return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
}
@bindThis
public async pack(
src: MiNoteDraft['id'] | MiNoteDraft,
me?: { id: MiUser['id'] } | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
withReactionAndUserPairCache?: boolean;
_hint_?: {
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
};
},
): Promise<Packed<'NoteDraft'>> {
const opts = Object.assign({
detail: true,
}, options);
const noteDraft = typeof src === 'object' ? src : await this.noteDraftLoader.load(src);
const text = noteDraft.text;
const channel = noteDraft.channelId
? noteDraft.channel
? noteDraft.channel
: await this.channelsRepository.findOneBy({ id: noteDraft.channelId })
: null;
const packedFiles = options?._hint_?.packedFiles;
const packedUsers = options?._hint_?.packedUsers;
const packed: Packed<'NoteDraft'> = await awaitAll({
id: noteDraft.id,
createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
userId: noteDraft.userId,
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
text: text,
cw: noteDraft.cw,
visibility: noteDraft.visibility,
localOnly: noteDraft.localOnly,
reactionAcceptance: noteDraft.reactionAcceptance,
visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined,
hashtag: noteDraft.hashtag ?? undefined,
fileIds: noteDraft.fileIds,
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
replyId: noteDraft.replyId,
renoteId: noteDraft.renoteId,
channelId: noteDraft.channelId ?? undefined,
channel: channel ? {
id: channel.id,
name: channel.name,
color: channel.color,
isSensitive: channel.isSensitive,
allowRenoteToExternal: channel.allowRenoteToExternal,
userId: channel.userId,
} : undefined,
...(opts.detail ? {
reply: noteDraft.replyId ? this.noteEntityService.pack(noteDraft.replyId, me, {
detail: false,
skipHide: opts.skipHide,
}) : undefined,
renote: noteDraft.renoteId ? this.noteEntityService.pack(noteDraft.renoteId, me, {
detail: true,
skipHide: opts.skipHide,
}) : undefined,
poll: noteDraft.hasPoll ? {
choices: noteDraft.pollChoices,
multiple: noteDraft.pollMultiple,
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
expiredAfter: noteDraft.pollExpiredAfter,
} : undefined,
} : {} ),
});
return packed;
}
@bindThis
public async packMany(
noteDrafts: MiNoteDraft[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
detail?: boolean;
},
) {
if (noteDrafts.length === 0) return [];
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = noteDrafts.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
const users = [
...noteDrafts.map(({ user, userId }) => user ?? userId),
];
const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(noteDrafts.map(n => this.pack(n, me, {
...options,
_hint_: {
packedFiles,
packedUsers,
},
})));
}
@bindThis
private findNoteDraftOrFail(id: string): Promise<MiNoteDraft> {
return this.noteDraftsRepository.findOneOrFail({
where: { id },
relations: ['user'],
});
}
}
+1
View File
@@ -89,5 +89,6 @@ export const DI = {
chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'),
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
reversiGamesRepository: Symbol('reversiGamesRepository'),
noteDraftsRepository: Symbol('noteDraftsRepository'),
//#endregion
};
+2
View File
@@ -72,6 +72,7 @@ import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js';
import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js';
export const refs = {
UserLite: packedUserLiteSchema,
@@ -89,6 +90,7 @@ export const refs = {
Announcement: packedAnnouncementSchema,
App: packedAppSchema,
Note: packedNoteSchema,
NoteDraft: packedNoteDraftSchema,
NoteReaction: packedNoteReactionSchema,
NoteFavorite: packedNoteFavoriteSchema,
Notification: packedNotificationSchema,
+2 -2
View File
@@ -4,7 +4,7 @@
*/
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { noteVisibilities } from '@/types.js';
import { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
@@ -96,7 +96,7 @@ export class MiNote {
@Column('varchar', {
length: 64, nullable: true,
})
public reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
public reactionAcceptance: typeof noteReactionAcceptances[number];
@Column('smallint', {
default: 0,
+157
View File
@@ -0,0 +1,157 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
import { MiNote } from './Note.js';
import type { MiDriveFile } from './DriveFile.js';
@Entity('note_draft')
export class MiNoteDraft {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of reply target.',
})
public replyId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
public reply: MiNote | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of renote target.',
})
public renoteId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
public renote: MiNote | null;
// TODO: varcharにしたい(Note.tsと同じ)
@Column('text', {
nullable: true,
})
public text: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public cw: string | null;
@Index()
@Column({
...id(),
comment: 'The ID of author.',
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Column('boolean', {
default: false,
})
public localOnly: boolean;
@Column('varchar', {
length: 64, nullable: true,
})
public reactionAcceptance: typeof noteReactionAcceptances[number];
/**
* public ...
* home ... ()
* followers ...
* specified ... visibleUserIds
*/
@Column('enum', { enum: noteVisibilities })
public visibility: typeof noteVisibilities[number];
@Index('IDX_NOTE_DRAFT_FILE_IDS', { synchronize: false })
@Column({
...id(),
array: true, default: '{}',
})
public fileIds: MiDriveFile['id'][];
@Index('IDX_NOTE_DRAFT_VISIBLE_USER_IDS', { synchronize: false })
@Column({
...id(),
array: true, default: '{}',
})
public visibleUserIds: MiUser['id'][];
@Column('varchar', {
length: 128, nullable: true,
})
public hashtag: string | null;
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of source channel.',
})
public channelId: MiChannel['id'] | null;
@ManyToOne(type => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
public channel: MiChannel | null;
// 以下、Pollについて追加
@Column('boolean', {
default: false,
})
public hasPoll: boolean;
@Column('varchar', {
length: 256, array: true, default: '{}',
})
public pollChoices: string[];
@Column('boolean')
public pollMultiple: boolean;
@Column('timestamp with time zone', {
nullable: true,
})
public pollExpiresAt: Date | null;
@Column('bigint', {
nullable: true,
})
public pollExpiredAfter: number | null;
// ここまで追加
constructor(data: Partial<MiNoteDraft>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}
@@ -42,6 +42,7 @@ import {
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
MiNoteDraft,
MiPage,
MiPageLike,
MiPasswordResetRequest,
@@ -140,6 +141,12 @@ const $noteReactionsRepository: Provider = {
inject: [DI.db],
};
const $noteDraftsRepository: Provider = {
provide: DI.noteDraftsRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteDraft).extend(miRepository as MiRepository<MiNoteDraft>),
inject: [DI.db],
};
const $pollsRepository: Provider = {
provide: DI.pollsRepository,
useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>),
@@ -542,6 +549,7 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
$noteDraftsRepository,
$pollsRepository,
$pollVotesRepository,
$userProfilesRepository,
@@ -618,6 +626,7 @@ const $reversiGamesRepository: Provider = {
$noteFavoritesRepository,
$noteThreadMutingsRepository,
$noteReactionsRepository,
$noteDraftsRepository,
$pollsRepository,
$pollVotesRepository,
$userProfilesRepository,
+3
View File
@@ -55,6 +55,7 @@ import { MiMeta } from '@/models/Meta.js';
import { MiModerationLog } from '@/models/ModerationLog.js';
import { MiMuting } from '@/models/Muting.js';
import { MiNote } from '@/models/Note.js';
import { MiNoteDraft } from '@/models/NoteDraft.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
@@ -188,6 +189,7 @@ export {
MiMuting,
MiRenoteMuting,
MiNote,
MiNoteDraft,
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
@@ -266,6 +268,7 @@ export type ModerationLogsRepository = Repository<MiModerationLog> & MiRepositor
export type MutingsRepository = Repository<MiMuting> & MiRepository<MiMuting>;
export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<MiRenoteMuting>;
export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
export type NoteDraftsRepository = Repository<MiNoteDraft> & MiRepository<MiNoteDraft>;
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
@@ -0,0 +1,169 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedNoteDraftSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
text: {
type: 'string',
optional: false, nullable: true,
},
cw: {
type: 'string',
optional: true, nullable: true,
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
user: {
type: 'object',
ref: 'UserLite',
optional: false, nullable: false,
},
replyId: {
type: 'string',
optional: true, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
renoteId: {
type: 'string',
optional: true, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
reply: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
},
renote: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
},
visibility: {
type: 'string',
optional: false, nullable: false,
enum: ['public', 'home', 'followers', 'specified'],
},
visibleUserIds: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
fileIds: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
files: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'DriveFile',
},
},
hashtag: {
type: 'string',
optional: true, nullable: false,
},
poll: {
type: 'object',
optional: true, nullable: true,
properties: {
expiresAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
expiredAfter: {
type: 'number',
optional: true, nullable: true,
},
multiple: {
type: 'boolean',
optional: false, nullable: false,
},
choices: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
},
},
channelId: {
type: 'string',
optional: true, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
channel: {
type: 'object',
optional: true, nullable: true,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
color: {
type: 'string',
optional: false, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
allowRenoteToExternal: {
type: 'boolean',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
},
},
localOnly: {
type: 'boolean',
optional: true, nullable: false,
},
reactionAcceptance: {
type: 'string',
optional: false, nullable: true,
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
},
},
} as const;
@@ -309,6 +309,10 @@ export const packedRolePoliciesSchema = {
optional: false, nullable: false,
enum: ['available', 'readonly', 'unavailable'],
},
noteDraftLimit: {
type: 'integer',
optional: false, nullable: false,
},
},
} as const;
+2
View File
@@ -45,6 +45,7 @@ import { MiNote } from '@/models/Note.js';
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
import { MiNoteReaction } from '@/models/NoteReaction.js';
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
import { MiNoteDraft } from '@/models/NoteDraft.js';
import { MiPage } from '@/models/Page.js';
import { MiPageLike } from '@/models/PageLike.js';
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
@@ -210,6 +211,7 @@ export const entities = [
MiNoteFavorite,
MiNoteReaction,
MiNoteThreadMuting,
MiNoteDraft,
MiPage,
MiPageLike,
MiGalleryPost,
@@ -34,6 +34,11 @@ export class CleanRemoteFilesProcessorService {
let deletedCount = 0;
let cursor: MiDriveFile['id'] | null = null;
const total = await this.driveFilesRepository.countBy({
userHost: Not(IsNull()),
isLink: false,
});
while (true) {
const files = await this.driveFilesRepository.find({
where: {
@@ -58,12 +63,7 @@ export class CleanRemoteFilesProcessorService {
deletedCount += 8;
const total = await this.driveFilesRepository.countBy({
userHost: Not(IsNull()),
isLink: false,
});
job.updateProgress(100 / total * deletedCount);
job.updateProgress(deletedCount * total / 100);
}
this.logger.succ('All cached remote files has been deleted.');
@@ -43,6 +43,10 @@ export class DeleteDriveFilesProcessorService {
let deletedCount = 0;
let cursor: MiDriveFile['id'] | null = null;
const total = await this.driveFilesRepository.countBy({
userId: user.id,
});
while (true) {
const files = await this.driveFilesRepository.find({
where: {
@@ -67,11 +71,7 @@ export class DeleteDriveFilesProcessorService {
deletedCount++;
}
const total = await this.driveFilesRepository.countBy({
userId: user.id,
});
job.updateProgress(deletedCount / total);
job.updateProgress(deletedCount / total * 100);
}
this.logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`);
@@ -58,6 +58,10 @@ export class ExportBlockingProcessorService {
let exportedCount = 0;
let cursor: MiBlocking['id'] | null = null;
const total = await this.blockingsRepository.countBy({
blockerId: user.id,
});
while (true) {
const blockings = await this.blockingsRepository.find({
where: {
@@ -97,11 +101,7 @@ export class ExportBlockingProcessorService {
exportedCount++;
}
const total = await this.blockingsRepository.countBy({
blockerId: user.id,
});
job.updateProgress(exportedCount / total);
job.updateProgress(exportedCount / total * 100);
}
stream.end();
@@ -95,6 +95,10 @@ export class ExportClipsProcessorService {
let exportedClipsCount = 0;
let cursor: MiClip['id'] | null = null;
const total = await this.clipsRepository.countBy({
userId: user.id,
});
while (true) {
const clips = await this.clipsRepository.find({
where: {
@@ -126,11 +130,7 @@ export class ExportClipsProcessorService {
exportedClipsCount++;
}
const total = await this.clipsRepository.countBy({
userId: user.id,
});
job.updateProgress(exportedClipsCount / total);
job.updateProgress(exportedClipsCount / total * 100);
}
}
@@ -78,6 +78,10 @@ export class ExportFavoritesProcessorService {
let exportedFavoritesCount = 0;
let cursor: MiNoteFavorite['id'] | null = null;
const total = await this.noteFavoritesRepository.countBy({
userId: user.id,
});
while (true) {
const favorites = await this.noteFavoritesRepository.find({
where: {
@@ -109,11 +113,7 @@ export class ExportFavoritesProcessorService {
exportedFavoritesCount++;
}
const total = await this.noteFavoritesRepository.countBy({
userId: user.id,
});
job.updateProgress(exportedFavoritesCount / total);
job.updateProgress(exportedFavoritesCount / total * 100);
}
await write(']');
@@ -58,6 +58,10 @@ export class ExportMutingProcessorService {
let exportedCount = 0;
let cursor: MiMuting['id'] | null = null;
const total = await this.mutingsRepository.countBy({
muterId: user.id,
});
while (true) {
const mutes = await this.mutingsRepository.find({
where: {
@@ -98,11 +102,7 @@ export class ExportMutingProcessorService {
exportedCount++;
}
const total = await this.mutingsRepository.countBy({
muterId: user.id,
});
job.updateProgress(exportedCount / total);
job.updateProgress(exportedCount / total * 100);
}
stream.end();
@@ -37,6 +37,8 @@ class NoteStream extends ReadableStream<Record<string, unknown>> {
let exportedNotesCount = 0;
let cursor: MiNote['id'] | null = null;
const totalPromise = notesRepository.countBy({ userId });
const serialize = (
note: MiNote,
poll: MiPoll | null,
@@ -88,8 +90,8 @@ class NoteStream extends ReadableStream<Record<string, unknown>> {
exportedNotesCount++;
}
const total = await notesRepository.countBy({ userId });
job.updateProgress(exportedNotesCount / total);
const total = await totalPromise;
job.updateProgress(exportedNotesCount / total * 100);
},
});
}
@@ -482,9 +482,19 @@ export class ActivityPubServerService {
return true;
},
dbFallback: async (untilId, sinceId, limit) => {
return await this.getUserNotesFromDb(sinceId, untilId, limit, user.id);
return await this.getUserNotesFromDb({
untilId,
sinceId,
limit,
userId: user.id,
});
},
}) : await this.getUserNotesFromDb(sinceId ?? null, untilId ?? null, limit, user.id);
}) : await this.getUserNotesFromDb({
untilId: untilId ?? null,
sinceId: sinceId ?? null,
limit,
userId: user.id,
});
if (sinceId) notes.reverse();
@@ -523,16 +533,21 @@ export class ActivityPubServerService {
}
@bindThis
private async getUserNotesFromDb(untilId: string | null, sinceId: string | null, limit: number, userId: MiUser['id']) {
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), sinceId, untilId)
.andWhere('note.userId = :userId', { userId })
private async getUserNotesFromDb(ps: {
untilId: string | null,
sinceId: string | null,
limit: number,
userId: MiUser['id'],
}) {
return await this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.userId = :userId', { userId: ps.userId })
.andWhere(new Brackets(qb => {
qb
.where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\'');
}))
.andWhere('note.localOnly = FALSE')
.limit(limit)
.limit(ps.limit)
.getMany();
}
@@ -307,6 +307,11 @@ export * as 'notes/clips' from './endpoints/notes/clips.js';
export * as 'notes/conversation' from './endpoints/notes/conversation.js';
export * as 'notes/create' from './endpoints/notes/create.js';
export * as 'notes/delete' from './endpoints/notes/delete.js';
export * as 'notes/drafts/list' from './endpoints/notes/drafts/list.js';
export * as 'notes/drafts/create' from './endpoints/notes/drafts/create.js';
export * as 'notes/drafts/delete' from './endpoints/notes/drafts/delete.js';
export * as 'notes/drafts/update' from './endpoints/notes/drafts/update.js';
export * as 'notes/drafts/count' from './endpoints/notes/drafts/count.js';
export * as 'notes/favorites/create' from './endpoints/notes/favorites/create.js';
export * as 'notes/favorites/delete' from './endpoints/notes/favorites/delete.js';
export * as 'notes/featured' from './endpoints/notes/featured.js';
@@ -428,4 +433,5 @@ export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitat
export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
export * as 'chat/history' from './endpoints/chat/history.js';
export * as 'chat/read-all' from './endpoints/chat/read-all.js';
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['chat'],
requireCredential: true,
kind: 'write:chat',
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.readAllChatMessages(me.id);
});
}
}
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NoteDraftsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes', 'drafts'],
requireCredential: true,
prohibitMoved: true,
kind: 'read:account',
res: {
type: 'number',
optional: false, nullable: false,
description: 'The number of drafts',
},
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.noteDraftsRepository)
private noteDraftsRepository: NoteDraftsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const count = await this.noteDraftsRepository.createQueryBuilder('drafts')
.where('drafts.userId = :meId', { meId: me.id })
.getCount();
return count;
});
}
}
@@ -0,0 +1,258 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteDraftService } from '@/core/NoteDraftService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { ApiError } from '@/server/api/error.js';
import { NoteDraftEntityService } from '@/core/entities/NoteDraftEntityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
export const meta = {
tags: ['notes', 'drafts'],
requireCredential: true,
prohibitMoved: true,
kind: 'write:account',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
createdDraft: {
type: 'object',
optional: false, nullable: false,
ref: 'NoteDraft',
},
},
},
errors: {
noSuchRenoteTarget: {
message: 'No such renote target.',
code: 'NO_SUCH_RENOTE_TARGET',
id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4',
},
cannotReRenote: {
message: 'You can not Renote a pure Renote.',
code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
},
cannotRenoteDueToVisibility: {
message: 'You can not Renote due to target visibility.',
code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY',
id: 'be9529e9-fe72-4de0-ae43-0b363c4938af',
},
noSuchReplyTarget: {
message: 'No such reply target.',
code: 'NO_SUCH_REPLY_TARGET',
id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
},
cannotReplyToInvisibleNote: {
message: 'You cannot reply to an invisible Note.',
code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE',
id: 'b98980fa-3780-406c-a935-b6d0eeee10d1',
},
cannotReplyToPureRenote: {
message: 'You can not reply to a pure Renote.',
code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
},
cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
message: 'You cannot reply to a specified visibility note with extended visibility.',
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
id: 'ed940410-535c-4d5e-bfa3-af798671e93c',
},
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
id: '04da457d-b083-4055-9082-955525eda5a5',
},
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb',
},
youHaveBeenBlocked: {
message: 'You have been blocked by this user.',
code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
},
noSuchFile: {
message: 'Some files are not found.',
code: 'NO_SUCH_FILE',
id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
},
cannotRenoteOutsideOfChannel: {
message: 'Cannot renote outside of channel.',
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
id: '33510210-8452-094c-6227-4a6c05d99f00',
},
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
},
containsTooManyMentions: {
message: 'Cannot post because it exceeds the allowed number of mentions.',
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},
tooManyDrafts: {
message: 'You cannot create drafts any more.',
code: 'TOO_MANY_DRAFTS',
id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8',
},
cannotRenoteToExternal: {
message: 'Cannot Renote to External.',
code: 'CANNOT_RENOTE_TO_EXTERNAL',
id: 'ed1952ac-2d26-4957-8b30-2deda76bedf7',
},
},
limit: {
duration: ms('1hour'),
max: 300,
},
} as const;
export const paramDef = {
type: 'object',
properties: {
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
hashtag: { type: 'string', nullable: true, maxLength: 200 },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
// anyOf内にバリデーションを書いても最初の一つしかチェックされない
text: {
type: 'string',
minLength: 0,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true,
},
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 0,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private noteDraftService: NoteDraftService,
private noteDraftEntityService: NoteDraftEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.create(me, {
fileIds: ps.fileIds,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiredAfter: ps.poll.expiredAfter ?? null,
} : undefined,
text: ps.text ?? null,
replyId: ps.replyId ?? undefined,
renoteId: ps.renoteId ?? undefined,
cw: ps.cw ?? null,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? undefined,
}).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
case '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8':
throw new ApiError(meta.errors.tooManyDrafts);
case '04da457d-b083-4055-9082-955525eda5a5':
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
case 'b6992544-63e7-67f0-fa7f-32444b1b5306':
throw new ApiError(meta.errors.noSuchFile);
case '64929870-2540-4d11-af41-3b484d78c956':
throw new ApiError(meta.errors.noSuchRenoteTarget);
case '76cc5583-5a14-4ad3-8717-0298507e32db':
throw new ApiError(meta.errors.cannotReRenote);
case '075ca298-e6e7-485a-b570-51a128bb5168':
throw new ApiError(meta.errors.youHaveBeenBlocked);
case '81eb8188-aea1-4e35-9a8f-3334a3be9855':
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
case '6815399a-6f13-4069-b60d-ed5156249d12':
throw new ApiError(meta.errors.noSuchChannel);
case 'ed1952ac-2d26-4957-8b30-2deda76bedf7':
throw new ApiError(meta.errors.cannotRenoteToExternal);
case 'c4721841-22fc-4bb7-ad3d-897ef1d375b5':
throw new ApiError(meta.errors.noSuchReplyTarget);
case 'e6c10b57-2c09-4da3-bd4d-eda05d51d140':
throw new ApiError(meta.errors.cannotReplyToPureRenote);
case '593c323c-6b6a-4501-a25c-2f36bd2a93d6':
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
default:
throw err;
}
}
throw err;
});
const createdDraft = await this.noteDraftEntityService.pack(draft, me);
return {
createdDraft,
};
});
}
}
@@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteDraftService } from '@/core/NoteDraftService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['notes', 'drafts'],
requireCredential: true,
prohibitMoved: true,
kind: 'write:account',
errors: {
noSuchNoteDraft: {
message: 'No such note draft.',
code: 'NO_SUCH_NOTE_DRAFT',
id: '49cd6b9d-848e-41ee-b0b9-adaca711a6b1',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
draftId: { type: 'string', nullable: false, format: 'misskey:id' },
},
required: ['draftId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private noteDraftService: NoteDraftService,
) {
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.get(me, ps.draftId);
if (draft == null) {
throw new ApiError(meta.errors.noSuchNoteDraft);
}
if (draft.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}
await this.noteDraftService.delete(me, draft.id);
});
}
}
@@ -0,0 +1,66 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNoteDraft, NoteDraftsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteDraftEntityService } from '@/core/entities/NoteDraftEntityService.js';
export const meta = {
tags: ['notes', 'drafts'],
requireCredential: true,
prohibitMoved: true,
kind: 'read:account',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'NoteDraft',
},
},
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.noteDraftsRepository)
private noteDraftsRepository: NoteDraftsRepository,
private queryService: QueryService,
private noteDraftEntityService: NoteDraftEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId)
.andWhere('drafts.userId = :meId', { meId: me.id });
const drafts = await query
.limit(ps.limit)
.getMany();
return await this.noteDraftEntityService.packMany(drafts, me);
});
}
}
@@ -0,0 +1,302 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteDraftService } from '@/core/NoteDraftService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { NoteDraftEntityService } from '@/core/entities/NoteDraftEntityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['notes', 'drafts'],
requireCredential: true,
prohibitMoved: true,
kind: 'write:account',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
updatedDraft: {
type: 'object',
optional: false, nullable: false,
ref: 'NoteDraft',
},
},
},
errors: {
noSuchRenoteTarget: {
message: 'No such renote target.',
code: 'NO_SUCH_RENOTE_TARGET',
id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4',
},
cannotReRenote: {
message: 'You can not Renote a pure Renote.',
code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE',
id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
},
cannotRenoteDueToVisibility: {
message: 'You can not Renote due to target visibility.',
code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY',
id: 'be9529e9-fe72-4de0-ae43-0b363c4938af',
},
noSuchReplyTarget: {
message: 'No such reply target.',
code: 'NO_SUCH_REPLY_TARGET',
id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
},
cannotReplyToInvisibleNote: {
message: 'You cannot reply to an invisible Note.',
code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE',
id: 'b98980fa-3780-406c-a935-b6d0eeee10d1',
},
cannotReplyToPureRenote: {
message: 'You can not reply to a pure Renote.',
code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
},
cannotReplyToSpecifiedNoteWithExtendedVisibility: {
message: 'You cannot reply to a specified visibility note with extended visibility.',
code: 'CANNOT_REPLY_TO_SPECIFIED_NOTE_WITH_EXTENDED_VISIBILITY',
id: 'ed940410-535c-4d5e-bfa3-af798671e93c',
},
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
id: '04da457d-b083-4055-9082-955525eda5a5',
},
noSuchChannel: {
message: 'No such channel.',
code: 'NO_SUCH_CHANNEL',
id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb',
},
youHaveBeenBlocked: {
message: 'You have been blocked by this user.',
code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
},
noSuchFile: {
message: 'Some files are not found.',
code: 'NO_SUCH_FILE',
id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
},
cannotRenoteOutsideOfChannel: {
message: 'Cannot renote outside of channel.',
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
id: '33510210-8452-094c-6227-4a6c05d99f00',
},
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
},
containsTooManyMentions: {
message: 'Cannot post because it exceeds the allowed number of mentions.',
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},
noSuchNoteDraft: {
message: 'No such note draft.',
code: 'NO_SUCH_NOTE_DRAFT',
id: '49cd6b9d-848e-41ee-b0b9-adaca711a6b1',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
},
noSuchRenote: {
message: 'No such renote.',
code: 'NO_SUCH_RENOTE',
id: '64929870-2540-4d11-af41-3b484d78c956',
},
cannotRenote: {
message: 'Cannot renote.',
code: 'CANNOT_RENOTE',
id: '76cc5583-5a14-4ad3-8717-0298507e32db',
},
cannotRenoteToExternal: {
message: 'Cannot Renote to External.',
code: 'CANNOT_RENOTE_TO_EXTERNAL',
id: 'ed1952ac-2d26-4957-8b30-2deda76bedf7',
},
noSuchReply: {
message: 'No such reply.',
code: 'NO_SUCH_REPLY',
id: 'c4721841-22fc-4bb7-ad3d-897ef1d375b5',
},
cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: {
message: 'You cannot reply to a specified visibility note with extended visibility.',
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
id: '215dbc76-336c-4d2a-9605-95766ba7dab0',
},
},
limit: {
duration: ms('1hour'),
max: 300,
},
} as const;
export const paramDef = {
type: 'object',
properties: {
draftId: { type: 'string', nullable: false, format: 'misskey:id' },
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
hashtag: { type: 'string', nullable: true, maxLength: 200 },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
// anyOf内にバリデーションを書いても最初の一つしかチェックされない
// See https://github.com/misskey-dev/misskey/pull/10082
text: {
type: 'string',
minLength: 0,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true,
},
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
poll: {
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array',
uniqueItems: true,
minItems: 0,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean' },
expiresAt: { type: 'integer', nullable: true },
expiredAfter: { type: 'integer', nullable: true, minimum: 1 },
},
required: ['choices'],
},
},
required: ['draftId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private noteDraftService: NoteDraftService,
private noteDraftEntityService: NoteDraftEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.update(me, ps.draftId, {
fileIds: ps.fileIds,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiredAfter: ps.poll.expiredAfter ?? null,
} : undefined,
text: ps.text ?? null,
replyId: ps.replyId ?? undefined,
renoteId: ps.renoteId ?? undefined,
cw: ps.cw ?? null,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? undefined,
}).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
case '49cd6b9d-848e-41ee-b0b9-adaca711a6b1':
throw new ApiError(meta.errors.noSuchNoteDraft);
case '04da457d-b083-4055-9082-955525eda5a5':
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
case 'b6992544-63e7-67f0-fa7f-32444b1b5306':
throw new ApiError(meta.errors.noSuchFile);
case '64929870-2540-4d11-af41-3b484d78c956':
throw new ApiError(meta.errors.noSuchRenote);
case '76cc5583-5a14-4ad3-8717-0298507e32db':
throw new ApiError(meta.errors.cannotRenote);
case '075ca298-e6e7-485a-b570-51a128bb5168':
throw new ApiError(meta.errors.youHaveBeenBlocked);
case '81eb8188-aea1-4e35-9a8f-3334a3be9855':
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
case '6815399a-6f13-4069-b60d-ed5156249d12':
throw new ApiError(meta.errors.noSuchChannel);
case 'ed1952ac-2d26-4957-8b30-2deda76bedf7':
throw new ApiError(meta.errors.cannotRenoteToExternal);
case 'c4721841-22fc-4bb7-ad3d-897ef1d375b5':
throw new ApiError(meta.errors.noSuchReply);
case 'e6c10b57-2c09-4da3-bd4d-eda05d51d140':
throw new ApiError(meta.errors.cannotReplyToPureRenote);
case '593c323c-6b6a-4501-a25c-2f36bd2a93d6':
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
throw new ApiError(meta.errors.cannotReplyToSpecifiedNoteWithExtendedVisibility);
case 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4':
throw new ApiError(meta.errors.noSuchRenoteTarget);
case 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a':
throw new ApiError(meta.errors.cannotReRenote);
case '749ee0f6-d3da-459a-bf02-282e2da4292c':
throw new ApiError(meta.errors.noSuchReplyTarget);
case '33510210-8452-094c-6227-4a6c05d99f00':
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
case 'aa6e01d3-a85c-669d-758a-76aab43af334':
throw new ApiError(meta.errors.containsProhibitedWords);
case '4de0363a-3046-481b-9b0f-feff3e211025':
throw new ApiError(meta.errors.containsTooManyMentions);
default:
throw err;
}
}
throw err;
});
const updatedDraft = await this.noteDraftEntityService.pack(draft, me);
return {
updatedDraft,
};
});
}
}
@@ -116,9 +116,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apiLoggerService: ApiLoggerService,
) {
super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => {
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
throw new ApiError(meta.errors.noSuchUser);
}
// ログイン時にusers/showできなくなってしまう
//if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
// throw new ApiError(meta.errors.noSuchUser);
//}
let user;
+2
View File
@@ -54,6 +54,8 @@ export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
export const noteReactionAcceptances = ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null] as const;
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
export const followingVisibilities = ['public', 'followers', 'private'] as const;
@@ -380,9 +380,7 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob
await alice.client.request('i/delete-account', { password: alice.password });
// NOTE: user deletion query is slow
// FIXME: ensure user is removed successfully
await sleep(10000);
await sleep();
const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation
@@ -480,9 +478,7 @@ describe('User', () => {
strictEqual(followers.length, 1); // followed by Bob
await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
// NOTE: user deletion query is slow
// FIXME: ensure user is removed successfully
await sleep(10000);
await sleep();
const following = await bob.client.request('users/following', { userId: bob.id });
strictEqual(following.length, 0); // no following relation
+1
View File
@@ -41,6 +41,7 @@ export default [
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-interface': ['error', {
allowSingleExtends: true,
}],
+16 -16
View File
@@ -16,7 +16,7 @@
"@rollup/pluginutils": "5.1.4",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.14",
"@vue/compiler-sfc": "3.5.16",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@@ -26,40 +26,40 @@
"mfm-js": "0.24.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.41.0",
"sass": "1.89.0",
"shiki": "3.4.2",
"rollup": "4.42.0",
"sass": "1.89.2",
"shiki": "3.6.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.8.3",
"uuid": "11.1.0",
"vite": "6.3.5",
"vue": "3.5.14"
"vue": "3.5.16"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.1",
"@tabler/icons-webfont": "3.33.0",
"@tabler/icons-webfont": "3.34.0",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.7",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.15.21",
"@types/node": "22.15.31",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@vitest/coverage-v8": "3.1.4",
"@vue/runtime-core": "3.5.14",
"acorn": "8.14.1",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@vitest/coverage-v8": "3.2.3",
"@vue/runtime-core": "3.5.16",
"acorn": "8.15.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "10.1.0",
"eslint-plugin-vue": "10.2.0",
"fast-glob": "3.3.3",
"happy-dom": "17.4.7",
"happy-dom": "17.6.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.8.4",
"msw": "2.10.2",
"nodemon": "3.1.10",
"prettier": "3.5.3",
"start-server-and-test": "2.0.12",
@@ -46,6 +46,7 @@ export default [
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-interface': ['error', {
allowSingleExtends: true,
}],
+1
View File
@@ -111,6 +111,7 @@ export const ROLE_POLICIES = [
'canImportUserLists',
'chatAvailability',
'uploadableFileTypes',
'noteDraftLimit',
] 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'];
+4
View File
@@ -48,6 +48,10 @@ export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
?? char;
}
export function isSupportedEmoji(char: string): boolean {
return unicodeEmojisMap.has(colorizeEmoji(char)) || unicodeEmojisMap.has(char);
}
export function getEmojiName(char: string): string {
// Colorize it because emojilist.json assumes that
const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char);
+7 -3
View File
@@ -39,7 +39,11 @@ export class I18n<T extends ILocale> {
private devMode: boolean;
constructor(public locale: T, devMode = false) {
this.devMode = devMode;
// 場合によってはバージョンアップ前の翻訳データを参照した結果存在しないプロパティにアクセスしてクライアントが起動できなくなることがある問題の応急処置として非devモードでもプロキシする
// TODO: https://github.com/misskey-dev/misskey/issues/14453 が実装されたらそのようなことは発生し得なくなるため消す
const oukyuusyoti = true;
this.devMode = devMode || oukyuusyoti;
//#region BIND
this.t = this.t.bind(this);
@@ -68,7 +72,7 @@ export class I18n<T extends ILocale> {
console.error(`Unexpected locale key: ${String(p)}`);
return p;
return new Proxy({} as any, new Handler<TTarget[keyof TTarget] & ILocale>());
}
}
@@ -137,7 +141,7 @@ export class I18n<T extends ILocale> {
console.error(`Unexpected locale key: ${String(p)}`);
return p;
return new Proxy((() => p) as any, new Handler<TTarget[keyof TTarget] & ILocale>());
}
}
+6 -6
View File
@@ -21,11 +21,11 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.15.21",
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"esbuild": "0.25.4",
"eslint-plugin-vue": "10.1.0",
"@types/node": "22.15.31",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"esbuild": "0.25.5",
"eslint-plugin-vue": "10.2.0",
"nodemon": "3.1.10",
"typescript": "5.8.3",
"vue-eslint-parser": "10.1.3"
@@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.14"
"vue": "3.5.16"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

+1
View File
@@ -41,6 +41,7 @@ export default [
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-interface': ['error', {
allowSingleExtends: true,
}],
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import path from 'node:path'
import locales from '../../../locales/index.js';
const localesDir = path.resolve(__dirname, '../../../locales')
/**
* webSocketでメッセージを送るViteプラグイン
* @returns {import('vite').Plugin}
*/
export default function pluginWatchLocales() {
return {
name: 'watch-locales',
configureServer(server) {
const localeYmlPaths = Object.keys(locales).map(locale => path.join(localesDir, `${locale}.yml`));
// watcherにパスを追加
server.watcher.add(localeYmlPaths);
server.watcher.on('change', (filePath) => {
if (localeYmlPaths.includes(filePath)) {
server.ws.send({
type: 'custom',
event: 'locale-update',
data: filePath.match(/([^\/]+)\.yml$/)?.[1] || null,
})
}
});
},
};
}
+23 -24
View File
@@ -24,11 +24,11 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@sentry/vue": "9.22.0",
"@sentry/vue": "9.27.0",
"@syuilo/aiscript": "0.19.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.14",
"@vue/compiler-sfc": "3.5.16",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.16",
"astring": "1.9.0",
@@ -40,7 +40,7 @@
"chartjs-chart-matrix": "2.1.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "11.28.2",
"chromatic": "11.29.0",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0",
"date-fns": "4.1.0",
@@ -60,22 +60,21 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.41.0",
"rollup": "4.42.0",
"sanitize-html": "2.17.0",
"sass": "1.89.0",
"shiki": "3.4.2",
"sass": "1.89.2",
"shiki": "3.6.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.176.0",
"three": "0.177.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.8.3",
"uuid": "11.1.0",
"v-code-diff": "1.13.1",
"vite": "6.3.5",
"vue": "3.5.14",
"vue": "3.5.16",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@@ -99,36 +98,36 @@
"@storybook/types": "8.6.14",
"@storybook/vue3": "8.6.14",
"@storybook/vue3-vite": "8.6.14",
"@tabler/icons-webfont": "3.33.0",
"@tabler/icons-webfont": "3.34.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.7",
"@types/estree": "1.0.8",
"@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.15.21",
"@types/node": "22.15.31",
"@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.32.1",
"@typescript-eslint/parser": "8.32.1",
"@vitest/coverage-v8": "3.1.4",
"@vue/compiler-core": "3.5.14",
"@vue/runtime-core": "3.5.14",
"acorn": "8.14.1",
"@typescript-eslint/eslint-plugin": "8.34.0",
"@typescript-eslint/parser": "8.34.0",
"@vitest/coverage-v8": "3.2.3",
"@vue/compiler-core": "3.5.16",
"@vue/runtime-core": "3.5.16",
"acorn": "8.15.0",
"cross-env": "7.0.3",
"cypress": "14.4.0",
"cypress": "14.4.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "10.1.0",
"eslint-plugin-vue": "10.2.0",
"fast-glob": "3.3.3",
"happy-dom": "17.4.7",
"happy-dom": "17.6.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.1",
"msw": "2.8.4",
"msw-storybook-addon": "2.0.4",
"msw": "2.10.2",
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.5.3",
"react": "19.1.0",
@@ -138,7 +137,7 @@
"storybook": "8.6.14",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.1.4",
"vitest": "3.2.3",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "2.2.10",
"vue-eslint-parser": "10.1.3",
+2 -2
View File
@@ -4,7 +4,7 @@
*/
import { utils, values } from '@syuilo/aiscript';
import { v4 as uuid } from 'uuid';
import { genId } from '@/utility/id.js';
import { ref } from 'vue';
import type { Ref } from 'vue';
import * as Misskey from 'misskey-js';
@@ -543,7 +543,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
call: C,
) {
if (id) utils.assertString(id);
const _id = id?.value ?? uuid();
const _id = id?.value ?? genId();
const component = ref({
...getOptions(def, call),
type,
+21 -2
View File
@@ -81,8 +81,10 @@ export async function common(createVue: () => Promise<App<Element>>) {
//#region Detect language & fetch translations
const localeVersion = miLocalStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
async function fetchAndUpdateLocale({ useCache } = { useCache: true }) {
const fetchOptions: RequestInit | undefined = useCache ? undefined : { cache: 'no-store' };
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`, fetchOptions);
if (res.status === 200) {
const newLocale = await res.text();
const parsedNewLocale = JSON.parse(newLocale);
@@ -92,6 +94,23 @@ export async function common(createVue: () => Promise<App<Element>>) {
updateI18n(parsedNewLocale);
}
}
if (localeOutdated) {
fetchAndUpdateLocale();
}
if (import.meta.hot) {
import.meta.hot.on('locale-update', async (updatedLang: string) => {
console.info(`Locale updated: ${updatedLang}`);
if (updatedLang === lang) {
await new Promise(resolve => {
window.setTimeout(resolve, 500);
});
await fetchAndUpdateLocale({ useCache: false });
window.location.reload();
}
});
}
//#endregion
// タッチデバイスでCSSの:hoverを機能させる
@@ -282,8 +282,8 @@ function onContextmenu(ev: MouseEvent) {
menu = [{
text: i18n.ts.openInWindow,
icon: 'ti ti-app-window',
action: () => {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), {
action: async () => {
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkDriveWindow.vue').then(x => x.default), {
initialFolder: props.folder,
}, {
closed: () => dispose(),
+2 -2
View File
@@ -113,7 +113,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="filesPaginator.items.value.length == 0 && foldersPaginator.items.value.length == 0 && !fetching" :class="$style.empty">
<div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
<div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong></div>
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
</div>
</div>
@@ -185,7 +185,7 @@ const isRootSelected = ref(false);
watch(selectedFiles, () => {
emit('changeSelectedFiles', selectedFiles.value);
});
}, { deep: true });
watch([selectedFolders, isRootSelected], () => {
emit('changeSelectedFolders', isRootSelected.value ? [null, ...selectedFolders.value] : selectedFolders.value);
@@ -64,6 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCustomEmoji v-if="!emoji.hasOwnProperty('char')" class="emoji" :name="getKey(emoji)" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="getKey(emoji)" :normal="true"/>
</button>
<button v-tooltip="i18n.ts.settings" class="_button config" @click="settings"><i class="ti ti-settings"></i></button>
</div>
</section>
@@ -139,6 +140,9 @@ import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-e
import { $i } from '@/i.js';
import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
import { prefer } from '@/preferences.js';
import { useRouter } from '@/router.js';
const router = useRouter();
const props = withDefaults(defineProps<{
showPinned?: boolean;
@@ -489,6 +493,11 @@ function done(query?: string): boolean | void {
}
}
function settings() {
emit('esc');
router.push('settings/emoji-palette');
}
onMounted(() => {
focus();
});
@@ -720,6 +729,15 @@ defineExpose({
position: relative;
padding: $pad;
> .config {
position: relative;
padding: 0 3px;
width: var(--eachSize);
height: var(--eachSize);
contain: strict;
opacity: 0.5;
}
> .item {
position: relative;
padding: 0 3px;
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<template #header>{{ i18n.ts.describeFile }}</template>
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
<MkDriveFileThumbnail :file="file" fit="contain" style="height: 100px; margin-bottom: 16px;"/>
<MkDriveFileThumbnail v-if="file" :file="file" fit="contain" style="height: 100px; margin-bottom: 16px;"/>
<MkTextarea v-model="caption" autofocus :placeholder="i18n.ts.inputNewDescription">
<template #label>{{ i18n.ts.caption }}</template>
</MkTextarea>
@@ -33,8 +33,8 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{
file: Misskey.entities.DriveFile;
default: string;
file?: Misskey.entities.DriveFile | null;
default?: string | null;
}>();
const emit = defineEmits<{
@@ -44,7 +44,7 @@ const emit = defineEmits<{
const dialog = useTemplateRef('dialog');
const caption = ref(props.default);
const caption = ref(props.default ?? '');
async function ok() {
emit('done', caption.value);
@@ -5,33 +5,35 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
<MkA
v-for="file in (items as Misskey.entities.DriveFile[])"
:key="file.id"
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
:to="`/admin/file/${file.id}`"
class="file _button"
>
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
<div v-if="viewMode === 'list'" class="body">
<div>
<small style="opacity: 0.7;">{{ file.name }}</small>
<MkPagination v-slot="{ items }" :pagination="pagination">
<div :class="[$style.fileList, { [$style.grid]: viewMode === 'grid', [$style.list]: viewMode === 'list', '_gaps_s': viewMode === 'list' }]">
<MkA
v-for="file in items"
:key="file.id"
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
:to="`/admin/file/${file.id}`"
:class="[$style.file, '_button']"
>
<div v-if="file.isSensitive" :class="$style.sensitiveLabel">{{ i18n.ts.sensitive }}</div>
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain" :highlightWhenSensitive="true"/>
<div v-if="viewMode === 'list'" :class="$style.body">
<div>
<small style="opacity: 0.7;">{{ file.name }}</small>
</div>
<div>
<MkAcct v-if="file.user" :user="file.user"/>
<div v-else>{{ i18n.ts.system }}</div>
</div>
<div>
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
</div>
<div>
<span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
</div>
</div>
<div>
<MkAcct v-if="file.user" :user="file.user"/>
<div v-else>{{ i18n.ts.system }}</div>
</div>
<div>
<span style="margin-right: 1em;">{{ file.type }}</span>
<span>{{ bytes(file.size) }}</span>
</div>
<div>
<span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
</div>
</div>
</MkA>
</MkA>
</div>
</MkPagination>
</div>
</template>
@@ -43,76 +45,76 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import bytes from '@/filters/bytes.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
import type { PagingCtx } from '@/composables/use-pagination.js';
const props = defineProps<{
pagination: any;
defineProps<{
pagination: PagingCtx<'admin/drive/files'>;
viewMode: 'grid' | 'list';
}>();
</script>
<style lang="scss" scoped>
<style lang="scss" module>
@keyframes sensitive-blink {
0% { opacity: 1; }
50% { opacity: 0; }
}
.urempief {
&.list {
> .file {
display: flex;
width: 100%;
box-sizing: border-box;
text-align: left;
align-items: center;
&:hover {
color: var(--MI_THEME-accent);
}
> .thumbnail {
width: 128px;
height: 128px;
}
> .body {
margin-left: 0.3em;
padding: 8px;
flex: 1;
@media (max-width: 500px) {
font-size: 14px;
}
}
}
.list {
> .file {
display: flex;
width: 100%;
height: auto;
box-sizing: border-box;
text-align: left;
align-items: center;
}
&.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
grid-gap: 12px;
> .file:hover {
color: var(--MI_THEME-accent);
}
> .file {
position: relative;
aspect-ratio: 1;
> .file > .thumbnail {
width: 128px;
height: 128px;
}
> .thumbnail {
width: 100%;
height: 100%;
}
> .file > .body {
margin-left: 0.3em;
padding: 8px;
flex: 1;
> .sensitive-label {
position: absolute;
z-index: 10;
top: 8px;
left: 8px;
padding: 2px 4px;
background: #ff0000bf;
color: #fff;
border-radius: 4px;
font-size: 85%;
animation: sensitive-blink 1s infinite;
}
@media (max-width: 500px) {
font-size: 14px;
}
}
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
grid-gap: 12px;
> .file {
position: relative;
aspect-ratio: 1;
}
.thumbnail {
width: 100%;
height: 100%;
}
}
.sensitiveLabel {
position: absolute;
z-index: 10;
top: 8px;
left: 8px;
padding: 2px 4px;
background: #ff0000bf;
color: #fff;
border-radius: 4px;
font-size: 85%;
animation: sensitive-blink 1s infinite;
}
</style>
@@ -41,7 +41,7 @@ const emit = defineEmits<{
(_: 'closed'): void
}>();
const zIndex = claimZIndex('middle');
const zIndex = claimZIndex('low');
const showing = ref(true);
function closePage() {
@@ -51,7 +51,10 @@ if (props.fileId) {
}
function selectButton(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target).then(async (file) => {
selectFile({
anchorElement: ev.currentTarget ?? ev.target,
multiple: false,
}).then(async (file) => {
if (!file) return;
if (props.validate && !await props.validate(file)) return;
@@ -0,0 +1,121 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkFolder :defaultOpen="true" :canPage="false">
<template #label>{{ fx.name }}</template>
<template #footer>
<div class="_buttons">
<MkButton iconOnly @click="emit('del')"><i class="ti ti-trash"></i></MkButton>
<MkButton iconOnly @click="emit('swapUp')"><i class="ti ti-arrow-up"></i></MkButton>
<MkButton iconOnly @click="emit('swapDown')"><i class="ti ti-arrow-down"></i></MkButton>
</div>
</template>
<div :class="$style.root" class="_gaps">
<div v-for="[k, v] in Object.entries(fx.params)" :key="k">
<MkSwitch
v-if="v.type === 'boolean'"
v-model="layer.params[k]"
>
<template #label>{{ fx.params[k].label ?? k }}</template>
</MkSwitch>
<MkRange
v-else-if="v.type === 'number'"
v-model="layer.params[k]"
continuousUpdate
:min="v.min"
:max="v.max"
:step="v.step"
:textConverter="fx.params[k].toViewValue"
@thumbDoubleClicked="() => {
if (fx.params[k].default != null) {
layer.params[k] = fx.params[k].default;
} else {
layer.params[k] = v.min;
}
}"
>
<template #label>{{ fx.params[k].label ?? k }}</template>
</MkRange>
<MkRadios
v-else-if="v.type === 'number:enum'"
v-model="layer.params[k]"
>
<template #label>{{ fx.params[k].label ?? k }}</template>
<option v-for="item in v.enum" :value="item.value">{{ item.label }}</option>
</MkRadios>
<div v-else-if="v.type === 'seed'">
<MkRange
v-model="layer.params[k]"
continuousUpdate
type="number"
:min="0"
:max="10000"
:step="1"
>
<template #label>{{ fx.params[k].label ?? k }}</template>
</MkRange>
</div>
<MkInput
v-else-if="v.type === 'color'"
:modelValue="getHex(layer.params[k])"
type="color"
@update:modelValue="v => { const c = getRgb(v); if (c != null) layer.params[k] = c; }"
>
<template #label>{{ fx.params[k].label ?? k }}</template>
</MkInput>
</div>
</div>
</MkFolder>
</template>
<script setup lang="ts">
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
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 { FXS } from '@/utility/image-effector/fxs.js';
const layer = defineModel<ImageEffectorLayer>('layer', { required: true });
const fx = FXS.find((fx) => fx.id === layer.value.fxId);
if (fx == null) {
throw new Error(`Unrecognized effect: ${layer.value.fxId}`);
}
const emit = defineEmits<{
(e: 'del'): void;
(e: 'swapUp'): void;
(e: 'swapDown'): void;
}>();
function getHex(c: [number, number, number]) {
return `#${c.map(x => (x * 255).toString(16).padStart(2, '0')).join('')}`;
}
function getRgb(hex: string | number): [number, number, number] | null {
if (
typeof hex === 'number' ||
typeof hex !== 'string' ||
!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)
) {
return null;
}
const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g);
if (m == null) return [0, 0, 0];
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
}
</script>
<style module>
.root {
}
</style>
@@ -0,0 +1,303 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="1000"
:height="600"
:scroll="false"
:withOkButton="true"
@close="cancel()"
@ok="save()"
@closed="emit('closed')"
>
<template #header><i class="ti ti-sparkles"></i> {{ i18n.ts._imageEffector.title }}</template>
<div :class="$style.root">
<div :class="$style.container">
<div :class="$style.preview">
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
<div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</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>
</div>
</div>
</div>
<div :class="$style.controls">
<div class="_spacer _gaps">
<XLayer
v-for="(layer, i) in layers"
:key="layer.id"
v-model:layer="layers[i]"
@del="onLayerDelete(layer)"
@swapUp="onLayerSwapUp(layer)"
@swapDown="onLayerSwapDown(layer)"
></XLayer>
<MkButton rounded primary style="margin: 0 auto;" @click="addEffect"><i class="ti ti-plus"></i> {{ i18n.ts._imageEffector.addEffect }}</MkButton>
</div>
</div>
</div>
</div>
</MkModalWindow>
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick } from 'vue';
import type { ImageEffectorLayer } from '@/utility/image-effector/ImageEffector.js';
import { i18n } from '@/i18n.js';
import { ImageEffector } from '@/utility/image-effector/ImageEffector.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import XLayer from '@/components/MkImageEffectorDialog.Layer.vue';
import * as os from '@/os.js';
import { deepClone } from '@/utility/clone.js';
import { FXS } from '@/utility/image-effector/fxs.js';
import { genId } from '@/utility/id.js';
const props = defineProps<{
image: File;
}>();
const emit = defineEmits<{
(ev: 'ok', image: File): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const dialog = useTemplateRef('dialog');
async function cancel() {
if (layers.length > 0) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._imageEffector.discardChangesConfirm,
});
if (canceled) return;
}
emit('cancel');
dialog.value?.close();
}
const layers = reactive<ImageEffectorLayer[]>([]);
watch(layers, async () => {
if (renderer != null) {
renderer.setLayers(layers);
}
}, { deep: true });
function addEffect(ev: MouseEvent) {
os.popupMenu(FXS.map((fx) => ({
text: fx.name,
action: () => {
layers.push({
id: genId(),
fxId: fx.id,
params: Object.fromEntries(Object.entries(fx.params).map(([k, v]) => [k, v.default])),
});
},
})), ev.currentTarget ?? ev.target);
}
function onLayerSwapUp(layer: ImageEffectorLayer) {
const index = layers.indexOf(layer);
if (index > 0) {
layers.splice(index, 1);
layers.splice(index - 1, 0, layer);
}
}
function onLayerSwapDown(layer: ImageEffectorLayer) {
const index = layers.indexOf(layer);
if (index < layers.length - 1) {
layers.splice(index, 1);
layers.splice(index + 1, 0, layer);
}
}
function onLayerDelete(layer: ImageEffectorLayer) {
const index = layers.indexOf(layer);
if (index !== -1) {
layers.splice(index, 1);
}
}
const canvasEl = useTemplateRef('canvasEl');
let renderer: ImageEffector<typeof FXS> | null = null;
let imageBitmap: ImageBitmap | null = null;
onMounted(async () => {
if (canvasEl.value == null) return;
const closeWaiting = os.waiting();
await nextTick(); // waiting
imageBitmap = await window.createImageBitmap(props.image);
const MAX_W = 1000;
const MAX_H = 1000;
let w = imageBitmap.width;
let h = imageBitmap.height;
if (w > MAX_W || h > MAX_H) {
const scale = Math.min(MAX_W / w, MAX_H / h);
w *= scale;
h *= scale;
}
renderer = new ImageEffector({
canvas: canvasEl.value,
renderWidth: w,
renderHeight: h,
image: imageBitmap,
fxs: FXS,
});
await renderer.setLayers(layers);
renderer.render();
closeWaiting();
});
onUnmounted(() => {
if (renderer != null) {
renderer.destroy();
renderer = null;
}
if (imageBitmap != null) {
imageBitmap.close();
imageBitmap = null;
}
});
async function save() {
if (layers.length === 0 || renderer == null || imageBitmap == null || canvasEl.value == null) {
cancel();
return;
}
const closeWaiting = os.waiting();
await nextTick(); // waiting
renderer.changeResolution(imageBitmap.width, imageBitmap.height); //
renderer.render(); // toBlob
canvasEl.value.toBlob((blob) => {
emit('ok', new File([blob!], `image-${Date.now()}.png`, { type: 'image/png' }));
dialog.value?.close();
closeWaiting();
}, 'image/png');
}
const enabled = ref(true);
watch(enabled, () => {
if (renderer != null) {
if (enabled.value) {
renderer.setLayers(layers);
} else {
renderer.setLayers([]);
}
renderer.render();
}
});
</script>
<style module>
.root {
container-type: inline-size;
height: 100%;
}
.container {
height: 100%;
display: grid;
grid-template-columns: 1fr 400px;
}
.preview {
position: relative;
background-color: var(--MI_THEME-bg);
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
}
.previewContainer {
display: flex;
flex-direction: column;
height: 100%;
user-select: none;
-webkit-user-drag: none;
}
.previewTitle {
position: absolute;
z-index: 100;
top: 8px;
left: 8px;
padding: 6px 10px;
border-radius: 6px;
font-size: 85%;
}
.previewControls {
position: absolute;
z-index: 100;
bottom: 8px;
right: 8px;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
}
.previewControlsButton {
&.active {
color: var(--MI_THEME-accent);
}
}
.previewSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
.previewCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 20px;
box-sizing: border-box;
object-fit: contain;
}
.controls {
overflow-y: scroll;
}
@container (max-width: 800px) {
.container {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
}
</style>
@@ -82,7 +82,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
<script lang="ts" setup>
import { computed, nextTick, onMounted, onUnmounted, useTemplateRef, watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import { genId } from '@/utility/id.js';
import { render } from 'buraha';
import { prefer } from '@/preferences.js';
@@ -117,7 +117,7 @@ const props = withDefaults(defineProps<{
onlyAvgColor: false,
});
const viewId = uuid();
const viewId = genId();
const canvas = useTemplateRef('canvas');
const root = useTemplateRef('root');
const img = useTemplateRef('img');
+2 -1
View File
@@ -52,6 +52,7 @@ import type { SuggestionType } from '@/utility/autocomplete.js';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { Autocomplete } from '@/utility/autocomplete.js';
import { genId } from '@/utility/id.js';
const props = defineProps<{
modelValue: string | number | null;
@@ -87,7 +88,7 @@ const emit = defineEmits<{
const { modelValue, type, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const id = Math.random().toString(); // TODO: uuid?
const id = genId();
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import { genId } from '@/utility/id.js';
import tinycolor from 'tinycolor2';
import { useInterval } from '@@/js/use-interval.js';
@@ -42,7 +42,7 @@ const props = defineProps<{
const viewBoxX = 50;
const viewBoxY = 50;
const gradientId = uuid();
const gradientId = genId();
const polylinePoints = ref('');
const polygonPoints = ref('');
const headX = ref<number | null>(null);
@@ -4,15 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :preferType="'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
<div ref="rootEl" :class="$style.root" :style="{ width: `${width}px`, height: `min(${height}px, 100%)` }">
<MkModal ref="modal" v-slot="{ type }" :preferType="deviceKind === 'smartphone' ? 'drawer' : 'dialog'" @click="onBgClick" @closed="emit('closed')" @esc="emit('esc')">
<div ref="rootEl" :class="[$style.root, type === 'drawer' ? $style.asDrawer : null]" :style="{ width: type === 'drawer' ? '' : `${width}px`, height: type === 'drawer' ? '' : `min(${height}px, 100%)` }">
<div :class="$style.header">
<button v-if="withOkButton && withCloseButton" :class="$style.headerButton" class="_button" @click="emit('close')"><i class="ti ti-x"></i></button>
<button v-if="withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button>
<span :class="$style.title">
<slot name="header"></slot>
</span>
<button v-if="!withOkButton && withCloseButton" :class="$style.headerButton" class="_button" data-cy-modal-window-close @click="emit('close')"><i class="ti ti-x"></i></button>
<button v-if="withOkButton" :class="$style.headerButton" class="_button" :disabled="okButtonDisabled" @click="emit('ok')"><i class="ti ti-check"></i></button>
<div v-if="withOkButton" style="padding: 0 16px; place-content: center;">
<MkButton primary gradate small rounded :disabled="okButtonDisabled" @click="emit('ok')">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
</div>
</div>
<div :class="$style.body">
<slot></slot>
@@ -26,7 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef, ref } from 'vue';
import MkModal from './MkModal.vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
import { deviceKind } from '@/utility/device-kind.js';
const props = withDefaults(defineProps<{
withOkButton?: boolean;
@@ -82,6 +86,19 @@ defineExpose({
@media (max-width: 500px) {
--root-margin: 16px;
}
&.asDrawer {
height: calc(100dvh - 30px);
border-radius: 0;
.body {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.footer {
padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
}
}
}
.header {
+31 -23
View File
@@ -265,21 +265,21 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
let note = deepClone(props.note);
// plugin
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result: Misskey.entities.Note | null = deepClone(note);
for (const interruptor of noteViewInterruptors) {
try {
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
} catch (err) {
console.error(err);
}
}
note = result as Misskey.entities.Note;
});
}
// Transition
// https://github.com/aiscript-dev/aiscript/issues/937
//// plugin
//const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
//if (noteViewInterruptors.length > 0) {
// let result: Misskey.entities.Note | null = deepClone(note);
// for (const interruptor of noteViewInterruptors) {
// try {
// result = await interruptor.handler(result!) as Misskey.entities.Note | null;
// } catch (err) {
// console.error(err);
// }
// }
// note = result as Misskey.entities.Note;
//}
const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note);
@@ -321,20 +321,27 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${appearNote.id}`,
}));
/* Overload FunctionLint
/* eslint-disable no-redeclare */
/** checkOnlyでは純粋なワードミュート結果をbooleanで返却する */
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): Array<string | string[]> | false | 'sensitiveMute';
*/
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly?: false): Array<string | string[]> | false | 'sensitiveMute';
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | boolean | 'sensitiveMute' {
if (mutedWords != null) {
const result = checkWordMute(noteToCheck, $i, mutedWords);
if (Array.isArray(result)) return result;
if (Array.isArray(result)) {
return checkOnly ? (result.length > 0) : result;
}
const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
if (Array.isArray(replyResult)) return replyResult;
if (Array.isArray(replyResult)) {
return checkOnly ? (replyResult.length > 0) : replyResult;
}
const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
if (Array.isArray(renoteResult)) return renoteResult;
if (Array.isArray(renoteResult)) {
return checkOnly ? (renoteResult.length > 0) : renoteResult;
}
}
if (checkOnly) return false;
@@ -345,6 +352,7 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
return false;
}
/* eslint-enable no-redeclare */
const keymap = {
'r': () => {
@@ -417,7 +425,7 @@ if (!props.mock) {
const users = renotes.map(x => x.user);
if (users.length < 1) return;
if (users.length < 1 || renoteButton.value == null) return;
const { dispose } = os.popup(MkUsersTooltip, {
showing,

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