From 168be7f13e2d9abd65a708d7e6cb6f45b11e384e Mon Sep 17 00:00:00 2001 From: "julia.kirschenheuter" Date: Tue, 12 Apr 2022 18:15:34 +0200 Subject: [PATCH] Add possibility to send scheduled emails. Add few action buttons for pre-defined time to send. Add a new submenu for scheduled time. Compute a time to send as a timestamp. Correct the styles. Add disabled parameters to Datetimepicker. Dispatch sendMessage only if sendAt is not available. Fix saving attachments. Fix sending workflow for scheduled messages. Correct locale at DatetimePicker. Correct the confirmation dialogue. Signed-off-by: julia.kirschenheuter --- src/components/Composer.vue | 270 ++++++++++++++++++++----- src/components/ComposerAttachments.vue | 2 +- src/components/NewMessageModal.vue | 81 ++++---- 3 files changed, 263 insertions(+), 90 deletions(-) diff --git a/src/components/Composer.vue b/src/components/Composer.vue index 1472b84d90..210a04d9cc 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -165,49 +165,124 @@ {{ t('mail', 'Draft saved') }}

- - {{ - t('mail', 'Upload attachment') - }} - - - {{ - t('mail', 'Add attachment from Files') - }} - - - {{ - addShareLink - }} - - - {{ t('mail', 'Enable formatting') }} - - - {{ t('mail', 'Request a read receipt') }} - - - {{ t('mail', 'Encrypt message with Mailvelope') }} - - - {{ - t('mail', 'Looking for a way to encrypt your emails? Install the Mailvelope browser extension!') - }} - + +
-

{{ t('mail', 'Message sent!') }}

+

{{ sendAtVal ? t('mail', 'Message will be sent at ') + convertToLocalDate(sendAtVal) : t('mail', 'Message sent!') }}

@@ -268,14 +343,17 @@ import debouncePromise from 'debounce-promise' import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import ActionCheckbox from '@nextcloud/vue/dist/Components/ActionCheckbox' +import ActionInput from '@nextcloud/vue/dist/Components/ActionInput' import ActionLink from '@nextcloud/vue/dist/Components/ActionLink' +import ActionRadio from '@nextcloud/vue/dist/Components/ActionRadio' import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' import { showError } from '@nextcloud/dialogs' -import { translate as t } from '@nextcloud/l10n' +import { translate as t, getCanonicalLocale, getFirstDay, getLocale } from '@nextcloud/l10n' import Vue from 'vue' import ComposerAttachments from './ComposerAttachments' +import ChevronLeft from 'vue-material-design-icons/ChevronLeft' import { findRecipient } from '../service/AutocompleteService' import { detect, html, plain, toHtml, toPlain } from '../util/text' import Loading from './Loading' @@ -292,6 +370,8 @@ import NoDraftsMailboxConfiguredError from '../errors/NoDraftsMailboxConfiguredError' import ManyRecipientsError from '../errors/ManyRecipientsError' +import SendClock from 'vue-material-design-icons/SendClock' +import moment from '@nextcloud/moment' const debouncedSearch = debouncePromise(findRecipient, 500) @@ -317,12 +397,16 @@ export default { Actions, ActionButton, ActionCheckbox, + ActionInput, ActionLink, + ActionRadio, ComposerAttachments, + ChevronLeft, Loading, Multiselect, TextEditor, EmptyContent, + SendClock, }, props: { fromAccount: { @@ -376,6 +460,14 @@ export default { required: false, default: () => [], }, + sendAt: { + type: Number, + default: undefined, + }, + attachmentsData: { + type: Array, + default: () => [], + }, }, data() { let bodyVal = toHtml(this.body).value @@ -391,7 +483,7 @@ export default { newRecipients: [], subjectVal: this.subject, bodyVal, - attachments: [], + attachments: this.attachmentsData, noReply: this.to.some((to) => to.email.startsWith('noreply@') || to.email.startsWith('no-reply@')), draftsPromise: Promise.resolve(this.draftId), attachmentsPromise: Promise.resolve(), @@ -418,6 +510,19 @@ export default { loadingIndicatorTo: false, loadingIndicatorCc: false, loadingIndicatorBcc: false, + isMoreActionsOpen: false, + selectedDate: new Date(), + isCustomSendTime: false, + sendAtVal: this.sendAt, + firstDayDatetimePicker: getFirstDay() === 0 ? 7 : getFirstDay(), + formatter: { + stringify: (date) => { + return date ? moment(date).format('LLL') : '' + }, + parse: (value) => { + return value ? moment(value, 'LLL').toDate() : null + }, + }, } }, computed: { @@ -478,12 +583,40 @@ export default { return this.editorMode === 'plaintext' }, submitButtonTitle() { + if (this.sendAtVal) { + return t('mail', 'Send later') + ` ${this.convertToLocalDate(this.sendAtVal)}` + } if (!this.mailvelope.available) { return t('mail', 'Send') } return this.encrypt ? t('mail', 'Encrypt and send') : t('mail', 'Send unencrypted') }, + dateTomorrowMorning() { + const today = new Date() + today.setTime(today.getTime() + 24 * 60 * 60 * 1000) + return today.setHours(9, 0, 0, 0) + + }, + dateTomorrowAfternoon() { + const today = new Date() + today.setTime(today.getTime() + 24 * 60 * 60 * 1000) + return today.setHours(14, 0, 0, 0) + }, + dateMondayMorning() { + const today = new Date() + today.setHours(9, 0, 0, 0) + return today.setDate(today.getDate() + (7 - today.getDay()) % 7 + 1) + }, + customSendTime() { + return new Date(this.selectedDate).getTime() + }, + showAmPm() { + const localeData = moment().locale(getLocale()).localeData() + const timeFormat = localeData.longDateFormat('LT').toLowerCase() + + return timeFormat.indexOf('a') !== -1 + }, }, watch: { '$route.params.threadId'() { @@ -619,6 +752,7 @@ export default { messageId: this.replyTo ? this.replyTo.databaseId : undefined, isHtml: !this.editorPlainText, requestMdn: this.requestMdn, + sendAt: this.sendAtVal ? Math.floor(this.sendAtVal / 1000) : undefined, } }, saveDraft(data) { @@ -633,6 +767,7 @@ export default { && !draftData.cc && !draftData.bcc && !draftData.to + && !draftData.sendAt ) { // this might happen after a call to reset() // where the text input gets reset as well @@ -677,6 +812,19 @@ export default { this.appendSignature = false } }, + onChangeSendLater(value, isCustomSendTime = false) { + this.isCustomSendTime = isCustomSendTime + this.sendAtVal = value ? Number.parseInt(value, 10) : undefined + }, + convertToLocalDate(timestamp) { + const options = { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + } + return new Date(timestamp).toLocaleString(getCanonicalLocale(), options) + }, onAliasChange(alias) { logger.debug('changed alias', { alias }) this.selectedAlias = alias @@ -806,6 +954,7 @@ export default { this.requestMdn = false this.appendSignature = true this.savingDraft = undefined + this.sendAtVal = undefined this.setAlias() this.initBody() @@ -835,6 +984,32 @@ export default { this.state = STATES.DISCARDED this.$emit('close') }, + /** + * Whether the date is acceptable + * + * @param {Date} date The date to compare to + * @returns {boolean} + */ + disabledDatetimepickerDate(date) { + const minimumDate = new Date() + // Make it one sec before midnight so it shows the next full day as available + minimumDate.setHours(0, 0, 0) + minimumDate.setSeconds(minimumDate.getSeconds() - 1) + + return date.getTime() <= minimumDate + }, + + /** + * Whether the time for date is acceptable + * + * @param {Date} date The date to compare to + * @returns {boolean} + */ + disabledDatetimepickerTime(date) { + const now = new Date() + const minimumDate = new Date(now.getTime()) + return date.getTime() <= minimumDate + }, }, } @@ -979,4 +1154,7 @@ export default { margin-top: 250px; height: 120px; } +.send-action-radio { + padding: 5px 0 5px 0; +} diff --git a/src/components/ComposerAttachments.vue b/src/components/ComposerAttachments.vue index bb873ccca8..00fc4b4784 100644 --- a/src/components/ComposerAttachments.vue +++ b/src/components/ComposerAttachments.vue @@ -25,7 +25,7 @@