Skip to content

Commit

Permalink
Show recipients in each thread envelope
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
  • Loading branch information
st3iny committed Feb 22, 2021
1 parent 74d60ae commit 6107a97
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 105 deletions.
104 changes: 3 additions & 101 deletions src/components/Thread.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,7 @@
<h2 :title="threadSubject">
{{ threadSubject }}
</h2>
<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"
:label="participant.label" />
<!-- Indicator to show that there are more participants than displayed -->
<Popover v-if="threadParticipants.length > participantsToDisplay"
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" />
</Popover>
<!-- Remaining participants, if any (Needed to have avatarHeader reactive) -->
<RecipientBubble v-for="participant in threadParticipants.slice(participantsToDisplay)"
:key="participant.email"
class="avatar-hidden"
:email="participant.email"
:label="participant.label" />
</div>
<ThreadAvatarHeader :participants="threadParticipants" />
</div>
</div>
<ThreadEnvelope v-for="env in thread"
Expand All @@ -47,25 +24,22 @@

<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'
import ThreadAvatarHeader from './ThreadAvatarHeader'
export default {
name: 'Thread',
components: {
RecipientBubble,
AppContentDetails,
Loading,
ThreadEnvelope,
Popover,
ThreadAvatarHeader,
},
data() {
Expand All @@ -75,16 +49,10 @@ export default {
errorMessage: '',
error: undefined,
expandedThreads: [],
participantsToDisplay: 999,
resizeDebounced: debounce(500, this.updateParticipantsToDisplay),
}
},
computed: {
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)
},
Expand Down Expand Up @@ -123,49 +91,8 @@ export default {
},
created() {
this.resetThread()
window.addEventListener('resize', this.resizeDebounced)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeDebounced)
},
methods: {
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
let idx = 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++
idx++
}
if (childrenWidth > maxWidth) {
// There's not enough space to show all thread participants
if (fits > 1) {
this.participantsToDisplay = fits - 1
} else if (fits === 0) {
this.participantsToDisplay = 1
} else {
this.participantsToDisplay = fits
}
} 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}`)
Expand All @@ -191,7 +118,6 @@ export default {
async resetThread() {
this.expandedThreads = [this.threadId]
await this.fetchThread()
this.updateParticipantsToDisplay()
},
async fetchThread() {
this.loading = true
Expand Down Expand Up @@ -376,31 +302,7 @@ export default {
user-select: text;
}
.avatar-header {
max-height: 24px;
overflow: hidden;
}
.avatar-more {
display: inline;
background-color: var(--color-background-dark);
padding: 0px 0px 1px 1px;
border-radius: 10px;
cursor: pointer;
}
.avatar-hidden {
visibility: hidden;
}
.popover__wrapper {
max-width: 500px;
}
.app-content-list-item-star.icon-starred {
display: none;
}
.user-bubble__wrapper {
margin-right: 4px;
}
.user-bubble__title {
cursor: pointer;
}
</style>
175 changes: 175 additions & 0 deletions src/components/ThreadAvatarHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<template>
<div ref="avatarHeader" class="avatar-header">
<div v-if="label" class="label">
<span>{{ label }}</span>
</div>
<!-- Participants that can fit in the parent div -->
<RecipientBubble v-for="participant in participants.slice(0,participantsToDisplay)"
:key="participant.email"
:email="participant.email"
:label="participant.label" />
<!-- Indicator to show that there are more participants than displayed -->
<Popover v-if="participants.length > participantsToDisplay" class="avatar-more">
<span slot="trigger">
{{ moreParticipantsString }}
</span>
<RecipientBubble v-for="participant in participants.slice(participantsToDisplay)"
:key="participant.email"
:email="participant.email"
:label="participant.label" />
</Popover>
<!-- Remaining participants, if any (Needed to have avatarHeader reactive) -->
<RecipientBubble v-for="participant in participants.slice(participantsToDisplay)"
:key="participant.email"
class="avatar-hidden"
:email="participant.email"
:label="participant.label" />
</div>
</template>

<script>
import Popover from '@nextcloud/vue/dist/Components/Popover'
import RecipientBubble from './RecipientBubble'
import debounce from 'lodash/fp/debounce'
export default {
name: 'ThreadAvatarHeader',
components: {
RecipientBubble,
Popover,
},
props: {
label: {
type: String,
required: false,
default: '',
},
participants: {
type: Array,
required: true,
},
},
data() {
return {
participantsToDisplay: 999,
resizeDebounced: debounce(500, this.updateParticipantsToDisplay),
}
},
computed: {
moreParticipantsString() {
// Returns a number showing the number of thread participants that are not shown in the avatar-header
return `+${this.participants.length - this.participantsToDisplay}`
},
},
watch: {
participants() {
this.updateParticipantsToDisplay()
},
},
created() {
window.addEventListener('resize', this.resizeDebounced)
},
mounted() {
this.updateParticipantsToDisplay()
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeDebounced)
},
methods: {
updateParticipantsToDisplay() {
// Wait until everything is in place
if (!this.$refs.avatarHeader || !this.participants) {
return
}
// Only include dom element nodes and ignore avatar-more span
const children = Array
.from(this.$refs.avatarHeader.childNodes)
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
.filter((node) => !node.classList.contains('avatar-more'))
// Reserve 100px for the avatar-more span
const maxWidth = this.$refs.avatarHeader.clientWidth - 100
let childrenWidth = 0
let fits = 0
// Calculate full width including margins
const getFullWidth = (node) => {
const styles = window.getComputedStyle(node)
const margins = parseFloat(styles.marginLeft) + parseFloat(styles.marginRight)
return node.clientWidth + margins
}
// Measure label width if present
if (this.label) {
childrenWidth += getFullWidth(children[0])
children.splice(0, 1)
}
for (const child of children) {
const newWidth = childrenWidth + getFullWidth(child)
if (newWidth > maxWidth) {
break
}
fits++
childrenWidth = newWidth
}
this.participantsToDisplay = fits
},
},
}
</script>

<style lang="scss" scoped>
.avatar-header {
max-height: 24px;
overflow: hidden;
}
.avatar-more {
display: inline-block;
vertical-align: middle;
span {
display: inline-block;
vertical-align: top;
height: 20px;
line-height: 20px;
background-color: var(--color-background-dark);
border-radius: 10px;
cursor: pointer;
padding: 0 4px;
}
}
.avatar-hidden {
visibility: hidden;
}
.popover__wrapper {
max-width: 500px;
}
::v-deep .user-bubble__wrapper:not(:last-child) {
margin-right: 4px;
}
::v-deep .user-bubble__title {
cursor: pointer;
}
.label {
display: inline-block;
margin-right: 2px;
vertical-align: middle;
span {
height: 20px;
line-height: 20px;
vertical-align: top;
}
}
</style>
25 changes: 21 additions & 4 deletions src/components/ThreadEnvelope.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,21 @@
</div>
</div>
<Loading v-if="loading" />
<Message v-else-if="message"
:envelope="envelope"
:message="message"
:full-height="fullHeight" />
<template v-else-if="message">
<ThreadAvatarHeader
class="recipients"
:label="`${t('mail', 'To')}:`"
:participants="envelope.to" />
<ThreadAvatarHeader
v-if="envelope.cc.length > 0"
class="recipients"
:label="`${t('mail', 'Cc')}:`"
:participants="envelope.cc" />
<Message
:envelope="envelope"
:message="message"
:full-height="fullHeight" />
</template>
<Error v-else-if="error"
:error="error && error.message ? error.message : t('mail', 'Not found')"
:message="errorMessage"
Expand All @@ -91,6 +102,7 @@ import MenuEnvelope from './MenuEnvelope'
import Moment from './Moment'
import Avatar from './Avatar'
import importantSvg from '../../img/important.svg'
import ThreadAvatarHeader from './ThreadAvatarHeader'
import { buildRecipients as buildReplyRecipients } from '../ReplyBuilder'

export default {
Expand All @@ -102,6 +114,7 @@ export default {
Moment,
Message,
Avatar,
ThreadAvatarHeader,
},
props: {
envelope: {
Expand Down Expand Up @@ -343,5 +356,9 @@ export default {
.left:not(.seen) {
font-weight: bold;
}
.recipients {
margin-left: 60px;
margin-right: 38px;
}

</style>

0 comments on commit 6107a97

Please sign in to comment.