Newer
Older
Cyrille Bollu
committed
<AppContentDetails id="mail-message">
<div id="mail-thread-header">
<div id="mail-thread-header-fields">
<h2 :title="threadSubject">
{{ threadSubject }}
Cyrille Bollu
committed
<div ref="avatarHeader" class="avatar-header">
<!-- Participants that can fit in the parent div -->
Cyrille Bollu
committed
<RecipientBubble v-for="participant in threadParticipants.slice(0,participantsToDisplay)"
:key="participant.email"
:email="participant.email"
Cyrille Bollu
committed
:label="participant.label" />
<!-- Indicator to show that there are more participants than displayed -->
<Popover v-if="threadParticipants.length > participantsToDisplay"
Cyrille Bollu
committed
class="avatar-more">
<span slot="trigger" class="avatar-more">
{{ moreParticipantsString }}
</span>
<RecipientBubble v-for="participant in threadParticipants.slice(participantsToDisplay)"
: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"
:email="participant.email"
:label="participant.label" />
Cyrille Bollu
committed
</div>
<ThreadEnvelope v-for="env in thread"
:key="env.databaseId"
:envelope="env"
:mailbox-id="$route.params.mailboxId"
:expanded="expandedThreads.includes(env.databaseId)"
@move="onMove(env.databaseId)"
@toggleExpand="toggleExpand(env.databaseId)" />
import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails'
import Popover from '@nextcloud/vue/dist/Components/Popover'
Cyrille Bollu
committed
import { prop, uniqBy } from 'ramda'
Cyrille Bollu
committed
import debounce from 'lodash/fp/debounce'
import { getRandomMessageErrorMessage } from '../util/ErrorMessageFactory'
Cyrille Bollu
committed
import RecipientBubble from './RecipientBubble'
import ThreadEnvelope from './ThreadEnvelope'
Cyrille Bollu
committed
RecipientBubble,
ThreadEnvelope,
data() {
return {
loading: true,
message: undefined,
errorMessage: '',
error: undefined,
expandedThreads: [],
participantsToDisplay: 999,
resizeDebounced: debounce(500, this.updateParticipantsToDisplay)
Cyrille Bollu
committed
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)
},
thread() {
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
},
watch: {
$route(to, from) {
&& 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 })
window.addEventListener('resize', this.resizeDebounced)
},
beforeDestroy() {
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
const maxWidth = (avatarHeader.clientWidth - 100) // Reserve 100px for the avatar-more span
let childrenWidth = 0
let fits = 0
while (childrenWidth < maxWidth && fits < this.threadParticipants.length) {
// Skipping the 'avatar-more' span
if (avatarHeader.childNodes[idx].clientWidth === undefined) {
idx += 3
continue
}
childrenWidth += avatarHeader.childNodes[idx].clientWidth
fits++
}
// There's not enough space to show all thread participants
} 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()
async fetchThread() {
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
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,
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 })
this.errorMessage = t('mail', 'Could not load your message thread')
this.error = error
this.loading = false
}
}
<style lang="scss">
Cyrille Bollu
committed
#mail-message {
flex-grow: 1;
}
#mail-thread-header {
Cyrille Bollu
committed
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));
Cyrille Bollu
committed
}
#mail-thread-header-fields {
Cyrille Bollu
committed
// initial width
width: 0;
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;
}
}
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;
}
Cyrille Bollu
committed
.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
}
@media print {
#mail-thread-header-fields {
#reply-composer,
#forward-button,
#mail-message-has-blocked-content,
.app-content-list,
.message-composer,
.mail-message-attachments {
display: none !important;
}
margin-left: 0 !important;
}
.mail-message-body {
margin-bottom: 0 !important;
}
}
.message-source {
white-space: pre-wrap;
user-select: text;
.avatar-header {
overflow: hidden;
}
Cyrille Bollu
committed
.avatar-more {
display: inline;
background-color: var(--color-background-dark);
padding: 0px 0px 1px 1px;
border-radius: 10px;
cursor: pointer;
Cyrille Bollu
committed
}
.avatar-hidden {
visibility: hidden;
}
.popover__wrapper {
.app-content-list-item-star.icon-starred {
display: none;
}
.user-bubble__wrapper {
margin-right: 4px;
}
.user-bubble__title {
cursor: pointer;
}