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', 'Upload attachment')
+ }}
+
+
+ {{
+ t('mail', 'Add attachment from Files')
+ }}
+
+
+ {{
+ addShareLink
+ }}
+
+
+
+
+
+ {{
+ t('mail', 'Send later')
+ }}
+
+
+ {{ 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', 'Send later') }}
+
+
+
+ {{ t('mail', 'Send now') }}
+
+
+ {{ t('mail', 'Tomorrow morning') }} - {{ convertToLocalDate(dateTomorrowMorning) }}
+
+
+ {{ t('mail', 'Tomorrow afternoon') }} - {{ convertToLocalDate(dateTomorrowAfternoon) }}
+
+
+ {{ t('mail', 'Monday morning') }} - {{ convertToLocalDate(dateMondayMorning) }}
+
+
+ {{ t('mail', 'Custom date and time') }}
+
+
+ {{ t('mail', 'Enter a date') }}
+
+
- {{ 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 @@