Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2022-01-21 18:42:03 +0300
committerRichard Steinmetz <richard@steinmetz.cloud>2022-03-24 18:34:56 +0300
commit791520d41cd98b11d55cc6f55b7592f5f722b484 (patch)
tree69a88ea1e8b4be6de95faf8c0a03f8d6f7047883 /src
parent564737d05673f3632f80714203b6460bf76303e9 (diff)
Implement frontend for outbox
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
Diffstat (limited to 'src')
-rw-r--r--src/components/Composer.vue28
-rw-r--r--src/components/Navigation.vue47
-rw-r--r--src/components/NavigationOutbox.vue66
-rw-r--r--src/components/NewMessageModal.vue37
-rw-r--r--src/components/Outbox.vue115
-rw-r--r--src/components/OutboxComposer.vue180
-rw-r--r--src/components/OutboxMessageList.vue64
-rw-r--r--src/components/OutboxMessageListItem.vue147
-rw-r--r--src/mixins/OutboxAvatarMixin.js37
-rw-r--r--src/router.js5
-rw-r--r--src/service/OutboxService.js64
-rw-r--r--src/store/index.js115
-rw-r--r--src/store/outbox/actions.js67
-rw-r--r--src/store/outbox/getters.js26
-rw-r--r--src/store/outbox/index.js34
-rw-r--r--src/store/outbox/mutations.js38
-rw-r--r--src/store/outbox/state.js25
-rw-r--r--src/views/Home.vue5
18 files changed, 1010 insertions, 90 deletions
diff --git a/src/components/Composer.vue b/src/components/Composer.vue
index 54c4b026d..e4d7fd93a 100644
--- a/src/components/Composer.vue
+++ b/src/components/Composer.vue
@@ -407,7 +407,7 @@ export default {
keyRing: undefined,
keysMissing: [],
},
- editorMode: 'html',
+ editorMode: (this.body?.format !== 'html') ? 'plaintext' : 'html',
addShareLink: t('mail', 'Add share link from {productName} Files', { productName: OC?.theme?.name ?? 'Nextcloud' }),
requestMdn: false,
appendSignature: true,
@@ -562,7 +562,8 @@ export default {
} else {
this.selectedAlias = this.aliases[0]
}
- if (previous === NO_ALIAS_SET) {
+ // only overwrite editormode if no body provided
+ if (previous === NO_ALIAS_SET && !this.body) {
this.editorMode = this.selectedAlias.editorMode
}
},
@@ -598,28 +599,15 @@ export default {
}
this.bodyVal = html(body).value
},
- recipientToRfc822(recipient) {
- if (recipient.email === recipient.label) {
- // From mailto or sender without proper label
- return recipient.email
- } else if (recipient.label === '') {
- // Invalid label
- return recipient.email
- } else if (recipient.email.search(/^[a-zA-Z]+:/) === 0) {
- // Group integration
- return recipient.email
- } else {
- // Proper layout with label
- return `"${recipient.label}" <${recipient.email}>`
- }
- },
getMessageData(id) {
return {
+ // TODO: Rename account to accountId
account: this.selectedAlias.id,
+ accountId: this.selectedAlias.id,
aliasId: this.selectedAlias.aliasId,
- to: this.selectTo.map(this.recipientToRfc822).join(', '),
- cc: this.selectCc.map(this.recipientToRfc822).join(', '),
- bcc: this.selectBcc.map(this.recipientToRfc822).join(', '),
+ to: this.selectTo,
+ cc: this.selectCc,
+ bcc: this.selectBcc,
draftId: id,
subject: this.subjectVal,
body: this.encrypt ? plain(this.bodyVal) : html(this.bodyVal),
diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue
index fcb11f200..e35bfe220 100644
--- a/src/components/Navigation.vue
+++ b/src/components/Navigation.vue
@@ -34,6 +34,16 @@
@click="refreshMailbox" />
<template #list>
<ul id="accounts-list">
+ <!-- Special mailboxes first -->
+ <NavigationMailbox
+ v-for="mailbox in unifiedMailboxes"
+ :key="'mailbox-' + mailbox.databaseId"
+ :account="unifiedAccount"
+ :mailbox="mailbox" />
+ <NavigationOutbox />
+ <AppNavigationSpacer />
+
+ <!-- All other mailboxes grouped by their account -->
<template v-for="group in menu">
<NavigationAccount
v-if="group.account"
@@ -87,9 +97,11 @@ import logger from '../logger'
import NavigationAccount from './NavigationAccount'
import NavigationAccountExpandCollapse from './NavigationAccountExpandCollapse'
import NavigationMailbox from './NavigationMailbox'
+import NavigationOutbox from './NavigationOutbox'
import AppSettingsMenu from '../components/AppSettingsMenu'
import NewMessageModal from './NewMessageModal'
+import { UNIFIED_ACCOUNT_ID } from '../store/constants'
export default {
name: 'Navigation',
@@ -103,6 +115,7 @@ export default {
NavigationAccountExpandCollapse,
NavigationMailbox,
NewMessageModal,
+ NavigationOutbox,
},
data() {
return {
@@ -112,20 +125,22 @@ export default {
},
computed: {
menu() {
- return this.$store.getters.accounts.map((account) => {
- const mailboxes = this.$store.getters.getMailboxes(account.id)
- const nonSpecialRoleMailboxes = mailboxes.filter(
- (mailbox) => this.isCollapsed(account, mailbox)
- )
- const isCollapsible = nonSpecialRoleMailboxes.length > 1
+ return this.$store.getters.accounts
+ .filter(account => account.id !== UNIFIED_ACCOUNT_ID)
+ .map(account => {
+ const mailboxes = this.$store.getters.getMailboxes(account.id)
+ const nonSpecialRoleMailboxes = mailboxes.filter(
+ (mailbox) => this.isCollapsed(account, mailbox)
+ )
+ const isCollapsible = nonSpecialRoleMailboxes.length > 1
- return {
- id: account.id,
- account,
- mailboxes,
- isCollapsible,
- }
- })
+ return {
+ id: account.id,
+ account,
+ mailboxes,
+ isCollapsible,
+ }
+ })
},
currentMailbox() {
if (this.$route.name === 'message' || this.$route.name === 'mailbox') {
@@ -133,6 +148,12 @@ export default {
}
return undefined
},
+ unifiedAccount() {
+ return this.$store.getters.getAccount(UNIFIED_ACCOUNT_ID)
+ },
+ unifiedMailboxes() {
+ return this.$store.getters.getMailboxes(UNIFIED_ACCOUNT_ID)
+ },
},
methods: {
isCollapsed(account, mailbox) {
diff --git a/src/components/NavigationOutbox.vue b/src/components/NavigationOutbox.vue
new file mode 100644
index 000000000..0a8a40f49
--- /dev/null
+++ b/src/components/NavigationOutbox.vue
@@ -0,0 +1,66 @@
+<!--
+ - @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @author Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <AppNavigationItem
+ id="navigation-outbox"
+ key="navigation-outbox"
+ icon="icon-mail"
+ :title="t('mail', 'Outbox')"
+ :to="to">
+ <template #counter>
+ <CounterBubble v-if="count">
+ {{ count }}
+ </CounterBubble>
+ </template>
+ </AppNavigationItem>
+</template>
+
+<script>
+import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
+import CounterBubble from '@nextcloud/vue/dist/Components/CounterBubble'
+
+export default {
+ name: 'NavigationOutbox',
+ components: {
+ AppNavigationItem,
+ CounterBubble,
+ },
+ computed: {
+ to() {
+ return {
+ name: 'outbox',
+ }
+ },
+ count() {
+ // TODO: fix outbox initial state that doesnt happen when the page loads.
+ return Object.keys(this.$store.getters['outbox/getAllMessages']).length
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep .counter-bubble__counter {
+ margin-right: 43px;
+}
+</style>
diff --git a/src/components/NewMessageModal.vue b/src/components/NewMessageModal.vue
index c8abf4bdf..fe37d7584 100644
--- a/src/components/NewMessageModal.vue
+++ b/src/components/NewMessageModal.vue
@@ -25,6 +25,7 @@ import { showWarning } from '@nextcloud/dialogs'
import Axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { translate as t } from '@nextcloud/l10n'
+
export default {
name: 'NewMessageModal',
components: {
@@ -112,6 +113,9 @@ export default {
}
const dataForServer = {
...data,
+ to: data.to.map(this.recipientToRfc822).join(', '),
+ cc: data.cc.map(this.recipientToRfc822).join(', '),
+ bcc: data.bcc.map(this.recipientToRfc822).join(', '),
body: data.isHtml ? data.body.value : toPlain(data.body).value,
}
const { id } = await saveDraft(data.account, dataForServer)
@@ -127,11 +131,25 @@ export default {
},
async sendMessage(data) {
logger.debug('sending message', { data })
+ const now = new Date().getTime()
const dataForServer = {
- ...data,
+ accountId: data.account,
+ sendAt: Math.floor(now / 1000), // JS timestamp is in milliseconds
+ subject: data.subject,
body: data.isHtml ? data.body.value : toPlain(data.body).value,
+ isHtml: data.isHtml,
+ isMdn: false,
+ inReplyToMessageId: '',
+ to: data.to,
+ cc: data.cc,
+ bcc: data.bcc,
+ attachmentIds: [],
}
- await sendMessage(data.account, dataForServer)
+ const message = await this.$store.dispatch('outbox/enqueueMessage', {
+ message: dataForServer,
+ })
+
+ await this.$store.dispatch('outbox/sendMessage', { id: message.id })
// Remove old draft envelope
this.$store.commit('removeEnvelope', { id: data.draftId })
@@ -177,6 +195,21 @@ export default {
}
this.fetchingTemplateMessage = false
},
+ recipientToRfc822(recipient) {
+ if (recipient.email === recipient.label) {
+ // From mailto or sender without proper label
+ return recipient.email
+ } else if (recipient.label === '') {
+ // Invalid label
+ return recipient.email
+ } else if (recipient.email.search(/^[a-zA-Z]+:/) === 0) {
+ // Group integration
+ return recipient.email
+ } else {
+ // Proper layout with label
+ return `"${recipient.label}" <${recipient.email}>`
+ }
+ },
},
}
diff --git a/src/components/Outbox.vue b/src/components/Outbox.vue
new file mode 100644
index 000000000..52f43ab88
--- /dev/null
+++ b/src/components/Outbox.vue
@@ -0,0 +1,115 @@
+<!--
+ - @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @author Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <AppContent
+ pane-config-key="mail"
+ :show-details="isMessageShown"
+ @update:showDetails="hideMessage">
+ <!-- List -->
+ <template #list>
+ <AppContentList>
+ <Error
+ v-if="error"
+ :error="t('mail', 'Could not open outbox')"
+ message=""
+ role="alert" />
+ <Loading
+ v-else-if="loading"
+ :hint="t('mail', 'Loading messages …')" />
+ <EmptyMailbox v-else-if="messages.length === 0" />
+ <OutboxMessageList
+ v-else
+ :messages="messages" />
+ </AppContentList>
+ </template>
+ </AppContent>
+</template>
+
+<script>
+import AppContent from '@nextcloud/vue/dist/Components/AppContent'
+import AppContentList from '@nextcloud/vue/dist/Components/AppContentList'
+import Loading from './Loading'
+import Error from './Error'
+import EmptyMailbox from './EmptyMailbox'
+import OutboxMessageList from './OutboxMessageList'
+import logger from '../logger'
+
+export default {
+ name: 'Outbox',
+ components: {
+ AppContent,
+ AppContentList,
+ Error,
+ Loading,
+ EmptyMailbox,
+ OutboxMessageList,
+ },
+ data() {
+ return {
+ error: false,
+ loading: false,
+ }
+ },
+ computed: {
+ isMessageShown() {
+ return !!this.$route.params.messageId
+ },
+ currentMessage() {
+ if (!this.isMessageShown) {
+ return null
+ }
+
+ return this.$store.getters['outbox/getMessage'](this.$route.params.messageId)
+ },
+ messages() {
+ return this.$store.getters['outbox/getAllMessages']
+ },
+ },
+ async mounted() {
+ await this.fetchMessages()
+ },
+ methods: {
+ hideMessage() {
+ this.$router.push({
+ name: 'outbox',
+ })
+ },
+ async fetchMessages() {
+ this.loading = true
+ this.error = false
+
+ try {
+ await this.$store.dispatch('outbox/fetchMessages')
+ } catch (error) {
+ this.error = true
+ logger.error('Failed to fetch outbox messages', { error })
+ }
+
+ this.loading = false
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/src/components/OutboxComposer.vue b/src/components/OutboxComposer.vue
new file mode 100644
index 000000000..d88f83007
--- /dev/null
+++ b/src/components/OutboxComposer.vue
@@ -0,0 +1,180 @@
+<template>
+ <Modal
+ size="normal"
+ :title="t('mail', 'Outbox draft')"
+ @close="$emit('close')">
+ <Composer
+ :from-account="message.accountId"
+ :to="message.to"
+ :cc="message.cc"
+ :bcc="message.bcc"
+ :subject="message.subject"
+ :body="outboxBody"
+ :draft="saveDraft"
+ :send="sendMessage"
+ :forwarded-messages="forwardedMessages" />
+ </Modal>
+</template>
+<script>
+import Modal from '@nextcloud/vue/dist/Components/Modal'
+import logger from '../logger'
+import { html, plain, toPlain } from '../util/text'
+import Composer from './Composer'
+import Axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+import { translate as t } from '@nextcloud/l10n'
+
+export default {
+ name: 'OutboxComposer',
+ components: {
+ Modal,
+ Composer,
+ },
+ props: {
+ message: {
+ type: Object,
+ required: true,
+ },
+ forwardedMessages: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ templateMessageId: {
+ type: Number,
+ required: false,
+ default: undefined,
+ },
+ },
+ data() {
+ return {
+ original: undefined,
+ originalBody: undefined,
+ fetchingTemplateMessage: true,
+ }
+ },
+ computed: {
+ outboxBody() {
+ if (this.message.html) {
+ return html(this.message.text)
+ }
+ return plain(this.message.text)
+ },
+ },
+ created() {
+ this.fetchOriginalMessage()
+ },
+ methods: {
+ stringToRecipients(str) {
+ if (str === undefined) {
+ return []
+ }
+
+ return [
+ {
+ label: str,
+ email: str,
+ },
+ ]
+ },
+ 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,
+ }
+ await this.$store.dispatch('outbox/updateMessage', { message: dataForServer, id: this.message.id })
+ },
+ async sendMessage(data) {
+ logger.debug('sending message', { data })
+ const now = new Date().getTime()
+ const dataForServer = {
+ accountId: data.account,
+ sendAt: Math.floor(now / 1000), // JS timestamp is in milliseconds
+ subject: data.subject,
+ body: data.isHtml ? data.body.value : toPlain(data.body).value,
+ isHtml: data.isHtml,
+ isMdn: false,
+ inReplyToMessageId: '',
+ to: data.to,
+ cc: data.cc,
+ bcc: data.bcc,
+ attachmentIds: [],
+ }
+ const message = await this.$store.dispatch('outbox/enqueueMessage', {
+ message: dataForServer,
+ })
+
+ await this.$store.dispatch('outbox/sendMessage', { id: message.id })
+
+ // Remove old draft envelope
+ this.$store.commit('removeEnvelope', { id: data.draftId })
+ this.$store.commit('removeMessage', { id: data.draftId })
+ },
+ async fetchOriginalMessage() {
+ if (this.templateMessageId === undefined) {
+ this.fetchingTemplateMessage = false
+ return
+ }
+ this.loading = true
+ this.error = undefined
+ this.errorMessage = ''
+
+ logger.debug(`fetching original message ${this.templateMessageId}`)
+
+ try {
+ const message = await this.$store.dispatch('fetchMessage', this.templateMessageId)
+ 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: this.templateMessageId,
+ })
+ )
+
+ body = html(resp.data)
+ }
+ this.originalBody = body
+ } catch (error) {
+ logger.error('could not load original message ' + this.templateMessageId, { error })
+ if (error.isError) {
+ this.errorMessage = t('mail', 'Could not load original message')
+ this.error = error
+ this.loading = false
+ }
+ } finally {
+ this.loading = false
+ }
+ this.fetchingTemplateMessage = false
+ },
+ },
+}
+
+</script>
+
+<style lang="scss" scoped>
+@media only screen and (max-width: 600px) {
+ ::v-deep .modal-container {
+ max-width: 80%;
+ }
+}
+::v-deep .modal-container {
+ width: 80%;
+ min-height: 60%;
+}
+::v-deep .modal-wrapper .modal-container {
+ overflow-y: auto !important;
+ overflow-x: auto !important;
+}
+</style>
diff --git a/src/components/OutboxMessageList.vue b/src/components/OutboxMessageList.vue
new file mode 100644
index 000000000..02bb07db0
--- /dev/null
+++ b/src/components/OutboxMessageList.vue
@@ -0,0 +1,64 @@
+<!--
+ - @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @author Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <div>
+ <OutboxMessageListItem
+ v-for="message in messages"
+ :key="message.id"
+ :message="message"
+ @click="openModal" />
+ <OutboxComposer v-if="showOutboxComposer" :message="messages" @close="showOutboxComposer = false;" />
+ </div>
+</template>
+
+<script>
+import OutboxMessageListItem from './OutboxMessageListItem'
+import OutboxComposer from './OutboxComposer'
+
+export default {
+ name: 'OutboxMessageList',
+ components: {
+ OutboxMessageListItem,
+ OutboxComposer,
+ },
+ props: {
+ messages: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showOutboxComposer: false,
+ }
+ },
+ methods: {
+ openModal() {
+ this.showOutboxComposer = true
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+</style>
diff --git a/src/components/OutboxMessageListItem.vue b/src/components/OutboxMessageListItem.vue
new file mode 100644
index 000000000..b3fb86edd
--- /dev/null
+++ b/src/components/OutboxMessageListItem.vue
@@ -0,0 +1,147 @@
+<!--
+ - @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @author Richard Steinmetz <richard@steinmetz.cloud>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - This program is free software: you can redistribute it and/or modify
+ - it under the terms of the GNU Affero General Public License as
+ - published by the Free Software Foundation, either version 3 of the
+ - License, or (at your option) any later version.
+ -
+ - This program is distributed in the hope that it will be useful,
+ - but WITHOUT ANY WARRANTY; without even the implied warranty of
+ - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ - GNU Affero General Public License for more details.
+ -
+ - You should have received a copy of the GNU Affero General Public License
+ - along with this program. If not, see <http://www.gnu.org/licenses/>.
+ -
+ -->
+
+<template>
+ <ListItem
+ class="outbox-message"
+ :class="{ selected }"
+ :title="title"
+ @click="openModal">
+ <template #icon>
+ <div
+ class="account-color"
+ :style="{'background-color': accountColor}" />
+ <Avatar :display-name="avatarDisplayName" :email="avatarEmail" />
+ </template>
+ <template #subtitle>
+ {{ message.subject }}
+ </template>
+ <template slot="actions">
+ <ActionButton
+ icon="icon-checkmark"
+ :close-after-click="true"
+ @click="sendMessage">
+ {{ t('mail', 'Send message now') }}
+ </ActionButton>
+ <ActionButton
+ icon="icon-delete"
+ :close-after-click="true"
+ @click="deleteMessage">
+ {{ t('mail', 'Delete message') }}
+ </ActionButton>
+ </template>
+ <template #extra>
+ <OutboxComposer v-if="showOutboxComposer" :message="message" @close="showOutboxComposer = false;" />
+ </template>
+ </ListItem>
+</template>
+
+<script>
+import ListItem from '@nextcloud/vue/dist/Components/ListItem'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import Avatar from './Avatar'
+import { calculateAccountColor } from '../util/AccountColor'
+import OutboxAvatarMixin from '../mixins/OutboxAvatarMixin'
+import logger from '../logger'
+import { showError, showSuccess } from '@nextcloud/dialogs'
+import { matchError } from '../errors/match'
+import { translate as t } from '@nextcloud/l10n'
+import OutboxComposer from './OutboxComposer'
+
+export default {
+ name: 'OutboxMessageListItem',
+ components: {
+ ListItem,
+ Avatar,
+ ActionButton,
+ OutboxComposer,
+ },
+ mixins: [
+ OutboxAvatarMixin,
+ ],
+ props: {
+ message: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showOutboxComposer: false,
+ }
+ },
+ computed: {
+ selected() {
+ return this.$route.params.messageId === this.message.id
+ },
+ accountColor() {
+ const account = this.$store.getters.getAccount(this.message.accountId)
+ return calculateAccountColor(account?.emailAddress ?? '')
+ },
+ title() {
+ return 'Due in 30 seconds'
+ },
+ },
+ methods: {
+ async deleteMessage() {
+ try {
+ await this.$store.dispatch('outbox/deleteMessage', {
+ id: this.message.id,
+ })
+ } catch (error) {
+ showError(await matchError(error, {
+ default(error) {
+ logger.error('could not delete message', error)
+ return t('mail', 'Could not delete message')
+ },
+ }))
+ }
+ },
+ async sendMessage(data) {
+ logger.debug('sending message', { data })
+ await this.$store.dispatch('outbox/sendMessage', { id: this.message.id })
+ showSuccess(t('mail', 'Message sent'))
+ },
+ openModal() {
+ this.showOutboxComposer = true
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.outbox-message {
+ list-style: none;
+ &.active {
+ background-color: var(--color-background-dark);
+ border-radius: 16px;
+ }
+
+ .account-color {
+ position: absolute;
+ left: 0;
+ width: 2px;
+ height: 69px;
+ z-index: 1;
+ }
+}
+</style>
diff --git a/src/mixins/OutboxAvatarMixin.js b/src/mixins/OutboxAvatarMixin.js
new file mode 100644
index 000000000..23a456269
--- /dev/null
+++ b/src/mixins/OutboxAvatarMixin.js
@@ -0,0 +1,37 @@
+/**
+ * @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export default {
+ computed: {
+ recipients() {
+ const to = this.message.to ?? []
+ const cc = this.message.cc ?? []
+ return [...to, ...cc]
+ },
+ avatarDisplayName() {
+ return this.recipients[0]?.label ?? this.recipients[0]?.email ?? '?'
+ },
+ avatarEmail() {
+ return this.recipients[0]?.email ?? ''
+ },
+ },
+}
diff --git a/src/router.js b/src/router.js
index 313b83116..7d7fb3d90 100644
--- a/src/router.js
+++ b/src/router.js
@@ -33,6 +33,11 @@ export default new Router({
component: Home,
},
{
+ path: '/outbox',
+ name: 'outbox',
+ component: Home,
+ },
+ {
path: '/setup',
name: 'setup',
component: Setup,
diff --git a/src/service/OutboxService.js b/src/service/OutboxService.js
new file mode 100644
index 000000000..0f89297cb
--- /dev/null
+++ b/src/service/OutboxService.js
@@ -0,0 +1,64 @@
+/**
+ * @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import { generateUrl } from '@nextcloud/router'
+import axios from '@nextcloud/axios'
+
+export async function fetchMessages() {
+ const url = generateUrl('/apps/mail/api/outbox')
+
+ const { data } = await axios.get(url)
+ return data.data
+}
+
+export async function deleteMessage(id) {
+ const url = generateUrl('/apps/mail/api/outbox/{id}', {
+ id,
+ })
+
+ const { data } = await axios.delete(url)
+ return data
+}
+
+export async function enqueueMessage(message) {
+ const url = generateUrl('/apps/mail/api/outbox')
+
+ const { data } = await axios.post(url, message)
+ return data.data
+}
+export async function updateMessage(message, id) {
+ const url = generateUrl('/apps/mail/api/outbox/{id}', {
+ id,
+ })
+
+ const { data } = await axios.put(url, message)
+ return data.data
+}
+
+export async function sendMessage(id) {
+ const url = generateUrl('/apps/mail/api/outbox/{id}', {
+ id,
+ })
+
+ const { data } = await axios.post(url)
+ return data
+}
diff --git a/src/store/index.js b/src/store/index.js
index 278b1435e..0c14b3fb6 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -30,66 +30,73 @@ import {
import actions from './actions'
import { getters } from './getters'
import mutations from './mutations'
+import outbox from './outbox'
Vue.use(Vuex)
export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
- state: {
- preferences: {},
- accounts: {
- [UNIFIED_ACCOUNT_ID]: {
- id: UNIFIED_ACCOUNT_ID,
- accountId: UNIFIED_ACCOUNT_ID,
- isUnified: true,
- mailboxes: [PRIORITY_INBOX_ID, UNIFIED_INBOX_ID],
- aliases: [],
- collapsed: false,
- emailAddress: '',
- name: '',
- showSubscribedOnly: false,
- signatureAboveQuote: false,
+ modules: {
+ root: {
+ namespaced: false,
+ state: {
+ preferences: {},
+ accounts: {
+ [UNIFIED_ACCOUNT_ID]: {
+ id: UNIFIED_ACCOUNT_ID,
+ accountId: UNIFIED_ACCOUNT_ID,
+ isUnified: true,
+ mailboxes: [PRIORITY_INBOX_ID, UNIFIED_INBOX_ID],
+ aliases: [],
+ collapsed: false,
+ emailAddress: '',
+ name: '',
+ showSubscribedOnly: false,
+ signatureAboveQuote: false,
+ },
+ },
+ accountList: [UNIFIED_ACCOUNT_ID],
+ allAccountSettings: [],
+ mailboxes: {
+ [UNIFIED_INBOX_ID]: {
+ id: UNIFIED_INBOX_ID,
+ databaseId: UNIFIED_INBOX_ID,
+ accountId: 0,
+ attributes: ['\\subscribed'],
+ isUnified: true,
+ path: '',
+ specialUse: ['inbox'],
+ specialRole: 'inbox',
+ unread: 0,
+ mailboxes: [],
+ envelopeLists: {},
+ name: 'UNIFIED INBOX',
+ },
+ [PRIORITY_INBOX_ID]: {
+ id: PRIORITY_INBOX_ID,
+ databaseId: PRIORITY_INBOX_ID,
+ accountId: 0,
+ attributes: ['\\subscribed'],
+ isPriorityInbox: true,
+ path: '',
+ specialUse: ['inbox'],
+ specialRole: 'inbox',
+ unread: 0,
+ mailboxes: [],
+ envelopeLists: {},
+ name: 'PRIORITY INBOX',
+ },
+ },
+ envelopes: {},
+ messages: {},
+ autocompleteEntries: [],
+ tags: {},
+ tagList: [],
},
+ getters,
+ mutations,
+ actions,
},
- accountList: [UNIFIED_ACCOUNT_ID],
- allAccountSettings: [],
- mailboxes: {
- [UNIFIED_INBOX_ID]: {
- id: UNIFIED_INBOX_ID,
- databaseId: UNIFIED_INBOX_ID,
- accountId: 0,
- attributes: ['\\subscribed'],
- isUnified: true,
- path: '',
- specialUse: ['inbox'],
- specialRole: 'inbox',
- unread: 0,
- mailboxes: [],
- envelopeLists: {},
- name: 'UNIFIED INBOX',
- },
- [PRIORITY_INBOX_ID]: {
- id: PRIORITY_INBOX_ID,
- databaseId: PRIORITY_INBOX_ID,
- accountId: 0,
- attributes: ['\\subscribed'],
- isPriorityInbox: true,
- path: '',
- specialUse: ['inbox'],
- specialRole: 'inbox',
- unread: 0,
- mailboxes: [],
- envelopeLists: {},
- name: 'PRIORITY INBOX',
- },
- },
- envelopes: {},
- messages: {},
- autocompleteEntries: [],
- tags: {},
- tagList: [],
+ outbox,
},
- getters,
- mutations,
- actions,
})
diff --git a/src/store/outbox/actions.js b/src/store/outbox/actions.js
new file mode 100644
index 000000000..a7661b756
--- /dev/null
+++ b/src/store/outbox/actions.js
@@ -0,0 +1,67 @@
+/**
+ * @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import * as OutboxService from '../../service/OutboxService'
+import logger from '../../logger'
+
+export default {
+ async fetchMessages({ commit }) {
+ const { messages } = await OutboxService.fetchMessages()
+ for (const message of messages) {
+ commit('addMessage', { message })
+ }
+ return messages
+ },
+
+ async deleteMessage({ commit }, { id }) {
+ await OutboxService.deleteMessage(id)
+ commit('deleteMessage', { id })
+ },
+
+ async enqueueMessage({ commit }, { message }) {
+ message = await OutboxService.enqueueMessage(message)
+ commit('addMessage', { message })
+ return message
+ },
+ async updateMessage({ commit }, { message, id }) {
+ const updatedMessage = await OutboxService.updateMessage(message, id)
+ commit('updateMessage', { message: updatedMessage })
+ return updatedMessage
+ },
+
+ async sendMessage({ commit, getters }, { id }) {
+ // Skip if the message has been deleted/undone in the meantime
+ if (!getters.getMessage(id)) {
+ logger.debug('Skipped sending message that was undone')
+ return
+ }
+
+ try {
+ await OutboxService.sendMessage(id)
+ } catch (error) {
+ logger.error(`Failed to send message ${id} from outbox`)
+ return
+ }
+
+ commit('deleteMessage', id)
+ },
+}
diff --git a/src/store/outbox/getters.js b/src/store/outbox/getters.js
new file mode 100644
index 000000000..aaf8ec2a5
--- /dev/null
+++ b/src/store/outbox/getters.js
@@ -0,0 +1,26 @@
+/**
+ * @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export default {
+ getAllMessages: state => Object.values(state.messages),
+ getMessage: state => id => state.messages[id],
+}
diff --git a/src/store/outbox/index.js b/src/store/outbox/index.js
new file mode 100644
index 000000000..0b2edea0f
--- /dev/null
+++ b/src/store/outbox/index.js
@@ -0,0 +1,34 @@
+/**
+ * @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import actions from './actions.js'
+import getters from './getters.js'
+import mutations from './mutations.js'
+import state from './state.js'
+
+export default {
+ namespaced: true,
+ actions,
+ getters,
+ mutations,
+ state,
+}
diff --git a/src/store/outbox/mutations.js b/src/store/outbox/mutations.js
new file mode 100644
index 000000000..7fe3bc4d3
--- /dev/null
+++ b/src/store/outbox/mutations.js
@@ -0,0 +1,38 @@
+/**
+ * @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+import Vue from 'vue'
+
+export default {
+ addMessage(state, { message }) {
+ const existing = state.messages[message.id] ?? {}
+ Vue.set(state.messages, message.id, Object.assign({}, existing, message))
+ },
+
+ deleteMessage(state, { id }) {
+ Vue.delete(state.messages, id)
+ },
+ updateMessage(state, { message }) {
+ const existing = state.messages[message.id]
+ Vue.set(state.messages, message.id, Object.assign({}, existing, message))
+ },
+}
diff --git a/src/store/outbox/state.js b/src/store/outbox/state.js
new file mode 100644
index 000000000..77c82c33d
--- /dev/null
+++ b/src/store/outbox/state.js
@@ -0,0 +1,25 @@
+/**
+ * @copyright Copyright (c) 2022 Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
+ *
+ * @license AGPL-3.0-or-later
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+export default {
+ messages: {},
+}
diff --git a/src/views/Home.vue b/src/views/Home.vue
index f7f9b1628..033790701 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -3,7 +3,8 @@
app-name="mail"
@shortkey.native="onNewMessage">
<Navigation />
- <MailboxThread v-if="activeAccount"
+ <Outbox v-if="$route.name === 'outbox'" />
+ <MailboxThread v-else-if="activeAccount"
:account="activeAccount"
:mailbox="activeMailbox" />
</Content>
@@ -16,6 +17,7 @@ import isMobile from '@nextcloud/vue/dist/Mixins/isMobile'
import logger from '../logger'
import MailboxThread from '../components/MailboxThread'
import Navigation from '../components/Navigation'
+import Outbox from '../components/Outbox'
export default {
name: 'Home',
@@ -23,6 +25,7 @@ export default {
Content,
MailboxThread,
Navigation,
+ Outbox,
},
mixins: [isMobile],
computed: {