show muted words in NoteDetailed / NoteSub components

This commit is contained in:
Hazelnoot 2025-05-10 20:01:26 -04:00
parent 7cd1d9ad93
commit 05e5be8218
5 changed files with 191 additions and 10 deletions

View file

@ -59,6 +59,7 @@ export class I18n<T extends ILocale> {
if (typeof value === 'string') { if (typeof value === 'string') {
const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter); const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter);
// TODO add a flag to suppress this warning from uses of <I18n> component
if (parameters.length) { if (parameters.length) {
console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`); console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
} }

View file

@ -230,11 +230,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div v-else class="_panel" :class="$style.muted" @click="muted = false"> <div v-else class="_panel" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small"> <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
<template #name> <template #name>
<MkUserName :user="appearNote.user"/> <MkUserName :user="appearNote.user"/>
</template> </template>
</I18n> </I18n>
<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>
</template> </template>
@ -245,6 +262,7 @@ import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
import { host } from '@@/js/config.js'; import { host } from '@@/js/config.js';
import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { Ref } from 'vue';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import type { Paging } from '@/components/MkPagination.vue'; import type { Paging } from '@/components/MkPagination.vue';
import type { Keymap } from '@/utility/hotkey.js'; import type { Keymap } from '@/utility/hotkey.js';
@ -340,7 +358,6 @@ const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(prefer.s.uncollapseCW); const showContent = ref(prefer.s.uncollapseCW);
const isDeleted = ref(false); const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false); const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
@ -358,6 +375,36 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted); const renoteTooltip = computeRenoteTooltip(renoted);
const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
/* Overload FunctionLint
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): Array<string | string[]> | false | 'sensitiveMute';
*/
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
if (mutedWords != null) {
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;
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
return 'sensitiveMute';
}
return false;
}
watch(() => props.expandAllCws, (expandAllCws) => { watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws; if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
}); });

View file

@ -73,19 +73,33 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div v-else :class="$style.muted" @click="muted = false"> <div v-else :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small"> <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
<template #name> <template #name>
<MkUserName :user="appearNote.user"/> <MkUserName :user="appearNote.user"/>
</template> </template>
</I18n> </I18n>
<I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkUserName :user="appearNote.user"/>
</template>
</I18n>
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
<template #name>
<MkUserName :user="appearNote.user"/>
</template>
<template #word>
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
</template>
</I18n>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, shallowRef, watch } from 'vue'; import { computed, inject, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import { host } from '@@/js/config.js'; import { host } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { Visibility } from '@/utility/boost-quote.js'; import type { Visibility } from '@/utility/boost-quote.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue';
@ -126,7 +140,6 @@ const props = withDefaults(defineProps<{
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id); const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i?.id);
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
const muted = computed(() => $i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
const translation = ref<any>(null); const translation = ref<any>(null);
const translating = ref(false); const translating = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
@ -170,6 +183,36 @@ async function removeReply(id: Misskey.entities.Note['id']) {
} }
} }
const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
/* Overload FunctionLint
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): Array<string | string[]> | false | 'sensitiveMute';
*/
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
if (mutedWords != null) {
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;
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
return 'sensitiveMute';
}
return false;
}
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
note: appearNote, note: appearNote,

View file

@ -235,11 +235,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div v-else class="_panel" :class="$style.muted" @click="muted = false"> <div v-else class="_panel" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small"> <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
<template #name> <template #name>
<MkUserName :user="appearNote.user"/> <MkUserName :user="appearNote.user"/>
</template> </template>
</I18n> </I18n>
<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>
</template> </template>
@ -250,6 +267,7 @@ import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
import { host } from '@@/js/config.js'; import { host } from '@@/js/config.js';
import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import type { Ref } from 'vue';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import type { Paging } from '@/components/MkPagination.vue'; import type { Paging } from '@/components/MkPagination.vue';
import type { Keymap } from '@/utility/hotkey.js'; import type { Keymap } from '@/utility/hotkey.js';
@ -346,7 +364,6 @@ const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(prefer.s.uncollapseCW); const showContent = ref(prefer.s.uncollapseCW);
const isDeleted = ref(false); const isDeleted = ref(false);
const renoted = ref(false); const renoted = ref(false);
const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false);
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false); const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
@ -364,6 +381,36 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted); const renoteTooltip = computeRenoteTooltip(renoted);
const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
/* Overload FunctionLint
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): Array<string | string[]> | false | 'sensitiveMute';
*/
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
if (mutedWords != null) {
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;
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
return 'sensitiveMute';
}
return false;
}
watch(() => props.expandAllCws, (expandAllCws) => { watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws; if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
}); });

View file

@ -81,19 +81,33 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div v-else :class="$style.muted" @click="muted = false"> <div v-else :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small"> <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
<template #name> <template #name>
<MkUserName :user="appearNote.user"/> <MkUserName :user="appearNote.user"/>
</template> </template>
</I18n> </I18n>
<I18n v-else-if="showSoftWordMutedWord !== true" :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkUserName :user="appearNote.user"/>
</template>
</I18n>
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
<template #name>
<MkUserName :user="appearNote.user"/>
</template>
<template #word>
{{ Array.isArray(muted) ? muted.map(words => Array.isArray(words) ? words.join() : words).slice(0, 3).join(' ') : muted }}
</template>
</I18n>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, shallowRef, watch } from 'vue'; import { computed, inject, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { computeMergedCw } from '@@/js/compute-merged-cw.js'; import { computeMergedCw } from '@@/js/compute-merged-cw.js';
import { host } from '@@/js/config.js'; import { host } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { Visibility } from '@/utility/boost-quote.js'; import type { Visibility } from '@/utility/boost-quote.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import SkNoteHeader from '@/components/SkNoteHeader.vue'; import SkNoteHeader from '@/components/SkNoteHeader.vue';
@ -140,7 +154,6 @@ const canRenote = computed(() => ['public', 'home'].includes(props.note.visibili
const hideLine = computed(() => props.detail); const hideLine = computed(() => props.detail);
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
const translation = ref<any>(null); const translation = ref<any>(null);
const translating = ref(false); const translating = ref(false);
const isDeleted = ref(false); const isDeleted = ref(false);
@ -184,6 +197,36 @@ async function removeReply(id: Misskey.entities.Note['id']) {
} }
} }
const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
/* Overload FunctionLint
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): Array<string | string[]> | false | 'sensitiveMute';
*/
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): Array<string | string[]> | false | 'sensitiveMute' {
if (mutedWords != null) {
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;
if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
return 'sensitiveMute';
}
return false;
}
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,
note: appearNote, note: appearNote,