Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show recipients in each thread envelope #4315

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/RecipientBubble.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<template>
<UserBubble :display-name="label"
:avatar-image="avatarUrlAbsolute"
:avatar-only="true"
@click="onClick">
<span class="user-bubble-email">{{ email }}</span>
</UserBubble>
Expand Down
205 changes: 205 additions & 0 deletions src/components/RecipientList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<template>
<div class="avatar-header">
<div v-if="label" ref="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"
class="avatar"
: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>
<!-- Optional caret for showing more information -->
<div v-if="showCaret" class="caret">
<div :class="expandCaret ? 'icon-triangle-n' : 'icon-triangle-s'"
@click="$emit('click-caret')" />
</div>
<!-- Remaining participants, if any (Needed to have avatarHeader reactive) -->
<RecipientBubble v-for="participant in participants.slice(participantsToDisplay)"
:key="participant.email"
class="avatar 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: 'RecipientList',
components: {
RecipientBubble,
Popover,
},
props: {
label: {
type: String,
required: false,
default: '',
},
participants: {
type: Array,
required: true,
},
showCaret: {
type: Boolean,
required: false,
default: false,
},
expandCaret: {
type: Boolean,
required: false,
default: false,
},
},
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.participants) {
return
}

// Only include recipient bubbles
const children = Array
.from(this.$el.childNodes)
.filter((node) => node.nodeType === Node.ELEMENT_NODE)
.filter((node) => node.classList.contains('avatar'))

// Reserve 100px for the avatar-more span and header toggle caret
const maxWidth = this.$el.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.$refs.label) {
childrenWidth += getFullWidth(this.$refs.label)
}

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 + .avatar {
margin-left: 4px;
}

.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__title {
cursor: pointer;
}

.label {
display: inline-block;
margin-right: 2px;
vertical-align: middle;

span {
height: 20px;
line-height: 20px;
vertical-align: top;
}
}

.caret {
display: inline-block;
vertical-align: middle;

div {
display: inline-block;
vertical-align: top;
width: 20px;
height: 20px;
background-color: var(--color-background-dark);
border-radius: 10px;
cursor: pointer;
}
}
</style>
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>
<RecipientList :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 RecipientList from './RecipientList'

export default {
name: 'Thread',
components: {
RecipientBubble,
AppContentDetails,
Loading,
ThreadEnvelope,
Popover,
RecipientList,
},

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>
Loading