Skip to content

Commit

Permalink
Allow possibility to choose a time for scheduled sending
Browse files Browse the repository at this point in the history
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.

Signed-off-by: julia.kirschenheuter <julia.kirschenheuter@nextcloud.com>
  • Loading branch information
JuliaKirschenheuter committed Apr 8, 2022
1 parent def4e55 commit 8709cf3
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 75 deletions.
237 changes: 193 additions & 44 deletions src/components/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,49 +165,119 @@
<span v-else-if="savingDraft === false" id="draft-status">{{ t('mail', 'Draft saved') }}</span>
</p>
<Actions>
<ActionButton icon="icon-upload" @click="onAddLocalAttachment">
{{
t('mail', 'Upload attachment')
}}
</ActionButton>
<ActionButton icon="icon-folder" @click="onAddCloudAttachment">
{{
t('mail', 'Add attachment from Files')
}}
</ActionButton>
<ActionButton :disabled="encrypt" icon="icon-public" @click="onAddCloudAttachmentLink">
{{
addShareLink
}}
</ActionButton>
<ActionCheckbox
:checked="!encrypt && !editorPlainText"
:disabled="encrypt"
@check="editorMode = 'html'"
@uncheck="editorMode = 'plaintext'">
{{ t('mail', 'Enable formatting') }}
</ActionCheckbox>
<ActionCheckbox
:checked="requestMdn"
@check="requestMdn = true"
@uncheck="requestMdn = false">
{{ t('mail', 'Request a read receipt') }}
</ActionCheckbox>
<ActionCheckbox
v-if="mailvelope.available"
:checked="encrypt"
@check="encrypt = true"
@uncheck="encrypt = false">
{{ t('mail', 'Encrypt message with Mailvelope') }}
</ActionCheckbox>
<ActionLink v-else
href="https://www.mailvelope.com/"
target="_blank"
icon="icon-password">
{{
t('mail', 'Looking for a way to encrypt your emails? Install the Mailvelope browser extension!')
}}
</ActionLink>
<template v-if="!isMoreActionsOpen">
<ActionButton icon="icon-upload" @click="onAddLocalAttachment">
{{
t('mail', 'Upload attachment')
}}
</ActionButton>
<ActionButton icon="icon-folder" @click="onAddCloudAttachment">
{{
t('mail', 'Add attachment from Files')
}}
</ActionButton>
<ActionButton :disabled="encrypt" icon="icon-public" @click="onAddCloudAttachmentLink">
{{
addShareLink
}}
</ActionButton>
<ActionButton :close-after-click="false" @click="isMoreActionsOpen=true">
<template #icon>
<SendClock :size="20" :title="t('mail', 'Send later')" />
</template>
{{
t('mail', 'Send later')
}}
</ActionButton>
<ActionCheckbox
:checked="!encrypt && !editorPlainText"
:disabled="encrypt"
@check="editorMode = 'html'"
@uncheck="editorMode = 'plaintext'">
{{ t('mail', 'Enable formatting') }}
</ActionCheckbox>
<ActionCheckbox
:checked="requestMdn"
@check="requestMdn = true"
@uncheck="requestMdn = false">
{{ t('mail', 'Request a read receipt') }}
</ActionCheckbox>
<ActionCheckbox
v-if="mailvelope.available"
:checked="encrypt"
@check="encrypt = true"
@uncheck="encrypt = false">
{{ t('mail', 'Encrypt message with Mailvelope') }}
</ActionCheckbox>
<ActionLink v-else
href="https://www.mailvelope.com/"
target="_blank"
icon="icon-password">
{{
t('mail', 'Looking for a way to encrypt your emails? Install the Mailvelope browser extension!')
}}
</ActionLink>
</template>
<template v-if="isMoreActionsOpen">
<ActionButton :close-after-click="false"
@click="isMoreActionsOpen=false">
<template #icon>
<ChevronLeft
:title="t('mail', 'Send later')"
:size="20" />
{{ t('mail', 'Send later') }}
</template>
</ActionButton>
<ActionRadio :value="undefined"
name="sendLater"
:checked="!sendAt"
class="send-action-radio"
@update:checked="sendAt = undefined"
@change="onChangeSendLater(undefined)">
{{ t('mail', 'Send now') }}
</ActionRadio>
<ActionRadio :value="dateTomorrowMorning"
name="sendLater"
:checked="dateTomorrowMorning === sendAt"
class="send-action-radio send-action-radio--multiline"
@update:checked="sendAt = dateTomorrowMorning"
@change="onChangeSendLater(dateTomorrowMorning)">
{{ t('mail', 'Tomorrow morning') }} - {{ convertToLocalDate(dateTomorrowMorning) }}
</ActionRadio>
<ActionRadio :value="dateTomorrowAfternoon"
name="sendLater"
:checked="dateTomorrowAfternoon === sendAt"
class="send-action-radio send-action-radio--multiline"
@update:checked="sendAt = dateTomorrowAfternoon"
@change="onChangeSendLater(dateTomorrowAfternoon)">
{{ t('mail', 'Tomorrow afternoon') }} - {{ convertToLocalDate(dateTomorrowAfternoon) }}
</ActionRadio>
<ActionRadio :value="dateMondayMorning"
name="sendLater"
:checked="dateMondayMorning === sendAt"
class="send-action-radio send-action-radio--multiline"
@update:checked="sendAt = dateMondayMorning"
@change="onChangeSendLater(dateMondayMorning)">
{{ t('mail', 'Monday morning') }} - {{ convertToLocalDate(dateMondayMorning) }}
</ActionRadio>
<ActionRadio name="sendLater"
class="send-action-radio"
:checked="customSendTime === sendAt || isCustomSendTime"
:value="customSendTime"
@update:checked="sendAt = customSendTime"
@change="onChangeSendLater(customSendTime)">
{{ t('mail', 'Custom') }}
</ActionRadio>
<ActionInput v-model="selectedDate"
type="datetime-local"
:minute-step="5"
:second-step="30"
:disabled-date="disabledDatetimepickerDate"
:disabled-time="disabledDatetimepickerTime"
@change="onChangeSendLater(customSendTime, true)">
{{ t('mail', 'Enter a date') }}
</ActionInput>
</template>
</Actions>
<div>
<input
Expand Down Expand Up @@ -268,14 +338,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 } 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'
Expand All @@ -292,6 +365,7 @@ import NoDraftsMailboxConfiguredError
from '../errors/NoDraftsMailboxConfiguredError'
import ManyRecipientsError
from '../errors/ManyRecipientsError'
import SendClock from 'vue-material-design-icons/SendClock'
const debouncedSearch = debouncePromise(findRecipient, 500)
Expand All @@ -317,12 +391,16 @@ export default {
Actions,
ActionButton,
ActionCheckbox,
ActionInput,
ActionLink,
ActionRadio,
ComposerAttachments,
ChevronLeft,
Loading,
Multiselect,
TextEditor,
EmptyContent,
SendClock,
},
props: {
fromAccount: {
Expand Down Expand Up @@ -418,6 +496,10 @@ export default {
loadingIndicatorTo: false,
loadingIndicatorCc: false,
loadingIndicatorBcc: false,
isMoreActionsOpen: false,
selectedDate: new Date(),
isCustomSendTime: false,
sendAt: undefined,
}
},
computed: {
Expand Down Expand Up @@ -478,12 +560,34 @@ export default {
return this.editorMode === 'plaintext'
},
submitButtonTitle() {
if (this.sendAt) {
return t('mail', 'Send later')
}
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()
},
},
watch: {
'$route.params.threadId'() {
Expand Down Expand Up @@ -619,6 +723,7 @@ export default {
messageId: this.replyTo ? this.replyTo.databaseId : undefined,
isHtml: !this.editorPlainText,
requestMdn: this.requestMdn,
sendAt: this.sendAt,
}
},
saveDraft(data) {
Expand Down Expand Up @@ -677,6 +782,20 @@ export default {
this.appendSignature = false
}
},
onChangeSendLater(value, isCustomSendTime = false) {
this.isCustomSendTime = isCustomSendTime
this.sendAt = value ? Number.parseInt(value, 10) : undefined
},
convertToLocalDate(timestamp) {
const options = {
year: 'numeric',
month: 'numeric',
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
Expand Down Expand Up @@ -835,6 +954,33 @@ 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 now = new Date()
const minimumDate = new Date(now.getTime())
// 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
},
},
}
</script>
Expand Down Expand Up @@ -979,4 +1125,7 @@ export default {
margin-top: 250px;
height: 120px;
}
.send-action-radio {
padding: 5px 0 5px 0;
}
</style>
56 changes: 25 additions & 31 deletions src/components/NewMessageModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,47 +97,41 @@ export default {
},
async sendMessage(data) {
logger.debug('sending message', { data })
const now = new Date().getTime()
const dataForServer = {
accountId: data.account,
subject: data.subject,
body: data.isHtml ? data.body.value : toPlain(data.body).value,
isHtml: data.isHtml,
to: data.to,
cc: data.cc,
bcc: data.bcc,
attachments: data.attachments,
aliasId: null,
inReplyToMessageId: null,
sendAt: data.sendAt ?? Math.floor(now / 1000),
}
if (dataForServer.sendAt < now) {
dataForServer.sendAt = now
}
if (this.composerMessage.type === 'outbox') {
const now = new Date().getTime()
const dataForServer = {
accountId: data.account,
subject: data.subject,
body: data.isHtml ? data.body.value : toPlain(data.body).value,
isHtml: data.isHtml,
to: data.to,
cc: data.cc,
bcc: data.bcc,
attachments: data.attachments,
aliasId: data.aliasId,
inReplyToMessageId: null,
sendAt: Math.floor(now / 1000), // JS timestamp is in milliseconds
}
const message = await this.$store.dispatch('outbox/updateMessage', {
// TODO: update the message instead of enqueing another time
const message = await this.$store.dispatch('outbox/enqueueMessage', {
message: dataForServer,
id: this.composerData.id,
})
if (!data.sendAt) {
await this.$store.dispatch('outbox/sendMessage', { id: message.id })
}
await this.$store.dispatch('outbox/sendMessage', { id: message.id })
} else {
const now = new Date().getTime()
const dataForServer = {
accountId: data.account,
subject: data.subject,
body: data.isHtml ? data.body.value : toPlain(data.body).value,
isHtml: data.isHtml,
to: data.to,
cc: data.cc,
bcc: data.bcc,
attachments: data.attachments,
aliasId: data.aliasId,
inReplyToMessageId: null,
sendAt: Math.floor(now / 1000), // JS timestamp is in milliseconds
}
const message = await this.$store.dispatch('outbox/enqueueMessage', {
message: dataForServer,
})
await this.$store.dispatch('outbox/sendMessage', { id: message.id })
if (!data.sendAt) {
await this.$store.dispatch('outbox/sendMessage', { id: message.id })
}
if (data.draftId) {
// Remove old draft envelope
Expand Down

0 comments on commit 8709cf3

Please sign in to comment.