diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index a028e2685e..8b11c8413c 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -215,20 +215,9 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - allowedPrivateNetworks: [ '127.0.0.1/32' ] -# Disable automatic redirect for ActivityPub object lookup. (default: false) -# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. -# However it will make it impossible for other instances to lookup third-party user and notes through your URL. -#disallowExternalApRedirect: true - # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 4be1352bd7..dc354324dc 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -227,12 +227,6 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). @@ -240,11 +234,6 @@ signToActivityPubGet: true # '127.0.0.1/32' #] -# Disable automatic redirect for ActivityPub object lookup. (default: false) -# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. -# However it will make it impossible for other instances to lookup third-party user and notes through your URL. -#disallowExternalApRedirect: true - # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.config/example.yml b/.config/example.yml index d4584215c9..c127eaae22 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -319,19 +319,12 @@ proxyBypassHosts: # * Perform image compression (on a different server resource than the main process) #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. -proxyRemoteFiles: true - # Movie Thumbnail Generation URL # There is no reference implementation. # For example, Misskey will point to the following URL: # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - # For security reasons, uploading attachments from the intranet is prohibited, # but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). @@ -339,11 +332,6 @@ signToActivityPubGet: true # '127.0.0.1/32' #] -# Disable automatic redirect for ActivityPub object lookup. (default: false) -# This is a strong defense against potential impersonation attacks if the viewer instance has inadequate validation. -# However it will make it impossible for other instances to lookup third-party user and notes through your URL. -#disallowExternalApRedirect: true - # Upload or download file size limits (bytes) #maxFileSize: 262144000 diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 6d904e87b9..fb0d25c214 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -202,12 +202,6 @@ proxyBypassHosts: # Media Proxy #mediaProxy: https://example.com/proxy -# Proxy remote files (default: true) -proxyRemoteFiles: true - -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - allowedPrivateNetworks: [ '127.0.0.1/32' ] diff --git a/CHANGELOG.md b/CHANGELOG.md index bf98a44ac4..21ce5932b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## 2025.5.1 +### Note +- 設定ファイルの以下の項目がコントロールパネルから設定するようになりました + - signToActivityPubGet + - proxyRemoteFiles + - disallowExternalApRedirect + - 許可しないかどうかではなく、許可するかどうかの設定(allowExternalApRedirect)になりました + ### General - Feat: 非ログインでサーバーを閲覧された際に、サーバー内のコンテンツを非公開にすることができるようになりました - モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます @@ -7,6 +14,15 @@ - デフォルト値は「ローカルのコンテンツだけ公開」になっています ### Client +- Feat: ドライブのUIが強化されました + - 複数のファイルをまとめて移動できるようになりました +- Feat: ファイルのアップロードUIが一新されました + - アップロード前にファイル情報を確認できるようになりました + - 圧縮の品質を選択できるようになりました + - アップロードに失敗したときに再試行できるようになりました + - アップロード前に画像のクロッピングを行えるようになりました + - ファイルサイズのチェックは圧縮後の実際にアップロードされるサイズで行われるようになりました + - ファイルのアップロードを中断できるようになりました - Feat: サーバー初期設定ウィザードが実装されました - 簡単なウィザードに従うだけで、サーバーに最適な設定が適用されます - Feat: Websocket接続を行わずにMisskeyを利用するNo Websocketモードが実装されました(beta) @@ -14,19 +30,28 @@ - 何らの理由によりWebsocket接続が行えない環境でも快適に利用可能です - 従来のWebsocket接続を行うモードはリアルタイムモードとして再定義されました - チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます +- Feat: 絵文字をミュート可能にする機能 + - 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました +- Feat: モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように(実験的) - Enhance: メモリ使用量を軽減しました +- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加 - Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように - Enhance: リプライ元にアンケートがあることが表示されるように - Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上 (Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283) +- Enhance: ユーザー設定でURLプレビューを無効化できるように +- Enhance: AiScriptからtoastを表示する関数 `Mk:toast` を追加 - Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 ### Server +- Enhance: チャットルームの最大メンバー数を30人から50人に調整 - Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加 - Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加 +- Enhance: レートリミットの計算方法を調整 (#13997) - Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正 - Fix: ユーザ除外アンテナをインポートできない問題を修正 - Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正 +- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように ## 2025.5.0 diff --git a/chart/files/default.yml b/chart/files/default.yml index 06f762aafa..8fa0b39eff 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -221,9 +221,6 @@ id: "aidx" # Media Proxy #mediaProxy: https://example.com/proxy -# Sign to ActivityPub GET request (default: true) -signToActivityPubGet: true - #allowedPrivateNetworks: [ # '127.0.0.1/32' #] diff --git a/locales/index.d.ts b/locales/index.d.ts index a49f7b4d35..0a0e28e02e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -707,7 +707,7 @@ export interface Locale extends ILocale { */ "cacheRemoteFiles": string; /** - * この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします。 + * この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。 */ "cacheRemoteFilesDescription": string; /** @@ -1210,6 +1210,10 @@ export interface Locale extends ILocale { * アップロードが完了するまで時間がかかる場合があります。 */ "uploadFromUrlMayTakeTime": string; + /** + * {n}個のファイルをアップロード + */ + "uploadNFiles": ParameterizedString<"n">; /** * みつける */ @@ -2330,6 +2334,10 @@ export interface Locale extends ILocale { * サウンド */ "sound": string; + /** + * 通知音の設定 + */ + "notificationSoundSettings": string; /** * 聴く */ @@ -3186,6 +3194,10 @@ export interface Locale extends ILocale { * 反映には再起動が必要です。 */ "needReloadToApply": string; + /** + * 反映にはサーバーの再起動が必要です。 + */ + "needToRestartServerToApply": string; /** * タイトルバーを表示する */ @@ -5425,6 +5437,26 @@ export interface Locale extends ILocale { * オフにする */ "turnItOff": string; + /** + * 絵文字ミュート + */ + "emojiMute": string; + /** + * 絵文字ミュート解除 + */ + "emojiUnmute": string; + /** + * {x}をミュート + */ + "muteX": ParameterizedString<"x">; + /** + * {x}のミュートを解除 + */ + "unmuteX": ParameterizedString<"x">; + /** + * 中止 + */ + "abort": string; "_chat": { /** * まだメッセージはありません @@ -5713,6 +5745,14 @@ export interface Locale extends ILocale { * アイコンをスクロールに追従させる */ "useStickyIcons": string; + /** + * 高品質な画像のプレースホルダを表示 + */ + "enableHighQualityImagePlaceholders": string; + /** + * UIのアニメーション + */ + "uiAnimations": string; /** * ナビゲーションバーに副ボタンを表示 */ @@ -5753,6 +5793,10 @@ export interface Locale extends ILocale { * リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。 */ "contentsUpdateFrequency_description2": string; + /** + * URLプレビューを表示する + */ + "showUrlPreview": string; "_chat": { /** * 送信者の名前を表示 @@ -6428,6 +6472,30 @@ export interface Locale extends ILocale { * このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。 */ "singleUserMode_description": string; + /** + * GETリクエストに署名する + */ + "signToActivityPubGet": string; + /** + * 通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。 + */ + "signToActivityPubGet_description": string; + /** + * リモートファイルをプロキシする + */ + "proxyRemoteFiles": string; + /** + * 有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。 + */ + "proxyRemoteFiles_description": string; + /** + * ActivityPub経由の照会にリダイレクトを許可する + */ + "allowExternalApRedirect": string; + /** + * 有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。 + */ + "allowExternalApRedirect_description": string; /** * 非利用者に対するユーザー作成コンテンツの公開範囲 */ @@ -8479,10 +8547,6 @@ export interface Locale extends ILocale { * 入力ボックスの縁取り */ "inputBorder": string; - /** - * ドライブフォルダーの背景 - */ - "driveFolderBg": string; /** * バッジ */ @@ -10902,7 +10966,7 @@ export interface Locale extends ILocale { */ "description": string; }; - "_urlPreview": { + "_urlPreviewThumbnail": { /** * URLプレビューのサムネイルを非表示 */ @@ -10912,6 +10976,16 @@ export interface Locale extends ILocale { */ "description": string; }; + "_disableUrlPreview": { + /** + * URLプレビューを無効化 + */ + "title": string; + /** + * URLプレビュー機能を無効化します。サムネイル画像だけと違い、リンク先の情報の読み込み自体を削減できます。 + */ + "description": string; + }; "_code": { /** * コードハイライトを非表示 @@ -11397,22 +11471,6 @@ export interface Locale extends ILocale { * ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を"category"に入力します。 */ "directoryToCategoryCaption": string; - /** - * いずれかの方法で登録する絵文字を選択してください。 - */ - "emojiInputAreaCaption": string; - /** - * この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ - */ - "emojiInputAreaList1": string; - /** - * このリンクをクリックしてPCから選択する - */ - "emojiInputAreaList2": string; - /** - * このリンクをクリックしてドライブから選択する - */ - "emojiInputAreaList3": string; /** * リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです) */ @@ -11846,6 +11904,58 @@ export interface Locale extends ILocale { "text3": string; }; }; + "_uploader": { + /** + * {x}に圧縮 + */ + "compressedToX": ParameterizedString<"x">; + /** + * {x}%節約 + */ + "savedXPercent": ParameterizedString<"x">; + /** + * アップロードされていないファイルがありますが、中止しますか? + */ + "abortConfirm": string; + /** + * アップロードされていないファイルがありますが、完了しますか? + */ + "doneConfirm": string; + /** + * アップロード可能な最大ファイルサイズは{x}です。 + */ + "maxFileSizeIsX": ParameterizedString<"x">; + }; + "_clientPerformanceIssueTip": { + /** + * バッテリー消費が多いと感じたら + */ + "title": string; + /** + * アドブロッカーを無効にしてください + */ + "makeSureDisabledAdBlocker": string; + /** + * アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。 + */ + "makeSureDisabledAdBlocker_description": string; + /** + * カスタムCSSを無効にしてください + */ + "makeSureDisabledCustomCss": string; + /** + * スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。 + */ + "makeSureDisabledCustomCss_description": string; + /** + * 拡張機能を無効にしてください + */ + "makeSureDisabledAddons": string; + /** + * 一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。 + */ + "makeSureDisabledAddons_description": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8b2d31f7cd..b9e778741c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -172,7 +172,7 @@ emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" settingGuide: "おすすめ設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" -cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持しますが、画像のサムネイル生成やユーザーのプライバシー保護のために、default.ymlでproxyRemoteFilesをtrueにすることをお勧めします。" +cacheRemoteFilesDescription: "この設定を有効にすると、リモートファイルをこのサーバーのストレージにキャッシュするようになります。画像の表示が高速になりますが、サーバーのストレージを多く消費します。リモートユーザーがどれほどキャッシュを保持するかは、ロールによるドライブ容量制限によって決定されます。この制限を超えた場合、古いファイルからキャッシュが削除されリンクになります。この設定が無効の場合、リモートのファイルを最初からリンクとして保持します。" youCanCleanRemoteFilesCache: "ファイル管理の🗑️ボタンで全てのキャッシュを削除できます。" cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする" cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。" @@ -298,6 +298,7 @@ uploadFromUrl: "URLアップロード" uploadFromUrlDescription: "アップロードしたいファイルのURL" uploadFromUrlRequested: "アップロードをリクエストしました" uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がかかる場合があります。" +uploadNFiles: "{n}個のファイルをアップロード" explore: "みつける" messageRead: "既読" noMoreHistory: "これより過去の履歴はありません" @@ -578,6 +579,7 @@ newNoteRecived: "新しいノートがあります" newNote: "新しいノート" sounds: "サウンド" sound: "サウンド" +notificationSoundSettings: "通知音の設定" listen: "聴く" none: "なし" showInPage: "ページで表示" @@ -792,6 +794,7 @@ wide: "広い" narrow: "狭い" reloadToApplySetting: "設定はページリロード後に反映されます。" needReloadToApply: "反映には再起動が必要です。" +needToRestartServerToApply: "反映にはサーバーの再起動が必要です。" showTitlebar: "タイトルバーを表示する" clearCache: "キャッシュをクリア" onlineUsersCount: "{n}人がオンライン" @@ -1351,6 +1354,11 @@ advice: "アドバイス" realtimeMode: "リアルタイムモード" turnItOn: "オンにする" turnItOff: "オフにする" +emojiMute: "絵文字ミュート" +emojiUnmute: "絵文字ミュート解除" +muteX: "{x}をミュート" +unmuteX: "{x}のミュートを解除" +abort: "中止" _chat: noMessagesYet: "まだメッセージはありません" @@ -1428,6 +1436,8 @@ _settings: makeEveryTextElementsSelectable: "全てのテキスト要素を選択可能にする" makeEveryTextElementsSelectable_description: "有効にすると、一部のシチュエーションでのユーザビリティが低下する場合があります。" useStickyIcons: "アイコンをスクロールに追従させる" + enableHighQualityImagePlaceholders: "高品質な画像のプレースホルダを表示" + uiAnimations: "UIのアニメーション" showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示" ifOn: "オンのとき" ifOff: "オフのとき" @@ -1438,6 +1448,7 @@ _settings: contentsUpdateFrequency: "コンテンツの取得頻度" contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されますが、パフォーマンスが低下し、通信量とバッテリーの消費が多くなります。" contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。" + showUrlPreview: "URLプレビューを表示する" _chat: showSenderName: "送信者の名前を表示" @@ -1633,6 +1644,12 @@ _serverSettings: deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" singleUserMode: "お一人様モード" singleUserMode_description: "このサーバーを利用するのが自分だけの場合、このモードを有効にすることで動作が最適化されます。" + signToActivityPubGet: "GETリクエストに署名する" + signToActivityPubGet_description: "通常は有効にしてください。連合の通信に関する問題がある場合に、無効にすると改善することがありますが、逆にサーバーによっては通信が不可になることがあります。" + proxyRemoteFiles: "リモートファイルをプロキシする" + proxyRemoteFiles_description: "有効にすると、リモートのファイルをプロキシして提供します。画像のサムネイル生成やユーザーのプライバシー保護に役立ちます。" + allowExternalApRedirect: "ActivityPub経由の照会にリダイレクトを許可する" + allowExternalApRedirect_description: "有効にすると、他のサーバーがこのサーバーを通して第三者のコンテンツを照会することが可能になりますが、コンテンツのなりすましが発生する可能性があります。" userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲" userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。" userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。" @@ -2223,7 +2240,6 @@ _theme: buttonBg: "ボタンの背景" buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" - driveFolderBg: "ドライブフォルダーの背景" badge: "バッジ" messageBg: "チャットの背景" fgHighlighted: "強調された文字" @@ -2897,9 +2913,12 @@ _dataSaver: _avatar: title: "アイコン画像のアニメーションを無効化" description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。" - _urlPreview: + _urlPreviewThumbnail: title: "URLプレビューのサムネイルを非表示" description: "URLプレビューのサムネイル画像が読み込まれなくなります。" + _disableUrlPreview: + title: "URLプレビューを無効化" + description: "URLプレビュー機能を無効化します。サムネイル画像だけと違い、リンク先の情報の読み込み自体を削減できます。" _code: title: "コードハイライトを非表示" description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" @@ -3039,10 +3058,6 @@ _customEmojisManager: uploadSettingDescription: "この画面で絵文字アップロードを行う際の動作を設定できます。" directoryToCategoryLabel: "ディレクトリ名を\"category\"に入力する" directoryToCategoryCaption: "ディレクトリをドラッグ・ドロップした時に、ディレクトリ名を\"category\"に入力します。" - emojiInputAreaCaption: "いずれかの方法で登録する絵文字を選択してください。" - emojiInputAreaList1: "この枠に画像ファイルまたはディレクトリをドラッグ&ドロップ" - emojiInputAreaList2: "このリンクをクリックしてPCから選択する" - emojiInputAreaList3: "このリンクをクリックしてドライブから選択する" confirmRegisterEmojisDescription: "リストに表示されている絵文字を新たなカスタム絵文字として登録します。よろしいですか?(負荷を避けるため、一度の操作で登録可能な絵文字は{count}件までです)" confirmClearEmojisDescription: "編集内容を破棄し、リストに表示されている絵文字をクリアします。よろしいですか?" confirmUploadEmojisDescription: "ドラッグ&ドロップされた{count}個のファイルをドライブにアップロードします。実行しますか?" @@ -3168,3 +3183,19 @@ _serverSetupWizard: text1: "Misskeyは有志によって開発されている無料のソフトウェアです。" text2: "今後も開発を続けられるように、よろしければぜひカンパをお願いいたします。" text3: "支援者向け特典もあります!" + +_uploader: + compressedToX: "{x}に圧縮" + savedXPercent: "{x}%節約" + abortConfirm: "アップロードされていないファイルがありますが、中止しますか?" + doneConfirm: "アップロードされていないファイルがありますが、完了しますか?" + maxFileSizeIsX: "アップロード可能な最大ファイルサイズは{x}です。" + +_clientPerformanceIssueTip: + title: "バッテリー消費が多いと感じたら" + makeSureDisabledAdBlocker: "アドブロッカーを無効にしてください" + makeSureDisabledAdBlocker_description: "アドブロッカーはパフォーマンスに影響を及ぼすことがあります。OSの機能やブラウザの機能・アドオンなどでアドブロッカーが有効になっていないか確認してください。" + makeSureDisabledCustomCss: "カスタムCSSを無効にしてください" + makeSureDisabledCustomCss_description: "スタイルを上書きするとパフォーマンスに影響を及ぼすことがあります。カスタムCSSや、スタイルを上書きする拡張機能が有効になっていないか確認してください。" + makeSureDisabledAddons: "拡張機能を無効にしてください" + makeSureDisabledAddons_description: "一部の拡張機能はクライアントの動作に干渉しパフォーマンスに影響を及ぼすことがあります。ブラウザの拡張機能を無効にして改善するか確認してください。" diff --git a/package.json b/package.json index e0565ff073..abc4bcdaa9 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "2025.5.1-alpha.1", + "version": "2025.5.1-alpha.4", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@10.10.0", + "packageManager": "pnpm@10.11.0", "workspaces": [ "packages/frontend-shared", "packages/frontend", @@ -51,30 +51,30 @@ "lodash": "4.17.21" }, "dependencies": { - "cssnano": "7.0.6", - "esbuild": "0.25.3", - "execa": "9.5.2", + "cssnano": "7.0.7", + "esbuild": "0.25.4", + "execa": "9.5.3", "fast-glob": "3.3.3", "glob": "11.0.2", "ignore-walk": "7.0.0", "js-yaml": "4.1.0", "postcss": "8.5.3", "tar": "7.4.3", - "terser": "5.39.0", + "terser": "5.39.2", "typescript": "5.8.3" }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.1.0", - "@types/node": "22.15.2", - "@typescript-eslint/eslint-plugin": "8.31.0", - "@typescript-eslint/parser": "8.31.0", + "@types/node": "22.15.21", + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", "cross-env": "7.0.3", - "cypress": "14.3.2", - "eslint": "9.25.1", - "globals": "16.0.0", + "cypress": "14.4.0", + "eslint": "9.27.0", + "globals": "16.1.0", "ncp": "2.0.0", - "pnpm": "10.10.0", - "start-server-and-test": "2.0.11" + "pnpm": "10.11.0", + "start-server-and-test": "2.0.12" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.22.0" diff --git a/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js b/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js new file mode 100644 index 0000000000..3243f43b91 --- /dev/null +++ b/packages/backend/migration/1746949539915-migrateSomeConfigFileSettingsToMeta.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import {loadConfig} from "./js/migration-config.js"; + +export class MigrateSomeConfigFileSettingsToMeta1746949539915 { + name = 'MigrateSomeConfigFileSettingsToMeta1746949539915' + + async up(queryRunner) { + const config = loadConfig(); + // $1 cannot be used in ALTER TABLE queries + await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT ${config.proxyRemoteFiles}`); + await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT ${config.signToActivityPubGet}`); + await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT ${!config.disallowExternalApRedirect}`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowExternalApRedirect"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "signToActivityPubGet"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyRemoteFiles"`); + } +} diff --git a/packages/backend/migration/js/migration-config.js b/packages/backend/migration/js/migration-config.js index 8cfbb21470..853735661b 100644 --- a/packages/backend/migration/js/migration-config.js +++ b/packages/backend/migration/js/migration-config.js @@ -3,6 +3,29 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { path as configYamlPath } from '../../built/config.js'; +import * as yaml from 'js-yaml'; +import fs from "node:fs"; + export function isConcurrentIndexMigrationEnabled() { return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1'; } + +let loadedConfigCache = undefined; + +function loadConfigInternal() { + const config = yaml.load(fs.readFileSync(configYamlPath, 'utf-8')); + + return { + disallowExternalApRedirect: Boolean(config.disallowExternalApRedirect ?? false), + proxyRemoteFiles: Boolean(config.proxyRemoteFiles ?? false), + signToActivityPubGet: Boolean(config.signToActivityPubGet ?? true), + } +} + +export function loadConfig() { + if (loadedConfigCache === undefined) { + loadedConfigCache = loadConfigInternal(); + } + return loadedConfigCache; +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 36f7781908..8edaf27c2a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -37,17 +37,17 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.11.22", - "@swc/core-darwin-x64": "1.11.22", + "@swc/core-darwin-arm64": "1.11.29", + "@swc/core-darwin-x64": "1.11.29", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.11.22", - "@swc/core-linux-arm64-gnu": "1.11.22", - "@swc/core-linux-arm64-musl": "1.11.22", - "@swc/core-linux-x64-gnu": "1.11.22", - "@swc/core-linux-x64-musl": "1.11.22", - "@swc/core-win32-arm64-msvc": "1.11.22", - "@swc/core-win32-ia32-msvc": "1.11.22", - "@swc/core-win32-x64-msvc": "1.11.22", + "@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", "@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.797.0", - "@aws-sdk/lib-storage": "3.797.0", + "@aws-sdk/client-s3": "3.815.0", + "@aws-sdk/lib-storage": "3.815.0", "@discordapp/twemoji": "15.1.0", "@fastify/accepts": "5.0.2", "@fastify/cookie": "11.0.2", @@ -76,22 +76,22 @@ "@fastify/express": "4.0.2", "@fastify/http-proxy": "10.0.2", "@fastify/multipart": "9.0.3", - "@fastify/static": "8.1.1", + "@fastify/static": "8.2.0", "@fastify/view": "10.0.2", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.1", - "@napi-rs/canvas": "0.1.69", - "@nestjs/common": "11.1.0", - "@nestjs/core": "11.1.0", - "@nestjs/testing": "11.1.0", + "@napi-rs/canvas": "0.1.70", + "@nestjs/common": "11.1.1", + "@nestjs/core": "11.1.1", + "@nestjs/testing": "11.1.1", "@peertube/http-signature": "1.7.0", "@sentry/node": "8.55.0", "@sentry/profiling-node": "8.55.0", "@simplewebauthn/server": "12.0.0", "@sinonjs/fake-timers": "11.3.1", "@smithy/node-http-handler": "2.5.0", - "@swc/cli": "0.7.3", - "@swc/core": "1.11.22", + "@swc/cli": "0.7.7", + "@swc/core": "1.11.29", "@twemoji/parser": "15.1.1", "@types/redis-info": "3.0.3", "accepts": "1.3.8", @@ -101,18 +101,18 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.51.1", + "bullmq": "5.53.0", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.4.1", "chalk-template": "1.1.0", - "chokidar": "3.6.0", + "chokidar": "4.0.3", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "5.3.2", + "fastify": "5.3.3", "fastify-raw-body": "5.0.0", "feed": "4.2.2", "file-type": "19.6.0", @@ -151,7 +151,7 @@ "os-utils": "0.0.14", "otpauth": "9.4.0", "parse5": "7.3.0", - "pg": "8.15.6", + "pg": "8.16.0", "pkce-challenge": "4.1.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -159,37 +159,37 @@ "qrcode": "1.5.4", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.21.4", + "re2": "1.21.5", "redis-info": "3.1.0", "redis-lock": "0.1.4", "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.2", - "sanitize-html": "2.16.0", + "sanitize-html": "2.17.0", "secure-json-parse": "3.0.2", "sharp": "0.33.5", - "semver": "7.7.1", + "semver": "7.7.2", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.25.11", + "systeminformation": "5.26.1", "tinycolor2": "1.6.0", "tmp": "0.2.3", - "tsc-alias": "1.8.15", + "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.22", + "typeorm": "0.3.24", "typescript": "5.8.3", "ulid": "2.4.0", "vary": "1.1.2", "web-push": "3.6.7", - "ws": "8.18.1", + "ws": "8.18.2", "xev": "3.0.2" }, "devDependencies": { "@jest/globals": "29.7.0", "@nestjs/platform-express": "10.4.17", - "@sentry/vue": "9.14.0", + "@sentry/vue": "9.22.0", "@simplewebauthn/types": "12.0.0", "@swc/jest": "0.2.38", "@types/accepts": "1.3.7", @@ -208,18 +208,18 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "22.15.2", + "@types/node": "22.15.21", "@types/nodemailer": "6.4.17", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", - "@types/pg": "8.11.14", + "@types/pg": "8.15.2", "@types/pug": "2.0.10", "@types/qrcode": "1.5.5", "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.15.0", + "@types/sanitize-html": "2.16.0", "@types/semver": "7.7.0", "@types/simple-oauth2": "5.0.7", "@types/sinonjs__fake-timers": "8.1.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.31.0", - "@typescript-eslint/parser": "8.31.0", + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1", "aws-sdk-client-mock": "4.1.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.31.0", @@ -241,6 +241,6 @@ "nodemon": "3.1.10", "pid-port": "1.0.2", "simple-oauth2": "5.1.0", - "supertest": "7.1.0" + "supertest": "7.1.1" } } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 646fa07911..9031096745 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -79,7 +79,6 @@ type Source = { proxyBypassHosts?: string[]; allowedPrivateNetworks?: string[]; - disallowExternalApRedirect?: boolean; maxFileSize?: number; @@ -100,11 +99,8 @@ type Source = { inboxJobMaxAttempts?: number; mediaProxy?: string; - proxyRemoteFiles?: boolean; videoThumbnailGenerator?: string; - signToActivityPubGet?: boolean; - perChannelMaxNoteCacheCount?: number; perUserNotificationsMaxCount?: number; deactivateAntennaThreshold?: number; @@ -156,7 +152,6 @@ export type Config = { proxySmtp: string | undefined; proxyBypassHosts: string[] | undefined; allowedPrivateNetworks: string[] | undefined; - disallowExternalApRedirect: boolean; maxFileSize: number; clusterLimit: number | undefined; id: string; @@ -170,8 +165,6 @@ export type Config = { relationshipJobPerSec: number | undefined; deliverJobMaxAttempts: number | undefined; inboxJobMaxAttempts: number | undefined; - proxyRemoteFiles: boolean | undefined; - signToActivityPubGet: boolean | undefined; logging?: { sql?: { disableQueryTruncation?: boolean, @@ -229,7 +222,7 @@ const dir = `${_dirname}/../../../.config`; /** * Path of configuration file */ -const path = process.env.MISSKEY_CONFIG_YML +export const path = process.env.MISSKEY_CONFIG_YML ? resolve(dir, process.env.MISSKEY_CONFIG_YML) : process.env.NODE_ENV === 'test' ? resolve(dir, 'test.yml') @@ -300,7 +293,6 @@ export function loadConfig(): Config { proxySmtp: config.proxySmtp, proxyBypassHosts: config.proxyBypassHosts, allowedPrivateNetworks: config.allowedPrivateNetworks, - disallowExternalApRedirect: config.disallowExternalApRedirect ?? false, maxFileSize: config.maxFileSize ?? 262144000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, @@ -313,8 +305,6 @@ export function loadConfig(): Config { relationshipJobPerSec: config.relationshipJobPerSec, deliverJobMaxAttempts: config.deliverJobMaxAttempts, inboxJobMaxAttempts: config.inboxJobMaxAttempts, - proxyRemoteFiles: config.proxyRemoteFiles, - signToActivityPubGet: config.signToActivityPubGet ?? true, mediaProxy: externalMediaProxy ?? internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, videoThumbnailGenerator: config.videoThumbnailGenerator ? diff --git a/packages/backend/src/core/ChatService.ts b/packages/backend/src/core/ChatService.ts index 2ceff341cc..4e81847a52 100644 --- a/packages/backend/src/core/ChatService.ts +++ b/packages/backend/src/core/ChatService.ts @@ -29,7 +29,7 @@ import { emojiRegex } from '@/misc/emoji-regex.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -const MAX_ROOM_MEMBERS = 30; +const MAX_ROOM_MEMBERS = 50; const MAX_REACTIONS_PER_MESSAGE = 100; const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/; diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 5f1e373429..0d5ac022aa 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -8,7 +8,7 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; import sharp from 'sharp'; import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; -import { IsNull } from 'typeorm'; +import { In, IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; @@ -720,6 +720,21 @@ export class DriveService { return fileObj; } + @bindThis + public async moveFiles(fileIds: MiDriveFile['id'][], folderId: MiDriveFolder['id'] | null, userId: MiUser['id']) { + const folder = folderId ? await this.driveFoldersRepository.findOneByOrFail({ + id: folderId, + userId: userId, + }) : null; + + await this.driveFilesRepository.update({ + id: In(fileIds), + userId: userId, + }, { + folderId: folder ? folder.id : null, + }); + } + @bindThis public async deleteFile(file: MiDriveFile, isExpired = false, deleter?: MiUser) { if (file.storedInternal) { diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index a1e806816b..04bbc7e38a 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -39,6 +39,7 @@ import type { } from './QueueModule.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; +import type { Packed } from '@/misc/json-schema.js'; export const QUEUE_TYPES = [ 'system', @@ -774,13 +775,13 @@ export class QueueService { } @bindThis - private packJobData(job: Bull.Job) { + private packJobData(job: Bull.Job): Packed<'QueueJob'> { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : []; stacktrace.reverse(); return { - id: job.id, + id: job.id!, name: job.name, data: job.data, opts: job.opts, diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 2534899ad1..646150455b 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -104,7 +104,7 @@ export class Resolver { throw new IdentifiableError('09d79f9e-64f1-4316-9cfa-e75c4d091574', 'Instance is blocked'); } - if (this.config.signToActivityPubGet && !this.user) { + if (this.meta.signToActivityPubGet && !this.user) { this.user = await this.systemAccountService.fetch('actor'); } diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index c485555f90..a6f7f369a6 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -6,7 +6,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; @@ -34,6 +34,9 @@ export class DriveFileEntityService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, @@ -95,7 +98,7 @@ export class DriveFileEntityService { return this.getProxiedUrl(file.uri, 'static'); } - if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) { // リモートかつ期限切れはローカルプロキシを試みる // 従来は/files/${thumbnailAccessKey}にアクセスしていたが、 // /filesはメディアプロキシにリダイレクトするようにしたため直接メディアプロキシを指定する @@ -115,7 +118,7 @@ export class DriveFileEntityService { } // リモートかつ期限切れはローカルプロキシを試みる - if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) { + if (file.uri != null && file.isLink && this.meta.proxyRemoteFiles) { const key = file.webpublicAccessKey; if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外 diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index e4eb10efca..23f6b692a7 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -31,7 +31,11 @@ import { packedChannelSchema } from '@/models/json-schema/channel.js'; import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; import { packedClipSchema } from '@/models/json-schema/clip.js'; import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js'; -import { packedQueueCountSchema } from '@/models/json-schema/queue.js'; +import { + packedQueueCountSchema, + packedQueueMetricsSchema, + packedQueueJobSchema, +} from '@/models/json-schema/queue.js'; import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js'; import { packedEmojiDetailedAdminSchema, @@ -100,6 +104,8 @@ export const refs = { PageBlock: packedPageBlockSchema, Channel: packedChannelSchema, QueueCount: packedQueueCountSchema, + QueueMetrics: packedQueueMetricsSchema, + QueueJob: packedQueueJobSchema, Antenna: packedAntennaSchema, Clip: packedClipSchema, FederationInstance: packedFederationInstanceSchema, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index e3625b247c..545173ff3c 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -680,6 +680,21 @@ export class MiMeta { default: false, }) public singleUserMode: boolean; + + @Column('boolean', { + default: true, + }) + public proxyRemoteFiles: boolean; + + @Column('boolean', { + default: true, + }) + public signToActivityPubGet: boolean; + + @Column('boolean', { + default: true, + }) + public allowExternalApRedirect: boolean; } export type SoftwareSuspension = { diff --git a/packages/backend/src/models/json-schema/queue.ts b/packages/backend/src/models/json-schema/queue.ts index 2ecf5c831f..dad0cf57f6 100644 --- a/packages/backend/src/models/json-schema/queue.ts +++ b/packages/backend/src/models/json-schema/queue.ts @@ -28,3 +28,110 @@ export const packedQueueCountSchema = { }, }, } as const; + +// Bull.Metrics +export const packedQueueMetricsSchema = { + type: 'object', + properties: { + meta: { + type: 'object', + optional: false, nullable: false, + properties: { + count: { + type: 'number', + optional: false, nullable: false, + }, + prevTS: { + type: 'number', + optional: false, nullable: false, + }, + prevCount: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + data: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'number', + optional: false, nullable: false, + }, + }, + count: { + type: 'number', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedQueueJobSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + data: { + type: 'object', + optional: false, nullable: false, + }, + opts: { + type: 'object', + optional: false, nullable: false, + }, + timestamp: { + type: 'number', + optional: false, nullable: false, + }, + processedOn: { + type: 'number', + optional: true, nullable: false, + }, + processedBy: { + type: 'string', + optional: true, nullable: false, + }, + finishedOn: { + type: 'number', + optional: true, nullable: false, + }, + progress: { + type: 'object', + optional: false, nullable: false, + }, + attempts: { + type: 'number', + optional: false, nullable: false, + }, + delay: { + type: 'number', + optional: false, nullable: false, + }, + failedReason: { + type: 'string', + optional: false, nullable: false, + }, + stacktrace: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + returnValue: { + type: 'object', + optional: false, nullable: false, + }, + isFailed: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index c859f1d82c..23c085ee27 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -108,7 +108,7 @@ export class ServerService implements OnApplicationShutdown { // this will break lookup that involve copying a URL from a third-party server, like trying to lookup http://charlie.example.com/@alice@alice.com // // this is not required by standard but protect us from peers that did not validate final URL. - if (this.config.disallowExternalApRedirect) { + if (!this.meta.allowExternalApRedirect) { const maybeApLookupRegex = /application\/activity\+json|application\/ld\+json.+activitystreams/i; fastify.addHook('onSend', (request, reply, _, done) => { const location = reply.getHeader('location'); @@ -133,8 +133,8 @@ export class ServerService implements OnApplicationShutdown { reply.header('content-type', 'text/plain; charset=utf-8'); reply.header('link', `<${encodeURI(location)}>; rel="canonical"`); done(null, [ - "Refusing to relay remote ActivityPub object lookup.", - "", + 'Refusing to relay remote ActivityPub object lookup.', + '', `Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`, ].join('\n')); }); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index a42fdaf730..7a4af407a3 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown { if (factor > 0) { // Rate limit - await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor).catch(err => { - if ('info' in err) { - // errはLimiter.LimiterInfoであることが期待される - throw new ApiError({ - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - httpStatusCode: 429, - }, err.info); - } else { - throw new TypeError('information must be a rate-limiter information.'); - } - }); + const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable }, limitActor, factor); + if (rateLimit != null) { + throw new ApiError({ + message: 'Rate limit exceeded. Please try again later.', + code: 'RATE_LIMIT_EXCEEDED', + id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', + httpStatusCode: 429, + }, rateLimit.info); + } } } diff --git a/packages/backend/src/server/api/RateLimiterService.ts b/packages/backend/src/server/api/RateLimiterService.ts index 52d73baa0a..a730d8c60e 100644 --- a/packages/backend/src/server/api/RateLimiterService.ts +++ b/packages/backend/src/server/api/RateLimiterService.ts @@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type { IEndpointMeta } from './endpoints.js'; +type RateLimitInfo = { + code: 'BRIEF_REQUEST_INTERVAL', + info: Limiter.LimiterInfo, +} | { + code: 'RATE_LIMIT_EXCEEDED', + info: Limiter.LimiterInfo, +}; + @Injectable() export class RateLimiterService { private logger: Logger; @@ -31,77 +39,55 @@ export class RateLimiterService { } @bindThis - public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1) { - { - if (this.disabled) { - return Promise.resolve(); - } + private checkLimiter(options: Limiter.LimiterOption): Promise { + return new Promise((resolve, reject) => { + new Limiter(options).get((err, info) => { + if (err) { + return reject(err); + } + resolve(info); + }); + }); + } - // Short-term limit - const min = new Promise((ok, reject) => { - const minIntervalLimiter = new Limiter({ - id: `${actor}:${limitation.key}:min`, - duration: limitation.minInterval! * factor, - max: 1, - db: this.redisClient, - }); + @bindThis + public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable }, actor: string, factor = 1): Promise { + if (this.disabled) { + return null; + } - minIntervalLimiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'BRIEF_REQUEST_INTERVAL', info }); - } else { - if (hasLongTermLimit) { - return max.then(ok, reject); - } else { - return ok(); - } - } - }); + // Short-term limit + if (limitation.minInterval != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}:min`, + duration: limitation.minInterval * factor, + max: 1, + db: this.redisClient, }); - // Long term limit - const max = new Promise((ok, reject) => { - const limiter = new Limiter({ - id: `${actor}:${limitation.key}`, - duration: limitation.duration! * factor, - max: limitation.max! / factor, - db: this.redisClient, - }); + this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`); - limiter.get((err, info) => { - if (err) { - return reject({ code: 'ERR', info }); - } - - this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); - - if (info.remaining === 0) { - return reject({ code: 'RATE_LIMIT_EXCEEDED', info }); - } else { - return ok(); - } - }); - }); - - const hasShortTermLimit = typeof limitation.minInterval === 'number'; - - const hasLongTermLimit = - typeof limitation.duration === 'number' && - typeof limitation.max === 'number'; - - if (hasShortTermLimit) { - return min; - } else if (hasLongTermLimit) { - return max; - } else { - return Promise.resolve(); + if (info.remaining === 0) { + return { code: 'BRIEF_REQUEST_INTERVAL', info }; } } + + // Long term limit + if (limitation.duration != null && limitation.max != null) { + const info = await this.checkLimiter({ + id: `${actor}:${limitation.key}`, + duration: limitation.duration, + max: limitation.max / factor, + db: this.redisClient, + }); + + this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`); + + if (info.remaining === 0) { + return { code: 'RATE_LIMIT_EXCEEDED', info }; + } + } + + return null; } } diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 1d983ca4bc..3e889372d8 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -89,10 +89,9 @@ export class SigninApiService { return { error }; } - try { // not more than 1 attempt per second and not more than 10 attempts per hour - await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); - } catch (err) { + const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip)); + if (rateLimit != null) { reply.code(429); return { error: { diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index bd466b3cad..1fdd000fdf 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -175,6 +175,7 @@ export * as 'drive/files/find' from './endpoints/drive/files/find.js'; export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js'; export * as 'drive/files/show' from './endpoints/drive/files/show.js'; export * as 'drive/files/update' from './endpoints/drive/files/update.js'; +export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js'; export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js'; export * as 'drive/folders' from './endpoints/drive/folders.js'; export * as 'drive/folders/create' from './endpoints/drive/folders/create.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index cd36985485..0cd46b614f 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -555,6 +555,18 @@ export const meta = { enum: ['all', 'local', 'none'], optional: false, nullable: false, }, + proxyRemoteFiles: { + type: 'boolean', + optional: false, nullable: false, + }, + signToActivityPubGet: { + type: 'boolean', + optional: false, nullable: false, + }, + allowExternalApRedirect: { + type: 'boolean', + optional: false, nullable: false, + }, }, }, } as const; @@ -702,6 +714,9 @@ export default class extends Endpoint { // eslint- deliverSuspendedSoftware: instance.deliverSuspendedSoftware, singleUserMode: instance.singleUserMode, ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor, + proxyRemoteFiles: instance.proxyRemoteFiles, + signToActivityPubGet: instance.signToActivityPubGet, + allowExternalApRedirect: instance.allowExternalApRedirect, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts index 79731c9786..155f2c4000 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/jobs.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,6 +13,15 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + optional: false, nullable: false, + ref: 'QueueJob', + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts index 10ce48332a..0098160165 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/queue-stats.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,6 +13,118 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + name: { + type: 'string', + optional: false, nullable: false, + enum: QUEUE_TYPES, + }, + qualifiedName: { + type: 'string', + optional: false, nullable: false, + }, + counts: { + type: 'object', + optional: false, nullable: false, + additionalProperties: { + type: 'number', + }, + }, + isPaused: { + type: 'boolean', + optional: false, nullable: false, + }, + metrics: { + type: 'object', + optional: false, nullable: false, + properties: { + completed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + failed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + }, + }, + db: { + type: 'object', + optional: false, nullable: false, + properties: { + version: { + type: 'string', + optional: false, nullable: false, + }, + mode: { + type: 'string', + optional: false, nullable: false, + enum: ['cluster', 'standalone', 'sentinel'], + }, + runId: { + type: 'string', + optional: false, nullable: false, + }, + processId: { + type: 'string', + optional: false, nullable: false, + }, + port: { + type: 'number', + optional: false, nullable: false, + }, + os: { + type: 'string', + optional: false, nullable: false, + }, + uptime: { + type: 'number', + optional: false, nullable: false, + }, + memory: { + type: 'object', + optional: false, nullable: false, + properties: { + total: { + type: 'number', + optional: false, nullable: false, + }, + used: { + type: 'number', + optional: false, nullable: false, + }, + fragmentationRatio: { + type: 'number', + optional: false, nullable: false, + }, + peak: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + clients: { + type: 'object', + optional: false, nullable: false, + properties: { + blocked: { + type: 'number', + optional: false, nullable: false, + }, + connected: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + }, + } + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts index 3a38275f60..8d27e38c84 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/queues.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/queues.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,6 +13,47 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + name: { + type: 'string', + optional: false, nullable: false, + enum: QUEUE_TYPES, + }, + counts: { + type: 'object', + optional: false, nullable: false, + additionalProperties: { + type: 'number', + }, + }, + isPaused: { + type: 'boolean', + optional: false, nullable: false, + }, + metrics: { + type: 'object', + optional: false, nullable: false, + properties: { + completed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + failed: { + optional: false, nullable: false, + ref: 'QueueMetrics', + }, + }, + }, + }, + }, + }, } as const; export const paramDef = { diff --git a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts index 63747b5540..1735c22674 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/show-job.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js'; export const meta = { @@ -14,6 +13,11 @@ export const meta = { requireCredential: true, requireModerator: true, kind: 'read:admin:queue', + + res: { + optional: false, nullable: false, + ref: 'QueueJob', + }, } as const; export const paramDef = { @@ -28,7 +32,6 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private moderationLogService: ModerationLogService, private queueService: QueueService, ) { super(meta, paramDef, async (ps, me) => { diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index a96fbd759c..0e3569d667 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -201,6 +201,9 @@ export const paramDef = { type: 'string', enum: ['all', 'local', 'none'], }, + proxyRemoteFiles: { type: 'boolean' }, + signToActivityPubGet: { type: 'boolean' }, + allowExternalApRedirect: { type: 'boolean' }, }, required: [], } as const; @@ -703,6 +706,18 @@ export default class extends Endpoint { // eslint- set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor; } + if (ps.proxyRemoteFiles !== undefined) { + set.proxyRemoteFiles = ps.proxyRemoteFiles; + } + + if (ps.signToActivityPubGet !== undefined) { + set.signToActivityPubGet = ps.signToActivityPubGet; + } + + if (ps.allowExternalApRedirect !== undefined) { + set.allowExternalApRedirect = ps.allowExternalApRedirect; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts new file mode 100644 index 0000000000..c8500895eb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/files/move-bulk.ts @@ -0,0 +1,41 @@ +/* + * 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 { DriveService } from '@/core/DriveService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['drive'], + + requireCredential: true, + + kind: 'write:drive', + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } }, + folderId: { type: 'string', format: 'misskey:id', nullable: true }, + }, + required: ['fileIds'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private driveService: DriveService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id); + }); + } +} diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 9a33d27d86..8ca61a497d 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -212,6 +212,7 @@ export class ClientServerService { instanceUrl: this.config.url, metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)), now: Date.now(), + federationEnabled: this.meta.federation !== 'none', }; } diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index fb659ce171..ea1993aed0 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -55,7 +55,8 @@ block meta if note.next link(rel='next' href=`${config.url}/notes/${note.next}`) - if !user.host - link(rel='alternate' href=url type='application/activity+json') - if note.uri - link(rel='alternate' href=note.uri type='application/activity+json') + if federationEnabled + if !user.host + link(rel='alternate' href=url type='application/activity+json') + if note.uri + link(rel='alternate' href=note.uri type='application/activity+json') diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index 2b0a7bab5c..b9f740f5b6 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -32,12 +32,13 @@ block meta meta(name='twitter:creator' content=`@${profile.twitter.screenName}`) if !sub - if !user.host - link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json') - if user.uri - link(rel='alternate' href=user.uri type='application/activity+json') - if profile.url - link(rel='alternate' href=profile.url type='text/html') + if federationEnabled + if !user.host + link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json') + if user.uri + link(rel='alternate' href=user.uri type='application/activity+json') + if profile.url + link(rel='alternate' href=profile.url type='text/html') each m in me link(rel='me' href=`${m}`) diff --git a/packages/backend/test-federation/.config/example.default.yml b/packages/backend/test-federation/.config/example.default.yml index 28d51ac86e..fd20613885 100644 --- a/packages/backend/test-federation/.config/example.default.yml +++ b/packages/backend/test-federation/.config/example.default.yml @@ -17,8 +17,6 @@ proxyBypassHosts: - www.recaptcha.net - hcaptcha.com - challenges.cloudflare.com -proxyRemoteFiles: true -signToActivityPubGet: true allowedPrivateNetworks: - 127.0.0.1/32 - 172.20.0.0/16 diff --git a/packages/frontend-embed/@types/global.d.ts b/packages/frontend-embed/@types/global.d.ts index 1025d1bedb..8a067a78ec 100644 --- a/packages/frontend-embed/@types/global.d.ts +++ b/packages/frontend-embed/@types/global.d.ts @@ -10,9 +10,6 @@ declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; declare const _PERF_PREFIX_: string; -declare const _DATA_TRANSFER_DRIVE_FILE_: string; -declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; -declare const _DATA_TRANSFER_DECK_COLUMN_: string; // for dev-mode declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js index 7805256fd4..2aef311e2e 100644 --- a/packages/frontend-embed/eslint.config.js +++ b/packages/frontend-embed/eslint.config.js @@ -30,9 +30,6 @@ export default [ _VERSION_: false, _ENV_: false, _PERF_PREFIX_: false, - _DATA_TRANSFER_DRIVE_FILE_: false, - _DATA_TRANSFER_DRIVE_FOLDER_: false, - _DATA_TRANSFER_DECK_COLUMN_: false, }, parser, parserOptions: { diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 19193e20fd..026ecd96de 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -14,10 +14,10 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", "@rollup/pluginutils": "5.1.4", - "@tabler/icons-webfont": "3.31.0", + "@tabler/icons-webfont": "3.33.0", "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.2.3", - "@vue/compiler-sfc": "3.5.13", + "@vitejs/plugin-vue": "5.2.4", + "@vue/compiler-sfc": "3.5.14", "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", @@ -26,42 +26,42 @@ "mfm-js": "0.24.0", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.40.0", - "sass": "1.87.0", - "shiki": "3.3.0", + "rollup": "4.41.0", + "sass": "1.89.0", + "shiki": "3.4.2", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.15", + "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", "typescript": "5.8.3", "uuid": "11.1.0", - "vite": "6.3.4", - "vue": "3.5.13" + "vite": "6.3.5", + "vue": "3.5.14" }, "devDependencies": { "@misskey-dev/summaly": "5.2.1", "@testing-library/vue": "8.1.0", "@types/estree": "1.0.7", "@types/micromatch": "4.0.9", - "@types/node": "22.15.2", + "@types/node": "22.15.21", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.31.0", - "@typescript-eslint/parser": "8.31.0", - "@vitest/coverage-v8": "3.1.2", - "@vue/runtime-core": "3.5.13", + "@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", "cross-env": "7.0.3", "eslint-plugin-import": "2.31.0", - "eslint-plugin-vue": "10.0.0", + "eslint-plugin-vue": "10.1.0", "fast-glob": "3.3.3", - "happy-dom": "17.4.4", + "happy-dom": "17.4.7", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.7.5", + "msw": "2.8.4", "nodemon": "3.1.10", "prettier": "3.5.3", - "start-server-and-test": "2.0.11", + "start-server-and-test": "2.0.12", "vite-plugin-turbosnap": "1.0.3", "vue-component-type-helpers": "2.2.10", "vue-eslint-parser": "10.1.3", diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue index 2c96ce3215..94f0268da4 100644 --- a/packages/frontend-embed/src/components/EmMediaImage.vue +++ b/packages/frontend-embed/src/components/EmMediaImage.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only target="_blank" rel="noopener" > - import { ref, computed } from 'vue'; import * as Misskey from 'misskey-js'; -import ImgWithBlurhash from '@/components/EmImgWithBlurhash.vue'; +import EmImgWithBlurhash from '@/components/EmImgWithBlurhash.vue'; import { i18n } from '@/i18n.js'; const props = withDefaults(defineProps<{ diff --git a/packages/frontend-shared/@types/global.d.ts b/packages/frontend-shared/@types/global.d.ts index 4b8d679e75..52081d07b3 100644 --- a/packages/frontend-shared/@types/global.d.ts +++ b/packages/frontend-shared/@types/global.d.ts @@ -11,9 +11,6 @@ declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; declare const _PERF_PREFIX_: string; -declare const _DATA_TRANSFER_DRIVE_FILE_: string; -declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; -declare const _DATA_TRANSFER_DECK_COLUMN_: string; // for dev-mode declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js index ac5c67d0b6..f6fd64153c 100644 --- a/packages/frontend-shared/eslint.config.js +++ b/packages/frontend-shared/eslint.config.js @@ -35,9 +35,6 @@ export default [ _VERSION_: false, _ENV_: false, _PERF_PREFIX_: false, - _DATA_TRANSFER_DRIVE_FILE_: false, - _DATA_TRANSFER_DRIVE_FOLDER_: false, - _DATA_TRANSFER_DECK_COLUMN_: false, }, parser, parserOptions: { diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 1ec6eb3559..eac3d420c8 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -21,11 +21,11 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "22.15.2", - "@typescript-eslint/eslint-plugin": "8.31.0", - "@typescript-eslint/parser": "8.31.0", - "esbuild": "0.25.3", - "eslint-plugin-vue": "10.0.0", + "@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", "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.13" + "vue": "3.5.14" } } diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 index 8ebaf20b64..0d719ef35c 100644 --- a/packages/frontend-shared/themes/_dark.json5 +++ b/packages/frontend-shared/themes/_dark.json5 @@ -61,7 +61,6 @@ switchOnFg: '@accent', inputBorder: 'rgba(255, 255, 255, 0.1)', inputBorderHover: 'rgba(255, 255, 255, 0.2)', - driveFolderBg: ':alpha<0.3<@accent', badge: '#31b1ce', messageBg: '@bg', success: '#86b300', diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5 index 63ad95ff84..51fb697922 100644 --- a/packages/frontend-shared/themes/_light.json5 +++ b/packages/frontend-shared/themes/_light.json5 @@ -61,7 +61,6 @@ switchOnFg: '@fgOnAccent', inputBorder: 'rgba(0, 0, 0, 0.1)', inputBorderHover: 'rgba(0, 0, 0, 0.2)', - driveFolderBg: ':alpha<0.3<@accent', badge: '#31b1ce', messageBg: '@bg', success: '#86b300', diff --git a/packages/frontend-shared/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5 index 6d34665528..8ddedbbb07 100644 --- a/packages/frontend-shared/themes/d-astro.json5 +++ b/packages/frontend-shared/themes/d-astro.json5 @@ -38,7 +38,6 @@ navIndicator: '@accent', buttonGradateA: '@accent', buttonGradateB: ':hue<-20<@accent', - driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', diff --git a/packages/frontend-shared/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5 index 4f6c04b906..8ce6d25328 100644 --- a/packages/frontend-shared/themes/d-u0.json5 +++ b/packages/frontend-shared/themes/d-u0.json5 @@ -47,7 +47,6 @@ inputBorder: 'rgba(255, 255, 255, 0.1)', panelBorder: '" solid 1px var(--MI_THEME-divider)', navIndicator: '@indicator', - driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', diff --git a/packages/frontend-shared/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5 index 35241986df..f685dfbb42 100644 --- a/packages/frontend-shared/themes/l-u0.json5 +++ b/packages/frontend-shared/themes/l-u0.json5 @@ -49,7 +49,6 @@ panelBorder: '" solid 1px var(--MI_THEME-divider)', navIndicator: '@indicator', buttonHoverBg: '#0000001a', - driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', diff --git a/packages/frontend-shared/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5 index 5ad8d60728..487393ea7c 100644 --- a/packages/frontend-shared/themes/l-vivid.json5 +++ b/packages/frontend-shared/themes/l-vivid.json5 @@ -39,7 +39,6 @@ inputBorderHover: 'rgba(0, 0, 0, 0.2)', panelBorder: '" solid 1px var(--MI_THEME-divider)', navIndicator: '@accent', - driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':darken<3<@fg', fgOnWhite: '@accent', panelHeaderBg: ':lighten<3<@panel', diff --git a/packages/frontend/@types/global.d.ts b/packages/frontend/@types/global.d.ts index 1025d1bedb..8a067a78ec 100644 --- a/packages/frontend/@types/global.d.ts +++ b/packages/frontend/@types/global.d.ts @@ -10,9 +10,6 @@ declare const _VERSION_: string; declare const _ENV_: string; declare const _DEV_: boolean; declare const _PERF_PREFIX_: string; -declare const _DATA_TRANSFER_DRIVE_FILE_: string; -declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; -declare const _DATA_TRANSFER_DECK_COLUMN_: string; // for dev-mode declare const _LANGS_FULL_: string[][]; diff --git a/packages/frontend/assets/unknown.png b/packages/frontend/assets/unknown.png new file mode 100644 index 0000000000..d27bdfc8b3 Binary files /dev/null and b/packages/frontend/assets/unknown.png differ diff --git a/packages/frontend/eslint.config.js b/packages/frontend/eslint.config.js index 1b9a9b68c0..8f835975a8 100644 --- a/packages/frontend/eslint.config.js +++ b/packages/frontend/eslint.config.js @@ -30,9 +30,6 @@ export default [ _VERSION_: false, _ENV_: false, _PERF_PREFIX_: false, - _DATA_TRANSFER_DRIVE_FILE_: false, - _DATA_TRANSFER_DRIVE_FOLDER_: false, - _DATA_TRANSFER_DECK_COLUMN_: false, }, parser, parserOptions: { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index ad2a72f7fd..2dcda56ceb 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,12 +24,12 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", "@rollup/pluginutils": "5.1.4", - "@sentry/vue": "9.14.0", + "@sentry/vue": "9.22.0", "@syuilo/aiscript": "0.19.0", - "@tabler/icons-webfont": "3.31.0", + "@tabler/icons-webfont": "3.33.0", "@twemoji/parser": "15.1.1", - "@vitejs/plugin-vue": "5.2.3", - "@vue/compiler-sfc": "3.5.13", + "@vitejs/plugin-vue": "5.2.4", + "@vue/compiler-sfc": "3.5.14", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "analytics": "0.8.16", "astring": "1.9.0", @@ -48,7 +48,7 @@ "estree-walker": "3.0.3", "eventemitter3": "5.0.1", "frontend-shared": "workspace:*", - "idb-keyval": "6.2.1", + "idb-keyval": "6.2.2", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", "json5": "2.2.3", @@ -60,84 +60,84 @@ "misskey-reversi": "workspace:*", "photoswipe": "5.4.4", "punycode.js": "2.3.1", - "rollup": "4.40.0", - "sanitize-html": "2.16.0", - "sass": "1.87.0", - "shiki": "3.3.0", + "rollup": "4.41.0", + "sanitize-html": "2.17.0", + "sass": "1.89.0", + "shiki": "3.4.2", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", "three": "0.176.0", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", - "tsc-alias": "1.8.15", + "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.4", - "vue": "3.5.13", + "vite": "6.3.5", + "vue": "3.5.14", "vuedraggable": "next", "wanakana": "5.3.1" }, "devDependencies": { "@misskey-dev/summaly": "5.2.1", - "@storybook/addon-actions": "8.6.12", - "@storybook/addon-essentials": "8.6.12", - "@storybook/addon-interactions": "8.6.12", - "@storybook/addon-links": "8.6.12", - "@storybook/addon-mdx-gfm": "8.6.12", - "@storybook/addon-storysource": "8.6.12", - "@storybook/blocks": "8.6.12", - "@storybook/components": "8.6.12", - "@storybook/core-events": "8.6.12", - "@storybook/manager-api": "8.6.12", - "@storybook/preview-api": "8.6.12", - "@storybook/react": "8.6.12", - "@storybook/react-vite": "8.6.12", - "@storybook/test": "8.6.12", - "@storybook/theming": "8.6.12", - "@storybook/types": "8.6.12", - "@storybook/vue3": "8.6.12", - "@storybook/vue3-vite": "8.6.12", + "@storybook/addon-actions": "8.6.14", + "@storybook/addon-essentials": "8.6.14", + "@storybook/addon-interactions": "8.6.14", + "@storybook/addon-links": "8.6.14", + "@storybook/addon-mdx-gfm": "8.6.14", + "@storybook/addon-storysource": "8.6.14", + "@storybook/blocks": "8.6.14", + "@storybook/components": "8.6.14", + "@storybook/core-events": "8.6.14", + "@storybook/manager-api": "8.6.14", + "@storybook/preview-api": "8.6.14", + "@storybook/react": "8.6.14", + "@storybook/react-vite": "8.6.14", + "@storybook/test": "8.6.14", + "@storybook/theming": "8.6.14", + "@storybook/types": "8.6.14", + "@storybook/vue3": "8.6.14", + "@storybook/vue3-vite": "8.6.14", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", "@types/estree": "1.0.7", "@types/matter-js": "0.19.8", "@types/micromatch": "4.0.9", - "@types/node": "22.15.2", + "@types/node": "22.15.21", "@types/punycode.js": "npm:@types/punycode@2.1.4", - "@types/sanitize-html": "2.15.0", + "@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.31.0", - "@typescript-eslint/parser": "8.31.0", - "@vitest/coverage-v8": "3.1.2", - "@vue/compiler-core": "3.5.13", - "@vue/runtime-core": "3.5.13", + "@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", "cross-env": "7.0.3", - "cypress": "14.3.2", + "cypress": "14.4.0", "eslint-plugin-import": "2.31.0", - "eslint-plugin-vue": "10.0.0", + "eslint-plugin-vue": "10.1.0", "fast-glob": "3.3.3", - "happy-dom": "17.4.4", + "happy-dom": "17.4.7", "intersection-observer": "0.12.2", "micromatch": "4.0.8", "minimatch": "10.0.1", - "msw": "2.7.5", + "msw": "2.8.4", "msw-storybook-addon": "2.0.4", "nodemon": "3.1.10", "prettier": "3.5.3", "react": "19.1.0", "react-dom": "19.1.0", "seedrandom": "3.0.5", - "start-server-and-test": "2.0.11", - "storybook": "8.6.12", + "start-server-and-test": "2.0.12", + "storybook": "8.6.14", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "vite-plugin-turbosnap": "1.0.3", - "vitest": "3.1.2", + "vitest": "3.1.4", "vitest-fetch-mock": "0.4.5", "vue-component-type-helpers": "2.2.10", "vue-eslint-parser": "10.1.3", diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts index 08ba89dd9d..a876e94ee8 100644 --- a/packages/frontend/src/aiscript/api.ts +++ b/packages/frontend/src/aiscript/api.ts @@ -66,6 +66,11 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string }) }); return confirm.canceled ? values.FALSE : values.TRUE; }), + 'Mk:toast': values.FN_NATIVE(async ([text]) => { + utils.assertString(text); + os.toast(text.value); + return values.NULL; + }), 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { utils.assertString(ep); if (ep.value.includes('://') || ep.value.includes('..')) { diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index 6bf55388af..d4f338e4c8 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -48,7 +48,6 @@ const props = withDefaults(defineProps<{ thin?: boolean; naked?: boolean; foldable?: boolean; - onUnfold?: () => boolean; // return false to prevent unfolding scrollable?: boolean; expanded?: boolean; maxHeight?: number | null; @@ -103,8 +102,6 @@ const omitObserver = new ResizeObserver((entries, observer) => { }); function showMore() { - if (props.onUnfold && !props.onUnfold()) return; - ignoreOmit.value = true; omitted.value = false; } diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue index ba21394cbc..7f592fba79 100644 --- a/packages/frontend/src/components/MkCropperDialog.vue +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -15,18 +15,16 @@ SPDX-License-Identifier: AGPL-3.0-only @closed="emit('closed')" > - + @@ -35,27 +33,23 @@ import { onMounted, useTemplateRef, ref } from 'vue'; import * as Misskey from 'misskey-js'; import Cropper from 'cropperjs'; import tinycolor from 'tinycolor2'; -import { apiUrl } from '@@/js/config.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import * as os from '@/os.js'; -import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { getProxiedImageUrl } from '@/utility/media-proxy.js'; -import { prefer } from '@/preferences.js'; + +const props = defineProps<{ + imageFile: File | Blob; + aspectRatio: number | null; + uploadFolder?: string | null; +}>(); const emit = defineEmits<{ - (ev: 'ok', cropped: Misskey.entities.DriveFile): void; + (ev: 'ok', cropped: File | Blob): void; (ev: 'cancel'): void; (ev: 'closed'): void; }>(); -const props = defineProps<{ - file: Misskey.entities.DriveFile; - aspectRatio: number; - uploadFolder?: string | null; -}>(); - -const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); +const imgUrl = URL.createObjectURL(props.imageFile); const dialogEl = useTemplateRef('dialogEl'); const imgEl = useTemplateRef('imgEl'); let cropper: Cropper | null = null; @@ -73,31 +67,10 @@ const ok = async () => { const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender }); croppedCanvas?.toBlob(blob => { if (!blob) return; - const formData = new FormData(); - formData.append('file', blob); - formData.append('name', `cropped_${props.file.name}`); - formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false'); - if (props.file.comment) { formData.append('comment', props.file.comment);} - formData.append('i', $i!.token); - if (props.uploadFolder) { - formData.append('folderId', props.uploadFolder); - } else if (props.uploadFolder !== null && prefer.s.uploadFolder) { - formData.append('folderId', prefer.s.uploadFolder); - } - - window.fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: formData, - }) - .then(response => response.json()) - .then(f => { - res(f); - }); + res(blob); }); }); - os.promiseDialog(promise); - const f = await promise; emit('ok', f); @@ -126,8 +99,8 @@ onMounted(() => { const selection = cropper.getCropperSelection()!; selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(); - selection.aspectRatio = props.aspectRatio; - selection.initialAspectRatio = props.aspectRatio; + if (props.aspectRatio != null) selection.aspectRatio = props.aspectRatio; + selection.initialAspectRatio = props.aspectRatio ?? 1; selection.outlined = true; window.setTimeout(() => { diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue index 70ab60cfae..0eca85b3a6 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -8,7 +8,6 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.isSelected]: isSelected }]" draggable="true" :title="title" - @click="onClick" @contextmenu.stop="onContextmenu" @dragstart="onDragstart" @dragend="onDragend" @@ -46,24 +45,18 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { $i } from '@/i.js'; import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; -import { deviceKind } from '@/utility/device-kind.js'; -import { useRouter } from '@/router.js'; - -const router = useRouter(); +import { setDragData } from '@/drag-and-drop.js'; const props = withDefaults(defineProps<{ file: Misskey.entities.DriveFile; folder: Misskey.entities.DriveFolder | null; isSelected?: boolean; - selectMode?: boolean; }>(), { isSelected: false, - selectMode: false, }); const emit = defineEmits<{ - (ev: 'chosen', r: Misskey.entities.DriveFile): void; - (ev: 'dragstart'): void; + (ev: 'dragstart', dragEvent: DragEvent): void; (ev: 'dragend'): void; }>(); @@ -71,18 +64,6 @@ const isDragging = ref(false); const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`); -function onClick(ev: MouseEvent) { - if (props.selectMode) { - emit('chosen', props.file); - } else { - if (deviceKind === 'desktop') { - router.push(`/my/drive/file/${props.file.id}`); - } else { - os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); - } - } -} - function onContextmenu(ev: MouseEvent) { os.contextMenu(getDriveFileMenu(props.file, props.folder), ev); } @@ -90,11 +71,11 @@ function onContextmenu(ev: MouseEvent) { function onDragstart(ev: DragEvent) { if (ev.dataTransfer) { ev.dataTransfer.effectAllowed = 'move'; - ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file)); + setDragData(ev, 'driveFiles', [props.file]); } isDragging.value = true; - emit('dragstart'); + emit('dragstart', ev); } function onDragend() { @@ -114,7 +95,7 @@ function onDragend() { &:hover { background: rgba(#000, 0.05); - > .label { + .label { &::before, &::after { background: #0b65a5; @@ -132,7 +113,7 @@ function onDragend() { &:active { background: rgba(#000, 0.1); - > .label { + .label { &::before, &::after { background: #0b588c; @@ -158,19 +139,19 @@ function onDragend() { background: hsl(from var(--MI_THEME-accent) h s calc(l - 10)); } - > .label { + .label { &::before, &::after { display: none; } } - > .name { - color: #fff; + .name { + color: var(--MI_THEME-fgOnAccent); } - > .thumbnail { - color: #fff; + .thumbnail { + color: var(--MI_THEME-fgOnAccent); } } } @@ -240,8 +221,9 @@ function onDragend() { .name { display: block; - margin: 4px 0 0 0; - font-size: 0.8em; + margin: 8px 0 0 0; + padding: 0 2px; + font-size: 82%; text-align: center; word-break: break-all; color: var(--MI_THEME-fg); diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 9c72691d21..83472eec3d 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -8,7 +8,6 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.draghover]: draghover }]" draggable="true" :title="title" - @click="onClick" @contextmenu.stop="onContextmenu" @mouseover="onMouseover" @mouseout="onMouseout" @@ -19,14 +18,13 @@ SPDX-License-Identifier: AGPL-3.0-only @dragstart="onDragstart" @dragend="onDragend" > -

- - - {{ folder.name }} -

-

+ + + +

{{ folder.name }}
+
{{ i18n.ts.uploadFolder }} -

+
@@ -43,6 +41,9 @@ import { i18n } from '@/i18n.js'; import { claimAchievement } from '@/utility/achievements.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { prefer } from '@/preferences.js'; +import { globalEvents } from '@/events.js'; +import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js'; +import { selectDriveFolder } from '@/utility/drive.js'; const props = withDefaults(defineProps<{ folder: Misskey.entities.DriveFolder; @@ -56,10 +57,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'chosen', v: Misskey.entities.DriveFolder): void; (ev: 'unchose', v: Misskey.entities.DriveFolder): void; - (ev: 'move', v: Misskey.entities.DriveFolder): void; - (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder); - (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void; - (ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void; + (ev: 'upload', files: File[], folder: Misskey.entities.DriveFolder); (ev: 'dragstart'): void; (ev: 'dragend'): void; }>(); @@ -78,10 +76,6 @@ function checkboxClicked() { } } -function onClick() { - emit('move', props.folder); -} - function onMouseover() { hover.value = true; } @@ -101,10 +95,7 @@ function onDragover(ev: DragEvent) { } const isFile = ev.dataTransfer.items[0].kind === 'file'; - const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_; - const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_; - - if (isFile || isDriveFile || isDriveFolder) { + if (isFile || checkDragDataType(ev, ['driveFiles', 'driveFolders'])) { switch (ev.dataTransfer.effectAllowed) { case 'all': case 'uninitialized': @@ -141,55 +132,64 @@ function onDrop(ev: DragEvent) { // ファイルだったら if (ev.dataTransfer.files.length > 0) { - for (const file of Array.from(ev.dataTransfer.files)) { - emit('upload', file, props.folder); - } + emit('upload', Array.from(ev.dataTransfer.files), props.folder); return; } //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile !== '') { - const file = JSON.parse(driveFile); - emit('removeFile', file.id); - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: props.folder.id, - }); + { + const droppedData = getDragData(ev, 'driveFiles'); + if (droppedData != null) { + misskeyApi('drive/files/move-bulk', { + fileIds: droppedData.map(f => f.id), + folderId: props.folder.id, + }).then(() => { + globalEvents.emit('driveFilesUpdated', droppedData.map(x => ({ + ...x, + folderId: props.folder.id, + folder: props.folder, + }))); + }); + } } //#endregion //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder !== '') { - const folder = JSON.parse(driveFolder); + { + const droppedData = getDragData(ev, 'driveFolders'); + if (droppedData != null) { + const droppedFolder = droppedData[0]; - // 移動先が自分自身ならreject - if (folder.id === props.folder.id) return; + // 移動先が自分自身ならreject + if (droppedFolder.id === props.folder.id) return; - emit('removeFolder', folder.id); - misskeyApi('drive/folders/update', { - folderId: folder.id, - parentId: props.folder.id, - }).then(() => { - // noop - }).catch(err => { - switch (err.code) { - case 'RECURSIVE_NESTING': - claimAchievement('driveFolderCircularReference'); - os.alert({ - type: 'error', - title: i18n.ts.unableToProcess, - text: i18n.ts.circularReferenceFolder, - }); - break; - default: - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); - } - }); + misskeyApi('drive/folders/update', { + folderId: droppedFolder.id, + parentId: props.folder.id, + }).then(() => { + globalEvents.emit('driveFoldersUpdated', [droppedFolder].map(x => ({ + ...x, + parentId: props.folder.id, + parent: props.folder, + }))); + }).catch(err => { + switch (err.code) { + case 'RECURSIVE_NESTING': + claimAchievement('driveFolderCircularReference'); + os.alert({ + type: 'error', + title: i18n.ts.unableToProcess, + text: i18n.ts.circularReferenceFolder, + }); + break; + default: + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); + } + }); + } } //#endregion } @@ -198,7 +198,7 @@ function onDragstart(ev: DragEvent) { if (!ev.dataTransfer) return; ev.dataTransfer.effectAllowed = 'move'; - ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder)); + setDragData(ev, 'driveFolders', [props.folder]); isDragging.value = true; // 親ブラウザに対して、ドラッグが開始されたフラグを立てる @@ -211,10 +211,6 @@ function onDragend() { emit('dragend'); } -function go() { - emit('move', props.folder); -} - function rename() { os.inputText({ title: i18n.ts.renameFolder, @@ -225,17 +221,28 @@ function rename() { misskeyApi('drive/folders/update', { folderId: props.folder.id, name: name, + }).then(() => { + globalEvents.emit('driveFoldersUpdated', [{ + ...props.folder, + name: name, + }]); }); }); } function move() { - os.selectDriveFolder(false).then(folder => { + selectDriveFolder(null).then(folder => { if (folder[0] && folder[0].id === props.folder.id) return; misskeyApi('drive/folders/update', { folderId: props.folder.id, parentId: folder[0] ? folder[0].id : null, + }).then(() => { + globalEvents.emit('driveFoldersUpdated', [{ + ...props.folder, + parentId: folder[0] ? folder[0].id : null, + parent: folder[0] ?? null, + }]); }); }); } @@ -247,6 +254,7 @@ function deleteFolder() { if (prefer.s.uploadFolder === props.folder.id) { prefer.commit('uploadFolder', null); } + globalEvents.emit('driveFoldersDeleted', [props.folder]); }).catch(err => { switch (err.id) { case 'b0fc8a17-963c-405d-bfbc-859a487295e1': @@ -311,10 +319,9 @@ function onContextmenu(ev: MouseEvent) { diff --git a/packages/frontend/src/components/MkDriveFolderSelectDialog.vue b/packages/frontend/src/components/MkDriveFolderSelectDialog.vue new file mode 100644 index 0000000000..2ebab1088f --- /dev/null +++ b/packages/frontend/src/components/MkDriveFolderSelectDialog.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue index c0142ec76e..0b8d0bfb8a 100644 --- a/packages/frontend/src/components/MkDriveWindow.vue +++ b/packages/frontend/src/components/MkDriveWindow.vue @@ -14,19 +14,19 @@ SPDX-License-Identifier: AGPL-3.0-only - + diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index e86861c874..9f5bc8da6c 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -19,13 +19,42 @@ SPDX-License-Identifier: AGPL-3.0-only
- + +
-
+
+ + + + + + + +
+ +
+
+ +
+ + +
+
+
+ +
(), { defaultOpen: false, maxHeight: null, withSpacer: true, spacerMin: 14, spacerMax: 22, + canPage: true, }); const rootEl = useTemplateRef('rootEl'); +const asPage = props.canPage && deviceKind === 'smartphone' && prefer.s['experimental.enableFolderPageView']; const bgSame = ref(false); -const opened = ref(props.defaultOpen); -const openedAtLeastOnce = ref(props.defaultOpen); +const opened = ref(asPage ? false : props.defaultOpen); +const openedAtLeastOnce = ref(opened.value); //#region interpolate-sizeに対応していないブラウザ向け(TODO: 主要ブラウザが対応したら消す) function enter(el: Element) { @@ -126,7 +161,22 @@ function afterLeave(el: Element) { } //#endregion -function toggle() { +let pageId = pageFolderTeleportCount.value; +pageFolderTeleportCount.value += 1000; + +async function toggle() { + if (asPage && !opened.value) { + pageId++; + const { dispose } = await popup(MkFolderPage, { + pageId, + }, { + closed: () => { + opened.value = false; + dispose(); + }, + }); + } + if (!opened.value) { openedAtLeastOnce.value = true; } diff --git a/packages/frontend/src/components/MkFolderPage.vue b/packages/frontend/src/components/MkFolderPage.vue new file mode 100644 index 0000000000..6d9ee1af1d --- /dev/null +++ b/packages/frontend/src/components/MkFolderPage.vue @@ -0,0 +1,157 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue index 0a902f3400..a11075c342 100644 --- a/packages/frontend/src/components/MkFormDialog.file.vue +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -15,7 +15,7 @@ import * as Misskey from 'misskey-js'; import { computed, ref } from 'vue'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; -import { selectFile } from '@/utility/select-file.js'; +import { selectFile } from '@/utility/drive.js'; import { misskeyApi } from '@/utility/misskey-api.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue index 49a6c65170..e6fae285b3 100644 --- a/packages/frontend/src/components/MkGalleryPostPreview.vue +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- import * as Misskey from 'misskey-js'; import { computed, ref } from 'vue'; -import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; +import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; import { prefer } from '@/preferences.js'; const props = defineProps<{ diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue index 3e5a88a170..584afff55c 100644 --- a/packages/frontend/src/components/MkLaunchPad.vue +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->