Merge pull request 'fix: word mutes' (#10034) from naskya/calckey:fix/word-mutes into develop
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/10034
This commit is contained in:
commit
4496507af2
|
@ -668,6 +668,9 @@ regexpErrorDescription: "An error occurred in the regular expression on line {li
|
||||||
instanceMute: "Instance Mutes"
|
instanceMute: "Instance Mutes"
|
||||||
userSaysSomething: "{name} said something"
|
userSaysSomething: "{name} said something"
|
||||||
userSaysSomethingReason: "{name} said {reason}"
|
userSaysSomethingReason: "{name} said {reason}"
|
||||||
|
userSaysSomethingReasonReply: "{name} replied to a post containing {reason}"
|
||||||
|
userSaysSomethingReasonRenote: "{name} boosted a post containing {reason}"
|
||||||
|
userSaysSomethingReasonQuote: "{name} quoted a post containing {reason}"
|
||||||
makeActive: "Activate"
|
makeActive: "Activate"
|
||||||
display: "Display"
|
display: "Display"
|
||||||
copy: "Copy"
|
copy: "Copy"
|
||||||
|
|
|
@ -619,6 +619,9 @@ regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表
|
||||||
instanceMute: "インスタンスミュート"
|
instanceMute: "インスタンスミュート"
|
||||||
userSaysSomething: "{name}が何かを言いました"
|
userSaysSomething: "{name}が何かを言いました"
|
||||||
userSaysSomethingReason: "{name}が{reason}と言いました"
|
userSaysSomethingReason: "{name}が{reason}と言いました"
|
||||||
|
userSaysSomethingReasonReply: "{name}が{reason}を含む投稿に返信しました"
|
||||||
|
userSaysSomethingReasonRenote: "{name}が{reason}を含む投稿をブーストしました"
|
||||||
|
userSaysSomethingReasonQuote: "{name}が{reason}を含む投稿を引用しました"
|
||||||
makeActive: "アクティブにする"
|
makeActive: "アクティブにする"
|
||||||
display: "表示"
|
display: "表示"
|
||||||
copy: "コピー"
|
copy: "コピー"
|
||||||
|
|
|
@ -12,67 +12,63 @@ type UserLike = {
|
||||||
id: User["id"];
|
id: User["id"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Muted = {
|
function checkWordMute(
|
||||||
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,
|
|
||||||
mutedWords: Array<string | string[]>,
|
mutedWords: Array<string | string[]>,
|
||||||
): Promise<Muted> {
|
): boolean {
|
||||||
// 自分自身
|
if (note == null) return false;
|
||||||
if (me && note.userId === me.id) {
|
|
||||||
return NotMuted;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mutedWords.length > 0) {
|
const text = ((note.cw ?? "") + " " + (note.text ?? "")).trim();
|
||||||
const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
|
if (text === "") return false;
|
||||||
|
|
||||||
if (text === "") {
|
for (const mutePattern of mutedWords) {
|
||||||
return NotMuted;
|
if (Array.isArray(mutePattern)) {
|
||||||
}
|
// Clean up
|
||||||
|
const keywords = mutePattern.filter((keyword) => keyword !== "");
|
||||||
|
|
||||||
for (const mutePattern of mutedWords) {
|
if (
|
||||||
let mute: RE2;
|
keywords.length > 0 &&
|
||||||
let matched: string[];
|
keywords.every((keyword) => text.includes(keyword))
|
||||||
if (Array.isArray(mutePattern)) {
|
)
|
||||||
matched = mutePattern.filter((keyword) => keyword !== "");
|
return true;
|
||||||
|
} else {
|
||||||
|
// represents RegExp
|
||||||
|
const regexp = mutePattern.match(/^\/(.+)\/(.*)$/);
|
||||||
|
|
||||||
if (matched.length === 0) {
|
// This should never happen due to input sanitisation.
|
||||||
continue;
|
if (!regexp) {
|
||||||
}
|
console.warn(`Found invalid regex in word mutes: ${mutePattern}`);
|
||||||
mute = new RE2(
|
continue;
|
||||||
`\\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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (mute.test(text)) {
|
if (new RE2(regexp[1], regexp[2]).test(text)) return true;
|
||||||
return { muted: true, matched };
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// This should never happen due to input sanitisation.
|
// This should never happen due to input sanitisation.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NotMuted;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWordHardMute(
|
||||||
|
note: NoteLike,
|
||||||
|
me: UserLike | null | undefined,
|
||||||
|
mutedWords: Array<string | string[]>,
|
||||||
|
): Promise<boolean> {
|
||||||
|
// 自分自身
|
||||||
|
if (me && note.userId === me.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutedWords.length > 0) {
|
||||||
|
return (
|
||||||
|
checkWordMute(note, mutedWords) ||
|
||||||
|
checkWordMute(note.reply, mutedWords) ||
|
||||||
|
checkWordMute(note.renote, mutedWords)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default class extends Channel {
|
||||||
|
|
||||||
constructor(id: string, connection: Channel["connection"]) {
|
constructor(id: string, connection: Channel["connection"]) {
|
||||||
super(id, connection);
|
super(id, connection);
|
||||||
this.onNote = this.onNote.bind(this);
|
this.onNote = this.withPackedNote(this.onNote.bind(this));
|
||||||
this.emitTypers = this.emitTypers.bind(this);
|
this.emitTypers = this.emitTypers.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { getWordMute } from "@/misc/check-word-mute.js";
|
import { getWordHardMute } 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";
|
||||||
|
@ -63,10 +63,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Channel from "../channel.js";
|
import Channel from "../channel.js";
|
||||||
import { getWordMute } from "@/misc/check-word-mute.js";
|
import { getWordHardMute } 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";
|
||||||
|
@ -62,10 +62,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||||
)
|
)
|
||||||
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 { getWordMute } from "@/misc/check-word-mute.js";
|
import { getWordHardMute } 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";
|
||||||
|
@ -79,10 +79,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||||
)
|
)
|
||||||
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 { getWordMute } from "@/misc/check-word-mute.js";
|
import { getWordHardMute } 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";
|
||||||
|
|
||||||
|
@ -55,10 +55,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||||
)
|
)
|
||||||
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 { getWordMute } from "@/misc/check-word-mute.js";
|
import { getWordHardMute } 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";
|
||||||
|
@ -77,10 +77,10 @@ export default class extends Channel {
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordMute(note, this.user, this.userProfile.mutedWords)).muted
|
(await getWordHardMute(note, this.user, this.userProfile.mutedWords))
|
||||||
)
|
)
|
||||||
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 { getWordMute } from "@/misc/check-word-mute.js";
|
import { getWordHardMute } 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";
|
||||||
|
@ -355,9 +355,9 @@ export default async (
|
||||||
)
|
)
|
||||||
.then((us) => {
|
.then((us) => {
|
||||||
for (const u of us) {
|
for (const u of us) {
|
||||||
getWordMute(note, { id: u.userId }, u.mutedWords).then(
|
getWordHardMute(data, { id: u.userId }, u.mutedWords).then(
|
||||||
(shouldMute) => {
|
(shouldMute) => {
|
||||||
if (shouldMute.muted) {
|
if (shouldMute) {
|
||||||
MutedNotes.insert({
|
MutedNotes.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
userId: u.userId,
|
userId: u.userId,
|
||||||
|
|
|
@ -198,14 +198,14 @@
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="muted" @click="muted.muted = false">
|
<div v-else class="muted" @click="muted.muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA
|
<MkA
|
||||||
v-user-preview="appearNote.userId"
|
v-user-preview="note.userId"
|
||||||
class="name"
|
class="name"
|
||||||
:to="userPage(appearNote.user)"
|
:to="userPage(note.user)"
|
||||||
>
|
>
|
||||||
<MkUserName :user="appearNote.user" />
|
<MkUserName :user="note.user" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
<template #reason>
|
<template #reason>
|
||||||
|
@ -236,7 +236,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 { getWordMute } from "@/scripts/check-word-mute";
|
import { getWordSoftMute } 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";
|
||||||
|
@ -261,6 +261,16 @@ const inChannel = inject("inChannel", null);
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
|
const softMuteReasonI18nSrc = (what?: string) => {
|
||||||
|
if (what === "note") return i18n.ts.userSaysSomethingReason;
|
||||||
|
if (what === "reply") return i18n.ts.userSaysSomethingReasonReply;
|
||||||
|
if (what === "renote") return i18n.ts.userSaysSomethingReasonRenote;
|
||||||
|
if (what === "quote") return i18n.ts.userSaysSomethingReasonQuote;
|
||||||
|
|
||||||
|
// I don't think here is reachable, but just in case
|
||||||
|
return i18n.ts.userSaysSomething;
|
||||||
|
};
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
if (noteViewInterruptors.length > 0) {
|
if (noteViewInterruptors.length > 0) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
@ -291,7 +301,7 @@ let appearNote = $computed(() =>
|
||||||
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(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||||
|
|
|
@ -39,14 +39,14 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="_panel muted" @click="muted.muted = false">
|
<div v-else class="_panel muted" @click="muted.muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomethingReason" tag="small">
|
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA
|
<MkA
|
||||||
v-user-preview="appearNote.userId"
|
v-user-preview="note.userId"
|
||||||
class="name"
|
class="name"
|
||||||
:to="userPage(appearNote.user)"
|
:to="userPage(note.user)"
|
||||||
>
|
>
|
||||||
<MkUserName :user="appearNote.user" />
|
<MkUserName :user="note.user" />
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
<template #reason>
|
<template #reason>
|
||||||
|
@ -83,7 +83,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 { getWordMute } from "@/scripts/check-word-mute";
|
import { getWordSoftMute } 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";
|
||||||
|
@ -110,6 +110,16 @@ const inChannel = inject("inChannel", null);
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
|
const softMuteReasonI18nSrc = (what?: string) => {
|
||||||
|
if (what === "note") return i18n.ts.userSaysSomethingReason;
|
||||||
|
if (what === "reply") return i18n.ts.userSaysSomethingReasonReply;
|
||||||
|
if (what === "renote") return i18n.ts.userSaysSomethingReasonRenote;
|
||||||
|
if (what === "quote") return i18n.ts.userSaysSomethingReasonQuote;
|
||||||
|
|
||||||
|
// I don't think here is reachable, but just in case
|
||||||
|
return i18n.ts.userSaysSomething;
|
||||||
|
};
|
||||||
|
|
||||||
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
|
||||||
|
|
||||||
// plugin
|
// plugin
|
||||||
|
@ -142,7 +152,7 @@ let appearNote = $computed(() =>
|
||||||
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(getWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const urls = appearNote.text
|
const urls = appearNote.text
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
v-if="!muted.muted || muted.what === 'reply'"
|
||||||
ref="el"
|
ref="el"
|
||||||
v-size="{ max: [450, 500] }"
|
v-size="{ max: [450, 500] }"
|
||||||
class="wrpstxzv"
|
class="wrpstxzv"
|
||||||
|
@ -161,6 +162,22 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="muted" @click="muted.muted = false">
|
||||||
|
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
|
||||||
|
<template #name>
|
||||||
|
<MkA
|
||||||
|
v-user-preview="note.userId"
|
||||||
|
class="name"
|
||||||
|
:to="userPage(note.user)"
|
||||||
|
>
|
||||||
|
<MkUserName :user="note.user" />
|
||||||
|
</MkA>
|
||||||
|
</template>
|
||||||
|
<template #reason>
|
||||||
|
<b class="_blur_text">{{ muted.matched.join(", ") }}</b>
|
||||||
|
</template>
|
||||||
|
</I18n>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -176,10 +193,13 @@ import XRenoteButton from "@/components/MkRenoteButton.vue";
|
||||||
import XQuoteButton from "@/components/MkQuoteButton.vue";
|
import XQuoteButton from "@/components/MkQuoteButton.vue";
|
||||||
import { pleaseLogin } from "@/scripts/please-login";
|
import { pleaseLogin } from "@/scripts/please-login";
|
||||||
import { getNoteMenu } from "@/scripts/get-note-menu";
|
import { getNoteMenu } from "@/scripts/get-note-menu";
|
||||||
|
import { getWordSoftMute } from "@/scripts/check-word-mute";
|
||||||
import { notePage } from "@/filters/note";
|
import { notePage } from "@/filters/note";
|
||||||
import { useRouter } from "@/router";
|
import { useRouter } from "@/router";
|
||||||
|
import { userPage } from "@/filters/user";
|
||||||
import * as os from "@/os";
|
import * as os from "@/os";
|
||||||
import { reactionPicker } from "@/scripts/reaction-picker";
|
import { reactionPicker } from "@/scripts/reaction-picker";
|
||||||
|
import { $i } from "@/account";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
import { useNoteCapture } from "@/scripts/use-note-capture";
|
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||||
import { defaultStore } from "@/store";
|
import { defaultStore } from "@/store";
|
||||||
|
@ -206,6 +226,16 @@ const props = withDefaults(
|
||||||
|
|
||||||
let note = $ref(deepClone(props.note));
|
let note = $ref(deepClone(props.note));
|
||||||
|
|
||||||
|
const softMuteReasonI18nSrc = (what?: string) => {
|
||||||
|
if (what === "note") return i18n.ts.userSaysSomethingReason;
|
||||||
|
if (what === "reply") return i18n.ts.userSaysSomethingReasonReply;
|
||||||
|
if (what === "renote") return i18n.ts.userSaysSomethingReasonRenote;
|
||||||
|
if (what === "quote") return i18n.ts.userSaysSomethingReasonQuote;
|
||||||
|
|
||||||
|
// I don't think here is reachable, but just in case
|
||||||
|
return i18n.ts.userSaysSomething;
|
||||||
|
};
|
||||||
|
|
||||||
const isRenote =
|
const isRenote =
|
||||||
note.renote != null &&
|
note.renote != null &&
|
||||||
note.text == null &&
|
note.text == null &&
|
||||||
|
@ -222,6 +252,7 @@ let appearNote = $computed(() =>
|
||||||
isRenote ? (note.renote as misskey.entities.Note) : note
|
isRenote ? (note.renote as misskey.entities.Note) : note
|
||||||
);
|
);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
|
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));
|
||||||
const translation = ref(null);
|
const translation = ref(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const replies: misskey.entities.Note[] =
|
const replies: misskey.entities.Note[] =
|
||||||
|
@ -615,4 +646,10 @@ function noteClick(e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,15 +1,58 @@
|
||||||
export type Muted = {
|
export type Muted = {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
matched: string[];
|
matched: string[];
|
||||||
|
what?: string; // "note" || "reply" || "renote" || "quote"
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotMuted = { muted: false, matched: [] };
|
const NotMuted = { muted: false, matched: [] };
|
||||||
|
|
||||||
function escapeRegExp(x: string) {
|
function checkWordMute(
|
||||||
return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
note: NoteLike,
|
||||||
|
mutedWords: Array<string | string[]>,
|
||||||
|
): Muted {
|
||||||
|
const text = ((note.cw ?? "") + " " + (note.text ?? "")).trim();
|
||||||
|
if (text === "") return NotMuted;
|
||||||
|
|
||||||
|
let result = { muted: false, matched: [] };
|
||||||
|
|
||||||
|
for (const mutePattern of mutedWords) {
|
||||||
|
if (Array.isArray(mutePattern)) {
|
||||||
|
// Clean up
|
||||||
|
const keywords = mutePattern.filter((keyword) => keyword !== "");
|
||||||
|
|
||||||
|
if (
|
||||||
|
keywords.length > 0 &&
|
||||||
|
keywords.every((keyword) => text.includes(keyword))
|
||||||
|
) {
|
||||||
|
result.muted = true;
|
||||||
|
result.matched.push(...keywords);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// represents RegExp
|
||||||
|
const regexp = mutePattern.match(/^\/(.+)\/(.*)$/);
|
||||||
|
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
if (!regexp) {
|
||||||
|
console.warn(`Found invalid regex in word mutes: ${mutePattern}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (new RegExp(regexp[1], regexp[2]).test(text)) {
|
||||||
|
result.muted = true;
|
||||||
|
result.matched.push(mutePattern);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// This should never happen due to input sanitisation.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.matched = [...new Set(result.matched)];
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWordMute(
|
export function getWordSoftMute(
|
||||||
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[]>,
|
||||||
|
@ -20,42 +63,25 @@ export function getWordMute(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mutedWords.length > 0) {
|
if (mutedWords.length > 0) {
|
||||||
const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
|
let noteMuted = checkWordMute(note, mutedWords);
|
||||||
|
if (noteMuted.muted) {
|
||||||
if (text === "") {
|
noteMuted.what = "note";
|
||||||
return NotMuted;
|
return noteMuted;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const mutePattern of mutedWords) {
|
if (note.renote) {
|
||||||
let mute: RegExp;
|
let renoteMuted = checkWordMute(note.renote, mutedWords);
|
||||||
let matched: string[];
|
if (renoteMuted.muted) {
|
||||||
if (Array.isArray(mutePattern)) {
|
renoteMuted.what = note.text == null ? "renote" : "quote";
|
||||||
matched = mutePattern.filter((keyword) => keyword !== "");
|
return renoteMuted;
|
||||||
|
|
||||||
if (matched.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
if (note.reply) {
|
||||||
if (mute.test(text)) {
|
let replyMuted = checkWordMute(note.reply, mutedWords);
|
||||||
return { muted: true, matched };
|
if (replyMuted.muted) {
|
||||||
}
|
replyMuted.what = "reply";
|
||||||
} catch (err) {
|
return replyMuted;
|
||||||
// This should never happen due to input sanitisation.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue