Newer
Older
Cyrille Bollu
committed
<AppContentDetails id="mail-message">
<Loading v-if="loading" />
<Error
v-else-if="!message"
:error="error && error.message ? error.message : t('mail', 'Not found')"
:message="errorMessage"
:data="error"
>
<div id="mail-message-header">
Cyrille Bollu
committed
<div id="mail-message-header-fields">
<h2 :title="message.subject">{{ message.subject }}</h2>
<p class="transparency">
<AddressList :entries="message.from" />
Cyrille Bollu
committed
<AddressList :entries="message.to" />
<template v-if="message.cc.length">
({{ t('mail', 'cc') }} <AddressList :entries="message.cc" />)
Cyrille Bollu
committed
</template>
</p>
</div>
<div id="mail-message-actions">
<div
:class="
hasMultipleRecipients
? 'icon-reply-all-white button primary'
: 'icon-reply-white button primary'
"
@click="hasMultipleRecipients ? replyAll() : replyMessage()"
>
<span class="action-label">{{ t('mail', 'Reply') }}</span>
</div>
<Actions id="mail-message-actions-menu" class="app-content-list-item-menu" menu-align="right">
<ActionButton v-if="hasMultipleRecipients" icon="icon-reply" @click="replyMessage">
{{ t('mail', 'Reply to sender only') }}
</ActionButton>
<ActionButton icon="icon-forward" @click="forwardMessage">
{{ t('mail', 'Forward') }}
</ActionButton>
<ActionButton icon="icon-mail" @click="onToggleSeen">
{{ envelope.flags.unseen ? t('mail', 'Mark read') : t('mail', 'Mark unread') }}
</ActionButton>
<ActionButton
:icon="sourceLoading ? 'icon-loading-small' : 'icon-details'"
:disabled="sourceLoading"
@click="onShowSource"
>
{{ t('mail', 'View source') }}
</ActionButton>
<ActionButton icon="icon-delete" @click.prevent="onDelete">
Cyrille Bollu
committed
{{ t('mail', 'Delete') }}
</ActionButton>
</Actions>
<Modal v-if="showSource" @close="onCloseSource">
<div class="section">
<h2>{{ t('mail', 'Message source') }}</h2>
<pre class="message-source">{{ rawMessage }}</pre>
</div>
Cyrille Bollu
committed
</div>
<div :class="[message.hasHtmlBody ? 'mail-message-body mail-message-body-html' : 'mail-message-body']">
<div v-if="message.itineraries.length > 0" class="message-itinerary">
<Itinerary :entries="message.itineraries" :message-id="message.messageId" />
</div>
<MessageHTMLBody v-if="message.hasHtmlBody" :url="htmlUrl" />
<MessagePlainTextBody v-else :body="message.body" :signature="message.signature" />
<Popover v-if="message.attachments[0]" class="attachment-popover">
<Actions slot="trigger">
<ActionButton icon="icon-public icon-attachment">Attachments</ActionButton>
</Actions>
<MessageAttachments :attachments="message.attachments" />
</Popover>
<div id="reply-composer"></div>
</div>
import Actions from '@nextcloud/vue/dist/Components/Actions'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import Popover from '@nextcloud/vue/dist/Components/Popover'
import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails'
import axios from '@nextcloud/axios'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import {generateUrl} from '@nextcloud/router'
import AddressList from './AddressList'
import {buildRecipients as buildReplyRecipients, buildReplySubject} from '../ReplyBuilder'
import Error from './Error'
import {getRandomMessageErrorMessage} from '../util/ErrorMessageFactory'
import Itinerary from './Itinerary'
import MessageHTMLBody from './MessageHTMLBody'
import MessagePlainTextBody from './MessagePlainTextBody'
import Loading from './Loading'
import MessageAttachments from './MessageAttachments'
export default {
name: 'Message',
components: {
Cyrille Bollu
committed
ActionButton,
Actions,
Itinerary,
Loading,
MessageAttachments,
MessageHTMLBody,
MessagePlainTextBody,
},
data() {
return {
loading: true,
message: undefined,
errorMessage: '',
error: undefined,
replyRecipient: {},
replySubject: '',
rawMessage: '',
sourceLoading: false,
showSource: false,
}
},
computed: {
htmlUrl() {
return generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/html', {
accountId: this.message.accountId,
folderId: this.message.folderId,
id: this.message.id,
})
accountId: this.message.accountId,
folderId: this.message.folderId,
messageId: this.message.id,
Cyrille Bollu
committed
hasMultipleRecipients() {
return this.replyRecipient.to.concat(this.replyRecipient.cc).length > 1
},
},
watch: {
$route(to, from) {
if (
from.name === to.name &&
Number.parseInt(from.params.accountId, 10) === Number.parseInt(to.params.accountId, 10) &&
from.params.folderId === to.params.folderId &&
from.params.messageUid === to.params.messageUid &&
from.params.filter === to.params.filter
) {
logger.debug('navigated but the message is still the same')
return
}
logger.debug('navigated to another message', {to, from})
},
created() {
this.fetchMessage()
},
methods: {
this.loading = true
this.message = undefined
this.errorMessage = ''
this.error = undefined
this.replyRecipient = {}
this.replySubject = ''
const messageUid = this.$route.params.messageUid
const [envelope, message] = await Promise.all([
this.$store.dispatch('fetchEnvelope', messageUid),
this.$store.dispatch('fetchMessage', messageUid),
])
logger.debug('envelope and message fetched', {envelope, message})
// TODO: add timeout so that message isn't flagged when only viewed
// for a few seconds
if (message && message.uid !== this.$route.params.messageUid) {
logger.debug("User navigated away, loaded message won't be shown nor flagged as seen")
if (envelope === undefined || message === undefined) {
logger.info('message could not be found', {messageUid, envelope, message})
this.errorMessage = getRandomMessageErrorMessage()
this.loading = false
return
}
const account = this.$store.getters.getAccount(message.accountId)
this.replyRecipient = buildReplyRecipients(message, {
label: account.name,
email: account.emailAddress,
})
Cyrille Bollu
committed
this.replySubject = buildReplySubject(message.subject)
if (envelope.flags.unseen) {
return this.$store.dispatch('toggleEnvelopeSeen', envelope)
logger.error('could not load message ', {messageUid, error})
if (error.isError) {
this.errorMessage = t('mail', 'Could not load your message')
this.error = error
this.loading = false
}
}
Cyrille Bollu
committed
replyMessage() {
this.$router.push({
name: 'message',
params: {
accountId: this.$route.params.accountId,
folderId: this.$route.params.folderId,
messageUid: 'reply',
filter: this.$route.params.filter ? this.$route.params.filter : undefined,
Cyrille Bollu
committed
},
query: {
uid: this.message.uid,
},
})
},
replyAll() {
this.$router.push({
name: 'message',
params: {
accountId: this.$route.params.accountId,
folderId: this.$route.params.folderId,
messageUid: 'replyAll',
filter: this.$route.params.filter ? this.$route.params.filter : undefined,
Cyrille Bollu
committed
},
query: {
uid: this.message.uid,
},
})
},
forwardMessage() {
this.$router.push({
name: 'message',
params: {
accountId: this.$route.params.accountId,
folderId: this.$route.params.folderId,
messageUid: 'new',
filter: this.$route.params.filter ? this.$route.params.filter : undefined,
},
query: {
uid: this.message.uid,
},
})
},
Cyrille Bollu
committed
onToggleSeen() {
this.$store.dispatch('toggleEnvelopeSeen', this.envelope)
},
this.$emit('delete', this.envelope.uid)
this.$store.dispatch('deleteMessage', {
accountId: this.message.accountId,
folderId: this.message.folderId,
id: this.message.id,
})
Cyrille Bollu
committed
},
async onShowSource() {
this.sourceLoading = true
try {
const resp = await axios.get(
generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages/{id}/source', {
accountId: this.message.accountId,
folderId: this.message.folderId,
id: this.message.id,
})
)
this.rawMessage = resp.data.source
this.showSource = true
} finally {
this.sourceLoading = false
}
},
onCloseSource() {
this.showSource = false
},
Cyrille Bollu
committed
<style lang="scss">
#mail-message {
flex-grow: 1;
}
flex-grow: 1;
margin-bottom: 10px;
Cyrille Bollu
committed
#mail-message-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 30px 0;
// somehow ios doesn't care about this !important rule
// so we have to manually set left/right padding to chidren
// for 100% to be used
box-sizing: content-box !important;
height: 44px;
width: 100%;
}
#mail-message-header-fields {
// initial width
width: 0;
padding-left: 38px;
Cyrille Bollu
committed
// grow and try to fill 100%
flex: 1 1 auto;
h2,
p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 7px;
margin-bottom: 0;
}
.transparency {
opacity: 0.6;
a {
font-weight: bold;
}
}
.v-popover > .trigger > .action-item {
border-radius: 22px;
background-color: var(--color-background-darker);
}
.attachment-popover {
position: sticky;
bottom: 12px;
text-align: center;
.tooltip-inner {
text-align: left;
#mail-content {
margin: 10px 38px 50px 38px;
.mail-message-body-html & {
margin-bottom: -44px; // accounting for the sticky attachment button
}
}
#mail-content iframe {
width: 100%;
}
#show-images-text {
display: none;
}
#mail-content a,
.mail-signature a {
color: #07d;
border-bottom: 1px dotted #07d;
text-decoration: none;
word-wrap: break-word;
}
Cyrille Bollu
committed
#mail-message-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-left: 10px;
margin-right: 22px;
Cyrille Bollu
committed
height: 44px;
}
.icon-reply-white,
.icon-reply-all-white {
height: 44px;
min-width: 44px;
margin: 0;
padding: 9px 18px 10px 32px;
Cyrille Bollu
committed
}
/* Show action button label and move icon to the left
on screens larger than 600px */
@media only screen and (max-width: 600px) {
.action-label {
display: none;
}
}
@media only screen and (min-width: 600px) {
.icon-reply-white,
.icon-reply-all-white {
background-position: 12px center;
Cyrille Bollu
committed
}
Cyrille Bollu
committed
#mail-message-actions-menu {
.modal-container {
overflow-y: scroll !important;
}
@media print {
Cyrille Bollu
committed
#mail-message-header-fields {
#header,
#app-navigation,
#reply-composer,
#forward-button,
#mail-message-has-blocked-content,
.app-content-list,
.message-composer,
.mail-message-attachments {
display: none !important;
}
#app-content {
margin-left: 0 !important;
}
.mail-message-body {
margin-bottom: 0 !important;
}
}
.message-source {
white-space: pre-wrap;
user-select: text;