diff options
Diffstat (limited to 'src/components/Thread.vue')
-rw-r--r-- | src/components/Thread.vue | 351 |
1 files changed, 69 insertions, 282 deletions
diff --git a/src/components/Thread.vue b/src/components/Thread.vue index 2679cdace..c905b0c39 100644 --- a/src/components/Thread.vue +++ b/src/components/Thread.vue @@ -1,188 +1,71 @@ <template> <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" /> <template v-else> - <div id="mail-message-header"> - <div id="mail-message-header-fields"> - <h2 :title="message.subject"> - {{ message.subject }} + <div id="mail-thread-header"> + <div id="mail-thread-header-fields"> + <h2 :title="threadSubject"> + {{ threadSubject }} </h2> <p class="transparency"> - <AddressList :entries="message.from" /> - {{ t('mail', 'to') }} - <AddressList :entries="message.to" /> - <template v-if="message.cc.length"> - ({{ t('mail', 'cc') }} <AddressList :entries="message.cc" />) - </template> + <AddressList :entries="threadParticipants" /> </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-important" @click.prevent="onToggleImportant"> - {{ - envelope.flags.important ? t('mail', 'Mark unimportant') : t('mail', 'Mark important') - }} - </ActionButton> - <ActionButton icon="icon-starred" @click.prevent="onToggleFlagged"> - {{ - envelope.flags.flagged ? t('mail', 'Mark unfavorite') : t('mail', 'Mark favorite') - }} - </ActionButton> - <ActionButton icon="icon-mail" @click="onToggleSeen"> - {{ envelope.flags.seen ? t('mail', 'Mark unread') : t('mail', 'Mark read') }} - </ActionButton> - - <ActionButton icon="icon-junk" @click="onToggleJunk"> - {{ envelope.flags.junk ? t('mail', 'Mark not spam') : t('mail', 'Mark as spam') }} - </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"> - {{ 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> - </Modal> - </div> - </div> - <ThreadMessage v-for="threadMessage in previousThread" - :key="threadMessage.databaseId" - :message="threadMessage" /> - <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" /> - <MessageEncryptedBody v-else-if="isEncrypted" :body="message.body" :from="from" /> - <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> - <ThreadMessage v-for="threadMessage in successiveThread" - :key="threadMessage.databaseId" - :message="threadMessage" /> + <ThreadEnvelope v-for="env in thread" + :key="env.databaseId" + :envelope="env" + :mailbox-id="$route.params.mailboxId" + :expanded="expandedThreads.includes(env.databaseId)" + @toggleExpand="toggleExpand(env.databaseId)" /> </template> </AppContentDetails> </template> <script> -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 { prop, uniqBy } from 'ramda' import AddressList from './AddressList' -import { - buildRecipients as buildReplyRecipients, - buildReplySubject, -} from '../ReplyBuilder' -import Error from './Error' import { getRandomMessageErrorMessage } from '../util/ErrorMessageFactory' -import { html, plain } from '../util/text' -import { isPgpgMessage } from '../crypto/pgp' -import Itinerary from './Itinerary' -import MessageEncryptedBody from './MessageEncryptedBody' -import MessageHTMLBody from './MessageHTMLBody' -import MessagePlainTextBody from './MessagePlainTextBody' import Loading from './Loading' import logger from '../logger' -import MessageAttachments from './MessageAttachments' -import ThreadMessage from './ThreadMessage' +import ThreadEnvelope from './ThreadEnvelope' export default { name: 'Thread', components: { - ActionButton, - Actions, AddressList, AppContentDetails, - Error, - Itinerary, Loading, - MessageAttachments, - MessageEncryptedBody, - MessageHTMLBody, - MessagePlainTextBody, - Modal, - Popover, - ThreadMessage, + ThreadEnvelope, }, data() { return { loading: true, message: undefined, - envelope: undefined, errorMessage: '', error: undefined, - replyRecipient: {}, - replySubject: '', - rawMessage: '', - sourceLoading: false, - showSource: false, + expandedThreads: [], } }, computed: { - from() { - return this.message.from.length === 0 ? '?' : this.message.from[0].label || this.message.from[0].email - }, - isEncrypted() { - return isPgpgMessage(this.message.hasHtmlBody ? html(this.message.body) : plain(this.message.body)) + thread() { + return this.$store.getters.getEnvelopeThread(this.$route.params.threadId) }, - htmlUrl() { - return generateUrl('/apps/mail/api/messages/{id}/html', { - id: this.envelope.databaseId, + threadParticipants() { + const recipients = this.thread.flatMap(envelope => { + return envelope.from.concat(envelope.to).concat(envelope.cc) }) + return uniqBy(prop('email'), recipients) }, - hasMultipleRecipients() { - return this.replyRecipient.to.concat(this.replyRecipient.cc).length > 1 - }, - previousThread() { - // Only show younger messages, not the current one - return this.$store.getters.getMessageThread(this.message.databaseId) - .filter(m => m.dateInt < this.message.dateInt) - }, - successiveThread() { - // Only show older messages, not the current one - return this.$store.getters.getMessageThread(this.message.databaseId) - .filter(m => m.dateInt > this.message.dateInt) + threadSubject() { + const thread = this.thread + if (thread.length === 0) { + console.warn('thread is empty') + return '' + } + return thread[0].subject }, }, watch: { @@ -193,156 +76,68 @@ export default { && from.params.threadId === to.params.threadId && from.params.filter === to.params.filter ) { - logger.debug('navigated but the message is still the same') + logger.debug('navigated but the thread is still the same') return } - logger.debug('navigated to another message', { to, from }) - this.fetchMessage() + logger.debug('navigated to another thread', { to, from }) + this.fetchThread() }, }, created() { - this.fetchMessage() + this.fetchThread() }, methods: { - async fetchMessage() { + toggleExpand(threadId) { + if (!this.expandedThreads.includes(threadId)) { + console.debug(`expand thread ${threadId}`) + this.expandedThreads.push(threadId) + } else { + console.debug(`collapse thread ${threadId}`) + this.expandedThreads = this.expandedThreads.filter(t => t !== threadId) + } + }, + async fetchThread() { this.loading = true - this.message = undefined this.errorMessage = '' this.error = undefined - this.replyRecipient = {} - this.replySubject = '' - - const threadId = this.$route.params.threadId + const threadId = parseInt(this.$route.params.threadId, 10) + this.expandedThreads = [threadId] try { - const [envelope, message] = await Promise.all([ - this.$store.dispatch('fetchEnvelope', threadId), - this.$store.dispatch('fetchMessage', threadId), - ]) - logger.debug('envelope and message fetched', { envelope, message }) - // TODO: add timeout so that message isn't flagged when only viewed + const thread = await this.$store.dispatch('fetchThread', threadId) + logger.debug(`thread for envelope ${threadId} fetched`, { thread }) + // TODO: add timeout so that envelope isn't flagged when only viewed // for a few seconds - if (envelope && envelope.databaseId !== parseInt(this.$route.params.threadId, 10)) { - logger.debug("User navigated away, loaded message won't be shown nor flagged as seen", { - messageId: envelope.databaseId, - threadId: this.$route.params.threadId, + if (threadId !== parseInt(this.$route.params.threadId, 10)) { + logger.debug("User navigated away, loaded envelope won't be shown nor flagged as seen", { + oldId: threadId, + newId: this.$route.params.threadId, }) return } - this.envelope = envelope - this.message = message - - if (envelope === undefined || message === undefined) { - logger.info('message could not be found', { threadId, envelope, message }) + if (thread.length === 0) { + logger.info('thread could not be found and is empty', { threadId }) this.errorMessage = getRandomMessageErrorMessage() this.loading = false return } - const account = this.$store.getters.getAccount(envelope.accountId) - this.replyRecipient = buildReplyRecipients(message, { - label: account.name, - email: account.emailAddress, - }) - - this.replySubject = buildReplySubject(message.subject) - this.loading = false - - if (!envelope.flags.seen) { - return this.$store.dispatch('toggleEnvelopeSeen', envelope) - } } catch (error) { - logger.error('could not load message ', { threadId, error }) + logger.error('could not load envelope thread', { threadId, error }) if (error.isError) { - this.errorMessage = t('mail', 'Could not load your message') + this.errorMessage = t('mail', 'Could not load your message thread') this.error = error this.loading = false } } }, - replyMessage() { - this.$router.push({ - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'reply', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.$route.params.threadId, - }, - }) - }, - replyAll() { - this.$router.push({ - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'replyAll', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.$route.params.threadId, - }, - }) - }, - forwardMessage() { - this.$router.push({ - name: 'message', - params: { - mailboxId: this.$route.params.mailboxId, - threadId: 'new', - filter: this.$route.params.filter ? this.$route.params.filter : undefined, - }, - query: { - messageId: this.$route.params.threadId, - }, - }) - }, - onToggleSeen() { - this.$store.dispatch('toggleEnvelopeSeen', this.envelope) - }, - onToggleJunk() { - this.$store.dispatch('toggleEnvelopeJunk', this.envelope) - }, - onDelete() { - this.$emit('delete', this.envelope.databaseId) - this.$store.dispatch('deleteMessage', { - id: this.envelope.databaseId, - }) - }, - async onShowSource() { - this.sourceLoading = true - - try { - const resp = await axios.get( - generateUrl('/apps/mail/api/messages/{id}/source', { - id: this.envelope.databaseId, - }) - ) - - this.rawMessage = resp.data.source - this.showSource = true - } finally { - this.sourceLoading = false - } - }, - onCloseSource() { - this.showSource = false - }, - onToggleImportant() { - this.$store.dispatch('toggleEnvelopeImportant', this.envelope) - }, - onToggleFlagged() { - this.$store.dispatch('toggleEnvelopeFlagged', this.envelope) - }, }, } </script> -<style lang="scss" scoped> +<style lang="scss"> #mail-message { flex-grow: 1; } @@ -353,7 +148,7 @@ export default { position: relative; } -#mail-message-header { +#mail-thread-header { display: flex; flex-direction: row; justify-content: space-between; @@ -365,9 +160,19 @@ export default { box-sizing: content-box !important; height: 44px; width: 100%; + + z-index: 100; + position: fixed; // ie fallback + position: -webkit-sticky; // ios/safari fallback + position: sticky; + top: var(--header-height); + background: -webkit-linear-gradient(var(--color-main-background), var(--color-main-background) 80%, rgba(255,255,255,0)); + background: -o-linear-gradient(var(--color-main-background), var(--color-main-background) 80%, rgba(255,255,255,0)); + background: -moz-linear-gradient(var(--color-main-background), var(--color-main-background) 80%, rgba(255,255,255,0)); + background: linear-gradient(var(--color-main-background), var(--color-main-background) 80%, rgba(255,255,255,0)); } -#mail-message-header-fields { +#mail-thread-header-fields { // initial width width: 0; padding-left: 38px; @@ -390,11 +195,6 @@ export default { } } -.v-popover > .trigger > .action-item { - border-radius: 22px; - background-color: var(--color-background-darker); -} - .attachment-popover { position: sticky; bottom: 12px; @@ -429,15 +229,6 @@ export default { word-wrap: break-word; } -#mail-message-actions { - display: flex; - flex-direction: row; - justify-content: flex-end; - margin-left: 10px; - margin-right: 22px; - height: 44px; -} - .icon-reply-white, .icon-reply-all-white { height: 44px; @@ -460,16 +251,12 @@ export default { } } -#mail-message-actions-menu { - margin-left: 4px; -} - ::v-deep .modal-container { overflow-y: scroll !important; } @media print { - #mail-message-header-fields { + #mail-thread-header-fields { position: relative; } |