feat: give reason for soft mutes
Bad UX when a post is muted and it just says "Some chick said something". Now provide some context too to help people decide if they want to view something potentially triggering.
This commit is contained in:
parent
73b778de2a
commit
15b1109947
12 changed files with 133 additions and 74 deletions
|
@ -612,6 +612,7 @@ regexpError: "Regular Expression error"
|
||||||
regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:"
|
regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:"
|
||||||
instanceMute: "Instance Mutes"
|
instanceMute: "Instance Mutes"
|
||||||
userSaysSomething: "{name} said something"
|
userSaysSomething: "{name} said something"
|
||||||
|
userSaysSomethingReason: "{name} said {reason}"
|
||||||
makeActive: "Activate"
|
makeActive: "Activate"
|
||||||
display: "Display"
|
display: "Display"
|
||||||
copy: "Copy"
|
copy: "Copy"
|
||||||
|
|
|
@ -612,6 +612,7 @@ regexpError: "正規表現エラー"
|
||||||
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
||||||
instanceMute: "インスタンスミュート"
|
instanceMute: "インスタンスミュート"
|
||||||
userSaysSomething: "{name}が何かを言いました"
|
userSaysSomething: "{name}が何かを言いました"
|
||||||
|
userSaysSomethingReason: "{name}前記{reason}"
|
||||||
makeActive: "アクティブにする"
|
makeActive: "アクティブにする"
|
||||||
display: "表示"
|
display: "表示"
|
||||||
copy: "コピー"
|
copy: "コピー"
|
||||||
|
|
|
@ -5,46 +5,74 @@ import type { User } from "@/models/entities/user.js";
|
||||||
type NoteLike = {
|
type NoteLike = {
|
||||||
userId: Note["userId"];
|
userId: Note["userId"];
|
||||||
text: Note["text"];
|
text: Note["text"];
|
||||||
|
cw?: Note["cw"];
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserLike = {
|
type UserLike = {
|
||||||
id: User["id"];
|
id: User["id"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function checkWordMute(
|
export type Muted = {
|
||||||
|
muted: boolean;
|
||||||
|
matched: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotMuted = { muted: false, matched: [] };
|
||||||
|
|
||||||
|
function escapeRegExp(x: string) {
|
||||||
|
return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWordMute(
|
||||||
note: NoteLike,
|
note: NoteLike,
|
||||||
me: UserLike | null | undefined,
|
me: UserLike | null | undefined,
|
||||||
mutedWords: Array<string | string[]>,
|
mutedWords: Array<string | string[]>,
|
||||||
): Promise<boolean> {
|
): Promise<Muted> {
|
||||||
// 自分自身
|
// 自分自身
|
||||||
if (me && note.userId === me.id) return false;
|
if (me && note.userId === me.id) {
|
||||||
|
return NotMuted;
|
||||||
|
}
|
||||||
|
|
||||||
if (mutedWords.length > 0) {
|
if (mutedWords.length > 0) {
|
||||||
const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
|
const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
|
||||||
|
|
||||||
if (text === "") return false;
|
if (text === "") {
|
||||||
|
return NotMuted;
|
||||||
|
}
|
||||||
|
|
||||||
const matched = mutedWords.some((filter) => {
|
for (const mutePattern of mutedWords) {
|
||||||
if (Array.isArray(filter)) {
|
let mute: RE2;
|
||||||
return filter.every((keyword) => text.includes(keyword));
|
let matched: string[];
|
||||||
} else {
|
if (Array.isArray(mutePattern)) {
|
||||||
// represents RegExp
|
matched = mutePattern.filter((keyword) => keyword !== "");
|
||||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
|
||||||
|
|
||||||
// This should never happen due to input sanitisation.
|
if (matched.length === 0) {
|
||||||
if (!regexp) return false;
|
continue;
|
||||||
|
|
||||||
try {
|
|
||||||
return new RE2(regexp[1], regexp[2]).test(text);
|
|
||||||
} catch (err) {
|
|
||||||
// This should never happen due to input sanitisation.
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
mute = new RE2(
|
||||||
|
`\\b${matched.map(escapeRegExp).join("\\b.*\\b")}\\b`,
|
||||||
|
"g",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const regexp = mutePattern.match(/^\/(.+)\/(.*)$/);
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
if (!regexp) {
|
||||||
|
console.warn(`Found invalid regex in word mutes: ${mutePattern}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mute = new RE2(regexp[1], regexp[2]);
|
||||||
|
matched = [mutePattern];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (matched) return true;
|
try {
|
||||||
|
if (mute.test(text)) {
|
||||||
|
return { muted: true, matched };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return NotMuted;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Channel from "../channel.js";
|
import Channel from "../channel.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
import { checkWordMute } from "@/misc/check-word-mute.js";
|
import { getWordMute } from "@/misc/check-word-mute.js";
|
||||||
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
||||||
import { isUserRelated } from "@/misc/is-user-related.js";
|
import { isUserRelated } from "@/misc/is-user-related.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
|
@ -60,10 +60,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await checkWordMute(note, this.user, this.userProfile.mutedWords))
|
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Channel from "../channel.js";
|
import Channel from "../channel.js";
|
||||||
import { checkWordMute } from "@/misc/check-word-mute.js";
|
import { getWordMute } from "@/misc/check-word-mute.js";
|
||||||
import { isUserRelated } from "@/misc/is-user-related.js";
|
import { isUserRelated } from "@/misc/is-user-related.js";
|
||||||
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
|
@ -58,10 +58,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await checkWordMute(note, this.user, this.userProfile.mutedWords))
|
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Channel from "../channel.js";
|
import Channel from "../channel.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
import { checkWordMute } from "@/misc/check-word-mute.js";
|
import { getWordMute } from "@/misc/check-word-mute.js";
|
||||||
import { isUserRelated } from "@/misc/is-user-related.js";
|
import { isUserRelated } from "@/misc/is-user-related.js";
|
||||||
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
|
@ -75,10 +75,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await checkWordMute(note, this.user, this.userProfile.mutedWords))
|
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Channel from "../channel.js";
|
import Channel from "../channel.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
import { checkWordMute } from "@/misc/check-word-mute.js";
|
import { getWordMute } from "@/misc/check-word-mute.js";
|
||||||
import { isUserRelated } from "@/misc/is-user-related.js";
|
import { isUserRelated } from "@/misc/is-user-related.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
|
|
||||||
|
@ -52,10 +52,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await checkWordMute(note, this.user, this.userProfile.mutedWords))
|
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Channel from "../channel.js";
|
import Channel from "../channel.js";
|
||||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||||
import { checkWordMute } from "@/misc/check-word-mute.js";
|
import { getWordMute } from "@/misc/check-word-mute.js";
|
||||||
import { isUserRelated } from "@/misc/is-user-related.js";
|
import { isUserRelated } from "@/misc/is-user-related.js";
|
||||||
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
|
||||||
import type { Packed } from "@/misc/schema.js";
|
import type { Packed } from "@/misc/schema.js";
|
||||||
|
@ -73,10 +73,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await checkWordMute(note, this.user, this.userProfile.mutedWords))
|
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ import { Poll } from "@/models/entities/poll.js";
|
||||||
import { createNotification } from "../create-notification.js";
|
import { createNotification } from "../create-notification.js";
|
||||||
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
|
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
|
||||||
import { checkHitAntenna } from "@/misc/check-hit-antenna.js";
|
import { checkHitAntenna } from "@/misc/check-hit-antenna.js";
|
||||||
import { checkWordMute } from "@/misc/check-word-mute.js";
|
import { getWordMute } from "@/misc/check-word-mute.js";
|
||||||
import { addNoteToAntenna } from "../add-note-to-antenna.js";
|
import { addNoteToAntenna } from "../add-note-to-antenna.js";
|
||||||
import { countSameRenotes } from "@/misc/count-same-renotes.js";
|
import { countSameRenotes } from "@/misc/count-same-renotes.js";
|
||||||
import { deliverToRelays } from "../relay.js";
|
import { deliverToRelays } from "../relay.js";
|
||||||
|
@ -343,9 +343,9 @@ export default async (
|
||||||
)
|
)
|
||||||
.then((us) => {
|
.then((us) => {
|
||||||
for (const u of us) {
|
for (const u of us) {
|
||||||
checkWordMute(note, { id: u.userId }, u.mutedWords).then(
|
getWordMute(note, { id: u.userId }, u.mutedWords).then(
|
||||||
(shouldMute) => {
|
(shouldMute) => {
|
||||||
if (shouldMute) {
|
if (shouldMute.muted) {
|
||||||
MutedNotes.insert({
|
MutedNotes.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
userId: u.userId,
|
userId: u.userId,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!muted"
|
v-if="!muted.muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="el"
|
ref="el"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
|
@ -96,13 +96,16 @@
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="muted" @click="muted = false">
|
<div v-else class="muted" @click="muted.muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
||||||
<MkUserName :user="appearNote.user"/>
|
<MkUserName :user="appearNote.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
<template #reason>
|
||||||
|
<b>{{ muted.matched.join(", ") }}</b>
|
||||||
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -126,7 +129,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import MkVisibility from '@/components/MkVisibility.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
import { getWordMute } from '@/scripts/check-word-mute';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
@ -184,7 +187,7 @@ const isLong = (appearNote.cw == null && appearNote.text != null && (
|
||||||
));
|
));
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!muted"
|
v-if="!muted.muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="el"
|
ref="el"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
|
@ -102,13 +102,16 @@
|
||||||
</article>
|
</article>
|
||||||
<MkNoteSub v-for="note in directReplies" :key="note.id" :note="note" class="reply" :conversation="replies"/>
|
<MkNoteSub v-for="note in directReplies" :key="note.id" :note="note" class="reply" :conversation="replies"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_panel muted" @click="muted = false">
|
<div v-else class="_panel muted" @click="muted.muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
||||||
<MkUserName :user="appearNote.user"/>
|
<MkUserName :user="appearNote.user"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
<template #reason>
|
||||||
|
<b>{{ muted.matched.join(", ") }}</b>
|
||||||
|
</template>
|
||||||
</I18n>
|
</I18n>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -130,7 +133,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import MkVisibility from '@/components/MkVisibility.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
import { getWordMute } from '@/scripts/check-word-mute';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from '@/filters/note';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
|
@ -186,7 +189,7 @@ let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
||||||
|
|
|
@ -1,41 +1,64 @@
|
||||||
export function checkWordMute(
|
export type Muted = {
|
||||||
|
muted: boolean;
|
||||||
|
matched: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotMuted = { muted: false, matched: [] };
|
||||||
|
|
||||||
|
function escapeRegExp(x: string) {
|
||||||
|
return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWordMute(
|
||||||
note: Record<string, any>,
|
note: Record<string, any>,
|
||||||
me: Record<string, any> | null | undefined,
|
me: Record<string, any> | null | undefined,
|
||||||
mutedWords: Array<string | string[]>,
|
mutedWords: Array<string | string[]>,
|
||||||
): boolean {
|
): Muted {
|
||||||
// 自分自身
|
// 自分自身
|
||||||
if (me && note.userId === me.id) return false;
|
if (me && note.userId === me.id) {
|
||||||
|
return NotMuted;
|
||||||
|
}
|
||||||
|
|
||||||
if (mutedWords.length > 0) {
|
if (mutedWords.length > 0) {
|
||||||
const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
|
const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
|
||||||
|
|
||||||
if (text === "") return false;
|
if (text === "") {
|
||||||
|
return NotMuted;
|
||||||
|
}
|
||||||
|
|
||||||
const matched = mutedWords.some((filter) => {
|
for (const mutePattern of mutedWords) {
|
||||||
if (Array.isArray(filter)) {
|
let mute: RegExp;
|
||||||
// Clean up
|
let matched: string[];
|
||||||
const filteredFilter = filter.filter((keyword) => keyword !== "");
|
if (Array.isArray(mutePattern)) {
|
||||||
if (filteredFilter.length === 0) return false;
|
matched = mutePattern.filter((keyword) => keyword !== "");
|
||||||
|
|
||||||
return filteredFilter.every((keyword) => text.includes(keyword));
|
if (matched.length === 0) {
|
||||||
} else {
|
continue;
|
||||||
// represents RegExp
|
|
||||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
|
||||||
|
|
||||||
// This should never happen due to input sanitisation.
|
|
||||||
if (!regexp) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new RegExp(regexp[1], regexp[2]).test(text);
|
|
||||||
} catch (err) {
|
|
||||||
// This should never happen due to input sanitisation.
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
mute = new RegExp(
|
||||||
|
`\\b${matched.map(escapeRegExp).join("\\b.*\\b")}\\b`,
|
||||||
|
"g",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const regexp = mutePattern.match(/^\/(.+)\/(.*)$/);
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
if (!regexp) {
|
||||||
|
console.warn(`Found invalid regex in word mutes: ${mutePattern}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mute = new RegExp(regexp[1], regexp[2]);
|
||||||
|
matched = [mutePattern];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (matched) return true;
|
try {
|
||||||
|
if (mute.test(text)) {
|
||||||
|
return { muted: true, matched };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return NotMuted;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue