hide muted threads behind a CW

This commit is contained in:
Hazelnoot 2025-06-09 22:11:34 -04:00
parent 87582034b5
commit 9bebf7718f
11 changed files with 83 additions and 47 deletions

4
locales/index.d.ts vendored
View file

@ -11988,6 +11988,10 @@ export interface Locale extends ILocale {
* Boosts muted
*/
"renoteMuted": string;
/**
* {name} said something in a muted thread
*/
"userSaysSomethingInMutedThread": ParameterizedString<"name">;
/**
* Mark all media from user as NSFW
*/

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!hardMuted && muted === false"
v-if="!hardMuted && muted === false && !threadMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -168,8 +168,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
<div v-else-if="!hardMuted" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
</div>
<div v-else>
<!--
@ -312,7 +312,7 @@ const isLong = shouldCollapsed(appearNote.value, urls.value);
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
const { muted, hardMuted, threadMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!muted"
v-if="!muted && !threadMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -226,8 +226,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
<div v-else class="_panel" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -354,7 +354,7 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted);
const { muted } = checkMutes(appearNote.value);
const { muted, threadMuted } = checkMutes(appearNote);
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-show="!isDeleted" v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
<div v-show="!isDeleted" v-if="!muted && !threadMuted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
@ -78,8 +78,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
</div>
</div>
<div v-else :class="$style.muted" @click="muted = false">
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
<div v-else :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -172,7 +172,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
}
}
const { muted } = checkMutes(appearNote.value);
const { muted, threadMuted } = checkMutes(appearNote);
useNoteCapture({
rootEl: el,

View file

@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</header>
<div>
<div v-if="muted" :class="[$style.text, $style.muted]">
<SkMutedNote :muted="muted" :note="note"></SkMutedNote>
<div v-if="muted || threadMuted" :class="[$style.text, $style.muted]">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="note"></SkMutedNote>
</div>
<Mfm v-else :class="$style.text" :text="getNoteSummary(note)" :isBlock="true" :plain="true" :nowrap="false" :isNote="true" nyaize="respect" :author="note.user"/>
</div>
@ -35,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { computed } from 'vue';
import { getNoteSummary } from '@/utility/get-note-summary.js';
import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
@ -49,8 +50,7 @@ defineEmits<{
(event: 'select', user: Misskey.entities.UserLite): void
}>();
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const { muted, hardMuted } = checkMutes(props.note);
const { muted, hardMuted, threadMuted } = checkMutes(computed(() => props.note));
</script>
<style lang="scss" module>

View file

@ -4,24 +4,34 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
<I18n v-if="threadMuted" :src="i18n.ts.userSaysSomethingInMutedThread" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
</I18n>
<I18n v-else-if="!prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
</I18n>
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
<template #word>
{{ mutedWords }}
</template>
</I18n>
<br v-if="threadMuted && muted">
<template v-if="muted">
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
</I18n>
<I18n v-else-if="!prefer.s.showSoftWordMutedWord" :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
</I18n>
<I18n v-else :src="i18n.ts.userSaysSomethingAbout" tag="small">
<template #name>
<MkUserName :user="note.user"/>
</template>
<template #word>
{{ mutedWords }}
</template>
</I18n>
</template>
</template>
<script setup lang="ts">
@ -32,6 +42,7 @@ import { prefer } from '@/preferences.js';
const props = defineProps<{
muted: false | 'sensitiveMute' | string[];
threadMuted: boolean;
note: Misskey.entities.Note;
}>();

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!hardMuted && muted === false"
v-if="!hardMuted && muted === false && !threadMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -168,8 +168,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
<div v-else-if="!hardMuted" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
</div>
<div v-else>
<!--
@ -311,7 +311,7 @@ const isLong = shouldCollapsed(appearNote.value, urls.value);
const collapsed = ref(prefer.s.expandLongNote && appearNote.value.cw == null && isLong ? false : appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const renoted = ref(false);
const { muted, hardMuted } = checkMutes(appearNote.value, props.withHardMute);
const { muted, hardMuted, threadMuted } = checkMutes(appearNote, computed(() => props.withHardMute));
const translation = ref<Misskey.entities.NotesTranslateResponse | false | null>(null);
const translating = ref(false);
const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
v-if="!muted"
v-if="!muted && !threadMuted"
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
@ -230,8 +230,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div v-else class="_panel" :class="$style.muted" @click="muted = false">
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
<div v-else class="_panel" :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="threadMuted" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -359,7 +359,7 @@ const mergedCW = computed(() => computeMergedCw(appearNote.value));
const renoteTooltip = computeRenoteTooltip(renoted);
const { muted } = checkMutes(appearNote.value);
const { muted, threadMuted } = checkMutes(appearNote);
watch(() => props.expandAllCws, (expandAllCws) => {
if (expandAllCws !== showContent.value) showContent.value = expandAllCws;

View file

@ -86,8 +86,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
</div>
</div>
<div v-else :class="$style.muted" @click="muted = false">
<SkMutedNote :muted="muted" :note="appearNote"></SkMutedNote>
<div v-else :class="$style.muted" @click.stop="muted = false">
<SkMutedNote :muted="muted" :threadMuted="false" :note="appearNote"></SkMutedNote>
</div>
</template>
@ -186,7 +186,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
}
}
const { muted } = checkMutes(appearNote.value);
const { muted } = checkMutes(appearNote);
useNoteCapture({
rootEl: el,

View file

@ -3,14 +3,34 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
import { inject, ref } from 'vue';
import type { Ref } from 'vue';
import { computed, inject, ref } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import { $i } from '@/i';
export function checkMutes(noteToCheck: Misskey.entities.Note, withHardMute = false) {
const muted = ref(checkMute(noteToCheck, $i?.mutedWords));
const hardMuted = ref(withHardMute && checkMute(noteToCheck, $i?.hardMutedWords, true));
return { muted, hardMuted };
export function checkMutes(noteToCheck: ComputedRef<Misskey.entities.Note>, withHardMute?: ComputedRef<boolean>) {
const muteEnable = ref(true);
const muted = computed<false | string[], boolean>({
get() {
if (!muteEnable.value) return false;
return checkMute(noteToCheck.value, $i?.mutedWords);
},
set(value: boolean) {
muteEnable.value = value;
},
});
const threadMuted = computed(() => {
if (!muteEnable.value) return false;
return noteToCheck.value.isMuting;
});
const hardMuted = computed(() => {
if (!withHardMute?.value) return false;
return checkMute(noteToCheck.value, $i?.hardMutedWords, true);
});
return { muted, hardMuted, threadMuted };
}
export function checkMute(note: Misskey.entities.Note, mutes: undefined | null): false;

View file

@ -29,6 +29,7 @@ muted: "Muted"
renoteMute: "Mute Boosts"
renoteMuted: "Boosts muted"
renoteUnmute: "Unmute Boosts"
userSaysSomethingInMutedThread: "{name} said something in a muted thread"
markAsNSFW: "Mark all media from user as NSFW"
markInstanceAsNSFW: "Mark as NSFW"
nsfwConfirm: "Are you sure that you want to mark all media from this account as NSFW?"