diff options
-rw-r--r-- | src/components/Composer.vue | 6 | ||||
-rw-r--r-- | src/components/Envelope.vue | 28 | ||||
-rw-r--r-- | src/components/Mailbox.vue | 10 | ||||
-rw-r--r-- | src/components/MailboxThread.vue | 12 | ||||
-rw-r--r-- | src/components/MenuEnvelope.vue | 75 | ||||
-rw-r--r-- | src/components/NewMessageDetail.vue | 285 | ||||
-rw-r--r-- | src/components/NewMessageModal.vue | 12 | ||||
-rw-r--r-- | src/components/ThreadEnvelope.vue | 42 | ||||
-rw-r--r-- | src/store/actions.js | 73 | ||||
-rw-r--r-- | src/views/Home.vue | 14 |
10 files changed, 141 insertions, 416 deletions
diff --git a/src/components/Composer.vue b/src/components/Composer.vue index e4d7fd93a..1472b84d9 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -349,6 +349,10 @@ export default { type: Object, default: () => html(''), }, + draftId: { + type: Number, + default: undefined, + }, draft: { type: Function, required: true, @@ -389,7 +393,7 @@ export default { bodyVal, attachments: [], noReply: this.to.some((to) => to.email.startsWith('noreply@') || to.email.startsWith('no-reply@')), - draftsPromise: Promise.resolve(), + draftsPromise: Promise.resolve(this.draftId), attachmentsPromise: Promise.resolve(), canSaveDraft: true, savingDraft: undefined, diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index 95fdff205..bd661da57 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -12,7 +12,8 @@ :to="link" :data-envelope-id="data.databaseId" :title="addresses" - :details="formatted()"> + :details="formatted()" + @click="onClick"> <template #icon> <div v-if="mailbox.isUnified" @@ -271,19 +272,7 @@ export default { }, link() { if (this.draft) { - // TODO: does not work with a unified drafts mailbox - // the query should also contain the account and mailbox - // id for that to work - return { - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - threadId: 'new', - draftId: this.data.databaseId, - }, - exact: true, - } + return undefined } else { return { name: 'message', @@ -357,6 +346,17 @@ export default { toggleSelected() { this.$emit('update:selected', !this.selected) }, + onClick() { + if (this.draft) { + this.$store.dispatch('showMessageComposer', { + data: { + ...this.data, + draftId: this.data.databaseId, + }, + templateMessageId: this.data.databaseId, + }) + } + }, onSelectMultiple() { this.$emit('select-multiple') }, diff --git a/src/components/Mailbox.vue b/src/components/Mailbox.vue index 0ba38e3c5..7873b7d3f 100644 --- a/src/components/Mailbox.vue +++ b/src/components/Mailbox.vue @@ -144,15 +144,7 @@ export default { if (typeof first !== 'undefined') { logger.debug('refreshing mailbox') if (this.$route.params.mailboxId === this.account.draftsMailboxId) { - this.$router.replace({ - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - threadId: 'new', - draftId: first.databaseId, - }, - }) + // Don't navigate } else { this.$router.replace({ name: 'message', diff --git a/src/components/MailboxThread.vue b/src/components/MailboxThread.vue index 5ec230d49..9b88418ec 100644 --- a/src/components/MailboxThread.vue +++ b/src/components/MailboxThread.vue @@ -59,8 +59,7 @@ </template> </AppContentList> </template> - <NewMessageDetail v-if="newMessage" /> - <Thread v-else-if="showThread" @delete="deleteMessage" /> + <Thread v-if="showThread" @delete="deleteMessage" /> <NoMessageSelected v-else-if="hasEnvelopes && !isMobile" /> </AppContent> </template> @@ -76,7 +75,6 @@ import Vue from 'vue' import infiniteScroll from '../directives/infinite-scroll' import logger from '../logger' import Mailbox from './Mailbox' -import NewMessageDetail from './NewMessageDetail' import NoMessageSelected from './NoMessageSelected' import Thread from './Thread' import { UNIFIED_ACCOUNT_ID, UNIFIED_INBOX_ID } from '../store/constants' @@ -96,7 +94,6 @@ export default { AppContent, AppContentList, Mailbox, - NewMessageDetail, NoMessageSelected, Popover, SectionTitle, @@ -154,13 +151,6 @@ export default { } return this.searchQuery }, - newMessage() { - return ( - this.$route.params.threadId === 'new' - || this.$route.params.threadId === 'reply' - || this.$route.params.threadId === 'replyAll' - ) - }, isThreadShown() { return !!this.$route.params.threadId }, diff --git a/src/components/MenuEnvelope.vue b/src/components/MenuEnvelope.vue index 74a3cc7fc..c2ce9d76c 100644 --- a/src/components/MenuEnvelope.vue +++ b/src/components/MenuEnvelope.vue @@ -32,23 +32,23 @@ }} </ActionButton> </EnvelopePrimaryActions> - <ActionRouter v-if="withReply" + <ActionButton v-if="withReply" :icon="hasMultipleRecipients ? 'icon-reply-all' : 'icon-reply'" :close-after-click="true" - :to="hasMultipleRecipients ? replyAllLink : replyOneLink"> + @click="onReply"> {{ t('mail', 'Reply') }} - </ActionRouter> - <ActionRouter v-if="hasMultipleRecipients" + </ActionButton> + <ActionButton v-if="hasMultipleRecipients" icon="icon-reply" :close-after-click="true" - :to="replyOneLink"> + @click="onReply"> {{ t('mail', 'Reply to sender only') }} - </ActionRouter> - <ActionRouter icon="icon-forward" + </ActionButton> + <ActionButton icon="icon-forward" :close-after-click="true" - :to="forwardLink"> + @click="onForward"> {{ t('mail', 'Forward') }} - </ActionRouter> + </ActionButton> <ActionButton icon="icon-junk" :close-after-click="true" @click.prevent="onToggleJunk"> @@ -151,7 +151,6 @@ import axios from '@nextcloud/axios' import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import ActionLink from '@nextcloud/vue/dist/Components/ActionLink' -import ActionRouter from '@nextcloud/vue/dist/Components/ActionRouter' import { Base64 } from 'js-base64' import ChevronLeft from 'vue-material-design-icons/ChevronLeft' import { buildRecipients as buildReplyRecipients } from '../ReplyBuilder' @@ -172,7 +171,6 @@ export default { Actions, ActionButton, ActionLink, - ActionRouter, ChevronLeft, EventModal, Modal, @@ -243,45 +241,6 @@ export default { }) return recipients.to.concat(recipients.cc).length > 1 }, - replyOneLink() { - return { - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'reply', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.envelope.databaseId, - }, - } - }, - replyAllLink() { - return { - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'replyAll', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.envelope.databaseId, - }, - } - }, - forwardLink() { - return { - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'new', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.envelope.databaseId, - }, - } - }, threadingFile() { return `data:text/plain;base64,${Base64.encode(JSON.stringify({ subject: this.envelope.subject, @@ -304,6 +263,14 @@ export default { }, }, methods: { + onForward() { + this.$store.dispatch('showMessageComposer', { + reply: { + mode: 'forward', + data: this.envelope, + }, + }) + }, onToggleFlagged() { this.$store.dispatch('toggleEnvelopeFlagged', this.envelope) }, @@ -380,6 +347,14 @@ export default { onOpenTagModal() { this.showTagModal = true }, + onReply() { + this.$store.dispatch('showMessageComposer', { + reply: { + mode: this.hasMultipleRecipients ? 'replyAll' : 'reply', + data: this.envelope, + }, + }) + }, onCloseTagModal() { this.showTagModal = false }, diff --git a/src/components/NewMessageDetail.vue b/src/components/NewMessageDetail.vue deleted file mode 100644 index b23e5d5ae..000000000 --- a/src/components/NewMessageDetail.vue +++ /dev/null @@ -1,285 +0,0 @@ -<template> - <AppContentDetails> - <Loading v-if="loading" /> - <Error - v-else-if="error" - :error="error && error.message ? error.message : t('mail', 'Not found')" - :message="errorMessage" - :data="error" - role="alert" /> - <Composer - v-else - :from-account="composerData.accountId" - :to="composerData.to" - :cc="composerData.cc" - :bcc="composerData.bcc" - :subject="composerData.subject" - :body="composerData.body" - :draft="saveDraft" - :send="sendMessage" - :reply-to="composerData.replyTo" - :forward-from="composerData.forwardFrom" - :template-message-id="templateMessageId" /> - </AppContentDetails> -</template> - -<script> -import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails' -import Axios from '@nextcloud/axios' -import { generateUrl } from '@nextcloud/router' -import { translate as t } from '@nextcloud/l10n' - -import { buildForwardSubject, buildReplySubject, buildRecipients as buildReplyRecipients } from '../ReplyBuilder' -import Composer from './Composer' -import { getRandomMessageErrorMessage } from '../util/ErrorMessageFactory' -import { html, plain, toPlain } from '../util/text' -import Loading from './Loading' -import logger from '../logger' -import { saveDraft, sendMessage } from '../service/MessageService' - -export default { - name: 'NewMessageDetail', - components: { - AppContentDetails, - Composer, - Loading, - }, - data() { - return { - loading: false, - draft: undefined, - original: undefined, - originalBody: undefined, - errorMessage: '', - error: undefined, - newDraftId: undefined, - templateMessageId: undefined, - } - }, - computed: { - composerData() { - if (this.draft !== undefined) { - logger.info('todo: handle draft data', { draft: this.draft }) - return { - to: this.draft.to, - cc: this.draft.cc, - bcc: this.draft.bcc, - subject: this.draft.subject, - body: this.draft.hasHtmlBody ? html(this.draft.body) : plain(this.draft.body), - } - } else if (this.$route.query.messageId !== undefined) { - // Forward or reply to a message - const message = this.original - logger.debug('forwarding or replying to message', { message }) - - if (this.$route.params.threadId === 'reply') { - logger.debug('simple reply', { - message, - }) - - return { - accountId: message.accountId, - to: message.from, - cc: [], - subject: buildReplySubject(message.subject), - body: this.originalBody, - originalBody: this.originalBody, - replyTo: message, - } - } else if (this.$route.params.threadId === 'replyAll') { - logger.debug('replying to all', { original: this.original }) - const account = this.$store.getters.getAccount(message.accountId) - const recipients = buildReplyRecipients(message, { - email: account.emailAddress, - label: account.name, - }) - - return { - accountId: message.accountId, - to: recipients.to, - cc: recipients.cc, - subject: buildReplySubject(message.subject), - body: this.originalBody, - originalBody: this.originalBody, - replyTo: message, - } - } else { - // forward - return { - accountId: message.accountId, - to: [], - cc: [], - subject: buildForwardSubject(message.subject), - body: this.originalBody, - originalBody: this.originalBody, - forwardFrom: message, - } - } - } else { - throw new Error('new message details can only be for replies and forwards') - } - }, - }, - watch: { - $route(to, from) { - // `saveDraft` replaced the current URL with the updated draft UID - // in that case we don't really start a new draft but just keep the - // URL consistent, hence not loading anything - if (to.name === 'message' && this.draft - && ( - to.params.draftId === parseInt(this.draft.databaseId, 10) - || to.params.draftId === this.newDraftId - ) - ) { - logger.debug('detected navigation to current (new) draft UID, not reloading') - return - } - logger.debug('the draft ID changed, we have to fetch the draft', { - currentId: this?.draft?.databaseId ?? 'no draft currently loaded', - newId: to.params.draftId, - }) - - this.fetchMessage() - }, - }, - created() { - this.fetchMessage() - }, - methods: { - fetchMessage() { - if (this.$route.params.draftId !== undefined) { - return this.fetchDraftMessage(this.$route.params.draftId) - } else if (this.$route.query.messageId !== undefined) { - return this.fetchOriginalMessage(this.$route.query.messageId) - } - }, - fetchDraftMessage(id) { - this.loading = true - this.draft = undefined - this.error = undefined - this.errorMessage = '' - - this.$store - .dispatch('fetchMessage', id) - .then((draft) => { - if (draft.databaseId !== parseInt(this.$route.params.draftId, 10)) { - logger.debug("User navigated away, loaded draft won't be shown", { - draft, - draftId: this.$route.params.draftId, - }) - return - } - - this.draft = draft - - if (this.draft === undefined) { - logger.info('draft could not be found', { id }) - this.errorMessage = getRandomMessageErrorMessage() - this.loading = false - return - } - - this.loading = false - }) - .catch((error) => { - logger.error(`could not load draft ${id}`, { error }) - if (error.isError) { - this.errorMessage = t('mail', 'Could not load your draft') - this.error = error - this.loading = false - } - }) - }, - async fetchOriginalMessage(id) { - this.loading = true - this.error = undefined - this.errorMessage = '' - - logger.debug(`fetching original message ${id}`) - - try { - const message = await this.$store.dispatch('fetchMessage', id) - if (message.databaseId !== parseInt(this.$route.query.messageId, 10)) { - logger.debug("User navigated away, loaded original message won't be used", { - message, - messageId: message.databaseId, - urlId: this.$route.query.messageId, - }) - return - } - - logger.debug('original message fetched', { message }) - this.original = message - - let body = plain(message.body || '') - if (message.hasHtmlBody) { - logger.debug('original message has HTML body') - const resp = await Axios.get( - generateUrl('/apps/mail/api/messages/{id}/html?plain=true', { - id, - }) - ) - - body = html(resp.data) - } - this.originalBody = body - } catch (error) { - logger.error('could not load original message ' + id, { error }) - if (error.isError) { - this.errorMessage = t('mail', 'Could not load original message') - this.error = error - this.loading = false - } - } finally { - this.loading = false - } - }, - async saveDraft(data) { - if (data.draftId === undefined && this.draft) { - logger.debug('draft data does not have a draftId, adding one', { draft: this.draft, data, id: this.draft.databaseId }) - data.draftId = this.draft.databaseId - } - const dataForServer = { - ...data, - body: data.isHtml ? data.body.value : toPlain(data.body).value, - } - const { id } = await saveDraft(data.account, dataForServer) - - // 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', id) - - // Update route to new draft (actual redirect will be skipped) - const account = this.$store.getters.getAccount(data.account) - if (parseInt(this.$route.params.mailboxId, 10) === account.draftsMailboxId) { - this.newDraftId = id - await this.$router.replace({ - to: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: this.$route.params.threadId, - draftId: id, - }, - }) - } - - return id - }, - async sendMessage(data) { - logger.debug('sending message', { data }) - const dataForServer = { - ...data, - body: data.isHtml ? data.body.value : toPlain(data.body).value, - } - await sendMessage(data.account, dataForServer) - - // Remove old draft envelope - this.$store.commit('removeEnvelope', { id: data.draftId }) - this.$store.commit('removeMessage', { id: data.draftId }) - }, - }, -} -</script> diff --git a/src/components/NewMessageModal.vue b/src/components/NewMessageModal.vue index 5b2280db2..7e0dc9a91 100644 --- a/src/components/NewMessageModal.vue +++ b/src/components/NewMessageModal.vue @@ -10,6 +10,8 @@ :bcc="composerData.bcc" :subject="composerData.subject" :body="composerData.body" + :reply-to="composerData.replyTo" + :draft-id="composerData.draftId" :draft="saveDraft" :send="sendMessage" :forwarded-messages="forwardedMessages" /> @@ -32,7 +34,6 @@ export default { data() { return { original: undefined, - originalBody: undefined, } }, computed: { @@ -40,6 +41,15 @@ export default { if (this.composerMessage.type === 'outbox') { return t('mail', 'Outbox draft') } + if (this.composerData.draftId !== undefined) { + return t('mail', 'Draft') + } + if (this.composerData.replyTo) { + return t('mail', 'Reply') + } + if (this.composerData.forwardFrom) { + return t('mail', 'Forward') + } return t('mail', 'New message') }, composerMessage() { diff --git a/src/components/ThreadEnvelope.vue b/src/components/ThreadEnvelope.vue index a1a7e6fda..0bd787b6b 100644 --- a/src/components/ThreadEnvelope.vue +++ b/src/components/ThreadEnvelope.vue @@ -70,16 +70,16 @@ </router-link> <div class="right"> <Moment class="timestamp" :timestamp="envelope.dateInt" /> - <router-link - :to="hasMultipleRecipients ? replyAllLink : replyOneLink" + <button :class="{ 'icon-reply-all-white': hasMultipleRecipients, 'icon-reply-white': !hasMultipleRecipients, primary: expanded, }" - class="button"> + class="button" + @click="onReply"> <span class="action-label"> {{ t('mail', 'Reply') }}</span> - </router-link> + </button> <MenuEnvelope class="app-content-list-item-menu" :envelope="envelope" :with-reply="false" @@ -174,32 +174,6 @@ export default { }) return recipients.to.concat(recipients.cc).length > 1 }, - replyOneLink() { - return { - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'reply', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.envelope.databaseId, - }, - } - }, - replyAllLink() { - return { - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'replyAll', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.envelope.databaseId, - }, - } - }, route() { return { name: 'message', @@ -278,6 +252,14 @@ export default { const top = this.$el.getBoundingClientRect().top - globalHeader - threadHeader window.scrollTo({ top }) }, + onReply() { + this.$store.dispatch('showMessageComposer', { + reply: { + mode: this.hasMultipleRecipients ? 'replyAll' : 'reply', + data: this.envelope, + }, + }) + }, onToggleImportant() { this.$store.dispatch('toggleEnvelopeImportant', this.envelope) }, diff --git a/src/store/actions.js b/src/store/actions.js index 8c5180d1f..69e4960b0 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -87,6 +87,11 @@ import Axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { showWarning } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' +import { + buildForwardSubject, + buildRecipients as buildReplyRecipients, + buildReplySubject, +} from '../ReplyBuilder' const sliceToPage = slice(0, PAGE_SIZE) @@ -258,8 +263,72 @@ export default { updated, }) }, - async showMessageComposer({ commit, dispatch }, { type = 'imap', data = {}, forwardedMessages = [], templateMessageId }) { - if (templateMessageId) { + async showMessageComposer({ commit, dispatch, getters }, { type = 'imap', data = {}, reply, forwardedMessages = [], templateMessageId }) { + if (reply) { + const original = await dispatch('fetchMessage', reply.data.databaseId) + + // Fetch and transform the body into a rich text object + if (original.hasHtmlBody) { + const resp = await Axios.get( + generateUrl('/apps/mail/api/messages/{id}/html?plain=true', { + id: original.databaseId, + }) + ) + + data.body = html(resp.data) + } else { + data.body = plain(original.body) + } + + if (reply.mode === 'reply') { + logger.debug('Show simple reply composer', { reply }) + commit('showMessageComposer', { + data: { + accountId: reply.data.accountId, + to: reply.data.from, + cc: [], + subject: buildReplySubject(reply.data.subject), + body: data.body, + originalBody: data.body, + replyTo: reply.data, + }, + }) + return + } else if (reply.mode === 'replyAll') { + logger.debug('Show reply all reply composer', { reply }) + const account = getters.getAccount(reply.data.accountId) + const recipients = buildReplyRecipients(reply.data, { + email: account.emailAddress, + label: account.name, + }) + commit('showMessageComposer', { + data: { + accountId: reply.data.accountId, + to: recipients.to, + cc: recipients.cc, + subject: buildReplySubject(reply.data.subject), + body: data.body, + originalBody: data.body, + replyTo: reply.data, + }, + }) + return + } else if (reply.mode === 'forward') { + logger.debug('Show forward composer', { reply }) + commit('showMessageComposer', { + data: { + accountId: reply.data.accountId, + to: [], + cc: [], + subject: buildForwardSubject(reply.data.subject), + body: data.body, + originalBody: data.body, + forwardFrom: reply.data, + }, + }) + return + } + } else if (templateMessageId) { const message = await dispatch('fetchMessage', templateMessageId) // Merge the original into any existing data data = { diff --git a/src/views/Home.vue b/src/views/Home.vue index 4ac7375f9..33bd93940 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,7 +1,5 @@ <template> - <Content v-shortkey.once="['c']" - app-name="mail" - @shortkey.native="onNewMessage"> + <Content app-name="mail"> <Navigation /> <Outbox v-if="$route.name === 'outbox'" /> <MailboxThread v-else-if="activeAccount" @@ -94,16 +92,6 @@ export default { } }, methods: { - onNewMessage() { - // FIXME: assumes that we're on the 'message' route already - this.$router.push({ - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'new', - }, - }) - }, hideMessage() { this.$router.replace({ name: 'mailbox', |