diff --git a/CHANGELOG.md b/CHANGELOG.md index 34860f6ef3..c2d0cc4e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ - Enhance: プッシュ通知を行うための権限確認をより確実に行うように - Enhance: 投稿フォームのチュートリアルを追加 - Enhance: 「自動でもっと見る」をほとんどの箇所で利用可能に +- Enhance: ミュートの付与期間を自由に設定できるように +- Enhance: ロールの付与期間を自由に設定できるように - Fix: 紙吹雪エフェクトがアニメーション設定を考慮せず常に表示される問題を修正 - Fix: ナビゲーションバーのリアルタイムモード切替ボタンの状態をよりわかりやすく表示するように - Fix: ページのタイトルが長いとき、はみ出る問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 1f6a79699e..652902e51d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -3846,6 +3846,10 @@ export interface Locale extends ILocale { * ミュートする期限 */ "mutePeriod": string; + /** + * 期限はあくまで目安です。反映が数分遅れる場合があります。 + */ + "mutePeriodDescription": string; /** * 期限 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 28b7fa2474..14eb74fb2e 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -957,6 +957,7 @@ instanceDefaultLightTheme: "サーバーデフォルトのライトテーマ" instanceDefaultDarkTheme: "サーバーデフォルトのダークテーマ" instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。" mutePeriod: "ミュートする期限" +mutePeriodDescription: "期限はあくまで目安です。反映が数分遅れる場合があります。" period: "期限" indefinitely: "無期限" tenMinutes: "10分" diff --git a/packages/frontend/src/components/MkMuteSettingDialog.vue b/packages/frontend/src/components/MkMuteSettingDialog.vue index 22dcff7c25..fba13c16e7 100644 --- a/packages/frontend/src/components/MkMuteSettingDialog.vue +++ b/packages/frontend/src/components/MkMuteSettingDialog.vue @@ -15,17 +15,27 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - - + +
+ + + + +
+ +
+
{{ i18n.ts.cancel }} - {{ i18n.ts.ok }} + {{ i18n.ts.ok }}
@@ -34,6 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index aafa1c4b21..d53904865f 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -15,6 +15,7 @@ import type { MenuItem } from '@/types/menu.js'; import type { PostFormProps } from '@/types/post-form.js'; import type { UploaderFeatures } from '@/composables/use-uploader.js'; import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue'; +import type { MkPeriodDialogDoneEvent } from '@/components/MkPeriodDialog.vue'; import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -530,6 +531,19 @@ export function select(props: }); } +export function selectPeriod(options: { title?: string } = {}): Promise { + return new Promise(async (resolve) => { + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkPeriodDialog.vue')), { + title: options.title, + }, { + done: result => { + resolve(result ? result : { canceled: true }); + }, + closed: () => dispose(), + }); + }); +} + export function success(): Promise { return new Promise(resolve => { const showing = ref(true); diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 6d3cc9c1b7..a72d683abe 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -447,31 +447,13 @@ async function assignRole() { }); if (canceled || roleId == null) return; - const { canceled: canceled2, result: period } = await os.select({ - title: i18n.ts.period + ': ' + roles.find(r => r.id === roleId)!.name, - items: [{ - value: 'indefinitely', label: i18n.ts.indefinitely, - }, { - value: 'oneHour', label: i18n.ts.oneHour, - }, { - value: 'oneDay', label: i18n.ts.oneDay, - }, { - value: 'oneWeek', label: i18n.ts.oneWeek, - }, { - value: 'oneMonth', label: i18n.ts.oneMonth, - }], - default: 'indefinitely', + const res = await os.selectPeriod({ + title: `${i18n.ts.period}: ${roles.find(r => r.id === roleId)!.name}`, }); - if (canceled2) return; - const expiresAt = period === 'indefinitely' ? null - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) - : null; + if (res.canceled) return; - await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.value.id, expiresAt }); + await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.value.id, expiresAt: res.expiresAt }); refreshUser(); } diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 2e249eee50..1a2c365669 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -112,31 +112,12 @@ async function del() { async function assign() { const user = await os.selectUser({ includeSelf: true }); - const { canceled: canceled2, result: period } = await os.select({ - title: i18n.ts.period + ': ' + role.name, - items: [{ - value: 'indefinitely', label: i18n.ts.indefinitely, - }, { - value: 'oneHour', label: i18n.ts.oneHour, - }, { - value: 'oneDay', label: i18n.ts.oneDay, - }, { - value: 'oneWeek', label: i18n.ts.oneWeek, - }, { - value: 'oneMonth', label: i18n.ts.oneMonth, - }], - default: 'indefinitely', + const res = await os.selectPeriod({ + title: `${i18n.ts.period}: ${role.name}`, }); - if (canceled2) return; + if (res.canceled) return; - const expiresAt = period === 'indefinitely' ? null - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) - : null; - - await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt }); + await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt: res.expiresAt }); //role.users.push(user); } diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index aed64ff3cb..f783c0bdbf 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -81,6 +81,7 @@ import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue' import XChannelFollowButton from '@/components/MkChannelFollowButton.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; +import { openMuteSettingDialog } from '@/utility/mute-confirm.js'; import { $i, iAmModerator } from '@/i.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; @@ -195,33 +196,14 @@ async function mute() { if (!channel.value) return; const _channel = channel.value; - const { canceled, result: period } = await os.select({ - title: i18n.ts.mutePeriod, - 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', + const res = await openMuteSettingDialog({ + withMuteType: false, }); - if (canceled) return; - - const expiresAt = period === 'indefinitely' ? null - : period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : null; + if (res.canceled) return; os.apiWithDialog('channels/mute/create', { channelId: _channel.id, - expiresAt, + expiresAt: res.expiresAt, }).then(() => { _channel.isMuting = true; }); diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts index 64d4b421a3..e33ccf897c 100644 --- a/packages/frontend/src/utility/get-user-menu.ts +++ b/packages/frontend/src/utility/get-user-menu.ts @@ -18,20 +18,9 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-pe import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router.js'; import { genEmbedCode } from '@/utility/get-embed-code.js'; +import { openMuteSettingDialog } from '@/utility/mute-confirm.js'; import { prefer } from '@/preferences.js'; import { getPluginHandlers } from '@/plugin.js'; -import type { MkMuteSettingDialogDoneEvent } from '@/components/MkMuteSettingDialog.vue'; - -function muteConfirm(): Promise { - 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) { const meId = $i ? $i.id : null; @@ -46,19 +35,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router user.isMuted = false; }); } else { - const res = await muteConfirm(); + const res = await openMuteSettingDialog({ + withMuteType: true, + }); if (res.canceled) return; - const expiresAt = res.period === 'indefinitely' ? null - : res.period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) - : res.period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : res.period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : res.period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : null; - os.apiWithDialog('mute/create', { userId: user.id, - expiresAt, + expiresAt: res.expiresAt, mutingType: res.type, }).then(() => { user.isMuted = true; @@ -323,31 +307,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router return roles.filter(r => r.target === 'manual').map(r => ({ text: r.name, action: async () => { - const { canceled, result: period } = await os.select({ - title: i18n.ts.period + ': ' + r.name, - items: [{ - value: 'indefinitely', label: i18n.ts.indefinitely, - }, { - value: 'oneHour', label: i18n.ts.oneHour, - }, { - value: 'oneDay', label: i18n.ts.oneDay, - }, { - value: 'oneWeek', label: i18n.ts.oneWeek, - }, { - value: 'oneMonth', label: i18n.ts.oneMonth, - }], - default: 'indefinitely', + const res = await os.selectPeriod({ + title: `${i18n.ts.period}: ${r.name}`, }); - if (canceled) return; - const expiresAt = period === 'indefinitely' ? null - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) - : null; + if (res.canceled) return; - os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt }); + os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt: res.expiresAt }); }, })); }, diff --git a/packages/frontend/src/utility/mute-confirm.ts b/packages/frontend/src/utility/mute-confirm.ts new file mode 100644 index 0000000000..343227bb6f --- /dev/null +++ b/packages/frontend/src/utility/mute-confirm.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { defineAsyncComponent } from 'vue'; +import * as os from '@/os.js'; +import type { MkMuteSettingDialogDoneEvent } from '@/components/MkMuteSettingDialog.vue'; + +export function openMuteSettingDialog(opts?: { withMuteType?: boolean }): Promise { + return new Promise(resolve => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkMuteSettingDialog.vue')), opts ?? {}, { + done: result => { + resolve(result ? result : { canceled: true }); + }, + closed: () => { + dispose(); + }, + }); + }); +}