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
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Thread.vue')
-rw-r--r--src/components/Thread.vue351
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;
}