Compare commits

..

1 Commits

Author SHA1 Message Date
syuilo e756de996a
Merge 8b1f889d1d into 0f8c068e84 2025-09-24 12:08:09 +09:00
26 changed files with 295 additions and 474 deletions

32
locales/index.d.ts vendored
View File

@ -5286,10 +5286,6 @@ export interface Locale extends ILocale {
* *
*/ */
"draft": string; "draft": string;
/**
* 稿
*/
"draftsAndScheduledNotes": string;
/** /**
* *
*/ */
@ -5561,22 +5557,6 @@ export interface Locale extends ILocale {
* 稿 * 稿
*/ */
"schedulePost": string; "schedulePost": string;
/**
* {x}稿
*/
"scheduleToPostOnX": ParameterizedString<"x">;
/**
* {x}稿
*/
"scheduledToPostOnX": ParameterizedString<"x">;
/**
*
*/
"schedule": string;
/**
*
*/
"scheduled": string;
"_compression": { "_compression": {
"_quality": { "_quality": {
/** /**
@ -10356,10 +10336,6 @@ export interface Locale extends ILocale {
* 稿 * 稿
*/ */
"scheduledNotePosted": string; "scheduledNotePosted": string;
/**
* 稿
*/
"scheduledNotePostFailed": string;
/** /**
* 稿 * 稿
*/ */
@ -12677,14 +12653,6 @@ export interface Locale extends ILocale {
* 稿 * 稿
*/ */
"schedule": string; "schedule": string;
/**
* 稿
*/
"listScheduledNotes": string;
/**
*
*/
"cancelSchedule": string;
}; };
/** /**
* *

View File

@ -1317,7 +1317,6 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
draft: "下書き" draft: "下書き"
draftsAndScheduledNotes: "下書きと予約投稿"
confirmOnReact: "リアクションする際に確認する" confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?" reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
@ -1385,10 +1384,6 @@ themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォル
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!" thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
createUserSpecifiedNote: "ユーザー指定ノートを作成" createUserSpecifiedNote: "ユーザー指定ノートを作成"
schedulePost: "投稿を予約" schedulePost: "投稿を予約"
scheduleToPostOnX: "{x}に投稿を予約します"
scheduledToPostOnX: "{x}に投稿が予約されています"
schedule: "予約"
scheduled: "予約"
_compression: _compression:
_quality: _quality:
@ -2735,7 +2730,6 @@ _notification:
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
scheduledNotePosted: "予約ノートが投稿されました" scheduledNotePosted: "予約ノートが投稿されました"
scheduledNotePostFailed: "予約ノートの投稿に失敗しました"
newNote: "新しい投稿" newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました" roleAssigned: "ロールが付与されました"
@ -3394,8 +3388,6 @@ _drafts:
restore: "復元" restore: "復元"
listDrafts: "下書き一覧" listDrafts: "下書き一覧"
schedule: "投稿予約" schedule: "投稿予約"
listScheduledNotes: "予約投稿一覧"
cancelSchedule: "予約解除"
qr: "二次元コード" qr: "二次元コード"
_qr: _qr:

View File

@ -11,14 +11,12 @@ 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

@ -18,7 +18,21 @@ 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';
export type NoteDraftOptions = Omit<MiNoteDraft, 'id' | 'userId' | 'user' | 'reply' | 'renote' | 'channel'>; export type NoteDraftOptions = {
replyId?: MiNote['id'] | null;
renoteId?: MiNote['id'] | null;
text?: string | null;
cw?: string | null;
localOnly?: boolean | null;
reactionAcceptance?: typeof noteReactionAcceptances[number];
visibility?: typeof noteVisibilities[number];
fileIds?: MiDriveFile['id'][];
visibleUserIds?: MiUser['id'][];
hashtag?: string;
channelId?: MiChannel['id'] | null;
poll?: (IPoll & { expiredAfter?: number | null }) | null;
scheduledAt?: Date | null;
};
@Injectable() @Injectable()
export class NoteDraftService { export class NoteDraftService {
@ -61,6 +75,7 @@ export class NoteDraftService {
@bindThis @bindThis
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> { public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
//#region check draft limit //#region check draft limit
const currentCount = await this.noteDraftsRepository.countBy({ const currentCount = await this.noteDraftsRepository.countBy({
userId: me.id, userId: me.id,
}); });
@ -69,15 +84,23 @@ export class NoteDraftService {
} }
//#endregion //#endregion
await this.validate(me, data); if (data.poll) {
if (typeof data.poll.expiresAt === 'number') {
if (data.poll.expiresAt < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
}
} else if (typeof data.poll.expiredAfter === 'number') {
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
}
}
const draft = await this.noteDraftsRepository.insertOne({ const appliedDraft = await this.checkAndSetDraftNoteOptions(me, this.noteDraftsRepository.create(), data);
...data,
id: this.idService.gen(),
userId: me.id,
});
if (draft.scheduledAt && draft.isActuallyScheduled) { appliedDraft.id = this.idService.gen();
appliedDraft.userId = me.id;
const draft = await this.noteDraftsRepository.insertOne(appliedDraft);
if (draft.scheduledAt) {
this.schedule(draft); this.schedule(draft);
} }
@ -85,7 +108,7 @@ export class NoteDraftService {
} }
@bindThis @bindThis
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial<NoteDraftOptions>): Promise<MiNoteDraft> { public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: 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,
@ -95,22 +118,30 @@ export class NoteDraftService {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft'); throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
} }
await this.validate(me, data); if (data.poll) {
if (typeof data.poll.expiresAt === 'number') {
if (data.poll.expiresAt < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
}
} else if (typeof data.poll.expiredAfter === 'number') {
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
}
}
const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update() const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data);
.set(data)
.where('id = :id', { id: draftId })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.clearSchedule(draftId).then(() => { await this.noteDraftsRepository.update(draftId, appliedDraft);
if (updatedDraft.scheduledAt != null && updatedDraft.isActuallyScheduled) {
this.schedule(updatedDraft); this.clearSchedule(draft).then(() => {
if (appliedDraft.scheduledAt) {
this.schedule(draft);
} }
}); });
return updatedDraft; return {
...draft,
...appliedDraft,
};
} }
@bindThis @bindThis
@ -126,7 +157,7 @@ export class NoteDraftService {
await this.noteDraftsRepository.delete(draft.id); await this.noteDraftsRepository.delete(draft.id);
this.clearSchedule(draftId); this.clearSchedule(draft);
} }
@bindThis @bindThis
@ -143,20 +174,27 @@ export class NoteDraftService {
return draft; return draft;
} }
// 関連エンティティを取得し紐づける部分を共通化する
@bindThis @bindThis
public async validate( public async checkAndSetDraftNoteOptions(
me: MiLocalUser, me: MiLocalUser,
data: Partial<NoteDraftOptions>, draft: MiNoteDraft,
): Promise<void> { data: NoteDraftOptions,
if (data.pollExpiresAt != null) { ): Promise<MiNoteDraft> {
if (data.pollExpiresAt.getTime() < Date.now()) { data.visibility ??= 'public';
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); data.localOnly ??= false;
} if (data.reactionAcceptance === undefined) data.reactionAcceptance = null;
if (data.channelId != null) {
data.visibility = 'public';
data.visibleUserIds = [];
data.localOnly = true;
} }
let appliedDraft = draft;
//#region visibleUsers //#region visibleUsers
let visibleUsers: MiUser[] = []; let visibleUsers: MiUser[] = [];
if (data.visibleUserIds != null && data.visibleUserIds.length > 0) { if (data.visibleUserIds != null) {
visibleUsers = await this.usersRepository.findBy({ visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds), id: In(data.visibleUserIds),
}); });
@ -166,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 && fileIds.length > 0) { if (fileIds != null) {
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,
@ -270,11 +308,33 @@ export class NoteDraftService {
} }
} }
//#endregion //#endregion
appliedDraft = {
...appliedDraft,
visibility: data.visibility,
cw: data.cw ?? null,
fileIds: fileIds ?? [],
replyId: data.replyId ?? null,
renoteId: data.renoteId ?? null,
channelId: data.channelId ?? null,
text: data.text ?? null,
hashtag: data.hashtag ?? null,
hasPoll: data.poll != null,
pollChoices: data.poll ? data.poll.choices : [],
pollMultiple: data.poll ? data.poll.multiple : false,
pollExpiresAt: data.poll ? data.poll.expiresAt : null,
pollExpiredAfter: data.poll ? data.poll.expiredAfter ?? null : null,
visibleUserIds: data.visibleUserIds ?? [],
localOnly: data.localOnly,
reactionAcceptance: data.reactionAcceptance,
scheduledAt: data.scheduledAt ?? null,
} satisfies MiNoteDraft;
return appliedDraft;
} }
@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;
@ -295,10 +355,10 @@ export class NoteDraftService {
} }
@bindThis @bindThis
public async clearSchedule(draftId: MiNoteDraft['id']): Promise<void> { public async clearSchedule(draft: MiNoteDraft): Promise<void> {
const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']); const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']);
for (const job of jobs) { for (const job of jobs) {
if (job.data.noteDraftId === draftId) { if (job.data.noteDraftId === draft.id) {
await job.remove(); await job.remove();
} }
} }

View File

@ -106,7 +106,6 @@ 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,
@ -114,13 +113,13 @@ export class NoteDraftEntityService implements OnModuleInit {
visibility: noteDraft.visibility, visibility: noteDraft.visibility,
localOnly: noteDraft.localOnly, localOnly: noteDraft.localOnly,
reactionAcceptance: noteDraft.reactionAcceptance, reactionAcceptance: noteDraft.reactionAcceptance,
visibleUserIds: noteDraft.visibleUserIds, visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined,
hashtag: noteDraft.hashtag, hashtag: noteDraft.hashtag ?? undefined,
fileIds: noteDraft.fileIds, fileIds: noteDraft.fileIds,
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds), files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
replyId: noteDraft.replyId, replyId: noteDraft.replyId,
renoteId: noteDraft.renoteId, renoteId: noteDraft.renoteId,
channelId: noteDraft.channelId, channelId: noteDraft.channelId ?? undefined,
channel: channel ? { channel: channel ? {
id: channel.id, id: channel.id,
name: channel.name, name: channel.name,

View File

@ -153,13 +153,17 @@ 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', { constructor(data: Partial<MiNoteDraft>) {
default: false, if (data == null) return;
})
public isActuallyScheduled: boolean; for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
} }

View File

@ -9,7 +9,6 @@ import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js'; import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js'; import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js'; import { MiDriveFile } from './DriveFile.js';
import { MiNoteDraft } from './NoteDraft.js';
// misskey-js の notificationTypes と同期すべし // misskey-js の notificationTypes と同期すべし
export type MiNotification = { export type MiNotification = {
@ -66,11 +65,6 @@ export type MiNotification = {
id: string; id: string;
createdAt: string; createdAt: string;
noteId: MiNote['id']; noteId: MiNote['id'];
} | {
type: 'scheduledNotePostFailed';
id: string;
createdAt: string;
noteDraftId: MiNoteDraft['id'];
} | { } | {
type: 'receiveFollowRequest'; type: 'receiveFollowRequest';
id: string; id: string;

View File

@ -171,9 +171,5 @@ 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

@ -222,21 +222,6 @@ export const packedNotificationSchema = {
optional: false, nullable: false, optional: false, nullable: false,
}, },
}, },
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['scheduledNotePostFailed'],
},
noteDraft: {
type: 'object',
ref: 'NoteDraft',
optional: false, nullable: false,
},
},
}, { }, {
type: 'object', type: 'object',
properties: { properties: {

View File

@ -610,7 +610,6 @@ export const packedMeDetailedOnlySchema = {
reaction: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig },

View File

@ -32,24 +32,16 @@ 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 || !draft.isActuallyScheduled) { if (draft == null || draft.user == null || draft.scheduledAt == null) {
return; return;
} }
try {
const note = await this.noteCreateService.create(draft.user, draft); const note = await this.noteCreateService.create(draft.user, draft);
// await不要
this.noteDraftsRepository.remove(draft); this.noteDraftsRepository.remove(draft);
// await不要
this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', { this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', {
noteId: note.id, noteId: note.id,
}); });
} catch (err) {
this.notificationService.createNotification(draft.userId, 'scheduledNotePostFailed', {
noteDraftId: draft.id,
});
}
} }
} }

View File

@ -104,7 +104,6 @@ export const meta = {
reaction: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig },

View File

@ -210,7 +210,6 @@ export const paramDef = {
reaction: notificationRecieveConfig, reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig, pollEnded: notificationRecieveConfig,
scheduledNotePosted: notificationRecieveConfig, scheduledNotePosted: notificationRecieveConfig,
scheduledNotePostFailed: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig, receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig, followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig, roleAssigned: notificationRecieveConfig,

View File

@ -162,7 +162,7 @@ export const paramDef = {
fileIds: { fileIds: {
type: 'array', type: 'array',
uniqueItems: true, uniqueItems: true,
minItems: 0, minItems: 1,
maxItems: 16, maxItems: 16,
items: { type: 'string', format: 'misskey:id' }, items: { type: 'string', format: 'misskey:id' },
}, },
@ -184,9 +184,8 @@ export const paramDef = {
required: ['choices'], required: ['choices'],
}, },
scheduledAt: { type: 'integer', nullable: true }, scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean', default: false },
}, },
required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'], required: [],
} as const; } as const;
@Injectable() @Injectable()
@ -198,23 +197,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.create(me, { const draft = await this.noteDraftService.create(me, {
fileIds: ps.fileIds, fileIds: ps.fileIds,
pollChoices: ps.poll?.choices ?? [], poll: ps.poll ? {
pollMultiple: ps.poll?.multiple ?? false, choices: ps.poll.choices,
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null, multiple: ps.poll.multiple ?? false,
pollExpiredAfter: ps.poll?.expiredAfter ?? null, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
hasPoll: ps.poll != null, expiredAfter: ps.poll.expiredAfter ?? null,
text: ps.text, } : undefined,
replyId: ps.replyId, text: ps.text ?? null,
renoteId: ps.renoteId, replyId: ps.replyId ?? undefined,
cw: ps.cw, renoteId: ps.renoteId ?? undefined,
hashtag: ps.hashtag, cw: ps.cw ?? null,
...(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, channelId: ps.channelId ?? undefined,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {

View File

@ -41,7 +41,6 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' }, sinceDate: { type: 'integer' },
untilDate: { type: 'integer' }, untilDate: { type: 'integer' },
scheduled: { type: 'boolean', nullable: true },
}, },
required: [], required: [],
} as const; } as const;
@ -59,12 +58,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('drafts.userId = :meId', { meId: me.id }); .andWhere('drafts.userId = :meId', { meId: me.id });
if (ps.scheduled === true) {
query.andWhere('drafts.isActuallyScheduled = true');
} else if (ps.scheduled === false) {
query.andWhere('drafts.isActuallyScheduled = false');
}
const drafts = await query const drafts = await query
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany();

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'] }, visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
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' }, localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
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: 0, minItems: 1,
maxItems: 16, maxItems: 16,
items: { type: 'string', format: 'misskey:id' }, items: { type: 'string', format: 'misskey:id' },
}, },
@ -216,7 +216,6 @@ export const paramDef = {
required: ['choices'], required: ['choices'],
}, },
scheduledAt: { type: 'integer', nullable: true }, scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean' },
}, },
required: ['draftId'], required: ['draftId'],
} as const; } as const;
@ -230,22 +229,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.update(me, ps.draftId, { const draft = await this.noteDraftService.update(me, ps.draftId, {
fileIds: ps.fileIds, fileIds: ps.fileIds,
pollChoices: ps.poll?.choices, poll: ps.poll ? {
pollMultiple: ps.poll?.multiple, choices: ps.poll.choices,
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null, multiple: ps.poll.multiple ?? false,
pollExpiredAfter: ps.poll?.expiredAfter, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
text: ps.text, expiredAfter: ps.poll.expiredAfter ?? null,
replyId: ps.replyId, } : undefined,
renoteId: ps.renoteId, text: ps.text ?? null,
cw: ps.cw, replyId: ps.replyId ?? undefined,
hashtag: ps.hashtag, renoteId: ps.renoteId ?? undefined,
cw: ps.cw ?? null,
...(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, channelId: ps.channelId ?? undefined,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {

View File

@ -13,7 +13,6 @@
* reaction - 稿 * reaction - 稿
* pollEnded - * pollEnded -
* scheduledNotePosted - 稿 * scheduledNotePosted - 稿
* scheduledNotePostFailed - 稿
* receiveFollowRequest - * receiveFollowRequest -
* followRequestAccepted - * followRequestAccepted -
* roleAssigned - * roleAssigned -
@ -35,7 +34,6 @@ export const notificationTypes = [
'reaction', 'reaction',
'pollEnded', 'pollEnded',
'scheduledNotePosted', 'scheduledNotePosted',
'scheduledNotePostFailed',
'receiveFollowRequest', 'receiveFollowRequest',
'followRequestAccepted', 'followRequestAccepted',
'roleAssigned', 'roleAssigned',

View File

@ -64,8 +64,6 @@ function toBase62(n: number): string {
} }
export function getConfig(): UserConfig { export function getConfig(): UserConfig {
const localesHash = toBase62(hash(JSON.stringify(locales)));
return { return {
base: '/embed_vite/', base: '/embed_vite/',
@ -150,9 +148,9 @@ export function getConfig(): UserConfig {
// dependencies of i18n.ts // dependencies of i18n.ts
'config': ['@@/js/config.js'], 'config': ['@@/js/config.js'],
}, },
entryFileNames: `scripts/${localesHash}-[hash:8].js`, entryFileNames: 'scripts/[hash:8].js',
chunkFileNames: `scripts/${localesHash}-[hash:8].js`, chunkFileNames: 'scripts/[hash:8].js',
assetFileNames: `assets/${localesHash}-[hash:8][extname]`, assetFileNames: 'assets/[hash:8][extname]',
paths(id) { paths(id) {
for (const p of externalPackages) { for (const p of externalPackages) {
if (p.match.test(id)) { if (p.match.test(id)) {

View File

@ -15,32 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
@esc="cancel()" @esc="cancel()"
> >
<template #header> <template #header>
{{ i18n.ts.draftsAndScheduledNotes }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }}) {{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
</template> </template>
<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"> <div class="_spacer">
<MkPagination :key="tab" :paginator="tab === 'scheduled' ? scheduledPaginator : draftsPaginator" withControl> <MkPagination :paginator="paginator" withControl>
<template #empty> <template #empty>
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/> <MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</template> </template>
@ -54,7 +32,6 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.draft]" :class="[$style.draft]"
> >
<div :class="$style.draftBody" class="_gaps_s"> <div :class="$style.draftBody" class="_gaps_s">
<MkInfo v-if="draft.scheduledAt != null && draft.isActuallyScheduled">{{ i18n.tsx.scheduledToPostOnX({ x: new Date(draft.scheduledAt).toLocaleString() }) }}</MkInfo>
<div :class="$style.draftInfo"> <div :class="$style.draftInfo">
<div :class="$style.draftMeta"> <div :class="$style.draftMeta">
<div v-if="draft.reply" class="_nowrap"> <div v-if="draft.reply" class="_nowrap">
@ -108,19 +85,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/> <MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
</div> </div>
</div> </div>
<div :class="$style.draftActions" class="_buttons"> <div :class="$style.draftActions" class="_buttons">
<MkButton <MkButton
v-if="draft.scheduledAt != null && draft.isActuallyScheduled"
:class="$style.itemButton"
small
@click="cancelSchedule(draft)"
>
<i class="ti ti-calendar-x"></i>
{{ i18n.ts._drafts.cancelSchedule }}
</MkButton>
<MkButton
v-else
:class="$style.itemButton" :class="$style.itemButton"
small small
@click="restoreDraft(draft)" @click="restoreDraft(draft)"
@ -128,6 +94,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-corner-up-left"></i> <i class="ti ti-corner-up-left"></i>
{{ i18n.ts._drafts.restore }} {{ i18n.ts._drafts.restore }}
</MkButton> </MkButton>
<MkButton
:class="$style.itemButton"
small
@click="schedule(draft)"
>
<i class="ti ti-calendar-time"></i>
{{ i18n.ts._drafts.schedule }}
</MkButton>
<MkButton <MkButton
v-tooltip="i18n.ts._drafts.delete" v-tooltip="i18n.ts._drafts.delete"
danger danger
@ -145,7 +119,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
</MkPagination> </MkPagination>
</div> </div>
</MkStickyContainer>
</MkModalWindow> </MkModalWindow>
</template> </template>
@ -161,12 +134,6 @@ import * as os from '@/os.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api'; import { misskeyApi } from '@/utility/misskey-api';
import { Paginator } from '@/utility/paginator.js'; 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<{ const emit = defineEmits<{
(ev: 'restore', draft: Misskey.entities.NoteDraft): void; (ev: 'restore', draft: Misskey.entities.NoteDraft): void;
@ -174,20 +141,8 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts'); const paginator = markRaw(new Paginator('notes/drafts/list', {
const draftsPaginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10, limit: 10,
params: {
scheduled: false,
},
}));
const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
params: {
scheduled: true,
},
})); }));
const currentDraftsCount = ref(0); const currentDraftsCount = ref(0);
@ -216,17 +171,7 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
if (canceled) return; if (canceled) return;
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => { os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
draftsPaginator.reload(); paginator.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>
@ -284,11 +229,4 @@ async function cancelSchedule(draft: Misskey.entities.NoteDraft) {
padding-top: 16px; padding-top: 16px;
border-top: solid 1px var(--MI_THEME-divider); 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> </style>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
@ -24,7 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_quote]: notification.type === 'quote', [$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted', [$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted',
[$style.t_scheduledNotePostFailed]: notification.type === 'scheduledNotePostFailed',
[$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login', [$style.t_login]: notification.type === 'login',
@ -42,7 +41,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i> <i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i>
<i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i> <i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
@ -65,7 +63,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.header"> <header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'scheduledNotePosted'">{{ i18n.ts._notification.scheduledNotePosted }}</span> <span v-else-if="notification.type === 'scheduledNotePosted'">{{ i18n.ts._notification.scheduledNotePosted }}</span>
<span v-else-if="notification.type === 'scheduledNotePostFailed'">{{ i18n.ts._notification.scheduledNotePostFailed }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span> <span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
@ -354,11 +351,6 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none; pointer-events: none;
} }
.t_scheduledNotePostFailed {
background: var(--eventOther);
pointer-events: none;
}
.t_achievementEarned { .t_achievementEarned {
background: var(--eventAchievement); background: var(--eventAchievement);
pointer-events: none; pointer-events: none;

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu"> <button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/> <img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
</button> </button>
<button v-if="$i.policies.noteDraftLimit > 0" v-tooltip="(postAccount != null && postAccount.id !== $i.id) ? null : i18n.ts.draftsAndScheduledNotes" class="_button" :class="$style.draftButton" :disabled="postAccount != null && postAccount.id !== $i.id" @click="showDraftMenu"><i class="ti ti-list"></i></button> <button v-if="$i.policies.noteDraftLimit > 0" v-tooltip="(postAccount != null && postAccount.id !== $i.id) ? null : i18n.ts.draft" class="_button" :class="$style.draftButton" :disabled="postAccount != null && postAccount.id !== $i.id" @click="showDraftMenu"><i class="ti ti-pencil-minus"></i></button>
</div> </div>
<div :class="$style.headerRight"> <div :class="$style.headerRight">
<template v-if="!(targetChannel != null && fixed)"> <template v-if="!(targetChannel != null && fixed)">
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="posted"></template> <template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template> <template v-else-if="posting"><MkEllipsis/></template>
<template v-else>{{ submitText }}</template> <template v-else>{{ submitText }}</template>
<i style="margin-left: 6px;" :class="submitIcon"></i> <i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : replyTargetNote ? 'ti ti-arrow-back-up' : renoteTargetNote ? 'ti ti-quote' : 'ti ti-send'"></i>
</div> </div>
</button> </button>
</div> </div>
@ -61,7 +61,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button> <button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
</div> </div>
</div> </div>
<MkInfo v-if="scheduledAt != null" :class="$style.scheduledAt">{{ i18n.tsx.scheduleToPostOnX({ x: new Date(scheduledAt).toLocaleString() }) }} - <button class="_textButton" @click="cancelSchedule()">{{ i18n.ts.cancel }}</button></MkInfo>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo> <MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<div v-show="useCw" :class="$style.cwOuter"> <div v-show="useCw" :class="$style.cwOuter">
<input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd"> <input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
@ -264,19 +263,13 @@ const placeholder = computed((): string => {
}); });
const submitText = computed((): string => { const submitText = computed((): string => {
return scheduledAt.value != null return renoteTargetNote.value
? i18n.ts.schedule
: renoteTargetNote.value
? i18n.ts.quote ? i18n.ts.quote
: replyTargetNote.value : replyTargetNote.value
? i18n.ts.reply ? i18n.ts.reply
: i18n.ts.note; : i18n.ts.note;
}); });
const submitIcon = computed((): string => {
return posted.value ? 'ti ti-check' : scheduledAt.value != null ? 'ti ti-calendar-time' : replyTargetNote.value ? 'ti ti-arrow-back-up' : renoteTargetNote.value ? 'ti ti-quote' : 'ti ti-send';
});
const textLength = computed((): number => { const textLength = computed((): number => {
return (text.value + imeText.value).length; return (text.value + imeText.value).length;
}); });
@ -669,7 +662,6 @@ 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) {
@ -840,9 +832,7 @@ function deleteDraft() {
miLocalStorage.setItem('drafts', JSON.stringify(draftData)); miLocalStorage.setItem('drafts', JSON.stringify(draftData));
} }
async function saveServerDraft(options: { async function saveServerDraft(clearLocal = false) {
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,
@ -850,15 +840,20 @@ async function saveServerDraft(options: {
visibility: visibility.value, visibility: visibility.value,
localOnly: localOnly.value, localOnly: localOnly.value,
hashtag: hashtags.value, hashtag: hashtags.value,
fileIds: files.value.map(f => f.id), ...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
poll: poll.value, poll: poll.value,
visibleUserIds: visibleUsers.value.map(x => x.id), ...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : null, renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined,
replyId: replyTargetNote.value ? replyTargetNote.value.id : null, replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
channelId: targetChannel.value ? targetChannel.value.id : null, channelId: targetChannel.value ? targetChannel.value.id : undefined,
reactionAcceptance: reactionAcceptance.value, reactionAcceptance: reactionAcceptance.value,
scheduledAt: scheduledAt.value, scheduledAt: scheduledAt.value,
isActuallyScheduled: options.isActuallyScheduled ?? false, }).then(() => {
if (clearLocal) {
clear();
deleteDraft();
}
}).catch((err) => {
}); });
} }
@ -893,21 +888,6 @@ 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' && (
@ -1079,14 +1059,6 @@ async function post(ev?: MouseEvent) {
}); });
} }
async function postAsScheduled() {
if (props.mock) return;
await saveServerDraft({
isActuallyScheduled: true,
});
}
function cancel() { function cancel() {
emit('cancel'); emit('cancel');
} }
@ -1256,28 +1228,14 @@ function showDraftMenu(ev: MouseEvent) {
action: () => { action: () => {
showDraftsDialog(); showDraftsDialog();
}, },
}, { type: 'divider' }, {
type: 'button',
text: i18n.ts._drafts.listScheduledNotes,
icon: 'ti ti-clock-down',
action: () => {
showDraftsDialog();
},
}], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); }], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
} }
async function schedule() { async function schedule() {
const { canceled, result } = await os.inputDatetime({ const { canceled, result } = await os.inputDate({
title: i18n.ts.schedulePost, title: i18n.ts.schedulePost,
}); });
if (canceled) return; if (canceled) return;
if (result.getTime() <= Date.now()) return;
scheduledAt.value = result.getTime();
}
function cancelSchedule() {
scheduledAt.value = null;
} }
onMounted(() => { onMounted(() => {
@ -1580,10 +1538,6 @@ html[data-color-scheme=light] .preview {
margin: 0 20px 16px 20px; margin: 0 20px 16px 20px;
} }
.scheduledAt {
margin: 0 20px 16px 20px;
}
.cw, .cw,
.hashtags, .hashtags,
.text { .text {

View File

@ -460,7 +460,7 @@ export function inputNumber(props: {
}); });
} }
export function inputDatetime(props: { export function inputDate(props: {
title?: string; title?: string;
text?: string; text?: string;
placeholder?: string | null; placeholder?: string | null;
@ -475,7 +475,7 @@ export function inputDatetime(props: {
title: props.title, title: props.title,
text: props.text, text: props.text,
input: { input: {
type: 'datetime-local', type: 'date',
placeholder: props.placeholder, placeholder: props.placeholder,
default: props.default ?? null, default: props.default ?? null,
}, },

View File

@ -85,8 +85,6 @@ export function toBase62(n: number): string {
} }
export function getConfig(): UserConfig { export function getConfig(): UserConfig {
const localesHash = toBase62(hash(JSON.stringify(locales)));
return { return {
base: '/vite/', base: '/vite/',
@ -190,9 +188,9 @@ export function getConfig(): UserConfig {
// dependencies of i18n.ts // dependencies of i18n.ts
'config': ['@@/js/config.js'], 'config': ['@@/js/config.js'],
}, },
entryFileNames: `scripts/${localesHash}-[hash:8].js`, entryFileNames: 'scripts/[hash:8].js',
chunkFileNames: `scripts/${localesHash}-[hash:8].js`, chunkFileNames: 'scripts/[hash:8].js',
assetFileNames: `assets/${localesHash}-[hash:8][extname]`, assetFileNames: 'assets/[hash:8][extname]',
paths(id) { paths(id) {
for (const p of externalPackages) { for (const p of externalPackages) {
if (p.match.test(id)) { if (p.match.test(id)) {

View File

@ -3226,7 +3226,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "scheduledNotePosted", "scheduledNotePostFailed", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"]; export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "scheduledNotePosted", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"];
// @public (undocumented) // @public (undocumented)
export function nyaize(text: string): string; export function nyaize(text: string): string;

View File

@ -4168,15 +4168,6 @@ export type components = {
/** Format: misskey:id */ /** Format: misskey:id */
userListId: string; userListId: string;
}; };
scheduledNotePostFailed?: {
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
} | {
/** @enum {string} */
type: 'list';
/** Format: misskey:id */
userListId: string;
};
receiveFollowRequest?: { receiveFollowRequest?: {
/** @enum {string} */ /** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
@ -4472,7 +4463,6 @@ 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 */
@ -4589,14 +4579,6 @@ export type components = {
/** @enum {string} */ /** @enum {string} */
type: 'scheduledNotePosted'; type: 'scheduledNotePosted';
note: components['schemas']['Note']; note: components['schemas']['Note'];
} | {
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
/** @enum {string} */
type: 'scheduledNotePostFailed';
noteDraft: components['schemas']['NoteDraft'];
} | { } | {
/** Format: id */ /** Format: id */
id: string; id: string;
@ -11698,15 +11680,6 @@ export interface operations {
/** Format: misskey:id */ /** Format: misskey:id */
userListId: string; userListId: string;
}; };
scheduledNotePostFailed?: {
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
} | {
/** @enum {string} */
type: 'list';
/** Format: misskey:id */
userListId: string;
};
receiveFollowRequest?: { receiveFollowRequest?: {
/** @enum {string} */ /** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
@ -26008,8 +25981,8 @@ export interface operations {
untilDate?: number; untilDate?: number;
/** @default true */ /** @default true */
markAsRead?: boolean; markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
}; };
}; };
}; };
@ -26093,8 +26066,8 @@ export interface operations {
untilDate?: number; untilDate?: number;
/** @default true */ /** @default true */
markAsRead?: boolean; markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'scheduledNotePostFailed' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
}; };
}; };
}; };
@ -27377,15 +27350,6 @@ export interface operations {
/** Format: misskey:id */ /** Format: misskey:id */
userListId: string; userListId: string;
}; };
scheduledNotePostFailed?: {
/** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
} | {
/** @enum {string} */
type: 'list';
/** Format: misskey:id */
userListId: string;
};
receiveFollowRequest?: { receiveFollowRequest?: {
/** @enum {string} */ /** @enum {string} */
type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never';
@ -29215,34 +29179,32 @@ 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 */
isActuallyScheduled: boolean;
}; };
}; };
}; };
@ -29389,7 +29351,6 @@ export interface operations {
untilId?: string; untilId?: string;
sinceDate?: number; sinceDate?: number;
untilDate?: number; untilDate?: number;
scheduled?: boolean | null;
}; };
}; };
}; };
@ -29456,13 +29417,20 @@ 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;
@ -29479,7 +29447,6 @@ export interface operations {
expiredAfter?: number | null; expiredAfter?: number | null;
} | null; } | null;
scheduledAt?: number | null; scheduledAt?: number | null;
isActuallyScheduled?: boolean;
}; };
}; };
}; };

View File

@ -27,7 +27,6 @@ export const notificationTypes = [
'reaction', 'reaction',
'pollEnded', 'pollEnded',
'scheduledNotePosted', 'scheduledNotePosted',
'scheduledNotePostFailed',
'receiveFollowRequest', 'receiveFollowRequest',
'followRequestAccepted', 'followRequestAccepted',
'groupInvited', 'groupInvited',