This commit is contained in:
syuilo 2025-09-24 16:02:52 +09:00
parent d9d90a04e2
commit 8fede2d670
10 changed files with 59 additions and 11 deletions

View File

@ -11,12 +11,14 @@ export class ScheduledPost1758677617888 {
*/ */
async up(queryRunner) { async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`); 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 * @param {QueryRunner} queryRunner
*/ */
async down(queryRunner) { async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`);
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`); await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`);
} }
} }

View File

@ -32,6 +32,7 @@ export type NoteDraftOptions = {
channelId?: MiChannel['id'] | null; channelId?: MiChannel['id'] | null;
poll?: (IPoll & { expiredAfter?: number | null }) | null; poll?: (IPoll & { expiredAfter?: number | null }) | null;
scheduledAt?: Date | null; scheduledAt?: Date | null;
isActuallyScheduled?: boolean;
}; };
@Injectable() @Injectable()
@ -100,7 +101,7 @@ export class NoteDraftService {
appliedDraft.userId = me.id; appliedDraft.userId = me.id;
const draft = await this.noteDraftsRepository.insertOne(appliedDraft); const draft = await this.noteDraftsRepository.insertOne(appliedDraft);
if (draft.scheduledAt) { if (draft.scheduledAt && draft.isActuallyScheduled) {
this.schedule(draft); this.schedule(draft);
} }
@ -133,7 +134,7 @@ export class NoteDraftService {
await this.noteDraftsRepository.update(draftId, appliedDraft); await this.noteDraftsRepository.update(draftId, appliedDraft);
this.clearSchedule(draft).then(() => { this.clearSchedule(draft).then(() => {
if (appliedDraft.scheduledAt) { if (appliedDraft.scheduledAt != null && appliedDraft.isActuallyScheduled) {
this.schedule(draft); this.schedule(draft);
} }
}); });
@ -328,6 +329,7 @@ export class NoteDraftService {
localOnly: data.localOnly, localOnly: data.localOnly,
reactionAcceptance: data.reactionAcceptance, reactionAcceptance: data.reactionAcceptance,
scheduledAt: data.scheduledAt ?? null, scheduledAt: data.scheduledAt ?? null,
isActuallyScheduled: data.isActuallyScheduled ?? false,
} satisfies MiNoteDraft; } satisfies MiNoteDraft;
return appliedDraft; return appliedDraft;
@ -335,6 +337,7 @@ export class NoteDraftService {
@bindThis @bindThis
public async schedule(draft: MiNoteDraft): Promise<void> { public async schedule(draft: MiNoteDraft): Promise<void> {
if (!draft.isActuallyScheduled) return;
if (draft.scheduledAt == null) return; if (draft.scheduledAt == null) return;
if (draft.scheduledAt.getTime() <= Date.now()) return; if (draft.scheduledAt.getTime() <= Date.now()) return;

View File

@ -106,6 +106,7 @@ export class NoteDraftEntityService implements OnModuleInit {
id: noteDraft.id, id: noteDraft.id,
createdAt: this.idService.parse(noteDraft.id).date.toISOString(), createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
scheduledAt: noteDraft.scheduledAt?.getTime() ?? null, scheduledAt: noteDraft.scheduledAt?.getTime() ?? null,
isActuallyScheduled: noteDraft.isActuallyScheduled,
userId: noteDraft.userId, userId: noteDraft.userId,
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
text: text, text: text,

View File

@ -153,12 +153,16 @@ export class MiNoteDraft {
//#endregion //#endregion
// 予約投稿
@Column('timestamp with time zone', { @Column('timestamp with time zone', {
nullable: true, nullable: true,
}) })
public scheduledAt: Date | null; public scheduledAt: Date | null;
@Column('boolean', {
default: false,
})
public isActuallyScheduled: boolean;
constructor(data: Partial<MiNoteDraft>) { constructor(data: Partial<MiNoteDraft>) {
if (data == null) return; if (data == null) return;

View File

@ -171,5 +171,9 @@ export const packedNoteDraftSchema = {
type: 'number', type: 'number',
optional: false, nullable: true, optional: false, nullable: true,
}, },
isActuallyScheduled: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
} as const; } as const;

View File

@ -32,7 +32,7 @@ export class PostScheduledNoteProcessorService {
@bindThis @bindThis
public async process(job: Bull.Job<PostScheduledNoteJobData>): Promise<void> { public async process(job: Bull.Job<PostScheduledNoteJobData>): Promise<void> {
const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] }); 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; return;
} }

View File

@ -184,6 +184,7 @@ export const paramDef = {
required: ['choices'], required: ['choices'],
}, },
scheduledAt: { type: 'integer', nullable: true }, scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean', default: false },
}, },
required: [], required: [],
} as const; } as const;
@ -214,6 +215,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
visibleUserIds: ps.visibleUserIds ?? [], visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? undefined, channelId: ps.channelId ?? undefined,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled ?? false,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {

View File

@ -216,6 +216,7 @@ export const paramDef = {
required: ['choices'], required: ['choices'],
}, },
scheduledAt: { type: 'integer', nullable: true }, scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean', default: false },
}, },
required: ['draftId'], required: ['draftId'],
} as const; } as const;
@ -246,6 +247,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
visibleUserIds: ps.visibleUserIds ?? [], visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? undefined, channelId: ps.channelId ?? undefined,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled ?? false,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {

View File

@ -669,6 +669,7 @@ function clear() {
files.value = []; files.value = [];
poll.value = null; poll.value = null;
quoteId.value = null; quoteId.value = null;
scheduledAt.value = null;
} }
function onKeydown(ev: KeyboardEvent) { function onKeydown(ev: KeyboardEvent) {
@ -839,7 +840,9 @@ function deleteDraft() {
miLocalStorage.setItem('drafts', JSON.stringify(draftData)); 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', { return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }), ...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
text: text.value, text: text.value,
@ -855,12 +858,7 @@ async function saveServerDraft(clearLocal = false) {
channelId: targetChannel.value ? targetChannel.value.id : undefined, channelId: targetChannel.value ? targetChannel.value.id : undefined,
reactionAcceptance: reactionAcceptance.value, reactionAcceptance: reactionAcceptance.value,
scheduledAt: scheduledAt.value, scheduledAt: scheduledAt.value,
}).then(() => { isActuallyScheduled: options.isActuallyScheduled ?? false,
if (clearLocal) {
clear();
deleteDraft();
}
}).catch((err) => {
}); });
} }
@ -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 (props.mock) return;
if (visibility.value === 'public' && ( 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() { function cancel() {
emit('cancel'); emit('cancel');
} }
@ -1247,6 +1268,10 @@ async function schedule() {
scheduledAt.value = result.getTime(); scheduledAt.value = result.getTime();
} }
function cancelSchedule() {
scheduledAt.value = null;
}
onMounted(() => { onMounted(() => {
if (props.autofocus) { if (props.autofocus) {
focus(); focus();

View File

@ -4463,6 +4463,7 @@ export type components = {
/** @enum {string|null} */ /** @enum {string|null} */
reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
scheduledAt: number | null; scheduledAt: number | null;
isActuallyScheduled: boolean;
}; };
NoteReaction: { NoteReaction: {
/** Format: id */ /** Format: id */
@ -29205,6 +29206,8 @@ export interface operations {
expiredAfter?: number | null; expiredAfter?: number | null;
} | null; } | null;
scheduledAt?: number | null; scheduledAt?: number | null;
/** @default false */
isActuallyScheduled?: boolean;
}; };
}; };
}; };
@ -29447,6 +29450,8 @@ export interface operations {
expiredAfter?: number | null; expiredAfter?: number | null;
} | null; } | null;
scheduledAt?: number | null; scheduledAt?: number | null;
/** @default false */
isActuallyScheduled?: boolean;
}; };
}; };
}; };