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 @@