From 9760f3d7c9113f5436732bf6d6e454b253061b1d Mon Sep 17 00:00:00 2001
From: taichan <40626578+tai-cha@users.noreply.github.com>
Date: Tue, 14 Jan 2025 22:49:59 +0900
Subject: [PATCH] =?UTF-8?q?enhance(frontend):=20=E3=83=AF=E3=83=BC?=
 =?UTF-8?q?=E3=83=89=E3=83=9F=E3=83=A5=E3=83=BC=E3=83=88=E3=81=A7=E5=BC=95?=
 =?UTF-8?q?=E3=81=A3=E3=81=8B=E3=81=8B=E3=81=A3=E3=81=9F=E3=83=AF=E3=83=BC?=
 =?UTF-8?q?=E3=83=89=E3=82=92=E8=A1=A8=E7=A4=BA=E5=8F=AF=E8=83=BD=E3=81=AB?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=20(#15195)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat(frontend): ソフトミュートで引っかかったものを表示できるように

* ソフトワードミュートのミュート文字列表示を切り替え可能に

* Chore(docs): Update CHANGELOG

* Fix: language file

* Fixed by review

* Fix by review

* Fix: reloadAskなおしきれていなかった

* perf: filter -> findに変更して最初の一個のみを表示するように変更

* Revert "perf: filter -> findに変更して最初の一個のみを表示するように変更"

This reverts commit 72ef92f0d62828754702cd00e26ad873adb4652f.
---
 CHANGELOG.md                                  |  1 +
 locales/index.d.ts                            |  8 +++++
 locales/ja-JP.yml                             |  2 ++
 packages/frontend/src/components/MkNote.vue   | 32 ++++++++++++++-----
 .../src/pages/settings/mute-block.vue         | 14 +++++++-
 .../frontend/src/scripts/check-word-mute.ts   |  6 ++--
 packages/frontend/src/store.ts                |  6 +++-
 7 files changed, 56 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9380f23cfb..0319cb8c30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
 - Enhance: PC画面でチャンネルが複数列で表示されるように  
   (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
 - Enhance: 照会に失敗した場合、その理由を表示するように
+- Enhance: ワードミュートで検知されたワードを表示できるように
 - Enhance: リモートのノートのリンクをコピーできるように
 - Enhance: 連合がホワイトリスト化・無効化されているサーバー向けのデザイン修正
 - Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 453d40feea..001dca29aa 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2762,6 +2762,10 @@ export interface Locale extends ILocale {
      * ハードワードミュート
      */
     "hardWordMute": string;
+    /**
+     * ミュートされたワードを表示
+     */
+    "showMutedWord": string;
     /**
      * 指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。
      */
@@ -2782,6 +2786,10 @@ export interface Locale extends ILocale {
      * {name}が何かを言いました
      */
     "userSaysSomething": ParameterizedString<"name">;
+    /**
+     * {name}が「{word}」について何かを言いました
+     */
+    "userSaysSomethingAbout": ParameterizedString<"name" | "word">;
     /**
      * アクティブにする
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ad9d9bea4b..2a8fd94522 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -687,11 +687,13 @@ testEmail: "配信テスト"
 wordMute: "ワードミュート"
 wordMuteDescription: "指定した語句を含むノートを最小化します。最小化されたノートをクリックすることで表示することができます。"
 hardWordMute: "ハードワードミュート"
+showMutedWord: "ミュートされたワードを表示"
 hardWordMuteDescription: "指定した語句を含むノートを隠します。ワードミュートとは異なり、ノートは完全に表示されなくなります。"
 regexpError: "正規表現エラー"
 regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
 instanceMute: "サーバーミュート"
 userSaysSomething: "{name}が何かを言いました"
+userSaysSomethingAbout: "{name}が「{word}」について何かを言いました"
 makeActive: "アクティブにする"
 display: "表示"
 copy: "コピー"
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 1a8814b7cb..4c26444b35 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -150,13 +150,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkA>
 		</template>
 	</I18n>
-	<I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
+	<I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small">
 		<template #name>
 			<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
 				<MkUserName :user="appearNote.user"/>
 			</MkA>
 		</template>
 	</I18n>
+	<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
+		<template #name>
+			<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
+				<MkUserName :user="appearNote.user"/>
+			</MkA>
+		</template>
+		<template #word>
+			{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
+		</template>
+	</I18n>
 </div>
 <div v-else>
 	<!--
@@ -272,6 +282,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 const isDeleted = ref(false);
 const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
 const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
+const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord);
 const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
 const translating = ref(false);
 const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
@@ -290,14 +301,19 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
 
 /* Overload FunctionにLintが対応していないのでコメントアウト
 function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
-function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): Array<string | string[]> | false | 'sensitiveMute';
 */
-function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
-	if (mutedWords != null) {
-		if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
-		if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
-		if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
-	}
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
+	if (mutedWords == null) return false;
+
+	const result = checkWordMute(noteToCheck, $i, mutedWords);
+	if (Array.isArray(result)) return result;
+
+	const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords);
+	if (Array.isArray(replyResult)) return replyResult;
+
+	const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords);
+	if (Array.isArray(renoteResult)) return renoteResult;
 
 	if (checkOnly) return false;
 
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index d32d4842bd..4caa556b15 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 		<div class="_gaps_m">
 			<MkInfo>{{ i18n.ts.wordMuteDescription }}</MkInfo>
+			<MkSwitch v-model="showSoftWordMutedWord">{{ i18n.ts.showMutedWord }}</MkSwitch>
 			<XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
 		</div>
 	</MkFolder>
@@ -132,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, watch } from 'vue';
 import XInstanceMute from './mute-block.instance-mute.vue';
 import XWordMute from './mute-block.word-mute.vue';
 import MkPagination from '@/components/MkPagination.vue';
@@ -146,6 +147,9 @@ import { instance, infoImageUrl } from '@/instance.js';
 import { signinRequired } from '@/account.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { defaultStore } from '@/store';
+import { reloadAsk } from '@/scripts/reload-ask.js';
 
 const $i = signinRequired();
 
@@ -168,6 +172,14 @@ const expandedRenoteMuteItems = ref([]);
 const expandedMuteItems = ref([]);
 const expandedBlockItems = ref([]);
 
+const showSoftWordMutedWord = computed(defaultStore.makeGetterSetter('showSoftWordMutedWord'));
+
+watch([
+	showSoftWordMutedWord,
+], async () => {
+	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
 async function unrenoteMute(user, ev) {
 	os.popupMenu([{
 		text: i18n.ts.renoteUnmute,
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts
index 0a37a08bf0..98fea1bced 100644
--- a/packages/frontend/src/scripts/check-word-mute.ts
+++ b/packages/frontend/src/scripts/check-word-mute.ts
@@ -4,7 +4,7 @@
  */
 import * as Misskey from 'misskey-js';
 
-export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
+export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): Array<string | string[]> | false {
 	// 自分自身
 	if (me && (note.userId === me.id)) return false;
 
@@ -13,7 +13,7 @@ export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.
 
 		if (text === '') return false;
 
-		const matched = mutedWords.some(filter => {
+		const matched = mutedWords.filter(filter => {
 			if (Array.isArray(filter)) {
 				// Clean up
 				const filteredFilter = filter.filter(keyword => keyword !== '');
@@ -36,7 +36,7 @@ export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.
 			}
 		});
 
-		if (matched) return true;
+		if (matched.length > 0) return matched;
 	}
 
 	return false;
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 1d981e897b..dbe90dba86 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -9,10 +9,10 @@ import { hemisphere } from '@@/js/intl-const.js';
 import lightTheme from '@@/themes/l-light.json5';
 import darkTheme from '@@/themes/d-green-lime.json5';
 import type { SoundType } from '@/scripts/sound.js';
+import type { Ast } from '@syuilo/aiscript';
 import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { Storage } from '@/pizzax.js';
-import type { Ast } from '@syuilo/aiscript';
 
 interface PostFormAction {
 	title: string,
@@ -474,6 +474,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
+	showSoftWordMutedWord: {
+		where: 'device',
+		default: false,
+	},
 
 	sound_masterVolume: {
 		where: 'device',
-- 
GitLab