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.mutePeriod }}
-
-
+
+
+
+ {{ i18n.ts.mutePeriod }}
+
+
+
+ {{ i18n.ts.mutePeriodDescription }}
+
+
{{ i18n.ts.muteType }}
{{ i18n.ts.muteTypeDescription }}
{{ 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();
+ },
+ });
+ });
+}