Skip to content

Commit

Permalink
Merge pull request #8469 from nextcloud/enh/rework-draft
Browse files Browse the repository at this point in the history
feat: Rework draft handling front-end
  • Loading branch information
ChristophWurst authored Jun 29, 2023
2 parents fdc229d + f1f757e commit f7d0678
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 52 deletions.
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud
<job>OCA\Mail\BackgroundJob\CleanupJob</job>
<job>OCA\Mail\BackgroundJob\OutboxWorkerJob</job>
<job>OCA\Mail\BackgroundJob\IMipMessageJob</job>
<job>OCA\Mail\BackgroundJob\DraftsJob</job>
</background-jobs>
<repair-steps>
<post-migration>
Expand Down
7 changes: 7 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,18 @@
'url' => '/api/list/unsubscribe/{id}',
'verb' => 'POST',
],
[
'name' => 'drafts#move',
'url' => '/api/drafts/move/{id}',
'verb' => 'POST',
],

],
'resources' => [
'accounts' => ['url' => '/api/accounts'],
'aliases' => ['url' => '/api/accounts/{accountId}/aliases'],
'autoComplete' => ['url' => '/api/autoComplete'],
'drafts' => ['url' => '/api/drafts'],
'localAttachments' => ['url' => '/api/attachments'],
'mailboxes' => ['url' => '/api/mailboxes'],
'messages' => ['url' => '/api/messages'],
Expand Down
36 changes: 35 additions & 1 deletion lib/Controller/DraftsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@
namespace OCA\Mail\Controller;

use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Http\JsonResponse;
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\DraftsService;
use OCA\Mail\Service\SmimeService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IRequest;
Expand All @@ -41,18 +44,22 @@ class DraftsController extends Controller {
private string $userId;
private AccountService $accountService;
private ITimeFactory $timeFactory;
private SmimeService $smimeService;


public function __construct(string $appName,
$UserId,
IRequest $request,
DraftsService $service,
AccountService $accountService,
ITimeFactory $timeFactory) {
ITimeFactory $timeFactory,
SmimeService $smimeService) {
parent::__construct($appName, $request);
$this->userId = $UserId;
$this->service = $service;
$this->accountService = $accountService;
$this->timeFactory = $timeFactory;
$this->smimeService = $smimeService;
}

/**
Expand All @@ -63,15 +70,21 @@ public function __construct(string $appName,
* @param string $body
* @param string $editorBody
* @param bool $isHtml
* @param bool $smimeSign
* @param bool $smimeEncrypt
* @param array<int, string[]> $to i. e. [['label' => 'Linus', 'email' => '[email protected]'], ['label' => 'Pierre', 'email' => '[email protected]']]
* @param array<int, string[]> $cc
* @param array<int, string[]> $bcc
* @param array $attachments
* @param int|null $aliasId
* @param string|null $inReplyToMessageId
* @param int|null $smimeCertificateId
* @param int|null $sendAt
* @param int|null $draftId
*
* @return JsonResponse
* @throws DoesNotExistException
* @throws ClientException
*/
#[TrapError]
public function create(
Expand All @@ -80,12 +93,15 @@ public function create(
string $body,
string $editorBody,
bool $isHtml,
?bool $smimeSign,
?bool $smimeEncrypt,
array $to = [],
array $cc = [],
array $bcc = [],
array $attachments = [],
?int $aliasId = null,
?string $inReplyToMessageId = null,
?int $smimeCertificateId = null,
?int $sendAt = null,
?int $draftId = null) : JsonResponse {
$account = $this->accountService->find($this->userId, $accountId);
Expand All @@ -103,9 +119,17 @@ public function create(
$message->setInReplyToMessageId($inReplyToMessageId);
$message->setUpdatedAt($this->timeFactory->getTime());
$message->setSendAt($sendAt);
$message->setSmimeSign($smimeSign);
$message->setSmimeEncrypt($smimeEncrypt);
if ($sendAt !== null) {
$message->setType(LocalMessage::TYPE_OUTGOING);
}

if (!empty($smimeCertificateId)) {
$smimeCertificate = $this->smimeService->findCertificate($smimeCertificateId, $this->userId);
$message->setSmimeCertificateId($smimeCertificate->getId());
}

$this->service->saveMessage($account, $message, $to, $cc, $bcc, $attachments);

return JsonResponse::success($message, Http::STATUS_CREATED);
Expand Down Expand Up @@ -137,13 +161,16 @@ public function update(int $id,
string $body,
string $editorBody,
bool $isHtml,
?bool $smimeSign,
?bool $smimeEncrypt,
bool $failed = false,
array $to = [],
array $cc = [],
array $bcc = [],
array $attachments = [],
?int $aliasId = null,
?string $inReplyToMessageId = null,
?int $smimeCertificateId = null,
?int $sendAt = null): JsonResponse {
$message = $this->service->getMessage($id, $this->userId);
$account = $this->accountService->find($this->userId, $accountId);
Expand All @@ -161,6 +188,13 @@ public function update(int $id,
$message->setInReplyToMessageId($inReplyToMessageId);
$message->setSendAt($sendAt);
$message->setUpdatedAt($this->timeFactory->getTime());
$message->setSmimeSign($smimeSign);
$message->setSmimeEncrypt($smimeEncrypt);

if (!empty($smimeCertificateId)) {
$smimeCertificate = $this->smimeService->findCertificate($smimeCertificateId, $this->userId);
$message->setSmimeCertificateId($smimeCertificate->getId());
}

$message = $this->service->updateMessage($account, $message, $to, $cc, $bcc, $attachments);
return JsonResponse::success($message, Http::STATUS_ACCEPTED);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Composer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ export default {
bodyVal: this.editorBody,
attachments: this.attachmentsData,
noReply: this.to.some((to) => to.email?.startsWith('noreply@') || to.email?.startsWith('no-reply@')),
saveDraftDebounced: debounce(10 * 1000, this.saveDraft),
saveDraftDebounced: debounce(5 * 1000, this.saveDraft),
selectTo: this.to,
selectCc: this.cc,
selectBcc: this.bcc,
Expand Down
78 changes: 39 additions & 39 deletions src/components/NewMessageModal.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<template>
<Modal
v-if="showMessageComposer"
Expand Down Expand Up @@ -107,7 +108,6 @@ import { translate as t } from '@nextcloud/l10n'
import logger from '../logger'
import { toPlain, toHtml, plain } from '../util/text'
import { saveDraft } from '../service/MessageService'
import Composer from './Composer'
import { UNDO_DELAY } from '../store/constants'
import { matchError } from '../errors/match'
Expand All @@ -116,6 +116,7 @@ import ManyRecipientsError from '../errors/ManyRecipientsError'
import Loading from './Loading'
import { mapGetters } from 'vuex'
import MinimizeIcon from 'vue-material-design-icons/Minus.vue'
import { deleteDraft, saveDraft, updateDraft } from '../service/DraftService'
export default {
name: 'NewMessageModal',
Expand Down Expand Up @@ -144,6 +145,7 @@ export default {
warning: undefined,
modalFirstOpen: true,
cookedComposerData: undefined,
changed: false,
}
},
computed: {
Expand Down Expand Up @@ -174,9 +176,9 @@ export default {
},
},
created() {
const draftId = this.composerData?.draftId
if (draftId) {
this.draftsPromise = Promise.resolve(draftId)
const id = this.composerData?.id
if (id) {
this.draftsPromise = Promise.resolve(id)
}
},
async mounted() {
Expand All @@ -195,43 +197,33 @@ export default {
* @param {boolean=} opts.showToast Show a toast after saving
* @return {Promise<number>} Draft id promise
*/
// TODO: when there's no draft is saved, Cloing wont move ie case 2 doesn't work
onDraft(data, { showToast = false } = {}) {
if (!this.composerMessage) {
logger.info('Ignoring draft because there is no message anymore', { data })
return this.draftsPromise
}
this.changed = true
this.draftsPromise = this.draftsPromise.then(async (id) => {
this.savingDraft = true
this.draftSaved = false
data.draftId = id
try {
let idToReturn
if (this.composerMessage.type === 'outbox') {
const dataForServer = this.getDataForServer(data)
delete dataForServer.sendAt
await this.$store.dispatch('outbox/updateMessage', {
message: dataForServer,
id: this.composerData.id,
})
const dataForServer = this.getDataForServer(data, true)
if (!id) {
const { id } = await saveDraft(data.account, dataForServer)
dataForServer.id = id
await this.$store.dispatch('patchComposerData', { id, draftId: dataForServer.draftId })
this.canSaveDraft = true
this.draftSaved = true
idToReturn = id
} else {
const dataForServer = this.getDataForServer(data, true)
const { id } = await saveDraft(data.account, dataForServer)
dataForServer.id = id
await updateDraft(dataForServer)
this.canSaveDraft = true
this.draftSaved = true
// Remove old draft envelope
this.$store.commit('removeEnvelope', { id: data.draftId })
this.$store.commit('removeMessage', { id: data.draftId })
// Fetch new draft envelope
await this.$store.dispatch('fetchEnvelope', {
accountId: data.account,
id,
})
idToReturn = id
}
Expand Down Expand Up @@ -267,20 +259,21 @@ export default {
return this.draftsPromise
},
getDataForServer(data, serializeRecipients = false) {
getDataForServer(data) {
return {
...data,
accountId: data.account,
id: data.id,
accountId: data.accountId,
body: data.isHtml ? data.body.value : toPlain(data.body).value,
editorBody: data.body.value,
to: serializeRecipients ? data.to.map(this.recipientToRfc822).join(', ') : data.to,
cc: serializeRecipients ? data.cc.map(this.recipientToRfc822).join(', ') : data.cc,
bcc: serializeRecipients ? data.bcc.map(this.recipientToRfc822).join(', ') : data.bcc,
to: data.to,
cc: data.cc,
bcc: data.bcc,
attachments: data.attachments,
aliasId: data.aliasId,
inReplyToMessageId: data.inReplyToMessageId,
sendAt: data.sendAt,
draftId: data.draftId,
draftId: this.composerData?.draftId,
}
},
onAttachmentUploading(done, data) {
Expand All @@ -306,18 +299,21 @@ export default {
}
const dataForServer = this.getDataForServer({
...data,
draftId: await this.draftsPromise,
id: await this.draftsPromise,
sendAt: data.sendAt ? data.sendAt : Math.floor((now + UNDO_DELAY) / 1000),
})
if (dataForServer.sendAt < Math.floor((now + UNDO_DELAY) / 1000)) {
dataForServer.sendAt = Math.floor((now + UNDO_DELAY) / 1000)
}
if (!this.composerData.id) {
const { id } = await saveDraft(data.account, dataForServer)
dataForServer.id = id
await this.$store.dispatch('outbox/enqueueMessage', {
message: dataForServer,
})
} else {
dataForServer.id = this.composerData.id
await this.$store.dispatch('outbox/updateMessage', {
message: dataForServer,
id: this.composerData.id,
Expand All @@ -326,12 +322,11 @@ export default {
if (!data.sendAt || data.sendAt < Math.floor((now + UNDO_DELAY) / 1000)) {
// Awaiting here would keep the modal open for a long time and thus block the user
this.$store.dispatch('outbox/sendMessageWithUndo', { id: this.composerData.id })
this.$store.dispatch('outbox/sendMessageWithUndo', { id: dataForServer.id })
}
if (data.draftId) {
if (dataForServer.id) {
// Remove old draft envelope
this.$store.commit('removeEnvelope', { id: data.draftId })
this.$store.commit('removeMessage', { id: data.draftId })
this.$store.commit('removeMessage', { id: dataForServer.id })
}
await this.$store.dispatch('stopComposerSession')
this.$emit('close')
Expand Down Expand Up @@ -396,7 +391,7 @@ export default {
if (isOutbox) {
await this.$store.dispatch('outbox/deleteMessage', { id })
} else {
await this.$store.dispatch('deleteMessage', { id })
deleteDraft(id)
}
showSuccess(t('mail', 'Message discarded'))
} catch (error) {
Expand Down Expand Up @@ -424,27 +419,32 @@ export default {
this.cookedComposerData = this.$refs.composer.getMessageData()
},
async patchComposerData(data) {
this.changed = true
this.updateCookedComposerData()
await this.$store.dispatch('patchComposerData', data)
},
async onMinimize() {
this.modalFirstOpen = false
await this.$store.dispatch('closeMessageComposer')
if (!this.$store.getters.composerMessageIsSaved) {
if (!this.$store.getters.composerMessageIsSaved && this.changed) {
await this.onDraft(this.cookedComposerData, { showToast: true })
}
},
async onClose() {
this.$store.commit('setComposerIndicatorDisabled', true)
await this.onMinimize()
// End the session only if all unsaved changes have been saved
if (this.canSaveDraft) {
if (this.canSaveDraft && ((this.changed && this.draftSaved) || !this.changed)) {
logger.debug('Closing composer session due to close button click')
await this.$store.dispatch('stopComposerSession', {
restoreOriginalSendAt: true,
moveToImap: this.changed,
id: this.composerData.id,
})
}
},
},
Expand Down
Loading

0 comments on commit f7d0678

Please sign in to comment.