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) {
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"`);
}
}

View File

@ -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<void> {
if (!draft.isActuallyScheduled) return;
if (draft.scheduledAt == null) return;
if (draft.scheduledAt.getTime() <= Date.now()) return;

View File

@ -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,

View File

@ -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<MiNoteDraft>) {
if (data == null) return;

View File

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

View File

@ -32,7 +32,7 @@ export class PostScheduledNoteProcessorService {
@bindThis
public async process(job: Bull.Job<PostScheduledNoteJobData>): Promise<void> {
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;
}

View File

@ -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<typeof meta, typeof paramDef> { // 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) {

View File

@ -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<typeof meta, typeof paramDef> { // 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) {

View File

@ -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();

View File

@ -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;
};
};
};