Skip to content
Snippets Groups Projects
Thread.vue 9.9 KiB
Newer Older
		<Loading v-if="loading" />
		<template v-else>
			<div id="mail-thread-header">
				<div id="mail-thread-header-fields">
					<h2 :title="threadSubject">
						{{ threadSubject }}
					<div ref="avatarHeader" class="avatar-header">
						<!-- Participants that can fit in the parent div -->
						<RecipientBubble v-for="participant in threadParticipants.slice(0,participantsToDisplay)"
							:key="participant.email"
							:email="participant.email"
						<!-- Indicator to show that there are more participants than displayed -->
						<Popover v-if="threadParticipants.length > participantsToDisplay"
							<span slot="trigger" class="avatar-more">
								{{ moreParticipantsString }}
							</span>
							<RecipientBubble v-for="participant in threadParticipants.slice(participantsToDisplay)"
Cyrille Bollu's avatar
Cyrille Bollu committed
								:key="participant.email"
								:email="participant.email"
								:label="participant.label" />
						<!-- Remaining participants, if any (Needed to have avatarHeader reactive) -->
						<RecipientBubble v-for="participant in threadParticipants.slice(participantsToDisplay)"
							:key="participant.email"
Cyrille Bollu's avatar
Cyrille Bollu committed
							class="avatar-hidden"
							:email="participant.email"
							:label="participant.label" />
			<ThreadEnvelope v-for="env in thread"
				:key="env.databaseId"
				:envelope="env"
				:mailbox-id="$route.params.mailboxId"
				:expanded="expandedThreads.includes(env.databaseId)"
				:full-height="thread.length === 1"
				@move="onMove(env.databaseId)"
				@toggleExpand="toggleExpand(env.databaseId)" />
	</AppContentDetails>
</template>

<script>
import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails'
import Popover from '@nextcloud/vue/dist/Components/Popover'
import { prop, uniqBy } from 'ramda'
import debounce from 'lodash/fp/debounce'
import { getRandomMessageErrorMessage } from '../util/ErrorMessageFactory'
import Loading from './Loading'
import logger from '../logger'
import RecipientBubble from './RecipientBubble'
import ThreadEnvelope from './ThreadEnvelope'
export default {
	name: 'Thread',
	components: {
		AppContentDetails,
	data() {
		return {
			loading: true,
			message: undefined,
			errorMessage: '',
			error: undefined,
			resizeDebounced: debounce(500, this.updateParticipantsToDisplay)
		moreParticipantsString() {
			// Returns a number showing the number of thread participants that are not shown in the avatar-header
			return `+${this.threadParticipants.length - this.participantsToDisplay}`
		},
		threadId() {
			return parseInt(this.$route.params.threadId, 10)
		},
			return this.$store.getters.getEnvelopeThread(this.threadId)
		threadParticipants() {
			const recipients = this.thread.flatMap(envelope => {
				return envelope.from.concat(envelope.to).concat(envelope.cc)
			return uniqBy(prop('email'), recipients)
		threadSubject() {
			const thread = this.thread
			if (thread.length === 0) {
				console.warn('thread is empty')
				return ''
			}
			return thread[0].subject
GretaD's avatar
GretaD committed
		},
	},
	watch: {
		$route(to, from) {
				from.name === to.name
Christoph Wurst's avatar
Christoph Wurst committed
				&& from.params.mailboxId === to.params.mailboxId
				&& from.params.threadId === to.params.threadId
				&& from.params.filter === to.params.filter
				logger.debug('navigated but the thread is still the same')
			logger.debug('navigated to another thread', { to, from })
			this.resetThread()
		this.resetThread()
		window.addEventListener('resize', this.resizeDebounced)
		window.removeEventListener('resize', this.resizeDebounced)
		updateParticipantsToDisplay() {
			// Wait until everything is in place
			if (!this.$refs.avatarHeader || !this.threadParticipants) {
				return
			}

			// Compute the number of participants to display depending on the width available
			const avatarHeader = this.$refs.avatarHeader
Cyrille Bollu's avatar
Cyrille Bollu committed
			const maxWidth = (avatarHeader.clientWidth - 100) // Reserve 100px for the avatar-more span
Cyrille Bollu's avatar
Cyrille Bollu committed
			while (childrenWidth < maxWidth && fits < this.threadParticipants.length) {
				// Skipping the 'avatar-more' span
				if (avatarHeader.childNodes[idx].clientWidth === undefined) {
					idx += 3
				childrenWidth += avatarHeader.childNodes[idx].clientWidth
Cyrille Bollu's avatar
Cyrille Bollu committed
			if (childrenWidth > maxWidth) {
				// There's not enough space to show all thread participants
Cyrille Bollu's avatar
Cyrille Bollu committed
				this.participantsToDisplay = fits - 1
			} else {
				// There's enough space to show all thread participants
				this.participantsToDisplay = this.threadParticipants.length
			}
		},
		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)
			}
		},
		onMove(threadId) {
			if (threadId === this.threadId) {
				this.$router.replace({
					name: 'mailbox',
					params: {
						mailboxId: this.$route.params.mailboxId,
					},
				})
			} else {
				this.expandedThreads = this.expandedThreads.filter((id) => id !== threadId)
				this.fetchThread()
			}
		},
		async resetThread() {
			this.expandedThreads = [this.threadId]
			await this.fetchThread()
			this.updateParticipantsToDisplay()
			this.loading = true
			this.errorMessage = ''
			this.error = undefined
			const threadId = this.threadId
				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
Christoph Wurst's avatar
Christoph Wurst committed
				//       for a few seconds
				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,
Christoph Wurst's avatar
Christoph Wurst committed
					})
				if (thread.length === 0) {
					logger.info('thread could not be found and is empty', { threadId })
					this.errorMessage = getRandomMessageErrorMessage()
					this.loading = false
					return
				}
				this.loading = false
			} catch (error) {
				logger.error('could not load envelope thread', { threadId, error })
				if (error.isError) {
					this.errorMessage = t('mail', 'Could not load your message thread')
					this.error = error
					this.loading = false
				}
			}
.mail-message-body {
	flex: 1;
	margin-bottom: 60px;
	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%;

	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));
	// 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;
		}
	}
.attachment-popover {
	position: sticky;
	bottom: 12px;
	text-align: center;
.tooltip-inner {
	text-align: left;
#mail-content, .mail-signature {
	margin: 10px 38px 50px 60px;

	.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;
}
.icon-reply-white,
.icon-reply-all-white {
	height: 44px;
	min-width: 44px;
	margin: 0;
	padding: 9px 18px 10px 32px;
}

/* 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;
Souren Araya's avatar
Souren Araya committed
	#header,
Souren Araya's avatar
Souren Araya committed
	#reply-composer,
	#forward-button,
	#mail-message-has-blocked-content,
	.app-content-list,
	.message-composer,
	.mail-message-attachments {
		margin-left: 0 !important;
	}
	.mail-message-body {
		margin-bottom: 0 !important;
	}
}
	font-family: monospace;
	white-space: pre-wrap;
	user-select: text;
	max-height: 24px;
	display: inline;
	background-color: var(--color-background-dark);
	padding: 0px 0px 1px 1px;
	border-radius: 10px;
	cursor: pointer;
	max-width: 500px;
}
.app-content-list-item-star.icon-starred {
	display: none;
}
.user-bubble__wrapper {
	margin-right: 4px;
}
.user-bubble__title {
	cursor: pointer;
}