diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2022-03-24 19:00:32 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-24 19:00:32 +0300 |
commit | 3b43e063fd7a4d199e39684a64dc34cdca914ab4 (patch) | |
tree | ffa5de59cb4cf16b3eb52d149696c880ac888679 /src | |
parent | ceb11c67a8be1733c3aa5ced93ed02b64874ade8 (diff) | |
parent | 55923f471c8aed4dcc2ac6c0bd542d188a6a209f (diff) |
Merge pull request #7024 from nextcloud/feature/noid/reactions-part-two
👍 Reactions part two
Diffstat (limited to 'src')
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> |