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 -->
Cyrille Bollu
committed
<span v-if="threadParticipants.length > participantsToDisplay"
v-tooltip.auto="{
content: remainingParticipants,
trigger: 'click',
html: true,
}"
Cyrille Bollu
committed
class="avatar-more">
{{ moreParticipantsString }}
</span>
<!-- Remaining participants (if any) -->
<RecipientBubble v-for="participant in threadParticipants.slice(participantsToDisplay)"
class="avatar-hidden"
: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 { prop, uniqBy } from 'ramda'
import { ReactiveRefs } from 'vue-reactive-refs'
import Vue from 'vue'
import { getRandomMessageErrorMessage } from '../util/ErrorMessageFactory'
Cyrille Bollu
committed
import RecipientBubble from './RecipientBubble'
import ThreadEnvelope from './ThreadEnvelope'
Vue.use(ReactiveRefs)
Cyrille Bollu
committed
RecipientBubble,
ThreadEnvelope,
refs: ['avatarHeader'],
data() {
return {
loading: true,
message: undefined,
errorMessage: '',
error: undefined,
expandedThreads: [],
participantsToDisplay: 999,
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}`
},
remainingParticipants() {
// Returns a string containing all thread participants that are not shown in the avatar-header
return this.threadParticipants.slice(this.participantsToDisplay)
.map(participant => {
return '<a href="mailto:' + participant.email + '">' + participant.label + '</a>'
Cyrille Bollu
committed
})
.join('<br>')
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.updateParticipantsToDisplay)
// Note: This watcher needs 'vue-reactive-refs'
this.$watch('$refs.avatarHeader', this.updateParticipantsToDisplay)
},
beforeDestroy() {
window.removeEventListener('resize', this.updateParticipantsToDisplay)
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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
let childrenWidth = 0
let fits = 0
let skipped = 0
while (childrenWidth < avatarHeader.clientWidth && fits < this.threadParticipants.length) {
// Skipping the 'avatar-more' span
if (avatarHeader.childNodes[fits].clientWidth === undefined) {
fits += 3
skipped = 3
continue
}
childrenWidth += avatarHeader.childNodes[fits].clientWidth
fits++
}
if (fits < this.threadParticipants.length) {
// There's not enough space to show all thread participants
this.participantsToDisplay = fits - 2 - skipped
} 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()
},
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 {
Cyrille Bollu
committed
max-height: 22px;
overflow: hidden;
}
Cyrille Bollu
committed
.avatar-more {
display: inline-block;
}
.avatar-hidden {
visibility: hidden;
}
.app-content-list-item-star.icon-starred {
display: none;
}
.user-bubble__wrapper {
margin-right: 4px;
}
.user-bubble__title {
cursor: pointer;
}