Merge branch 'develop' of codeberg.org:calckey/calckey into develop

This commit is contained in:
ThatOneCalculator 2023-05-05 13:28:39 -07:00
commit fe499ccf9d
No known key found for this signature in database
GPG key ID: 8703CACD01000000
15 changed files with 201 additions and 116 deletions

View file

@ -1311,7 +1311,7 @@ switchAccount: Canvia de compte
enabled: Activat enabled: Activat
configure: Configurar configure: Configurar
noBotProtectionWarning: La protecció contra bots no està configurada. noBotProtectionWarning: La protecció contra bots no està configurada.
ads: Anuncis ads: Publicitat
ratio: Ràtio ratio: Ràtio
global: Global global: Global
sent: Enviat sent: Enviat
@ -1710,7 +1710,7 @@ sendPushNotificationReadMessage: Suprimeix les notificacions push un cop s'hagin
sendPushNotificationReadMessageCaption: Es mostrarà una notificació amb el text "{emptyPushNotificationMessage}" sendPushNotificationReadMessageCaption: Es mostrarà una notificació amb el text "{emptyPushNotificationMessage}"
durant un breu temps. Això pot augmentar l'ús de la bateria del vostre dispositiu, durant un breu temps. Això pot augmentar l'ús de la bateria del vostre dispositiu,
si escau. si escau.
showAds: Mostrar anuncis showAds: Mostrar publicitat
enterSendsMessage: Pren retorn al formulari del missatge per enviar (quant no s'activa enterSendsMessage: Pren retorn al formulari del missatge per enviar (quant no s'activa
es Ctrl + Return) es Ctrl + Return)
customMOTD: MOTD personalitzat (missatges de la pantalla de benvinguda) customMOTD: MOTD personalitzat (missatges de la pantalla de benvinguda)

View file

@ -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"

View file

@ -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: "コピー"

View file

@ -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;
} }

View file

@ -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);
} }

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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;

View file

@ -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

View file

@ -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>

View file

@ -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.
} }
} }
} }