Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2022-03-24 19:00:32 +0300
committerGitHub <noreply@github.com>2022-03-24 19:00:32 +0300
commit3b43e063fd7a4d199e39684a64dc34cdca914ab4 (patch)
treeffa5de59cb4cf16b3eb52d149696c880ac888679 /src
parentceb11c67a8be1733c3aa5ced93ed02b64874ade8 (diff)
parent55923f471c8aed4dcc2ac6c0bd542d188a6a209f (diff)
Merge pull request #7024 from nextcloud/feature/noid/reactions-part-two
👍 Reactions part two
Diffstat (limited to 'src')
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/Message.spec.js158
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/Message.vue71
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue61
3 files changed, 206 insertions, 84 deletions
diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js
index 608fe10a8..3f73def72 100644
--- a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js
+++ b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js
@@ -4,6 +4,8 @@ import { cloneDeep } from 'lodash'
import { EventBus } from '../../../../services/EventBus'
import storeConfig from '../../../../store/storeConfig'
import { CONVERSATION, ATTENDEE } from '../../../../constants'
+
+// Components
import Check from 'vue-material-design-icons/Check'
import CheckAll from 'vue-material-design-icons/CheckAll'
import Quote from '../../../Quote'
@@ -13,9 +15,9 @@ import DeckCard from './MessagePart/DeckCard'
import Location from './MessagePart/Location'
import DefaultParameter from './MessagePart/DefaultParameter'
import MessageButtonsBar from './MessageButtonsBar/MessageButtonsBar.vue'
-
import Message from './Message'
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
// needed because of https://github.com/vuejs/vue-test-utils/issues/1507
const RichTextStub = {
@@ -561,7 +563,7 @@ describe('Message.vue', () => {
expect(messageButtonsBar.exists()).toBe(false)
})
- test('actions become visible on mouse over', async () => {
+ test('Buttons bar is rendered on mouse over', async () => {
messageProps.sendingFailure = 'timeout'
const wrapper = mount(Message, {
localVue,
@@ -569,26 +571,23 @@ describe('Message.vue', () => {
propsData: messageProps,
})
- await wrapper.vm.$nextTick()
-
- const messageButtonsBar = wrapper.findComponent(MessageButtonsBar)
-
+ // Initial state
expect(wrapper.vm.showMessageButtonsBar).toBe(false)
- expect(messageButtonsBar.isVisible()).toBe(false)
-
- await wrapper.find('.message-body').trigger('mouseover')
+ expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(false)
+ // Mouseover
+ await wrapper.find('.message').trigger('mouseover')
expect(wrapper.vm.showMessageButtonsBar).toBe(true)
- expect(messageButtonsBar.isVisible()).toBe(true)
-
- await wrapper.find('.message-body').trigger('mouseleave')
-
- expect(wrapper.vm.showMessageButtonsBar).toBe(false)
- expect(messageButtonsBar.isVisible()).toBe(false)
+ expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(true)
- // actions are always present and rendered
+ // actions are present and rendered when the buttonsBar is renderend
const actions = wrapper.findAllComponents({ name: 'Actions' })
expect(actions.length).toBe(2)
+
+ // Mouseleave
+ await wrapper.find('.message').trigger('mouseleave')
+ expect(wrapper.vm.showMessageButtonsBar).toBe(false)
+ expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(false)
})
})
@@ -614,7 +613,10 @@ describe('Message.vue', () => {
propsData: messageProps,
})
- wrapper.find(MessageButtonsBar).vm.$emit('delete')
+ // Hover the messages in order to render the MessageButtonsBar
+ // component
+ await wrapper.find('.message').trigger('mouseover')
+ wrapper.findComponent(MessageButtonsBar).vm.$emit('delete')
expect(deleteMessage).toHaveBeenCalledWith(expect.anything(), {
message: {
@@ -751,4 +753,126 @@ describe('Message.vue', () => {
expect(wrapper.findComponent(CheckAll).exists()).toBe(false)
})
})
+
+ describe('reactions', () => {
+ beforeEach(() => {
+ testStoreConfig.modules.messagesStore.getters.message
+ = jest.fn().mockReturnValue(() => {
+ return {
+ reactions: {
+ '❤️': 1,
+ '👍': 7,
+ },
+ id: messageProps.id,
+ }
+ })
+ testStoreConfig.modules.messagesStore.getters.hasReactions
+ = jest.fn().mockReturnValue(() => {
+ return true
+ })
+ store = new Store(testStoreConfig)
+ })
+
+ test('properly shows reactions', () => {
+ const wrapper = shallowMount(Message, {
+ localVue,
+ store,
+ propsData: messageProps,
+ })
+
+ const reactionsBar = wrapper.find('.message-body__reactions')
+ expect(reactionsBar.isVisible()).toBe(true)
+
+ })
+
+ test('shows reaction buttons with the right emoji count', () => {
+ const wrapper = shallowMount(Message, {
+ localVue,
+ store,
+ propsData: messageProps,
+ })
+
+ const reactionsBar = wrapper.find('.message-body__reactions')
+
+ // Array of buttons
+ const reactionButtons = reactionsBar.findAll('.reaction-button')
+
+ // Number of buttons, 2 passed into the getter and 1 is the emoji
+ // picker
+ expect(reactionButtons.length).toBe(3)
+
+ // Text of the buttons
+ expect(reactionButtons.wrappers[0].text()).toBe('❤️ 1')
+ expect(reactionButtons.wrappers[1].text()).toBe('👍 7')
+ })
+
+ test('dispatches store action upon picking an emoji from the emojipicker', () => {
+ const addReactionToMessageAction = jest.fn()
+ const userHasReactedGetter = jest.fn().mockReturnValue(() => false)
+ testStoreConfig.modules.quoteReplyStore.actions.addReactionToMessage = addReactionToMessageAction
+ testStoreConfig.modules.messagesStore.getters.userHasReacted = userHasReactedGetter
+
+ store = new Store(testStoreConfig)
+
+ const wrapper = shallowMount(Message, {
+ localVue,
+ store,
+ propsData: messageProps,
+ stubs: {
+ EmojiPicker,
+ },
+ data() {
+ return {
+ detailedReactionsRequested: true,
+ }
+ },
+ })
+
+ const emojiPicker = wrapper.findComponent(EmojiPicker)
+
+ emojiPicker.vm.$emit('select', '❤️')
+
+ expect(addReactionToMessageAction).toHaveBeenCalledWith(expect.anything(), {
+ token: messageProps.token,
+ messageId: messageProps.id,
+ selectedEmoji: '❤️',
+ actorId: messageProps.actorId,
+ })
+
+ })
+
+ test('dispatches store action to remove an emoji upon clicking reaction button', async () => {
+ const removeReactionFromMessageAction = jest.fn()
+ const userHasReactedGetter = jest.fn().mockReturnValue(() => true)
+ testStoreConfig.modules.quoteReplyStore.actions.removeReactionFromMessage = removeReactionFromMessageAction
+ testStoreConfig.modules.messagesStore.getters.userHasReacted = userHasReactedGetter
+
+ store = new Store(testStoreConfig)
+
+ const wrapper = shallowMount(Message, {
+ localVue,
+ store,
+ propsData: messageProps,
+ data() {
+ return {
+ detailedReactionsRequested: true,
+ }
+ },
+ })
+
+ // Click reaction button upon having already reacted
+ await wrapper.find('.reaction-button').trigger('click')
+
+ await wrapper.vm.$nextTick()
+ await wrapper.vm.$nextTick()
+
+ expect(removeReactionFromMessageAction).toHaveBeenCalledWith(expect.anything(), {
+ token: messageProps.token,
+ messageId: messageProps.id,
+ selectedEmoji: '❤️',
+ actorId: messageProps.actorId,
+ })
+
+ })
+ })
})
diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue
index 8cbe9650f..0998c2d02 100644
--- a/src/components/MessagesList/MessagesGroup/Message/Message.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue
@@ -31,11 +31,12 @@ the main body of the message as well as a quote.
:data-seen="seen"
:data-next-message-id="nextMessageId"
:data-previous-message-id="previousMessageId"
- class="message">
+ class="message"
+ tabindex="0"
+ @mouseover="handleMouseover"
+ @mouseleave="handleMouseleave">
<div :class="{'normal-message-body': !isSystemMessage && !isDeletedMessage, 'system' : isSystemMessage}"
- class="message-body"
- @mouseover="handleMouseover"
- @mouseleave="handleMouseleave">
+ class="message-body">
<div v-if="isFirstMessage && showAuthor"
class="message-body__author"
role="heading"
@@ -137,7 +138,7 @@ the main body of the message as well as a quote.
</Popover>
<!-- More reactions picker -->
- <EmojiPicker :per-line="5" @select="addReactionToMessage">
+ <EmojiPicker :per-line="5" :container="`#message_${id}`" @select="handleReactionClick">
<button class="reaction-button">
<EmoticonOutline :size="15" />
</button>
@@ -146,9 +147,10 @@ the main body of the message as well as a quote.
</div>
<!-- Message actions -->
- <MessageButtonsBar v-if="hasMessageButtonsBar"
- v-show="showMessageButtonsBar"
+ <MessageButtonsBar v-if="showMessageButtonsBar"
ref="messageButtonsBar"
+ :is-action-menu-open.sync="isActionMenuOpen"
+ :is-emoji-picker-open.sync="isEmojiPickerOpen"
:message-api-data="messageApiData"
:message-object="messageObject"
v-bind="$props"
@@ -348,7 +350,7 @@ export default {
data() {
return {
- showMessageButtonsBar: false,
+ isHovered: false,
// Is tall enough for both actions and date upon hovering
isTallEnough: false,
showReloadButton: false,
@@ -356,6 +358,7 @@ export default {
// whether the message was seen, only used if this was marked as last read message
seen: false,
isActionMenuOpen: false,
+ isEmojiPickerOpen: false,
detailedReactionsRequested: false,
}
},
@@ -502,11 +505,11 @@ export default {
return false
}
- return this.isSystemMessage || !this.showMessageButtonsBar || this.isTallEnough
+ return this.isSystemMessage || !this.isHovered || this.isTallEnough
},
- hasMessageButtonsBar() {
- return !this.isSystemMessage && !this.isTemporary
+ showMessageButtonsBar() {
+ return !this.isSystemMessage && !this.isTemporary && (this.isHovered || this.isActionMenuOpen || this.isEmojiPickerOpen)
},
isTemporaryUpload() {
@@ -616,8 +619,9 @@ export default {
},
handleMouseover() {
- this.showMessageButtonsBar = true
-
+ if (!this.isHovered) {
+ this.isHovered = true
+ }
},
handleReactionsMouseOver() {
@@ -627,8 +631,8 @@ export default {
},
handleMouseleave() {
- if (!this.isActionMenuOpen) {
- this.showMessageButtonsBar = false
+ if (this.isHovered) {
+ this.isHovered = false
}
},
@@ -657,7 +661,6 @@ export default {
const currentUserHasReacted = this.$store.getters.userHasReacted(this.actorId, this.token, this.id, clickedEmoji)
if (!currentUserHasReacted) {
- console.debug('adding reaction')
this.$store.dispatch('addReactionToMessage', {
token: this.token,
messageId: this.id,
@@ -665,7 +668,6 @@ export default {
actorId: this.actorId,
})
} else {
- console.debug('user has already reacted, removing reaction')
this.$store.dispatch('removeReactionFromMessage', {
token: this.token,
messageId: this.id,
@@ -708,21 +710,6 @@ export default {
this.isDeleting = false
},
-
- addReactionToMessage(selectedEmoji) {
- // Add reaction only if user hasn't reacted yet
- if (!this.$store.getters.userHasReacted(this.actorId, this.token, this.messageObject.id, selectedEmoji)) {
- this.$store.dispatch('addReactionToMessage', {
- token: this.token,
- messageId: this.messageObject.id,
- selectedEmoji,
- actorId: this.actorId,
- })
- } else {
- console.debug('Current user has already reacted')
- }
-
- },
},
}
</script>
@@ -731,11 +718,9 @@ export default {
@import '../../../../assets/variables';
@import '../../../../assets/buttons';
-.normal-message-body {
- &:hover {
- border-radius: 8px;
- background-color: var(--color-background-hover);
- }
+.message:hover .normal-message-body {
+ border-radius: 8px;
+ background-color: var(--color-background-hover);
}
.message {
@@ -807,6 +792,7 @@ export default {
}
&__reactions {
display: flex;
+ flex-wrap: wrap;
margin: 4px 0 4px -2px;
}
}
@@ -824,16 +810,9 @@ export default {
padding: 4px 4px 4px 8px;
}
-.hover, .highlight-animation {
- border-radius: 8px;
-}
-
-.hover {
- background-color: var(--color-background-hover);
-}
-
.highlight-animation {
animation: highlight-animation 5s 1;
+ border-radius: 8px;
}
@keyframes highlight-animation {
@@ -880,7 +859,7 @@ export default {
padding: 0 8px !important;
font-weight: normal !important;
- margin: 0 2px;
+ margin: 2px;
height: 26px;
background-color: var(--color-main-background);
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
index d1dc56aeb..fd7b7ca43 100644
--- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
@@ -23,7 +23,8 @@
<!-- Message Actions -->
<div class="message-buttons-bar">
<template v-if="page === 0">
- <Button type="tertiary"
+ <Button v-if="acceptsReactions"
+ type="tertiary"
@click="page = 1">
<template #icon>
<EmoticonOutline :size="20" />
@@ -36,8 +37,10 @@
</ActionButton>
</Actions>
<Actions :force-menu="true"
- :container="container"
- :boundaries-element="containerElement">
+ :container="`#message_${id}`"
+ :boundaries-element="containerElement"
+ @open="onMenuOpen"
+ @close="onMenuClose">
<ActionButton v-if="isPrivateReplyable"
icon="icon-user"
:close-after-click="true"
@@ -100,18 +103,21 @@
</template>
</Button>
<Button type="tertiary"
- @click="addReactionToMessage('👍')">
+ @click="handleReactionClick('👍')">
<template #icon>
<span>👍</span>
</template>
</Button>
<Button type="tertiary"
- @click="addReactionToMessage('❤️')">
+ @click="handleReactionClick('❤️')">
<template #icon>
<span>❤️</span>
</template>
</Button>
- <EmojiPicker @select="addReactionToMessage">
+ <EmojiPicker :container="`#message_${id}`"
+ @select="handleReactionClick"
+ @after-show="onEmojiPickerOpen"
+ @after-hide="onEmojiPickerClose">
<Button type="tertiary">
<template #icon>
<Plus :size="20" />
@@ -289,10 +295,6 @@ export default {
return this.$store.getters.conversation(this.token)
},
- container() {
- return this.$store.getters.getMainContainerSelector()
- },
-
containerElement() {
return document.querySelector(this.container)
},
@@ -325,6 +327,10 @@ export default {
&& this.$store.getters.getActorType() === ATTENDEE.ACTOR_TYPE.USERS
},
+ acceptsReactions() {
+ return !this.isConversationReadOnly && !this.isFileShare
+ },
+
messageActions() {
return this.$store.getters.messageActions
},
@@ -372,15 +378,6 @@ export default {
EventBus.$emit('focus-chat-input')
},
- handleActionMenuUpdate(type) {
- if (type === 'open') {
- this.isActionMenuOpen = true
- } else if (type === 'close') {
- this.isActionMenuOpen = false
- this.showActions = false
- }
- },
-
async handlePrivateReply() {
// open the 1:1 conversation
const conversation = await this.$store.dispatch('createOneToOneConversation', this.actorId)
@@ -410,7 +407,7 @@ export default {
await this.$store.dispatch('fetchConversation', { token: this.token })
},
- addReactionToMessage(selectedEmoji) {
+ handleReactionClick(selectedEmoji) {
// Add reaction only if user hasn't reacted yet
if (!this.$store.getters.userHasReacted(this.actorId, this.token, this.messageObject.id, selectedEmoji)) {
this.$store.dispatch('addReactionToMessage', {
@@ -420,7 +417,13 @@ export default {
actorId: this.actorId,
})
} else {
- console.debug('Current user has already reacted')
+ console.debug('user has already reacted, removing reaction')
+ this.$store.dispatch('removeReactionFromMessage', {
+ token: this.token,
+ messageId: this.id,
+ selectedEmoji,
+ actorId: this.actorId,
+ })
}
},
@@ -428,6 +431,22 @@ export default {
handleDelete() {
this.$emit('delete')
},
+
+ onMenuOpen() {
+ this.$emit('update:isActionMenuOpen', true)
+ },
+
+ onMenuClose() {
+ this.$emit('update:isActionMenuOpen', false)
+ },
+
+ onEmojiPickerOpen() {
+ this.$emit('update:isEmojiPickerOpen', true)
+ },
+
+ onEmojiPickerClose() {
+ this.$emit('update:isEmojiPickerOpen', false)
+ },
},
}
</script>