From d1446d195abb52c560c7c97177d08103a175acf7 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 26 Sep 2025 15:29:52 +0900
Subject: feat: scheduled post (#16577)

* Update NoteDraft.ts

* Update NoteDraft.ts

* wip

* Update CHANGELOG.md

* wip

* Update PostScheduledNoteProcessorService.ts

* Update PostScheduledNoteProcessorService.ts

* Update Notification.ts

* wip

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* wip

* Create 1758677617888-scheduled-post.js

* Update index.d.ts

* Update stats.ts

* wip

* wip

* wip

* wip

* wip

* Update MkNotification.vue

* wip

* wip

* wip

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* wip

* wip

* Update NoteDraftEntityService.ts

* wip

* Update index.d.ts

* Update MkPostForm.vue

* wip

* wip

* wip

* Update NoteCreateService.ts

* wip

* wip

* wip

* Update NoteDraftEntityService.ts

* Update NoteCreateService.ts

* Update NoteDraftService.ts

* wip

* Update NoteDraftService.ts

* wip

* wip

* Update MkPostForm.vue

* wip

* Update MkPostForm.vue

* Update os.ts

* wip

* Update MkNoteDraftsDialog.vue
---
 .../frontend/src/components/MkNoteDraftsDialog.vue | 263 ++++++++++++++-------
 1 file changed, 174 insertions(+), 89 deletions(-)

(limited to 'packages/frontend/src/components/MkNoteDraftsDialog.vue')

diff --git a/packages/frontend/src/components/MkNoteDraftsDialog.vue b/packages/frontend/src/components/MkNoteDraftsDialog.vue
index 5b8211b715..3f0a5a5247 100644
--- a/packages/frontend/src/components/MkNoteDraftsDialog.vue
+++ b/packages/frontend/src/components/MkNoteDraftsDialog.vue
@@ -15,101 +15,151 @@ SPDX-License-Identifier: AGPL-3.0-only
 	@esc="cancel()"
 >
 	<template #header>
-		{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
+		{{ i18n.ts.draftsAndScheduledNotes }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
 	</template>
-	<div class="_spacer">
-		<MkPagination :paginator="paginator" withControl>
-			<template #empty>
-				<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
-			</template>
-
-			<template #default="{ items }">
-				<div class="_gaps_s">
-					<div
-						v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
-						:key="draft.id"
-						v-panel
-						:class="[$style.draft]"
-					>
-						<div :class="$style.draftBody" class="_gaps_s">
-							<div :class="$style.draftInfo">
-								<div :class="$style.draftMeta">
-									<div v-if="draft.reply" class="_nowrap">
-										<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
-											<template #user>
-												<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
-												<MkAcct v-else :user="draft.reply.user"/>
-											</template>
-										</I18n>
-									</div>
-									<div v-else-if="draft.replyId" class="_nowrap">
-										<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
-											<template #user>
-												{{ i18n.ts.deletedNote }}
-											</template>
-										</I18n>
-									</div>
-									<div v-if="draft.renote && draft.text != null" class="_nowrap">
-										<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
-											<template #user>
-												<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
-												<MkAcct v-else :user="draft.renote.user"/>
-											</template>
-										</I18n>
-									</div>
-									<div v-else-if="draft.renoteId" class="_nowrap">
-										<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
-											<template #user>
-												{{ i18n.ts.deletedNote }}
-											</template>
-										</I18n>
+
+	<MkStickyContainer>
+		<template #header>
+			<MkTabs
+				v-model:tab="tab"
+				centered
+				:class="$style.tabs"
+				:tabs="[
+					{
+						key: 'drafts',
+						title: i18n.ts.drafts,
+						icon: 'ti ti-pencil-question',
+					},
+					{
+						key: 'scheduled',
+						title: i18n.ts.scheduled,
+						icon: 'ti ti-calendar-clock',
+					},
+				]"
+			/>
+		</template>
+
+		<div class="_spacer">
+			<MkPagination :key="tab" :paginator="tab === 'scheduled' ? scheduledPaginator : draftsPaginator" withControl>
+				<template #empty>
+					<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
+				</template>
+
+				<template #default="{ items }">
+					<div class="_gaps_s">
+						<div
+							v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
+							:key="draft.id"
+							v-panel
+							:class="[$style.draft]"
+						>
+							<div :class="$style.draftBody" class="_gaps_s">
+								<MkInfo v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
+									<I18n :src="i18n.ts.scheduledToPostOnX" tag="span">
+										<template #x>
+											<MkTime :time="draft.scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
+										</template>
+									</I18n>
+								</MkInfo>
+								<div :class="$style.draftInfo">
+									<div :class="$style.draftMeta">
+										<div v-if="draft.reply" class="_nowrap">
+											<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
+												<template #user>
+													<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
+													<MkAcct v-else :user="draft.reply.user"/>
+												</template>
+											</I18n>
+										</div>
+										<div v-else-if="draft.replyId" class="_nowrap">
+											<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
+												<template #user>
+													{{ i18n.ts.deletedNote }}
+												</template>
+											</I18n>
+										</div>
+										<div v-if="draft.renote && draft.text != null" class="_nowrap">
+											<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
+												<template #user>
+													<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
+													<MkAcct v-else :user="draft.renote.user"/>
+												</template>
+											</I18n>
+										</div>
+										<div v-else-if="draft.renoteId" class="_nowrap">
+											<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
+												<template #user>
+													{{ i18n.ts.deletedNote }}
+												</template>
+											</I18n>
+										</div>
+										<div v-if="draft.channel" class="_nowrap">
+											<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
+										</div>
 									</div>
-									<div v-if="draft.channel" class="_nowrap">
-										<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
+								</div>
+								<div :class="$style.draftContent">
+									<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
+								</div>
+								<div :class="$style.draftFooter">
+									<div :class="$style.draftVisibility">
+										<span :title="i18n.ts._visibility[draft.visibility]">
+											<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
+											<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
+											<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
+											<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
+										</span>
+										<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
 									</div>
+									<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
 								</div>
 							</div>
-							<div :class="$style.draftContent">
-								<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
-							</div>
-							<div :class="$style.draftFooter">
-								<div :class="$style.draftVisibility">
-									<span :title="i18n.ts._visibility[draft.visibility]">
-										<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
-										<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
-										<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
-										<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
-									</span>
-									<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
-								</div>
-								<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
+
+							<div :class="$style.draftActions" class="_buttons">
+								<template v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
+									<MkButton
+										:class="$style.itemButton"
+										small
+										@click="cancelSchedule(draft)"
+									>
+										<i class="ti ti-calendar-x"></i> {{ i18n.ts._drafts.cancelSchedule }}
+									</MkButton>
+									<!-- TODO
+									<MkButton
+										:class="$style.itemButton"
+										small
+										@click="reSchedule(draft)"
+									>
+										<i class="ti ti-calendar-time"></i> {{ i18n.ts._drafts.reSchedule }}
+									</MkButton>
+									-->
+								</template>
+								<MkButton
+									v-else
+									:class="$style.itemButton"
+									small
+									@click="restoreDraft(draft)"
+								>
+									<i class="ti ti-corner-up-left"></i> {{ i18n.ts._drafts.restore }}
+								</MkButton>
+								<MkButton
+									v-tooltip="i18n.ts._drafts.delete"
+									danger
+									small
+									:iconOnly="true"
+									:class="$style.itemButton"
+									style="margin-left: auto;"
+									@click="deleteDraft(draft)"
+								>
+									<i class="ti ti-trash"></i>
+								</MkButton>
 							</div>
 						</div>
-						<div :class="$style.draftActions" class="_buttons">
-							<MkButton
-								:class="$style.itemButton"
-								small
-								@click="restoreDraft(draft)"
-							>
-								<i class="ti ti-corner-up-left"></i>
-								{{ i18n.ts._drafts.restore }}
-							</MkButton>
-							<MkButton
-								v-tooltip="i18n.ts._drafts.delete"
-								danger
-								small
-								:iconOnly="true"
-								:class="$style.itemButton"
-								@click="deleteDraft(draft)"
-							>
-								<i class="ti ti-trash"></i>
-							</MkButton>
-						</div>
 					</div>
-				</div>
-			</template>
-		</MkPagination>
-	</div>
+				</template>
+			</MkPagination>
+		</div>
+	</MkStickyContainer>
 </MkModalWindow>
 </template>
 
@@ -125,6 +175,12 @@ import * as os from '@/os.js';
 import { $i } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api';
 import { Paginator } from '@/utility/paginator.js';
+import MkTabs from '@/components/MkTabs.vue';
+import MkInfo from '@/components/MkInfo.vue';
+
+const props = defineProps<{
+	scheduled?: boolean;
+}>();
 
 const emit = defineEmits<{
 	(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
@@ -132,8 +188,20 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const paginator = markRaw(new Paginator('notes/drafts/list', {
+const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts');
+
+const draftsPaginator = markRaw(new Paginator('notes/drafts/list', {
+	limit: 10,
+	params: {
+		scheduled: false,
+	},
+}));
+
+const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', {
 	limit: 10,
+	params: {
+		scheduled: true,
+	},
 }));
 
 const currentDraftsCount = ref(0);
@@ -162,7 +230,17 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
 	if (canceled) return;
 
 	os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
-		paginator.reload();
+		draftsPaginator.reload();
+	});
+}
+
+async function cancelSchedule(draft: Misskey.entities.NoteDraft) {
+	os.apiWithDialog('notes/drafts/update', {
+		draftId: draft.id,
+		isActuallyScheduled: false,
+		scheduledAt: null,
+	}).then(() => {
+		scheduledPaginator.reload();
 	});
 }
 </script>
@@ -220,4 +298,11 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
 	padding-top: 16px;
 	border-top: solid 1px var(--MI_THEME-divider);
 }
+
+.tabs {
+	background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
+	-webkit-backdrop-filter: var(--MI-blur, blur(15px));
+	backdrop-filter: var(--MI-blur, blur(15px));
+	border-bottom: solid 0.5px var(--MI_THEME-divider);
+}
 </style>
-- 
cgit v1.2.3-freya

