enhance(frontend): implement mute setting dialog for enhanced mute (#16763)

* enhance(frontend): implement mute setting dialog for enhanced mute

* remove unnecessary defs

* fix description

* fix lint
This commit is contained in:
かっこかり 2025-11-08 13:11:52 +09:00 committed by GitHub
parent 2b75b575ac
commit 7d1da29c48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 199 additions and 21 deletions

16
locales/index.d.ts vendored
View File

@ -594,6 +594,18 @@ export interface Locale extends ILocale {
* *
*/ */
"mute": string; "mute": string;
/**
*
*/
"muteType": string;
/**
*
*/
"muteTypeDescription": string;
/**
*
*/
"muteTypeTimeline": string;
/** /**
* *
*/ */
@ -5637,6 +5649,10 @@ export interface Locale extends ILocale {
* *
*/ */
"zeroPadding": string; "zeroPadding": string;
/**
*
*/
"muteConfirm": string;
"_imageEditing": { "_imageEditing": {
"_vars": { "_vars": {
/** /**

View File

@ -144,6 +144,9 @@ markAsSensitive: "センシティブとして設定"
unmarkAsSensitive: "センシティブを解除する" unmarkAsSensitive: "センシティブを解除する"
enterFileName: "ファイル名を入力" enterFileName: "ファイル名を入力"
mute: "ミュート" mute: "ミュート"
muteType: "ミュートする範囲"
muteTypeDescription: "ミュートを適用する範囲を設定できます。「タイムラインのみ」に設定すると、タイムラインや検索結果上からは見えなくなりますが、通知は受け取ります。"
muteTypeTimeline: "タイムラインのみ"
unmute: "ミュート解除" unmute: "ミュート解除"
renoteMute: "リノートをミュート" renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除" renoteUnmute: "リノートのミュートを解除"
@ -1404,6 +1407,7 @@ youAreAdmin: "あなたは管理者です"
frame: "フレーム" frame: "フレーム"
presets: "プリセット" presets: "プリセット"
zeroPadding: "ゼロ埋め" zeroPadding: "ゼロ埋め"
muteConfirm: "ミュートしますか?"
_imageEditing: _imageEditing:
_vars: _vars:

View File

@ -0,0 +1,159 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
<div :class="$style.root" class="_gaps_m">
<div class="_gaps_s">
<div :class="$style.header">
<div :class="$style.icon">
<i class="ti ti-alert-triangle"></i>
</div>
<div :class="$style.title">{{ i18n.ts.muteConfirm }}</div>
</div>
</div>
<div class="_gaps">
<MkSelect v-model="periodModel" :def="periodDef">
<template #label>{{ i18n.ts.mutePeriod }}</template>
</MkSelect>
<MkSelect v-model="muteTypeModel" :def="muteTypeDef">
<template #label>{{ i18n.ts.muteType }}</template>
<template #caption>{{ i18n.ts.muteTypeDescription }}</template>
</MkSelect>
</div>
<div :class="$style.buttons">
<MkButton inline rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton inline primary rounded @click="ok">{{ i18n.ts.ok }}</MkButton>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { i18n } from '@/i18n.js';
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
const periodItems = [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', label: i18n.ts.tenMinutes,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}] as const satisfies MkSelectItem[];
const muteTypeItems = [{
value: 'all', label: i18n.ts.all,
}, {
value: 'timelineOnly', label: i18n.ts.muteTypeTimeline,
}] as const satisfies MkSelectItem[];
export type MkMuteSettingDialogDoneEvent = { canceled: true } | { canceled: false, period: GetMkSelectValueTypesFromDef<typeof periodItems>, type: GetMkSelectValueTypesFromDef<typeof muteTypeItems> };
</script>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
const emit = defineEmits<{
(ev: 'done', v: MkMuteSettingDialogDoneEvent): void;
(ev: 'closed'): void;
}>();
const modal = shallowRef<InstanceType<typeof MkModal>>();
const {
def: periodDef,
model: periodModel,
} = useMkSelect({
items: periodItems,
initialValue: 'indefinitely',
});
const {
def: muteTypeDef,
model: muteTypeModel,
} = useMkSelect({
items: muteTypeItems,
initialValue: 'all',
});
// overload function 使 lint
function done(canceled: true): void;
function done(canceled: false, period: typeof periodModel.value, type: typeof muteTypeModel.value): void; // eslint-disable-line no-redeclare
function done(canceled: boolean, period?: typeof periodModel.value, type?: typeof muteTypeModel.value) { // eslint-disable-line no-redeclare
emit('done', { canceled, period: period!, type: type! });
modal.value?.close();
}
async function ok() {
done(false, periodModel.value, muteTypeModel.value);
}
function cancel() {
done(true);
}
/*
function onBgClick() {
if (props.cancelableByBgClick) cancel();
}
*/
function onKeydown(evt: KeyboardEvent) {
if (evt.key === 'Escape') cancel();
}
onMounted(() => {
window.document.addEventListener('keydown', onKeydown);
});
onBeforeUnmount(() => {
window.document.removeEventListener('keydown', onKeydown);
});
</script>
<style lang="scss" module>
.root {
position: relative;
margin: auto;
padding: 32px;
width: 100%;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
background: var(--MI_THEME-panel);
border-radius: 16px;
}
.header {
display: flex;
align-items: center;
gap: 0.75em;
}
.icon {
font-size: 18px;
color: var(--MI_THEME-warn);
}
.title {
font-weight: bold;
font-size: 1.1em;
}
.buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: right;
}
</style>

View File

@ -20,6 +20,18 @@ import { mainRouter } from '@/router.js';
import { genEmbedCode } from '@/utility/get-embed-code.js'; import { genEmbedCode } from '@/utility/get-embed-code.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js'; import { getPluginHandlers } from '@/plugin.js';
import type { MkMuteSettingDialogDoneEvent } from '@/components/MkMuteSettingDialog.vue';
function muteConfirm(): Promise<MkMuteSettingDialogDoneEvent> {
return new Promise(resolve => {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkMuteSettingDialog.vue')), {}, {
done: result => {
resolve(result ? result : { canceled: true });
},
closed: () => dispose(),
});
});
}
export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) { export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) {
const meId = $i ? $i.id : null; const meId = $i ? $i.id : null;
@ -34,33 +46,20 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
user.isMuted = false; user.isMuted = false;
}); });
} else { } else {
const { canceled, result: period } = await os.select({ const res = await muteConfirm();
title: i18n.ts.mutePeriod, if (res.canceled) return;
items: [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', label: i18n.ts.tenMinutes,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}],
default: 'indefinitely',
});
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null const expiresAt = res.period === 'indefinitely' ? null
: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) : res.period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60) : res.period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) : res.period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) : res.period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: null; : null;
os.apiWithDialog('mute/create', { os.apiWithDialog('mute/create', {
userId: user.id, userId: user.id,
expiresAt, expiresAt,
mutingType: res.type,
}).then(() => { }).then(() => {
user.isMuted = true; user.isMuted = true;
}); });