This commit is contained in:
syuilo 2025-09-24 18:30:39 +09:00
parent f6e481d8fb
commit f1421644cd
7 changed files with 74 additions and 83 deletions

View File

@ -17,22 +17,23 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRenote, isQuote } from '@/misc/is-renote.js'; import { isRenote, isQuote } from '@/misc/is-renote.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { deepClone } from '@/misc/clone.js';
export type NoteDraftOptions = { export type NoteDraftOptions = {
replyId?: MiNote['id'] | null; replyId: MiNote['id'] | null;
renoteId?: MiNote['id'] | null; renoteId: MiNote['id'] | null;
text?: string | null; text: string | null;
cw?: string | null; cw: string | null;
localOnly?: boolean | null; localOnly: boolean | null;
reactionAcceptance?: typeof noteReactionAcceptances[number]; reactionAcceptance: typeof noteReactionAcceptances[number];
visibility?: typeof noteVisibilities[number]; visibility: typeof noteVisibilities[number];
fileIds?: MiDriveFile['id'][]; fileIds: MiDriveFile['id'][];
visibleUserIds?: MiUser['id'][]; visibleUserIds: MiUser['id'][];
hashtag?: string; hashtag: string | null;
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; isActuallyScheduled: boolean;
}; };
@Injectable() @Injectable()
@ -109,7 +110,7 @@ export class NoteDraftService {
} }
@bindThis @bindThis
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise<MiNoteDraft> { public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial<NoteDraftOptions>): Promise<MiNoteDraft> {
const draft = await this.noteDraftsRepository.findOneBy({ const draft = await this.noteDraftsRepository.findOneBy({
id: draftId, id: draftId,
userId: me.id, userId: me.id,
@ -180,7 +181,7 @@ export class NoteDraftService {
public async checkAndSetDraftNoteOptions( public async checkAndSetDraftNoteOptions(
me: MiLocalUser, me: MiLocalUser,
draft: MiNoteDraft, draft: MiNoteDraft,
data: NoteDraftOptions, data: Partial<NoteDraftOptions>,
): Promise<MiNoteDraft> { ): Promise<MiNoteDraft> {
data.visibility ??= 'public'; data.visibility ??= 'public';
data.localOnly ??= false; data.localOnly ??= false;
@ -191,8 +192,6 @@ export class NoteDraftService {
data.localOnly = true; data.localOnly = true;
} }
let appliedDraft = draft;
//#region visibleUsers //#region visibleUsers
let visibleUsers: MiUser[] = []; let visibleUsers: MiUser[] = [];
if (data.visibleUserIds != null) { if (data.visibleUserIds != null) {
@ -205,7 +204,7 @@ export class NoteDraftService {
//#region files //#region files
let files: MiDriveFile[] = []; let files: MiDriveFile[] = [];
const fileIds = data.fileIds ?? null; const fileIds = data.fileIds ?? null;
if (fileIds != null) { if (fileIds != null && fileIds.length > 0) {
files = await this.driveFilesRepository.createQueryBuilder('file') files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', { .where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id, userId: me.id,
@ -310,8 +309,8 @@ export class NoteDraftService {
} }
//#endregion //#endregion
appliedDraft = { return {
...appliedDraft, ...draft,
visibility: data.visibility, visibility: data.visibility,
cw: data.cw ?? null, cw: data.cw ?? null,
fileIds: fileIds ?? [], fileIds: fileIds ?? [],
@ -331,8 +330,6 @@ export class NoteDraftService {
scheduledAt: data.scheduledAt ?? null, scheduledAt: data.scheduledAt ?? null,
isActuallyScheduled: data.isActuallyScheduled ?? false, isActuallyScheduled: data.isActuallyScheduled ?? false,
} satisfies MiNoteDraft; } satisfies MiNoteDraft;
return appliedDraft;
} }
@bindThis @bindThis

View File

@ -162,12 +162,4 @@ export class MiNoteDraft {
default: false, default: false,
}) })
public isActuallyScheduled: boolean; public isActuallyScheduled: boolean;
constructor(data: Partial<MiNoteDraft>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
} }

View File

@ -162,7 +162,7 @@ export const paramDef = {
fileIds: { fileIds: {
type: 'array', type: 'array',
uniqueItems: true, uniqueItems: true,
minItems: 1, minItems: 0,
maxItems: 16, maxItems: 16,
items: { type: 'string', format: 'misskey:id' }, items: { type: 'string', format: 'misskey:id' },
}, },
@ -186,7 +186,7 @@ export const paramDef = {
scheduledAt: { type: 'integer', nullable: true }, scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean', default: false }, isActuallyScheduled: { type: 'boolean', default: false },
}, },
required: [], required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'],
} as const; } as const;
@Injectable() @Injectable()
@ -203,19 +203,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
multiple: ps.poll.multiple ?? false, multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiredAfter: ps.poll.expiredAfter ?? null, expiredAfter: ps.poll.expiredAfter ?? null,
} : undefined, } : null,
text: ps.text ?? null, text: ps.text,
replyId: ps.replyId ?? undefined, replyId: ps.replyId,
renoteId: ps.renoteId ?? undefined, renoteId: ps.renoteId,
cw: ps.cw ?? null, cw: ps.cw,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}), hashtag: ps.hashtag,
localOnly: ps.localOnly, localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance, reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility, visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [], visibleUserIds: ps.visibleUserIds,
channelId: ps.channelId ?? undefined, channelId: ps.channelId,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled ?? false, isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {

View File

@ -171,14 +171,14 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
draftId: { type: 'string', nullable: false, format: 'misskey:id' }, draftId: { type: 'string', nullable: false, format: 'misskey:id' },
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] },
visibleUserIds: { type: 'array', uniqueItems: true, items: { visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id', type: 'string', format: 'misskey:id',
} }, } },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
hashtag: { type: 'string', nullable: true, maxLength: 200 }, hashtag: { type: 'string', nullable: true, maxLength: 200 },
localOnly: { type: 'boolean', default: false }, localOnly: { type: 'boolean' },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] },
replyId: { type: 'string', format: 'misskey:id', nullable: true }, replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true }, renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true },
@ -194,7 +194,7 @@ export const paramDef = {
fileIds: { fileIds: {
type: 'array', type: 'array',
uniqueItems: true, uniqueItems: true,
minItems: 1, minItems: 0,
maxItems: 16, maxItems: 16,
items: { type: 'string', format: 'misskey:id' }, items: { type: 'string', format: 'misskey:id' },
}, },
@ -216,7 +216,7 @@ export const paramDef = {
required: ['choices'], required: ['choices'],
}, },
scheduledAt: { type: 'integer', nullable: true }, scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean', default: false }, isActuallyScheduled: { type: 'boolean' },
}, },
required: ['draftId'], required: ['draftId'],
} as const; } as const;
@ -236,18 +236,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiredAfter: ps.poll.expiredAfter ?? null, expiredAfter: ps.poll.expiredAfter ?? null,
} : undefined, } : undefined,
text: ps.text ?? null, text: ps.text,
replyId: ps.replyId ?? undefined, replyId: ps.replyId,
renoteId: ps.renoteId ?? undefined, renoteId: ps.renoteId,
cw: ps.cw ?? null, cw: ps.cw,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}), ...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
localOnly: ps.localOnly, localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance, reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility, visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [], visibleUserIds: ps.visibleUserIds,
channelId: ps.channelId ?? undefined, channelId: ps.channelId,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled ?? false, isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {

View File

@ -219,6 +219,16 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
draftsPaginator.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> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@ -850,12 +850,12 @@ async function saveServerDraft(options: {
visibility: visibility.value, visibility: visibility.value,
localOnly: localOnly.value, localOnly: localOnly.value,
hashtag: hashtags.value, hashtag: hashtags.value,
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}), fileIds: files.value.map(f => f.id),
poll: poll.value, poll: poll.value,
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}), visibleUserIds: visibleUsers.value.map(x => x.id),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined, renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : null,
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined, replyId: replyTargetNote.value ? replyTargetNote.value.id : null,
channelId: targetChannel.value ? targetChannel.value.id : undefined, channelId: targetChannel.value ? targetChannel.value.id : null,
reactionAcceptance: reactionAcceptance.value, reactionAcceptance: reactionAcceptance.value,
scheduledAt: scheduledAt.value, scheduledAt: scheduledAt.value,
isActuallyScheduled: options.isActuallyScheduled ?? false, isActuallyScheduled: options.isActuallyScheduled ?? false,

View File

@ -29180,34 +29180,34 @@ export interface operations {
* @default public * @default public
* @enum {string} * @enum {string}
*/ */
visibility?: 'public' | 'home' | 'followers' | 'specified'; visibility: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[]; visibleUserIds: string[];
cw?: string | null; cw: string | null;
hashtag?: string | null; hashtag: string | null;
/** @default false */ /** @default false */
localOnly?: boolean; localOnly: boolean;
/** /**
* @default null * @default null
* @enum {string|null} * @enum {string|null}
*/ */
reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; reactionAcceptance: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
/** Format: misskey:id */ /** Format: misskey:id */
replyId?: string | null; replyId: string | null;
/** Format: misskey:id */ /** Format: misskey:id */
renoteId?: string | null; renoteId: string | null;
/** Format: misskey:id */ /** Format: misskey:id */
channelId?: string | null; channelId: string | null;
text?: string | null; text: string | null;
fileIds?: string[]; fileIds: string[];
poll?: { poll: {
choices: string[]; choices: string[];
multiple?: boolean; multiple?: boolean;
expiresAt?: number | null; expiresAt?: number | null;
expiredAfter?: number | null; expiredAfter?: number | null;
} | null; } | null;
scheduledAt?: number | null; scheduledAt: number | null;
/** @default false */ /** @default false */
isActuallyScheduled?: boolean; isActuallyScheduled: boolean;
}; };
}; };
}; };
@ -29421,20 +29421,13 @@ export interface operations {
'application/json': { 'application/json': {
/** Format: misskey:id */ /** Format: misskey:id */
draftId: string; draftId: string;
/** /** @enum {string} */
* @default public
* @enum {string}
*/
visibility?: 'public' | 'home' | 'followers' | 'specified'; visibility?: 'public' | 'home' | 'followers' | 'specified';
visibleUserIds?: string[]; visibleUserIds?: string[];
cw?: string | null; cw?: string | null;
hashtag?: string | null; hashtag?: string | null;
/** @default false */
localOnly?: boolean; localOnly?: boolean;
/** /** @enum {string|null} */
* @default null
* @enum {string|null}
*/
reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote';
/** Format: misskey:id */ /** Format: misskey:id */
replyId?: string | null; replyId?: string | null;
@ -29451,7 +29444,6 @@ export interface operations {
expiredAfter?: number | null; expiredAfter?: number | null;
} | null; } | null;
scheduledAt?: number | null; scheduledAt?: number | null;
/** @default false */
isActuallyScheduled?: boolean; isActuallyScheduled?: boolean;
}; };
}; };