diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-09-26 15:29:52 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-09-26 15:29:52 +0900 |
| commit | d1446d195abb52c560c7c97177d08103a175acf7 (patch) | |
| tree | 355689adb542333f4c5abb186ab9819c29274612 /packages/frontend/src/components/MkPostForm.vue | |
| parent | fix(frontend): ビルド成果物のファイル名にlocalesのhashを含め... (diff) | |
| download | misskey-d1446d195abb52c560c7c97177d08103a175acf7.tar.gz misskey-d1446d195abb52c560c7c97177d08103a175acf7.tar.bz2 misskey-d1446d195abb52c560c7c97177d08103a175acf7.zip | |
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
Diffstat (limited to 'packages/frontend/src/components/MkPostForm.vue')
| -rw-r--r-- | packages/frontend/src/components/MkPostForm.vue | 121 |
1 files changed, 97 insertions, 24 deletions
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 17f93a4ec8..c1b950a6c8 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -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 { |