Skip to content

Commit

Permalink
Merge pull request #11508 from nextcloud/feat/7139/add-all-reactions-…
Browse files Browse the repository at this point in the history
…button

feat(Reactions): add All button
  • Loading branch information
DorraJaouad authored Feb 10, 2024
2 parents 6947d67 + 47b50a5 commit 06af154
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,15 @@ describe('Reactions.vue', () => {
const reactionButtons = wrapper.findAllComponents(NcButton)
const emojiPicker = wrapper.findAllComponents(NcEmojiPicker)
// Act
reactionButtons.at(0).vm.$emit('click') // 🎄
reactionButtons.at(1).vm.$emit('click') // 🎄

// Assert
expect(showError).toHaveBeenCalled()
expect(emojiPicker).toHaveLength(0)
expect(reactionButtons).toHaveLength(3)
expect(reactionButtons.at(0).text()).toBe('🎄 2')
expect(reactionButtons.at(1).text()).toBe('🔥 2')
expect(reactionButtons.at(2).text()).toBe('🔒 2')
expect(reactionButtons).toHaveLength(4) // "All" + "🎄" + "🔥" + "🔒" buttons
expect(reactionButtons.at(1).text()).toBe('🎄 2')
expect(reactionButtons.at(2).text()).toBe('🔥 2')
expect(reactionButtons.at(3).text()).toBe('🔒 2')
})

test('doesn\'t mount emoji picker when there are no reactions', () => {
Expand Down Expand Up @@ -255,8 +255,8 @@ describe('Reactions.vue', () => {

// Act
const reactionButtons = wrapper.findAllComponents(NcButton)
reactionButtons.at(0).vm.$emit('click') // 🎄
reactionButtons.at(1).vm.$emit('click') // 🔥
reactionButtons.at(1).vm.$emit('click') // 🎄
reactionButtons.at(2).vm.$emit('click') // 🔥

// Assert
expect(reactionsStore.addReactionToMessage).toHaveBeenCalledWith({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@
<template>
<!-- reactions buttons and popover with details -->
<div class="reactions-wrapper">
<!-- all reactions button -->
<NcButton class="reaction-button"
:title="t('spreed', 'Show all reactions')"
@click="showAllReactions = true">
<HeartOutlineIcon :size="15" />
</NcButton>
<NcPopover v-for="reaction in reactionsSorted"
:key="reaction"
:delay="200"
:focus-trap="false"
:triggers="['hover']"
:popper-triggers="['hover']"
@after-show="fetchReactions">
<template #trigger>
<NcButton :type="userHasReacted(reaction) ? 'primary' : 'secondary'"
Expand All @@ -39,6 +46,11 @@

<div v-if="hasReactions" class="reaction-details">
<span>{{ getReactionSummary(reaction) }}</span>
<NcButton v-if="reactionsCount(reaction) > 3"
type="tertiary-no-background"
@click="showAllReactions = true">
{{ remainingReactionsLabel(reaction) }}
</NcButton>
</div>
<div v-else class="details-loading">
<NcLoadingIcon />
Expand All @@ -53,17 +65,26 @@
@after-show="emitEmojiPickerStatus"
@after-hide="emitEmojiPickerStatus">
<NcButton class="reaction-button"
:title="t('spreed', 'Add more reactions')"
:aria-label="t('spreed', 'Add more reactions')">
<template #icon>
<EmoticonOutline :size="15" />
<EmoticonPlusOutline :size="15" />
</template>
</NcButton>
</NcEmojiPicker>

<!-- all reactions -->
<ReactionsList v-if="showAllReactions"
:token="token"
:detailed-reactions="detailedReactions"
:reactions-sorted="reactionsSorted"
@close="showAllReactions = false" />
</div>
</template>

<script>
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue'
import EmoticonPlusOutline from 'vue-material-design-icons/EmoticonPlusOutline.vue'
import HeartOutlineIcon from 'vue-material-design-icons/HeartOutline.vue'

import { showError } from '@nextcloud/dialogs'

Expand All @@ -72,6 +93,8 @@ import NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcPopover from '@nextcloud/vue/dist/Components/NcPopover.js'

import ReactionsList from './ReactionsList.vue'

import { ATTENDEE } from '../../../../../constants.js'
import { useGuestNameStore } from '../../../../../stores/guestName.js'
import { useReactionsStore } from '../../../../../stores/reactions.js'
Expand All @@ -84,7 +107,9 @@ export default {
NcEmojiPicker,
NcLoadingIcon,
NcPopover,
EmoticonOutline,
ReactionsList,
EmoticonPlusOutline,
HeartOutlineIcon,
},

props: {
Expand Down Expand Up @@ -119,6 +144,12 @@ export default {
}
},

data() {
return {
showAllReactions: false,
}
},

computed: {
hasReactions() {
return Object.keys(Object(this.detailedReactions)).length !== 0
Expand Down Expand Up @@ -215,7 +246,7 @@ export default {
if (!this.hasReactions) {
return ''
}
const list = this.detailedReactions[reaction]
const list = this.detailedReactions[reaction].slice(0, 3)
const summary = []

for (const item in list) {
Expand All @@ -226,13 +257,16 @@ export default {
summary.push(this.getDisplayNameForReaction(list[item]))
}
}

return summary.join(', ')
},

emitEmojiPickerStatus() {
this.$emit('emoji-picker-toggled')
},

remainingReactionsLabel(reaction) {
return n('spreed', 'and %n other participant', 'and %n other participants', this.reactionsCount(reaction) - 3)
},
}
}
</script>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
<!--
- @copyright Copyright (c) 2024 Dorra Jaouad <dorra.jaoued7@gmail.com>
-
- @author Dorra Jaouad <dorra.jaoued7@gmail.com>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
<NcModal size="small"
:container="container"
@close="closeModal">
<div class="reactions__modal">
<h2>
{{ t('spreed', 'Reactions') }}
</h2>
<template v-if="Object.keys(reactionsOverview).length > 0">
<div class="reactions-list__navigation">
<NcButton v-for="reaction in reactionsMenu"
:key="reaction"
:class="{'active' : reactionFilter === reaction, 'all-reactions__button': reaction === '♡'}"
type="tertiary"
@click="handleTabClick(reaction)">
<HeartOutlineIcon v-if="reaction === '♡'" :size="15" />
<span v-else>
{{ reaction }}
</span>
{{ reactionsOverview[reaction].length }}
</NcButton>
</div>
<div class="scrollable">
<NcListItemIcon v-for="item in reactionsOverview[reactionFilter]"
:key="item.actorId + item.actorType"
:name="item.actorDisplayName">
<span class="reactions-emojis">
{{ item.reaction?.join('') ?? reactionFilter }}
</span>
</NcListItemIcon>
</div>
</template>
<NcLoadingIcon v-else :size="64" />
</div>
</NcModal>
</template>

<script>
import HeartOutlineIcon from 'vue-material-design-icons/HeartOutline.vue'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcListItemIcon from '@nextcloud/vue/dist/Components/NcListItemIcon.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'

import { ATTENDEE } from '../../../../../constants.js'
import { useGuestNameStore } from '../../../../../stores/guestName.js'

export default {

name: 'ReactionsList',

components: {
NcModal,
NcLoadingIcon,
NcListItemIcon,
NcButton,
HeartOutlineIcon,
},

props: {
token: {
type: String,
required: true,
},

detailedReactions: {
type: Object,
default: () => {},
},

reactionsSorted: {
type: Array,
default: () => [],
},
},

emits: ['close'],

setup() {
return {
guestNameStore: useGuestNameStore(),
}
},

data() {
return {
reactionFilter: '♡',
}
},

computed: {
container() {
return this.$store.getters.getMainContainerSelector()
},

reactionsOverview() {
const mergedReactionsMap = {}
const modifiedDetailedReactions = {}

Object.entries(this.detailedReactions).forEach(([reaction, actors]) => {
modifiedDetailedReactions[reaction] = []
actors.forEach(actor => {
const key = `${actor.actorId}-${actor.actorType}`
const actorDisplayName = this.getDisplayNameForReaction(actor)

modifiedDetailedReactions[reaction].push({
...actor,
actorDisplayName
})

if (mergedReactionsMap[key]) {
mergedReactionsMap[key].reaction.push(reaction)
} else {
mergedReactionsMap[key] = {
actorDisplayName,
actorId: actor.actorId,
actorType: actor.actorType,
reaction: [reaction]
}
}
})
})

return { '♡': Object.values(mergedReactionsMap), ...modifiedDetailedReactions }
},

reactionsMenu() {
return ['♡', ...this.reactionsSorted]
},
},

methods: {
closeModal() {
this.$emit('close')
},

getDisplayNameForReaction(reactingParticipant) {
if (reactingParticipant.actorType === ATTENDEE.ACTOR_TYPE.GUESTS) {
return this.guestNameStore.getGuestNameWithGuestSuffix(this.token, reactingParticipant.actorId)
}

const displayName = reactingParticipant.actorDisplayName.trim()
if (displayName === '') {
return t('spreed', 'Deleted user')
}

return displayName
},

handleTabClick(reaction) {
this.reactionFilter = reaction
},
},
}
</script>
<style lang="scss" scoped>
.reactions__modal{
min-height: 450px;
padding: 18px;
}
.reactions-list__navigation {
display: flex;
gap: 2px;
flex-wrap: wrap;

:deep(.button-vue) {
border-radius: var(--border-radius-large);
&.active {
background-color: var(--color-primary-element-light);
}
}
}

.all-reactions__button :deep(.button-vue__text) {
display: inline-flex;
gap: 4px;
}

.scrollable {
overflow-y: auto;
overflow-x: hidden;
height: calc(450px - 123px); // 123px is the height of the header 105px and the footer 18px
}

.reactions-emojis {
max-width: 180px;
direction: rtl;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
position: relative;
}
</style>

0 comments on commit 06af154

Please sign in to comment.