From 8fede2d6706f47d50cc72a59603062debb85df13 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:02:52 +0900 Subject: [PATCH] wip --- .../migration/1758677617888-scheduled-post.js | 2 + packages/backend/src/core/NoteDraftService.ts | 7 +++- .../core/entities/NoteDraftEntityService.ts | 1 + packages/backend/src/models/NoteDraft.ts | 6 ++- .../src/models/json-schema/note-draft.ts | 4 ++ .../PostScheduledNoteProcessorService.ts | 2 +- .../api/endpoints/notes/drafts/create.ts | 2 + .../api/endpoints/notes/drafts/update.ts | 2 + .../frontend/src/components/MkPostForm.vue | 39 +++++++++++++++---- packages/misskey-js/src/autogen/types.ts | 5 +++ 10 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/backend/migration/1758677617888-scheduled-post.js b/packages/backend/migration/1758677617888-scheduled-post.js index f8a5d1495d..b31313d9db 100644 --- a/packages/backend/migration/1758677617888-scheduled-post.js +++ b/packages/backend/migration/1758677617888-scheduled-post.js @@ -11,12 +11,14 @@ export class ScheduledPost1758677617888 { */ async up(queryRunner) { await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`); } /** * @param {QueryRunner} queryRunner */ async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`); await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`); } } diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index 2925aa9ea1..e01feb2545 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -32,6 +32,7 @@ export type NoteDraftOptions = { channelId?: MiChannel['id'] | null; poll?: (IPoll & { expiredAfter?: number | null }) | null; scheduledAt?: Date | null; + isActuallyScheduled?: boolean; }; @Injectable() @@ -100,7 +101,7 @@ export class NoteDraftService { appliedDraft.userId = me.id; const draft = await this.noteDraftsRepository.insertOne(appliedDraft); - if (draft.scheduledAt) { + if (draft.scheduledAt && draft.isActuallyScheduled) { this.schedule(draft); } @@ -133,7 +134,7 @@ export class NoteDraftService { await this.noteDraftsRepository.update(draftId, appliedDraft); this.clearSchedule(draft).then(() => { - if (appliedDraft.scheduledAt) { + if (appliedDraft.scheduledAt != null && appliedDraft.isActuallyScheduled) { this.schedule(draft); } }); @@ -328,6 +329,7 @@ export class NoteDraftService { localOnly: data.localOnly, reactionAcceptance: data.reactionAcceptance, scheduledAt: data.scheduledAt ?? null, + isActuallyScheduled: data.isActuallyScheduled ?? false, } satisfies MiNoteDraft; return appliedDraft; @@ -335,6 +337,7 @@ export class NoteDraftService { @bindThis public async schedule(draft: MiNoteDraft): Promise { + if (!draft.isActuallyScheduled) return; if (draft.scheduledAt == null) return; if (draft.scheduledAt.getTime() <= Date.now()) return; diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts index cdbe2cf643..338b0411e6 100644 --- a/packages/backend/src/core/entities/NoteDraftEntityService.ts +++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts @@ -106,6 +106,7 @@ export class NoteDraftEntityService implements OnModuleInit { id: noteDraft.id, createdAt: this.idService.parse(noteDraft.id).date.toISOString(), scheduledAt: noteDraft.scheduledAt?.getTime() ?? null, + isActuallyScheduled: noteDraft.isActuallyScheduled, userId: noteDraft.userId, user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), text: text, diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 4f07f4a8be..411374fc96 100644 --- a/packages/backend/src/models/NoteDraft.ts +++ b/packages/backend/src/models/NoteDraft.ts @@ -153,12 +153,16 @@ export class MiNoteDraft { //#endregion - // 予約投稿 @Column('timestamp with time zone', { nullable: true, }) public scheduledAt: Date | null; + @Column('boolean', { + default: false, + }) + public isActuallyScheduled: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts index 03c32b6548..c6519bad0b 100644 --- a/packages/backend/src/models/json-schema/note-draft.ts +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -171,5 +171,9 @@ export const packedNoteDraftSchema = { type: 'number', optional: false, nullable: true, }, + isActuallyScheduled: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts index 82f1d029c7..4793385990 100644 --- a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -32,7 +32,7 @@ export class PostScheduledNoteProcessorService { @bindThis public async process(job: Bull.Job): Promise { const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] }); - if (draft == null || draft.user == null || draft.scheduledAt == null) { + if (draft == null || draft.user == null || draft.scheduledAt == null || !draft.isActuallyScheduled) { return; } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts index 75c1c577d1..5d18a2e42c 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -184,6 +184,7 @@ export const paramDef = { required: ['choices'], }, scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean', default: false }, }, required: [], } as const; @@ -214,6 +215,7 @@ export default class extends Endpoint { // eslint- visibleUserIds: ps.visibleUserIds ?? [], channelId: ps.channelId ?? undefined, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled ?? false, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts index 2692b279a4..7457a82dee 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -216,6 +216,7 @@ export const paramDef = { required: ['choices'], }, scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean', default: false }, }, required: ['draftId'], } as const; @@ -246,6 +247,7 @@ export default class extends Endpoint { // eslint- visibleUserIds: ps.visibleUserIds ?? [], channelId: ps.channelId ?? undefined, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled ?? false, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 60eeb7bb81..bc490cdeb2 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -669,6 +669,7 @@ function clear() { files.value = []; poll.value = null; quoteId.value = null; + scheduledAt.value = null; } function onKeydown(ev: KeyboardEvent) { @@ -839,7 +840,9 @@ function deleteDraft() { miLocalStorage.setItem('drafts', JSON.stringify(draftData)); } -async function saveServerDraft(clearLocal = false) { +async function saveServerDraft(options: { + isActuallyScheduled?: boolean; +} = {}) { return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', { ...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }), text: text.value, @@ -855,12 +858,7 @@ async function saveServerDraft(clearLocal = false) { channelId: targetChannel.value ? targetChannel.value.id : undefined, reactionAcceptance: reactionAcceptance.value, scheduledAt: scheduledAt.value, - }).then(() => { - if (clearLocal) { - clear(); - deleteDraft(); - } - }).catch((err) => { + isActuallyScheduled: options.isActuallyScheduled ?? false, }); } @@ -895,6 +893,21 @@ async function post(ev?: MouseEvent) { } } + if (scheduledAt.value != null) { + if (uploader.items.value.some(x => x.uploaded == null)) { + await uploadFiles(); + + // アップロード失敗したものがあったら中止 + if (uploader.items.value.some(x => x.uploaded == null)) { + return; + } + } + + await postAsScheduled(); + clear(); + return; + } + if (props.mock) return; if (visibility.value === 'public' && ( @@ -1066,6 +1079,14 @@ async function post(ev?: MouseEvent) { }); } +async function postAsScheduled() { + if (props.mock) return; + + await saveServerDraft({ + isActuallyScheduled: true, + }); +} + function cancel() { emit('cancel'); } @@ -1247,6 +1268,10 @@ async function schedule() { scheduledAt.value = result.getTime(); } +function cancelSchedule() { + scheduledAt.value = null; +} + onMounted(() => { if (props.autofocus) { focus(); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 74ea5fbe5f..d187b56090 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4463,6 +4463,7 @@ export type components = { /** @enum {string|null} */ reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; scheduledAt: number | null; + isActuallyScheduled: boolean; }; NoteReaction: { /** Format: id */ @@ -29205,6 +29206,8 @@ export interface operations { expiredAfter?: number | null; } | null; scheduledAt?: number | null; + /** @default false */ + isActuallyScheduled?: boolean; }; }; }; @@ -29447,6 +29450,8 @@ export interface operations { expiredAfter?: number | null; } | null; scheduledAt?: number | null; + /** @default false */ + isActuallyScheduled?: boolean; }; }; };