feat: scheduled post (#16577)

* Update NoteDraft.ts

* Update NoteDraft.ts

* wip

* Update CHANGELOG.md

* wip

* Update PostScheduledNoteProcessorService.ts

* Update PostScheduledNoteProcessorService.ts

* Update Notification.ts

* wip

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* wip

* Create 1758677617888-scheduled-post.js

* Update index.d.ts

* Update stats.ts

* wip

* wip

* wip

* wip

* wip

* Update MkNotification.vue

* wip

* wip

* wip

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* wip

* wip

* Update NoteDraftEntityService.ts

* wip

* Update index.d.ts

* Update MkPostForm.vue

* wip

* wip

* wip

* Update NoteCreateService.ts

* wip

* wip

* wip

* Update NoteDraftEntityService.ts

* Update NoteCreateService.ts

* Update NoteDraftService.ts

* wip

* Update NoteDraftService.ts

* wip

* wip

* Update MkPostForm.vue

* wip

* Update MkPostForm.vue

* Update os.ts

* wip

* Update MkNoteDraftsDialog.vue
This commit is contained in:
syuilo
2025-09-26 15:29:52 +09:00
committed by GitHub
parent 218070eb13
commit d1446d195a
39 changed files with 1128 additions and 486 deletions

View File

@@ -15,101 +15,151 @@ SPDX-License-Identifier: AGPL-3.0-only
@esc="cancel()"
>
<template #header>
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
{{ i18n.ts.draftsAndScheduledNotes }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
</template>
<div class="_spacer">
<MkPagination :paginator="paginator" withControl>
<template #empty>
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</template>
<template #default="{ items }">
<div class="_gaps_s">
<div
v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
:key="draft.id"
v-panel
:class="[$style.draft]"
>
<div :class="$style.draftBody" class="_gaps_s">
<div :class="$style.draftInfo">
<div :class="$style.draftMeta">
<div v-if="draft.reply" class="_nowrap">
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
<template #user>
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
<MkAcct v-else :user="draft.reply.user"/>
</template>
</I18n>
</div>
<div v-else-if="draft.replyId" class="_nowrap">
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
<template #user>
{{ i18n.ts.deletedNote }}
</template>
</I18n>
</div>
<div v-if="draft.renote && draft.text != null" class="_nowrap">
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
<template #user>
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
<MkAcct v-else :user="draft.renote.user"/>
</template>
</I18n>
</div>
<div v-else-if="draft.renoteId" class="_nowrap">
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
<template #user>
{{ i18n.ts.deletedNote }}
</template>
</I18n>
</div>
<div v-if="draft.channel" class="_nowrap">
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
<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">
<MkPagination :key="tab" :paginator="tab === 'scheduled' ? scheduledPaginator : draftsPaginator" withControl>
<template #empty>
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</template>
<template #default="{ items }">
<div class="_gaps_s">
<div
v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
:key="draft.id"
v-panel
:class="[$style.draft]"
>
<div :class="$style.draftBody" class="_gaps_s">
<MkInfo v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
<I18n :src="i18n.ts.scheduledToPostOnX" tag="span">
<template #x>
<MkTime :time="draft.scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
</template>
</I18n>
</MkInfo>
<div :class="$style.draftInfo">
<div :class="$style.draftMeta">
<div v-if="draft.reply" class="_nowrap">
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
<template #user>
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
<MkAcct v-else :user="draft.reply.user"/>
</template>
</I18n>
</div>
<div v-else-if="draft.replyId" class="_nowrap">
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
<template #user>
{{ i18n.ts.deletedNote }}
</template>
</I18n>
</div>
<div v-if="draft.renote && draft.text != null" class="_nowrap">
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
<template #user>
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
<MkAcct v-else :user="draft.renote.user"/>
</template>
</I18n>
</div>
<div v-else-if="draft.renoteId" class="_nowrap">
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
<template #user>
{{ i18n.ts.deletedNote }}
</template>
</I18n>
</div>
<div v-if="draft.channel" class="_nowrap">
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
</div>
</div>
</div>
</div>
<div :class="$style.draftContent">
<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
</div>
<div :class="$style.draftFooter">
<div :class="$style.draftVisibility">
<span :title="i18n.ts._visibility[draft.visibility]">
<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
</span>
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
<div :class="$style.draftContent">
<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
</div>
<div :class="$style.draftFooter">
<div :class="$style.draftVisibility">
<span :title="i18n.ts._visibility[draft.visibility]">
<i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
</span>
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
</div>
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
</div>
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
</div>
</div>
<div :class="$style.draftActions" class="_buttons">
<MkButton
:class="$style.itemButton"
small
@click="restoreDraft(draft)"
>
<i class="ti ti-corner-up-left"></i>
{{ i18n.ts._drafts.restore }}
</MkButton>
<MkButton
v-tooltip="i18n.ts._drafts.delete"
danger
small
:iconOnly="true"
:class="$style.itemButton"
@click="deleteDraft(draft)"
>
<i class="ti ti-trash"></i>
</MkButton>
<div :class="$style.draftActions" class="_buttons">
<template v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
<MkButton
:class="$style.itemButton"
small
@click="cancelSchedule(draft)"
>
<i class="ti ti-calendar-x"></i> {{ i18n.ts._drafts.cancelSchedule }}
</MkButton>
<!-- TODO
<MkButton
:class="$style.itemButton"
small
@click="reSchedule(draft)"
>
<i class="ti ti-calendar-time"></i> {{ i18n.ts._drafts.reSchedule }}
</MkButton>
-->
</template>
<MkButton
v-else
:class="$style.itemButton"
small
@click="restoreDraft(draft)"
>
<i class="ti ti-corner-up-left"></i> {{ i18n.ts._drafts.restore }}
</MkButton>
<MkButton
v-tooltip="i18n.ts._drafts.delete"
danger
small
:iconOnly="true"
:class="$style.itemButton"
style="margin-left: auto;"
@click="deleteDraft(draft)"
>
<i class="ti ti-trash"></i>
</MkButton>
</div>
</div>
</div>
</div>
</template>
</MkPagination>
</div>
</template>
</MkPagination>
</div>
</MkStickyContainer>
</MkModalWindow>
</template>
@@ -125,6 +175,12 @@ import * as os from '@/os.js';
import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api';
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<{
(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
@@ -132,8 +188,20 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const paginator = markRaw(new Paginator('notes/drafts/list', {
const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts');
const draftsPaginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
params: {
scheduled: false,
},
}));
const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
params: {
scheduled: true,
},
}));
const currentDraftsCount = ref(0);
@@ -162,7 +230,17 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
if (canceled) return;
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
paginator.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>
@@ -220,4 +298,11 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
padding-top: 16px;
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>

View File

@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<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-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'].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'" :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>
@@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_mention]: notification.type === 'mention',
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted',
[$style.t_scheduledNotePostFailed]: notification.type === 'scheduledNotePostFailed',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login',
@@ -39,6 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></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 === '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 === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
@@ -60,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tail">
<header :class="$style.header">
<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 === '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 === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
@@ -103,6 +109,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
{{ notification.role.name }}
</div>
@@ -338,6 +349,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}
.t_scheduledNotePosted {
background: var(--eventOther);
pointer-events: none;
}
.t_scheduledNotePostFailed {
background: var(--eventOther);
pointer-events: none;
}
.t_achievementEarned {
background: var(--eventAchievement);
pointer-events: none;

View File

@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.headerLeft">
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
</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>
<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>
</div>
<div :class="$style.headerRight">
<template v-if="!(targetChannel != null && fixed)">
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
<template v-else>{{ submitText }}</template>
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : replyTargetNote ? 'ti ti-arrow-back-up' : renoteTargetNote ? 'ti ti-quote' : 'ti ti-send'"></i>
<i style="margin-left: 6px;" :class="submitIcon"></i>
</div>
</button>
</div>
@@ -61,6 +61,13 @@ 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>
</div>
</div>
<MkInfo v-if="scheduledAt != null" :class="$style.scheduledAt">
<I18n :src="i18n.ts.scheduleToPostOnX" tag="span">
<template #x>
<MkTime :time="scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
</template>
</I18n> - <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>
<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">
@@ -199,6 +206,7 @@ if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
}
const reactionAcceptance = ref(store.s.reactionAcceptance);
const scheduledAt = ref<number | null>(null);
const draghover = ref(false);
const quoteId = ref<string | null>(null);
const hasNotSpecifiedMentions = ref(false);
@@ -262,11 +270,17 @@ const placeholder = computed((): string => {
});
const submitText = computed((): string => {
return renoteTargetNote.value
? i18n.ts.quote
: replyTargetNote.value
? i18n.ts.reply
: i18n.ts.note;
return scheduledAt.value != null
? i18n.ts.schedule
: renoteTargetNote.value
? i18n.ts.quote
: replyTargetNote.value
? i18n.ts.reply
: 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 => {
@@ -414,6 +428,7 @@ function watchForDraft() {
watch(localOnly, () => saveDraft());
watch(quoteId, () => saveDraft());
watch(reactionAcceptance, () => saveDraft());
watch(scheduledAt, () => saveDraft());
}
function checkMissingMention() {
@@ -605,7 +620,13 @@ function showOtherSettings() {
action: () => {
toggleReactionAcceptance();
},
}, { type: 'divider' }, {
}, ...($i.policies.scheduledNoteLimit > 0 ? [{
icon: 'ti ti-calendar-time',
text: i18n.ts.schedulePost + '...',
action: () => {
schedule();
},
}] : []), { type: 'divider' }, {
type: 'switch',
icon: 'ti ti-eye',
text: i18n.ts.preview,
@@ -654,6 +675,7 @@ function clear() {
files.value = [];
poll.value = null;
quoteId.value = null;
scheduledAt.value = null;
}
function onKeydown(ev: KeyboardEvent) {
@@ -809,6 +831,7 @@ function saveDraft() {
...( visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
quoteId: quoteId.value,
reactionAcceptance: reactionAcceptance.value,
scheduledAt: scheduledAt.value,
},
};
@@ -823,7 +846,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,
@@ -831,19 +856,15 @@ async function saveServerDraft(clearLocal = false) {
visibility: visibility.value,
localOnly: localOnly.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,
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined,
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
channelId: targetChannel.value ? targetChannel.value.id : undefined,
visibleUserIds: visibleUsers.value.map(x => x.id),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : null,
replyId: replyTargetNote.value ? replyTargetNote.value.id : null,
channelId: targetChannel.value ? targetChannel.value.id : null,
reactionAcceptance: reactionAcceptance.value,
}).then(() => {
if (clearLocal) {
clear();
deleteDraft();
}
}).catch((err) => {
scheduledAt: scheduledAt.value,
isActuallyScheduled: options.isActuallyScheduled ?? false,
});
}
@@ -878,6 +899,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' && (
@@ -1049,6 +1085,14 @@ async function post(ev?: MouseEvent) {
});
}
async function postAsScheduled() {
if (props.mock) return;
await saveServerDraft({
isActuallyScheduled: true,
});
}
function cancel() {
emit('cancel');
}
@@ -1143,8 +1187,10 @@ function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent)
}
function showDraftMenu(ev: MouseEvent) {
function showDraftsDialog() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {}, {
function showDraftsDialog(scheduled: boolean) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {
scheduled,
}, {
restore: async (draft: Misskey.entities.NoteDraft) => {
text.value = draft.text ?? '';
useCw.value = draft.cw != null;
@@ -1175,6 +1221,7 @@ function showDraftMenu(ev: MouseEvent) {
renoteTargetNote.value = draft.renote;
replyTargetNote.value = draft.reply;
reactionAcceptance.value = draft.reactionAcceptance;
scheduledAt.value = draft.scheduledAt ?? null;
if (draft.channel) targetChannel.value = draft.channel as unknown as Misskey.entities.Channel;
visibleUsers.value = [];
@@ -1215,11 +1262,32 @@ function showDraftMenu(ev: MouseEvent) {
text: i18n.ts._drafts.listDrafts,
icon: 'ti ti-cloud-download',
action: () => {
showDraftsDialog();
showDraftsDialog(false);
},
}, { type: 'divider' }, {
type: 'button',
text: i18n.ts._drafts.listScheduledNotes,
icon: 'ti ti-clock-down',
action: () => {
showDraftsDialog(true);
},
}], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
async function schedule() {
const { canceled, result } = await os.inputDatetime({
title: i18n.ts.schedulePost,
});
if (canceled) return;
if (result.getTime() <= Date.now()) return;
scheduledAt.value = result.getTime();
}
function cancelSchedule() {
scheduledAt.value = null;
}
onMounted(() => {
if (props.autofocus) {
focus();
@@ -1255,6 +1323,7 @@ onMounted(() => {
}
quoteId.value = draft.data.quoteId;
reactionAcceptance.value = draft.data.reactionAcceptance;
scheduledAt.value = draft.data.scheduledAt ?? null;
}
}
@@ -1519,6 +1588,10 @@ html[data-color-scheme=light] .preview {
margin: 0 20px 16px 20px;
}
.scheduledAt {
margin: 0 20px 16px 20px;
}
.cw,
.hashtags,
.text {