Hard mute (#12376)
* feat(backend,misskey-js): hard mute storage in backend * fix(backend,misskey-js): mute word record type * chore(frontend): generalize XWordMute * feat(frontend): configure hard mute * feat(frontend): hard mute notes on the timelines * lint(backend,frontend): fix lint failure * chore(misskey-js): update api.md * fix(backend): test failure * chore(frontend): check word mute for reply * chore: limit hard mute count
This commit is contained in:
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!muted"
|
||||
v-if="!hardMuted && !muted"
|
||||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
@@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else :class="$style.muted" @click="muted = false">
|
||||
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
@@ -183,6 +183,7 @@ const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
pinned?: boolean;
|
||||
mock?: boolean;
|
||||
withHardMute?: boolean;
|
||||
}>(), {
|
||||
mock: false,
|
||||
});
|
||||
@@ -239,13 +240,23 @@ const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
|
||||
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||
const collapsed = ref(appearNote.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||
const muted = ref(checkMute(appearNote, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords));
|
||||
const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||
|
||||
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
|
||||
if (mutedWords == null) return false;
|
||||
|
||||
if (checkWordMute(note, $i, mutedWords)) return true;
|
||||
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
|
||||
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
'e|a|plus': () => react(true),
|
||||
|
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:ad="true"
|
||||
:class="$style.notes"
|
||||
>
|
||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</MkDateSeparatedList>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template #default="{ items: notifications }">
|
||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
|
||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
|
@@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #icon><i class="ti ti-message-off"></i></template>
|
||||
<template #label>{{ i18n.ts.wordMute }}</template>
|
||||
|
||||
<XWordMute/>
|
||||
<XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-message-off"></i></template>
|
||||
<template #label>{{ i18n.ts.hardWordMute }}</template>
|
||||
|
||||
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
@@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const renoteMutingPagination = {
|
||||
@@ -207,6 +215,14 @@ async function toggleBlockItem(item) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMutedWords(mutedWords: (string | string[])[]) {
|
||||
await os.api('i/update', { mutedWords });
|
||||
}
|
||||
|
||||
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
|
||||
await os.api('i/update', { hardMutedWords });
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
@@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import * as os from '@/os.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const props = defineProps<{
|
||||
muted: (string[] | string)[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'save', value: (string[] | string)[]): void;
|
||||
}>();
|
||||
|
||||
const render = (mutedWords) => mutedWords.map(x => {
|
||||
if (Array.isArray(x)) {
|
||||
@@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => {
|
||||
}
|
||||
}).join('\n');
|
||||
|
||||
const tab = ref('soft');
|
||||
const mutedWords = ref(render($i!.mutedWords));
|
||||
const mutedWords = ref(render(props.muted));
|
||||
const changed = ref(false);
|
||||
|
||||
watch(mutedWords, () => {
|
||||
@@ -85,9 +85,7 @@ async function save() {
|
||||
return;
|
||||
}
|
||||
|
||||
await os.api('i/update', {
|
||||
mutedWords: parsed,
|
||||
});
|
||||
emit('save', parsed);
|
||||
|
||||
changed.value = false;
|
||||
}
|
||||
|
Reference in New Issue
Block a user