mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-23 06:24:15 +02:00
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user