From 7ead98cbe592e6911e4a54550cb7bb507e782d7c Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 6 Mar 2024 21:08:42 +0900
Subject: enhance(frontend): リアクションの総数を表示するように (#13532)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): リアクションの総数を表示するように

* Update Changelog

* リアクション選択済の色をaccentに
---
 .../frontend/src/components/MkNoteDetailed.vue     | 31 ++++++++++++++--------
 1 file changed, 20 insertions(+), 11 deletions(-)

(limited to 'packages/frontend/src/components/MkNoteDetailed.vue')

diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index e3ef14120f..2d2930ee7c 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -106,10 +106,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkTime :time="appearNote.createdAt" mode="detail" colored/>
 				</MkA>
 			</div>
-			<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
+			<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" ref="reactionsViewer" :note="appearNote"/>
 			<button class="_button" :class="$style.noteFooterButton" @click="reply()">
 				<i class="ti ti-arrow-back-up"></i>
-				<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.repliesCount }}</p>
+				<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
 			</button>
 			<button
 				v-if="canRenote"
@@ -119,17 +119,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 				@mousedown="renote()"
 			>
 				<i class="ti ti-repeat"></i>
-				<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ appearNote.renoteCount }}</p>
+				<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
 			</button>
 			<button v-else class="_button" :class="$style.noteFooterButton" disabled>
 				<i class="ti ti-ban"></i>
 			</button>
-			<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
-				<i v-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
+			<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
+				<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
+				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
+				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 				<i v-else class="ti ti-plus"></i>
-			</button>
-			<button v-if="appearNote.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(appearNote)">
-				<i class="ti ti-minus"></i>
+				<p v-if="appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
 			</button>
 			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
 				<i class="ti ti-paperclip"></i>
@@ -209,6 +209,7 @@ import { pleaseLogin } from '@/scripts/please-login.js';
 import { checkWordMute } from '@/scripts/check-word-mute.js';
 import { userPage } from '@/filters/user.js';
 import { notePage } from '@/filters/note.js';
+import number from '@/filters/number.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as sound from '@/scripts/sound.js';
@@ -401,14 +402,22 @@ function react(viaKeyboard = false): void {
 	}
 }
 
-function undoReact(note): void {
-	const oldReaction = note.myReaction;
+function undoReact(targetNote: Misskey.entities.Note): void {
+	const oldReaction = targetNote.myReaction;
 	if (!oldReaction) return;
 	misskeyApi('notes/reactions/delete', {
-		noteId: note.id,
+		noteId: targetNote.id,
 	});
 }
 
+function toggleReact() {
+	if (appearNote.value.myReaction == null) {
+		react();
+	} else {
+		undoReact(appearNote.value);
+	}
+}
+
 function onContextmenu(ev: MouseEvent): void {
 	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === 'A') return true;
-- 
cgit v1.2.3-freya


From 27f823e8823225ce33f464c1d12acf7c37834426 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 8 Mar 2024 18:13:09 +0900
Subject: enhance(frontend): リアクションの総数を表示するか設定で選べるように
 (#13539)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): リプライ・リノート・リアクションの総数を表示するか設定で選べるように (MisskeyIO#512)

(cherry picked from commit 3c8475e5ac217f055eab0f6d0aedcbbcb2a2f27c)

* fix: いいねのみの場合は強制的にカウント表示

* make `showReactionsCount` default false

* リアクションだけ

* けしわすれ

* けしわすれ２

---------

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
---
 locales/index.d.ts                                           | 4 ++++
 locales/ja-JP.yml                                            | 1 +
 packages/frontend/src/components/MkNote.vue                  | 2 +-
 packages/frontend/src/components/MkNoteDetailed.vue          | 2 +-
 packages/frontend/src/pages/settings/general.vue             | 2 ++
 packages/frontend/src/pages/settings/preferences-backups.vue | 1 +
 packages/frontend/src/store.ts                               | 8 ++++++--
 7 files changed, 16 insertions(+), 4 deletions(-)

(limited to 'packages/frontend/src/components/MkNoteDetailed.vue')

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 3ac4ff9e74..53c3159da6 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1992,6 +1992,10 @@ export interface Locale extends ILocale {
      * ノートのアクションをホバー時のみ表示する
      */
     "showNoteActionsOnlyHover": string;
+    /**
+     * ノートのリアクション数を表示する
+     */
+    "showReactionsCount": string;
     /**
      * 履歴はありません
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 177d6a06c9..64705868b9 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -494,6 +494,7 @@ emojiStyle: "絵文字のスタイル"
 native: "ネイティブ"
 disableDrawer: "メニューをドロワーで表示しない"
 showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
+showReactionsCount: "ノートのリアクション数を表示する"
 noHistory: "履歴はありません"
 signinHistory: "ログイン履歴"
 enableAdvancedMfm: "高度なMFMを有効にする"
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 656ccc7959..5ca0eae012 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -121,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
 					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 					<i v-else class="ti ti-plus"></i>
-					<p v-if="appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
+					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
 				</button>
 				<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
 					<i class="ti ti-paperclip"></i>
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 2d2930ee7c..e271215516 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -129,7 +129,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
 				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 				<i v-else class="ti ti-plus"></i>
-				<p v-if="appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
+				<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
 			</button>
 			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
 				<i class="ti ti-paperclip"></i>
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index d13b6884bd..285f874dde 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitch v-model="advancedMfm">{{ i18n.ts.enableAdvancedMfm }}</MkSwitch>
 				<MkSwitch v-if="advancedMfm" v-model="animatedMfm">{{ i18n.ts.enableAnimatedMfm }}</MkSwitch>
 				<MkSwitch v-if="advancedMfm" v-model="enableQuickAddMfmFunction">{{ i18n.ts.enableQuickAddMfmFunction }}</MkSwitch>
+				<MkSwitch v-model="showReactionsCount">{{ i18n.ts.showReactionsCount }}</MkSwitch>
 				<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
 				<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
 				<MkRadios v-model="reactionsDisplaySize">
@@ -281,6 +282,7 @@ const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
 const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
 const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
 const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
+const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
 const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
 const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
 const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 942de19d82..b6f1043154 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -70,6 +70,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'animation',
 	'animatedMfm',
 	'advancedMfm',
+	'showReactionsCount',
 	'loadRawImages',
 	'imageNewTab',
 	'dataSaver',
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index dfc4169a54..a335ed33bb 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -227,6 +227,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
+	showReactionsCount: {
+		where: 'device',
+		default: false,
+	},
 	enableQuickAddMfmFunction: {
 		where: 'device',
 		default: false,
@@ -431,10 +435,10 @@ export const defaultStore = markRaw(new Storage('base', {
 			sfxVolume: 1,
 		},
 	},
-  hemisphere: {
+	hemisphere: {
 		where: 'device',
 		default: hemisphere as 'N' | 'S',
-  },
+	},
 	enableHorizontalSwipe: {
 		where: 'device',
 		default: true,
-- 
cgit v1.2.3-freya


From 831c74a25b2db0ba3f6d43a9a1a9072d342b2822 Mon Sep 17 00:00:00 2001
From: おさむのひと <46447427+samunohito@users.noreply.github.com>
Date: Thu, 21 Mar 2024 18:46:42 +0900
Subject: fix: URLプレビューの動作改善＋動作設定を可能にする (#13579)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* support new version

* URLプレビュー無効化時、フロント側も非表示にしてリクエストをしないようにする

* fix lint

* fix lint

* tweak preview request error handles

* fix: CHANGELOG.md

* fix

* fix

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 CHANGELOG.md                                       |  5 ++
 locales/index.d.ts                                 | 58 +++++++++++++++++
 locales/ja-JP.yml                                  | 15 +++++
 .../migration/1710512074000-url-preview-meta.js    | 42 ++++++++++++
 packages/backend/package.json                      |  2 +-
 .../backend/src/core/entities/MetaEntityService.ts |  1 +
 packages/backend/src/models/Meta.ts                | 38 +++++++++--
 packages/backend/src/models/json-schema/meta.ts    |  4 ++
 .../backend/src/server/api/endpoints/admin/meta.ts | 34 +++++++++-
 .../src/server/api/endpoints/admin/update-meta.ts  | 41 ++++++++++--
 .../backend/src/server/web/UrlPreviewService.ts    | 67 +++++++++++++++-----
 packages/frontend/src/components/MkLink.vue        | 17 +++--
 packages/frontend/src/components/MkNote.vue        |  7 +-
 .../frontend/src/components/MkNoteDetailed.vue     |  5 +-
 packages/frontend/src/components/MkUrlPreview.vue  | 12 ++--
 packages/frontend/src/components/global/MkUrl.vue  |  3 +-
 .../frontend/src/components/page/page.text.vue     |  5 +-
 packages/frontend/src/instance.ts                  |  2 +
 packages/frontend/src/pages/admin/security.vue     | 16 -----
 packages/frontend/src/pages/admin/settings.vue     | 74 +++++++++++++++++++++-
 packages/misskey-js/src/autogen/types.ts           | 20 +++++-
 pnpm-lock.yaml                                     | 18 +++++-
 22 files changed, 420 insertions(+), 66 deletions(-)
 create mode 100644 packages/backend/migration/1710512074000-url-preview-meta.js

(limited to 'packages/frontend/src/components/MkNoteDetailed.vue')

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0dce1a0496..188e146cdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 ## Unreleased
 
+### Note
+- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。
+
 ### General
+- Enhance: URLプレビューの有効化・無効化を設定できるように #13569
 - Enhance: アンテナでBotによるノートを除外できるように  
   (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545)
 - Fix: Play作成時に設定した公開範囲が機能していない問題を修正
@@ -25,6 +29,7 @@
 
 ### Server
 - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
+- Enhance: misskey-dev/summaly@5.1.0の取り込み（プレビュー生成処理の効率化）
 - Fix: フォローリクエストを作成する際に既存のものは削除するように  
   (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440)
 
diff --git a/locales/index.d.ts b/locales/index.d.ts
index afb4adac6c..70586d7a87 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -4916,6 +4916,10 @@ export interface Locale extends ILocale {
      * リトライ
      */
     "gameRetry": string;
+    /**
+     * 使用しない場合は空欄にしてください
+     */
+    "notUsePleaseLeaveBlank": string;
     "_bubbleGame": {
         /**
          * 遊び方
@@ -9768,6 +9772,60 @@ export interface Locale extends ILocale {
          */
         "header": string;
     };
+    "_urlPreviewSetting": {
+        /**
+         * URLプレビューの設定
+         */
+        "title": string;
+        /**
+         * URLプレビューを有効にする
+         */
+        "enable": string;
+        /**
+         * プレビュー取得時のタイムアウト(ms)
+         */
+        "timeout": string;
+        /**
+         * プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。
+         */
+        "timeoutDescription": string;
+        /**
+         * Content-Lengthの最大値(byte)
+         */
+        "maximumContentLength": string;
+        /**
+         * Content-Lengthがこの値を超えた場合、プレビューは生成されません。
+         */
+        "maximumContentLengthDescription": string;
+        /**
+         * Content-Lengthが取得できた場合のみプレビューを生成
+         */
+        "requireContentLength": string;
+        /**
+         * 相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。
+         */
+        "requireContentLengthDescription": string;
+        /**
+         * User-Agent
+         */
+        "userAgent": string;
+        /**
+         * プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。
+         */
+        "userAgentDescription": string;
+        /**
+         * プレビューを生成するプロキシのエンドポイント
+         */
+        "summaryProxy": string;
+        /**
+         * Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。
+         */
+        "summaryProxyDescription": string;
+        /**
+         * プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。
+         */
+        "summaryProxyDescription2": string;
+    };
 }
 declare const locales: {
     [lang: string]: Locale;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a64c83b10f..cada6d855f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1225,6 +1225,7 @@ enableHorizontalSwipe: "スワイプしてタブを切り替える"
 loading: "読み込み中"
 surrender: "やめる"
 gameRetry: "リトライ"
+notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください"
 
 _bubbleGame:
   howToPlay: "遊び方"
@@ -2602,3 +2603,17 @@ _offlineScreen:
   title: "オフライン - サーバーに接続できません"
   header: "サーバーに接続できません"
 
+_urlPreviewSetting:
+  title: "URLプレビューの設定"
+  enable: "URLプレビューを有効にする"
+  timeout: "プレビュー取得時のタイムアウト(ms)"
+  timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。"
+  maximumContentLength: "Content-Lengthの最大値(byte)"
+  maximumContentLengthDescription: "Content-Lengthがこの値を超えた場合、プレビューは生成されません。"
+  requireContentLength: "Content-Lengthが取得できた場合のみプレビューを生成"
+  requireContentLengthDescription: "相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。"
+  userAgent: "User-Agent"
+  userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。"
+  summaryProxy: "プレビューを生成するプロキシのエンドポイント"
+  summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。"
+  summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。"
diff --git a/packages/backend/migration/1710512074000-url-preview-meta.js b/packages/backend/migration/1710512074000-url-preview-meta.js
new file mode 100644
index 0000000000..8af521bbf4
--- /dev/null
+++ b/packages/backend/migration/1710512074000-url-preview-meta.js
@@ -0,0 +1,42 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class UrlPreviewMeta1710512074000 {
+    name = 'UrlPreviewMeta1710512074000'
+
+    async up(queryRunner) {
+        await queryRunner.query(`
+					alter table meta
+						rename column "summalyProxy" to "urlPreviewSummaryProxyUrl";
+					alter table meta
+						add "urlPreviewEnabled" boolean default true not null;
+					alter table meta
+						add "urlPreviewTimeout" integer default 10000 not null;
+					alter table meta
+						add "urlPreviewMaximumContentLength" bigint default 10485760 not null;
+					alter table meta
+						add "urlPreviewRequireContentLength" boolean default false not null;
+					alter table meta
+						add "urlPreviewUserAgent" varchar(1024) default null;
+				`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`
+					alter table meta
+						rename column "urlPreviewSummaryProxyUrl" to "summalyProxy";
+					alter table meta
+						drop column "urlPreviewEnabled";
+					alter table meta
+						drop column "urlPreviewTimeout";
+					alter table meta
+						drop column "urlPreviewMaximumContentLength";
+					alter table meta
+						drop column "urlPreviewRequireContentLength";
+					alter table meta
+						drop column "urlPreviewUserAgent";
+				`);
+    }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index eaad96d5f6..d64fcc3d2a 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -80,7 +80,7 @@
 		"@fastify/static": "6.12.0",
 		"@fastify/view": "8.2.0",
 		"@misskey-dev/sharp-read-bmp": "1.2.0",
-		"@misskey-dev/summaly": "5.0.3",
+		"@misskey-dev/summaly": "5.1.0",
 		"@nestjs/common": "10.3.3",
 		"@nestjs/core": "10.3.3",
 		"@nestjs/testing": "10.3.3",
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index b50d76288f..9d054ab6a1 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -111,6 +111,7 @@ export class MetaEntityService {
 			policies: { ...DEFAULT_POLICIES, ...instance.policies },
 
 			mediaProxy: this.config.mediaProxy,
+			enableUrlPreview: instance.urlPreviewEnabled,
 		};
 
 		return packed;
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 66f19ce197..04a34bbbb4 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -277,12 +277,6 @@ export class MiMeta {
 	})
 	public enableSensitiveMediaDetectionForVideos: boolean;
 
-	@Column('varchar', {
-		length: 1024,
-		nullable: true,
-	})
-	public summalyProxy: string | null;
-
 	@Column('boolean', {
 		default: false,
 	})
@@ -588,4 +582,36 @@ export class MiMeta {
 		default: 0,
 	})
 	public notesPerOneAd: number;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public urlPreviewEnabled: boolean;
+
+	@Column('integer', {
+		default: 10000,
+	})
+	public urlPreviewTimeout: number;
+
+	@Column('bigint', {
+		default: 1024 * 1024 * 10,
+	})
+	public urlPreviewMaximumContentLength: number;
+
+	@Column('boolean', {
+		default: true,
+	})
+	public urlPreviewRequireContentLength: boolean;
+
+	@Column('varchar', {
+		length: 1024,
+		nullable: true,
+	})
+	public urlPreviewSummaryProxyUrl: string | null;
+
+	@Column('varchar', {
+		length: 1024,
+		nullable: true,
+	})
+	public urlPreviewUserAgent: string | null;
 }
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 17789f3b46..473339a1ad 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -207,6 +207,10 @@ export const packedMetaLiteSchema = {
 			type: 'string',
 			optional: false, nullable: false,
 		},
+		enableUrlPreview: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		backgroundImageUrl: {
 			type: 'string',
 			optional: false, nullable: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 88c5907bcc..f4ff573271 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -434,6 +434,8 @@ export const meta = {
 			summalyProxy: {
 				type: 'string',
 				optional: false, nullable: true,
+				deprecated: true,
+				description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
 			},
 			themeColor: {
 				type: 'string',
@@ -451,6 +453,30 @@ export const meta = {
 				type: 'string',
 				optional: false, nullable: false,
 			},
+			urlPreviewEnabled: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
+			urlPreviewTimeout: {
+				type: 'number',
+				optional: false, nullable: false,
+			},
+			urlPreviewMaximumContentLength: {
+				type: 'number',
+				optional: false, nullable: false,
+			},
+			urlPreviewRequireContentLength: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
+			urlPreviewUserAgent: {
+				type: 'string',
+				optional: false, nullable: true,
+			},
+			urlPreviewSummaryProxyUrl: {
+				type: 'string',
+				optional: false, nullable: true,
+			},
 		},
 	},
 } as const;
@@ -533,7 +559,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
 				enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
 				proxyAccountId: instance.proxyAccountId,
-				summalyProxy: instance.summalyProxy,
 				email: instance.email,
 				smtpSecure: instance.smtpSecure,
 				smtpHost: instance.smtpHost,
@@ -577,6 +602,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
 				perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
 				notesPerOneAd: instance.notesPerOneAd,
+				summalyProxy: instance.urlPreviewSummaryProxyUrl,
+				urlPreviewEnabled: instance.urlPreviewEnabled,
+				urlPreviewTimeout: instance.urlPreviewTimeout,
+				urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength,
+				urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
+				urlPreviewUserAgent: instance.urlPreviewUserAgent,
+				urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
 			};
 		});
 	}
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 bffceef815..2f62d30ada 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -90,7 +90,6 @@ export const paramDef = {
 				type: 'string',
 			},
 		},
-		summalyProxy: { type: 'string', nullable: true },
 		deeplAuthKey: { type: 'string', nullable: true },
 		deeplIsPro: { type: 'boolean' },
 		enableEmail: { type: 'boolean' },
@@ -150,6 +149,16 @@ export const paramDef = {
 				type: 'string',
 			},
 		},
+		summalyProxy: {
+			type: 'string', nullable: true,
+			description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
+		},
+		urlPreviewEnabled: { type: 'boolean' },
+		urlPreviewTimeout: { type: 'integer' },
+		urlPreviewMaximumContentLength: { type: 'integer' },
+		urlPreviewRequireContentLength: { type: 'boolean' },
+		urlPreviewUserAgent: { type: 'string', nullable: true },
+		urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
 	},
 	required: [],
 } as const;
@@ -353,10 +362,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.langs = ps.langs.filter(Boolean);
 			}
 
-			if (ps.summalyProxy !== undefined) {
-				set.summalyProxy = ps.summalyProxy;
-			}
-
 			if (ps.enableEmail !== undefined) {
 				set.enableEmail = ps.enableEmail;
 			}
@@ -581,6 +586,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.bannedEmailDomains = ps.bannedEmailDomains;
 			}
 
+			if (ps.urlPreviewEnabled !== undefined) {
+				set.urlPreviewEnabled = ps.urlPreviewEnabled;
+			}
+
+			if (ps.urlPreviewTimeout !== undefined) {
+				set.urlPreviewTimeout = ps.urlPreviewTimeout;
+			}
+
+			if (ps.urlPreviewMaximumContentLength !== undefined) {
+				set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength;
+			}
+
+			if (ps.urlPreviewRequireContentLength !== undefined) {
+				set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength;
+			}
+
+			if (ps.urlPreviewUserAgent !== undefined) {
+				const value = (ps.urlPreviewUserAgent ?? '').trim();
+				set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent;
+			}
+
+			if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) {
+				const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim();
+				set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
+			}
+
 			const before = await this.metaService.fetch(true);
 
 			await this.metaService.update(set);
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index c6a96e94cb..8f8f08a305 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -5,6 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { summaly } from '@misskey-dev/summaly';
+import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import { MetaService } from '@/core/MetaService.js';
@@ -14,6 +15,7 @@ import { query } from '@/misc/prelude/url.js';
 import { LoggerService } from '@/core/LoggerService.js';
 import { bindThis } from '@/decorators.js';
 import { ApiError } from '@/server/api/error.js';
+import { MiMeta } from '@/models/Meta.js';
 import type { FastifyRequest, FastifyReply } from 'fastify';
 
 @Injectable()
@@ -62,24 +64,25 @@ export class UrlPreviewService {
 
 		const meta = await this.metaService.fetch();
 
-		this.logger.info(meta.summalyProxy
+		if (!meta.urlPreviewEnabled) {
+			reply.code(403);
+			return {
+				error: new ApiError({
+					message: 'URL preview is disabled',
+					code: 'URL_PREVIEW_DISABLED',
+					id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8',
+				}),
+			};
+		}
+
+		this.logger.info(meta.urlPreviewSummaryProxyUrl
 			? `(Proxy) Getting preview of ${url}@${lang} ...`
 			: `Getting preview of ${url}@${lang} ...`);
+
 		try {
-			const summary = meta.summalyProxy ?
-				await this.httpRequestService.getJson<ReturnType<typeof summaly>>(`${meta.summalyProxy}?${query({
-					url: url,
-					lang: lang ?? 'ja-JP',
-				})}`)
-				:
-				await summaly(url, {
-					followRedirects: false,
-					lang: lang ?? 'ja-JP',
-					agent: this.config.proxy ? {
-						http: this.httpRequestService.httpAgent,
-						https: this.httpRequestService.httpsAgent,
-					} : undefined,
-				});
+			const summary = meta.urlPreviewSummaryProxyUrl
+				? await this.fetchSummaryFromProxy(url, meta, lang)
+				: await this.fetchSummary(url, meta, lang);
 
 			this.logger.succ(`Got preview of ${url}: ${summary.title}`);
 
@@ -100,6 +103,7 @@ export class UrlPreviewService {
 			return summary;
 		} catch (err) {
 			this.logger.warn(`Failed to get preview of ${url}: ${err}`);
+
 			reply.code(422);
 			reply.header('Cache-Control', 'max-age=86400, immutable');
 			return {
@@ -111,4 +115,37 @@ export class UrlPreviewService {
 			};
 		}
 	}
+
+	private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
+		const agent = this.config.proxy
+			? {
+				http: this.httpRequestService.httpAgent,
+				https: this.httpRequestService.httpsAgent,
+			}
+			: undefined;
+
+		return summaly(url, {
+			followRedirects: false,
+			lang: lang ?? 'ja-JP',
+			agent: agent,
+			userAgent: meta.urlPreviewUserAgent ?? undefined,
+			operationTimeout: meta.urlPreviewTimeout,
+			contentLengthLimit: meta.urlPreviewMaximumContentLength,
+			contentLengthRequired: meta.urlPreviewRequireContentLength,
+		});
+	}
+
+	private fetchSummaryFromProxy(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
+		const proxy = meta.urlPreviewSummaryProxyUrl!;
+		const queryStr = query({
+			url: url,
+			lang: lang ?? 'ja-JP',
+			userAgent: meta.urlPreviewUserAgent ?? undefined,
+			operationTimeout: meta.urlPreviewTimeout,
+			contentLengthLimit: meta.urlPreviewMaximumContentLength,
+			contentLengthRequired: meta.urlPreviewRequireContentLength,
+		});
+
+		return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
+	}
 }
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 3f7aba2fe4..ca875242b4 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -18,6 +18,7 @@ import { defineAsyncComponent, ref } from 'vue';
 import { url as local } from '@/config.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
 import * as os from '@/os.js';
+import { isEnabledUrlPreview } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
 	url: string;
@@ -31,13 +32,15 @@ const target = self ? null : '_blank';
 
 const el = ref<HTMLElement | { $el: HTMLElement }>();
 
-useTooltip(el, (showing) => {
-	os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
-		showing,
-		url: props.url,
-		source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
-	}, {}, 'closed');
-});
+if (isEnabledUrlPreview.value) {
+	useTooltip(el, (showing) => {
+		os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
+			showing,
+			url: props.url,
+			source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
+		}, {}, 'closed');
+	});
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 5ca0eae012..50741f2cb7 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -82,7 +82,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkMediaList :mediaList="appearNote.files"/>
 					</div>
 					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
-					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
+					<div v-if="isEnabledUrlPreview">
+						<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
+					</div>
 					<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
 					<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
 						<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
@@ -194,6 +196,7 @@ import { MenuItem } from '@/types/menu.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
+import { isEnabledUrlPreview } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -268,7 +271,7 @@ const renoteCollapsed = ref(
 	defaultStore.state.collapseRenotes && isRenote && (
 		($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
 		(appearNote.value.myReaction != null)
-	)
+	),
 );
 
 /* Overload FunctionにLintが対応していないのでコメントアウト
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index e271215516..1b7dcda409 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -95,7 +95,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkMediaList :mediaList="appearNote.files"/>
 				</div>
 				<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
-				<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
+				<div v-if="isEnabledUrlPreview">
+					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
+				</div>
 				<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
 			</div>
 			<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
@@ -229,6 +231,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
+import { isEnabledUrlPreview } from '@/instance.js';
 
 const props = defineProps<{
 	note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index efc58b7e29..b3dc492616 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -110,7 +110,6 @@ const MOBILE_THRESHOLD = 500;
 const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
 
 const self = props.url.startsWith(local);
-const attr = self ? 'to' : 'href';
 const target = self ? null : '_blank';
 const fetching = ref(true);
 const title = ref<string | null>(null);
@@ -152,15 +151,16 @@ requestUrl.hash = '';
 window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`)
 	.then(res => {
 		if (!res.ok) {
-			fetching.value = false;
-			unknownUrl.value = true;
-			return;
+			if (_DEV_) {
+				console.warn(`[HTTP${res.status}] Failed to fetch url preview`);
+			}
+			return null;
 		}
 
 		return res.json();
 	})
-	.then((info: SummalyResult) => {
-		if (info.url == null) {
+	.then((info: SummalyResult | null) => {
+		if (!info || info.url == null) {
 			fetching.value = false;
 			unknownUrl.value = true;
 			return;
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 8d29a4da8c..d2945a78b9 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -30,6 +30,7 @@ import { url as local } from '@/config.js';
 import * as os from '@/os.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
 import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
+import { isEnabledUrlPreview } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
 	url: string;
@@ -44,7 +45,7 @@ const url = new URL(props.url);
 if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
 const el = ref();
 
-if (props.showUrlPreview) {
+if (props.showUrlPreview && isEnabledUrlPreview.value) {
 	useTooltip(el, (showing) => {
 		os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
 			showing,
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index 61247b381f..4e501bd699 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -6,7 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps" :class="$style.textRoot">
 	<Mfm :text="block.text ?? ''" :isNote="false"/>
-	<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
+	<div v-if="isEnabledUrlPreview">
+		<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
+	</div>
 </div>
 </template>
 
@@ -15,6 +17,7 @@ import { defineAsyncComponent } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
+import { isEnabledUrlPreview } from '@/instance.js';
 
 const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
 
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index 4232cbcd78..22337e7eb9 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -36,6 +36,8 @@ export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO
 
 export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
 
+export const isEnabledUrlPreview = computed(() => instance.enableUrlPreview ?? true);
+
 export async function fetchInstance(force = false): Promise<void> {
 	if (!force) {
 		const cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index c4745978df..9bccee89a5 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -118,19 +118,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkSwitch>
 					</div>
 				</MkFolder>
-
-				<MkFolder>
-					<template #label>Summaly Proxy</template>
-
-					<div class="_gaps_m">
-						<MkInput v-model="summalyProxy">
-							<template #prefix><i class="ti ti-link"></i></template>
-							<template #label>Summaly Proxy URL</template>
-						</MkInput>
-
-						<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
-					</div>
-				</MkFolder>
 			</div>
 		</FormSuspense>
 	</MkSpacer>
@@ -155,7 +142,6 @@ import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 
-const summalyProxy = ref<string>('');
 const enableHcaptcha = ref<boolean>(false);
 const enableMcaptcha = ref<boolean>(false);
 const enableRecaptcha = ref<boolean>(false);
@@ -175,7 +161,6 @@ const bannedEmailDomains = ref<string>('');
 
 async function init() {
 	const meta = await misskeyApi('admin/meta');
-	summalyProxy.value = meta.summalyProxy;
 	enableHcaptcha.value = meta.enableHcaptcha;
 	enableMcaptcha.value = meta.enableMcaptcha;
 	enableRecaptcha.value = meta.enableRecaptcha;
@@ -201,7 +186,6 @@ async function init() {
 
 function save() {
 	os.apiWithDialog('admin/update-meta', {
-		summalyProxy: summalyProxy.value,
 		sensitiveMediaDetection: sensitiveMediaDetection.value,
 		sensitiveMediaDetectionSensitivity:
 			sensitiveMediaDetectionSensitivity.value === 0 ? 'veryLow' :
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 9a198ee8a3..6f45c212ec 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -143,6 +143,53 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</div>
 						</div>
 					</FormSection>
+
+					<FormSection>
+						<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
+
+						<div class="_gaps_m">
+							<MkSwitch v-model="urlPreviewEnabled">
+								<template #label>{{ i18n.ts._urlPreviewSetting.enable }}</template>
+							</MkSwitch>
+
+							<MkSwitch v-model="urlPreviewRequireContentLength">
+								<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</template>
+								<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
+							</MkSwitch>
+
+							<MkInput v-model="urlPreviewMaximumContentLength" type="number">
+								<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</template>
+								<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
+							</MkInput>
+
+							<MkInput v-model="urlPreviewTimeout" type="number">
+								<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}</template>
+								<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
+							</MkInput>
+
+							<MkInput v-model="urlPreviewUserAgent" type="text">
+								<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}</template>
+								<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
+							</MkInput>
+
+							<div>
+								<MkInput v-model="urlPreviewSummaryProxyUrl" type="text">
+									<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</template>
+									<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
+								</MkInput>
+
+								<div :class="$style.subCaption">
+									{{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
+									<ul style="padding-left: 20px; margin: 4px 0">
+										<li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
+										<li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
+										<li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
+										<li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
+									</ul>
+								</div>
+							</div>
+						</div>
+					</FormSection>
 				</div>
 			</FormSuspense>
 		</MkSpacer>
@@ -173,6 +220,8 @@ import { fetchInstance, instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkSelect from '@/components/MkSelect.vue';
 
 const name = ref<string | null>(null);
 const shortName = ref<string | null>(null);
@@ -194,6 +243,12 @@ const perRemoteUserUserTimelineCacheMax = ref<number>(0);
 const perUserHomeTimelineCacheMax = ref<number>(0);
 const perUserListTimelineCacheMax = ref<number>(0);
 const notesPerOneAd = ref<number>(0);
+const urlPreviewEnabled = ref<boolean>(true);
+const urlPreviewTimeout = ref<number>(10000);
+const urlPreviewMaximumContentLength = ref<number>(1024 * 1024 * 10);
+const urlPreviewRequireContentLength = ref<boolean>(true);
+const urlPreviewUserAgent = ref<string | null>(null);
+const urlPreviewSummaryProxyUrl = ref<string | null>(null);
 
 async function init(): Promise<void> {
 	const meta = await misskeyApi('admin/meta');
@@ -217,9 +272,15 @@ async function init(): Promise<void> {
 	perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax;
 	perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax;
 	notesPerOneAd.value = meta.notesPerOneAd;
+	urlPreviewEnabled.value = meta.urlPreviewEnabled;
+	urlPreviewTimeout.value = meta.urlPreviewTimeout;
+	urlPreviewMaximumContentLength.value = meta.urlPreviewMaximumContentLength;
+	urlPreviewRequireContentLength.value = meta.urlPreviewRequireContentLength;
+	urlPreviewUserAgent.value = meta.urlPreviewUserAgent;
+	urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl;
 }
 
-async function save(): void {
+async function save() {
 	await os.apiWithDialog('admin/update-meta', {
 		name: name.value,
 		shortName: shortName.value === '' ? null : shortName.value,
@@ -241,6 +302,12 @@ async function save(): void {
 		perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value,
 		perUserListTimelineCacheMax: perUserListTimelineCacheMax.value,
 		notesPerOneAd: notesPerOneAd.value,
+		urlPreviewEnabled: urlPreviewEnabled.value,
+		urlPreviewTimeout: urlPreviewTimeout.value,
+		urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value,
+		urlPreviewRequireContentLength: urlPreviewRequireContentLength.value,
+		urlPreviewUserAgent: urlPreviewUserAgent.value,
+		urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
 	});
 
 	fetchInstance(true);
@@ -259,4 +326,9 @@ definePageMetadata(() => ({
 	-webkit-backdrop-filter: var(--blur, blur(15px));
 	backdrop-filter: var(--blur, blur(15px));
 }
+
+.subCaption {
+	font-size: 0.85em;
+	color: var(--fgTransparentWeak);
+}
 </style>
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 636bc62aaa..c6e36d80aa 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4810,6 +4810,7 @@ export type components = {
       enableServiceWorker: boolean;
       translatorAvailable: boolean;
       mediaProxy: string;
+      enableUrlPreview: boolean;
       backgroundImageUrl: string | null;
       impressumUrl: string | null;
       logoImageUrl: string | null;
@@ -4962,11 +4963,21 @@ export type operations = {
             objectStorageS3ForcePathStyle: boolean;
             privacyPolicyUrl: string | null;
             repositoryUrl: string | null;
+            /**
+             * @deprecated
+             * @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead.
+             */
             summalyProxy: string | null;
             themeColor: string | null;
             tosUrl: string | null;
             uri: string;
             version: string;
+            urlPreviewEnabled: boolean;
+            urlPreviewTimeout: number;
+            urlPreviewMaximumContentLength: number;
+            urlPreviewRequireContentLength: boolean;
+            urlPreviewUserAgent: string | null;
+            urlPreviewSummaryProxyUrl: string | null;
           };
         };
       };
@@ -8862,7 +8873,6 @@ export type operations = {
           maintainerName?: string | null;
           maintainerEmail?: string | null;
           langs?: string[];
-          summalyProxy?: string | null;
           deeplAuthKey?: string | null;
           deeplIsPro?: boolean;
           enableEmail?: boolean;
@@ -8916,6 +8926,14 @@ export type operations = {
           perUserListTimelineCacheMax?: number;
           notesPerOneAd?: number;
           silencedHosts?: string[] | null;
+          /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
+          summalyProxy?: string | null;
+          urlPreviewEnabled?: boolean;
+          urlPreviewTimeout?: number;
+          urlPreviewMaximumContentLength?: number;
+          urlPreviewRequireContentLength?: boolean;
+          urlPreviewUserAgent?: string | null;
+          urlPreviewSummaryProxyUrl?: string | null;
         };
       };
     };
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4c1e228a95..383b31b1f3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -117,8 +117,8 @@ importers:
         specifier: 1.2.0
         version: 1.2.0
       '@misskey-dev/summaly':
-        specifier: 5.0.3
-        version: 5.0.3
+        specifier: 5.1.0
+        version: 5.1.0
       '@nestjs/common':
         specifier: 10.3.3
         version: 10.3.3(reflect-metadata@0.2.1)(rxjs@7.8.1)
@@ -4772,6 +4772,20 @@ packages:
       jschardet: 3.0.0
       private-ip: 2.3.3
       trace-redirect: 1.0.6
+    dev: true
+
+  /@misskey-dev/summaly@5.1.0:
+    resolution: {integrity: sha512-WAUrgX3/z4h4aI8Y/WVwmJcJ6Fa1Zf2LJCSS651t9MHoWVGABLsQ2KCXRGmlpk4i+cMDNIwweObUroosE7j8rg==}
+    dependencies:
+      cheerio: 1.0.0-rc.12
+      escape-regexp: 0.0.1
+      got: 12.6.1
+      html-entities: 2.3.2
+      iconv-lite: 0.6.3
+      jschardet: 3.0.0
+      private-ip: 2.3.3
+      trace-redirect: 1.0.6
+    dev: false
 
   /@mole-inc/bin-wrapper@8.0.1:
     resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==}
-- 
cgit v1.2.3-freya


From 6bd78770de06bd3694127da17ccd051f05057329 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 22 Mar 2024 18:21:14 +0900
Subject: enhance(frontend):
 リアクション受け入れがいいねのみの場合はボタンホバーでツールチップが出るように
 (#13613)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkNote.vue        | 25 +++++++++++++++++++++-
 .../frontend/src/components/MkNoteDetailed.vue     | 25 +++++++++++++++++++++-
 2 files changed, 48 insertions(+), 2 deletions(-)

(limited to 'packages/frontend/src/components/MkNoteDetailed.vue')

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 50741f2cb7..4add3aa6dd 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -167,6 +167,7 @@ import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
+import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
 import MkMediaList from '@/components/MkMediaList.vue';
 import MkCwButton from '@/components/MkCwButton.vue';
 import MkPoll from '@/components/MkPoll.vue';
@@ -180,7 +181,7 @@ import { userPage } from '@/filters/user.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
 import * as sound from '@/scripts/sound.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
 import { defaultStore, noteViewInterruptors } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -340,6 +341,28 @@ if (!props.mock) {
 			targetElement: renoteButton.value,
 		}, {}, 'closed');
 	});
+
+	if (appearNote.value.reactionAcceptance === 'likeOnly') {
+		useTooltip(reactButton, async (showing) => {
+			const reactions = await misskeyApiGet('notes/reactions', {
+				noteId: appearNote.value.id,
+				limit: 10,
+				_cacheKey_: appearNote.value.reactionCount,
+			});
+
+			const users = reactions.map(x => x.user);
+
+			if (users.length < 1) return;
+
+			os.popup(MkReactionsViewerDetails, {
+				showing,
+				reaction: '❤️',
+				users,
+				count: appearNote.value.reactionCount,
+				targetElement: reactButton.value!,
+			}, {}, 'closed');
+		});
+	}
 }
 
 function renote(viaKeyboard = false) {
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 1b7dcda409..ebccd60986 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -201,6 +201,7 @@ import * as Misskey from 'misskey-js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
+import MkReactionsViewerDetails from '@/components/MkReactionsViewer.details.vue';
 import MkMediaList from '@/components/MkMediaList.vue';
 import MkCwButton from '@/components/MkCwButton.vue';
 import MkPoll from '@/components/MkPoll.vue';
@@ -213,7 +214,7 @@ import { userPage } from '@/filters/user.js';
 import { notePage } from '@/filters/note.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
 import * as sound from '@/scripts/sound.js';
 import { defaultStore, noteViewInterruptors } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
@@ -348,6 +349,28 @@ useTooltip(renoteButton, async (showing) => {
 	}, {}, 'closed');
 });
 
+if (appearNote.value.reactionAcceptance === 'likeOnly') {
+	useTooltip(reactButton, async (showing) => {
+		const reactions = await misskeyApiGet('notes/reactions', {
+			noteId: appearNote.value.id,
+			limit: 10,
+			_cacheKey_: appearNote.value.reactionCount,
+		});
+
+		const users = reactions.map(x => x.user);
+
+		if (users.length < 1) return;
+
+		os.popup(MkReactionsViewerDetails, {
+			showing,
+			reaction: '❤️',
+			users,
+			count: appearNote.value.reactionCount,
+			targetElement: reactButton.value!,
+		}, {}, 'closed');
+	});
+}
+
 function renote(viaKeyboard = false) {
 	pleaseLogin();
 	showMovedDialog();
-- 
cgit v1.2.3-freya


From d4ca973e3408a7d28788efe57bcd882c0ce9eedc Mon Sep 17 00:00:00 2001
From: 1Step621 <86859447+1STEP621@users.noreply.github.com>
Date: Sat, 30 Mar 2024 20:30:22 +0900
Subject: Enhance(frontend): もっと！ボタンでリアクション一覧が開けるように
 (#12935)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* もっと！ボタンでリアクション一覧が開けるように

* update CHANGELOG.md && デバッグ用に最大リアクション表示数を1にしてたのを一応戻した

* fix

* デザイン調整

* maxNumberもどす

* fix CHANGELOG

* fix

* move changelog

* :art:

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 CHANGELOG.md                                           | 1 +
 packages/frontend/src/components/MkNote.vue            | 7 +++----
 packages/frontend/src/components/MkNoteDetailed.vue    | 9 ++++++---
 packages/frontend/src/components/MkReactionsViewer.vue | 3 +++
 packages/frontend/src/pages/note.vue                   | 3 ++-
 packages/frontend/src/router/definition.ts             | 2 +-
 6 files changed, 16 insertions(+), 9 deletions(-)

(limited to 'packages/frontend/src/components/MkNoteDetailed.vue')

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebf9320129..390639bd69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@
 - Enhance: 映像・音声の再生にブラウザのネイティブプレイヤーを使用できるように
 - Enhance: 映像・音声の再生メニューに「再生速度」「ループ再生」「ピクチャインピクチャ」を追加
 - Enhance: 映像・音声の再生にキーボードショートカットが使えるように
+- Enhance: ノートについているリアクションの「もっと！」から、リアクションの一覧を表示できるように
 - Fix: 一部のページ内リンクが正しく動作しない問題を修正
 - Fix: 周年の実績が閏年を考慮しない問題を修正
 - Fix: ローカルURLのプレビューポップアップが左上に表示される
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 4add3aa6dd..f5536e79bf 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
 				<template #more>
-					<div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div>
+					<MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA>
 				</template>
 			</MkReactionsViewer>
 			<footer :class="$style.footer">
@@ -1020,9 +1020,8 @@ function emitUpdReaction(emoji: string, delta: number) {
 
 .reactionOmitted {
 	display: inline-block;
-	height: 32px;
-	margin: 2px;
-	padding: 0 6px;
+	margin-left: 8px;
 	opacity: .8;
+	font-size: 95%;
 }
 </style>
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index ebccd60986..eec1aad53c 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -234,9 +234,12 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 import { isEnabledUrlPreview } from '@/instance.js';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
-}>();
+	initialTab: string;
+}>(), {
+	initialTab: 'replies',
+});
 
 const inChannel = inject('inChannel', null);
 
@@ -304,7 +307,7 @@ provide('react', (reaction: string) => {
 	});
 });
 
-const tab = ref('replies');
+const tab = ref(props.initialTab);
 const reactionTabType = ref<string | null>(null);
 
 const renotesPagination = computed<Paging>(() => ({
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 1bd37d842b..63b202f9f3 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -100,6 +100,9 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
 }
 
 .root {
+	display: flex;
+	flex-wrap: wrap;
+	align-items: center;
 	margin: 4px -2px 0 -2px;
 
 	&:empty {
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 4c985b96e6..e14651742a 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 						<div class="_margin _gaps_s">
 							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
-							<MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note"/>
+							<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/>
 						</div>
 						<div v-if="clips && clips.length > 0" class="_margin">
 							<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
@@ -66,6 +66,7 @@ import { defaultStore } from '@/store.js';
 
 const props = defineProps<{
 	noteId: string;
+	initialTab?: string;
 }>();
 
 const note = ref<null | Misskey.entities.Note>();
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index eaeeafd499..c9f03b738f 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -35,7 +35,7 @@ const routes: RouteDef[] = [{
 	component: page(() => import('@/pages/user/index.vue')),
 }, {
 	name: 'note',
-	path: '/notes/:noteId',
+	path: '/notes/:noteId/:initialTab?',
 	component: page(() => import('@/pages/note.vue')),
 }, {
 	name: 'list',
-- 
cgit v1.2.3-freya


From 960c4df48e31483209ac0421a009686685acd82d Mon Sep 17 00:00:00 2001
From: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
Date: Sun, 7 Apr 2024 21:16:37 +0900
Subject: enhance(frontend): better condition for posting and displaying Notes
 (#13670)

* enhance(frontend): better condition for posting and displaying Notes

* Update CHANGELOG.md
---
 CHANGELOG.md                                        | 4 ++++
 packages/frontend/src/components/MkNote.vue         | 1 +
 packages/frontend/src/components/MkNoteDetailed.vue | 4 +++-
 packages/frontend/src/components/MkPostForm.vue     | 8 +++++++-
 4 files changed, 15 insertions(+), 2 deletions(-)

(limited to 'packages/frontend/src/components/MkNoteDetailed.vue')

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6b9cf0939..41cbdea023 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,8 @@
 - Enhance: 映像・音声の再生メニューに「再生速度」「ループ再生」「ピクチャインピクチャ」を追加
 - Enhance: 映像・音声の再生にキーボードショートカットが使えるように
 - Enhance: ノートについているリアクションの「もっと！」から、リアクションの一覧を表示できるように
+- Enhance: リプライにて引用がある場合テキストが空でもノートできるように
+  - 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます
 - Fix: 一部のページ内リンクが正しく動作しない問題を修正
 - Fix: 周年の実績が閏年を考慮しない問題を修正
 - Fix: ローカルURLのプレビューポップアップが左上に表示される
@@ -33,6 +35,8 @@
 - Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177
   - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。
 - Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正
+- Fix: CWのみの引用リノートが詳細ページで純粋なリノートとして誤って扱われてしまう問題を修正
+- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
 
 ### Server
 - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index f5536e79bf..22b1691a86 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -242,6 +242,7 @@ if (noteViewInterruptors.length > 0) {
 
 const isRenote = (
 	note.value.renote != null &&
+	note.value.reply == null &&
 	note.value.text == null &&
 	note.value.cw == null &&
 	note.value.fileIds && note.value.fileIds.length === 0 &&
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index eec1aad53c..ed1c0a9e96 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.noteContent">
 			<p v-if="appearNote.cw != null" :class="$style.cw">
 				<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
-				<MkCwButton v-model="showContent" :text="appearNote.text" :files="appearNote.files" :poll="appearNote.poll"/>
+				<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
 			</p>
 			<div v-show="appearNote.cw == null || showContent">
 				<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
@@ -266,7 +266,9 @@ if (noteViewInterruptors.length > 0) {
 
 const isRenote = (
 	note.value.renote != null &&
+	note.value.reply == null &&
 	note.value.text == null &&
+	note.value.cw == null &&
 	note.value.fileIds && note.value.fileIds.length === 0 &&
 	note.value.poll == null
 );
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index e03faeaf55..014b866fbd 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -253,7 +253,13 @@ const maxTextLength = computed((): number => {
 
 const canPost = computed((): boolean => {
 	return !props.mock && !posting.value && !posted.value &&
-		(1 <= textLength.value || 1 <= files.value.length || !!poll.value || !!props.renote) &&
+		(
+			1 <= textLength.value ||
+			1 <= files.value.length ||
+			poll.value != null ||
+			props.renote != null ||
+			(props.reply != null && quoteId.value != null)
+		) &&
 		(textLength.value <= maxTextLength.value) &&
 		(!poll.value || poll.value.choices.length >= 2);
 });
-- 
cgit v1.2.3-freya

