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
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar')
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js437
-rw-r--r--src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue453
2 files changed, 890 insertions, 0 deletions
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js
new file mode 100644
index 000000000..78d91b6ec
--- /dev/null
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js
@@ -0,0 +1,437 @@
+import Vuex, { Store } from 'vuex'
+import { createLocalVue, shallowMount } from '@vue/test-utils'
+import { cloneDeep } from 'lodash'
+import storeConfig from '../../../../../store/storeConfig'
+import { CONVERSATION, PARTICIPANT, ATTENDEE } from '../../../../../constants'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import { findActionButton } from '../../../../../test-helpers'
+import MessageButtonsBar from './../MessageButtonsBar/MessageButtonsBar.vue'
+
+describe('MessageButtonsBar.vue', () => {
+ const TOKEN = 'XXTOKENXX'
+ let localVue
+ let testStoreConfig
+ let store
+ let messageProps
+ let conversationProps
+ let getActorTypeMock
+
+ beforeEach(() => {
+ localVue = createLocalVue()
+ localVue.use(Vuex)
+
+ conversationProps = {
+ token: TOKEN,
+ lastCommonReadMessage: 0,
+ type: CONVERSATION.TYPE.GROUP,
+ readOnly: CONVERSATION.STATE.READ_WRITE,
+ }
+
+ testStoreConfig = cloneDeep(storeConfig)
+ testStoreConfig.modules.tokenStore.getters.getToken
+ = jest.fn().mockReturnValue(() => TOKEN)
+ testStoreConfig.modules.conversationsStore.getters.conversation
+ = jest.fn().mockReturnValue((token) => conversationProps)
+ testStoreConfig.modules.actorStore.getters.getActorId
+ = jest.fn().mockReturnValue(() => 'user-id-1')
+ getActorTypeMock = jest.fn().mockReturnValue(() => ATTENDEE.ACTOR_TYPE.USERS)
+ testStoreConfig.modules.actorStore.getters.getActorType = getActorTypeMock
+
+ messageProps = {
+ message: 'test message',
+ actorType: ATTENDEE.ACTOR_TYPE.USERS,
+ actorId: 'user-id-1',
+ actorDisplayName: 'user-display-name-1',
+ messageParameters: {},
+ id: 123,
+ isTemporary: false,
+ isFirstMessage: true,
+ isReplyable: true,
+ timestamp: new Date('2020-05-07 09:23:00').getTime() / 1000,
+ token: TOKEN,
+ systemMessage: '',
+ messageType: 'comment',
+ previousMessageId: 100,
+ messageObject: {},
+ messageApiData: {
+ apiDummyData: 1,
+ },
+ participant: {
+ actorId: 'user-id-1',
+ actorType: ATTENDEE.ACTOR_TYPE.USERS,
+ participantType: PARTICIPANT.TYPE.USER,
+ },
+ }
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ describe('actions', () => {
+
+ beforeEach(() => {
+ store = new Store(testStoreConfig)
+ })
+
+ describe('reply action', () => {
+ test('replies to message', async () => {
+ const replyAction = jest.fn()
+ testStoreConfig.modules.quoteReplyStore.actions.addMessageToBeReplied = replyAction
+ store = new Store(testStoreConfig)
+
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ stubs: {
+ ActionButton,
+ },
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Reply')
+ expect(actionButton.exists()).toBe(true)
+ expect(actionButton.isVisible()).toBe(true)
+ await actionButton.find('button').trigger('click')
+
+ expect(replyAction).toHaveBeenCalledWith(expect.anything(), {
+ id: 123,
+ actorId: 'user-id-1',
+ actorType: 'users',
+ actorDisplayName: 'user-display-name-1',
+ message: 'test message',
+ messageParameters: {},
+ messageType: 'comment',
+ systemMessage: '',
+ timestamp: new Date('2020-05-07 09:23:00').getTime() / 1000,
+ token: TOKEN,
+ previousMessageId: 100,
+ })
+ })
+
+ test('hides reply button when not replyable', async () => {
+ messageProps.isReplyable = false
+ store = new Store(testStoreConfig)
+
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ stubs: {
+ ActionButton,
+ },
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Reply')
+ expect(actionButton.isVisible()).toBe(false)
+ })
+ })
+
+ describe('private reply action', () => {
+ test('creates a new conversation when replying to message privately', async () => {
+ const routerPushMock = jest.fn().mockResolvedValue()
+ const createOneToOneConversation = jest.fn()
+ testStoreConfig.modules.conversationsStore.actions.createOneToOneConversation = createOneToOneConversation
+ store = new Store(testStoreConfig)
+
+ messageProps.actorId = 'another-user'
+
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ mocks: {
+ $router: {
+ push: routerPushMock,
+ },
+ },
+ stubs: {
+ ActionButton,
+ },
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Reply privately')
+ expect(actionButton.exists()).toBe(true)
+
+ createOneToOneConversation.mockResolvedValueOnce({
+ token: 'new-token',
+ })
+
+ await actionButton.find('button').trigger('click')
+
+ expect(createOneToOneConversation).toHaveBeenCalledWith(expect.anything(), 'another-user')
+
+ expect(routerPushMock).toHaveBeenCalledWith({
+ name: 'conversation',
+ params: {
+ token: 'new-token',
+ },
+ })
+ })
+
+ /**
+ * @param {boolean} visible Whether or not the reply-private action is visible
+ */
+ function testPrivateReplyActionVisible(visible) {
+ store = new Store(testStoreConfig)
+
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ stubs: {
+ ActionButton,
+ },
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Reply privately')
+ expect(actionButton.exists()).toBe(visible)
+ }
+
+ test('hides private reply action for own messages', async () => {
+ // using default message props which have the
+ // actor id set to the current user
+ testPrivateReplyActionVisible(false)
+ })
+
+ test('hides private reply action for one to one conversation type', async () => {
+ messageProps.actorId = 'another-user'
+ conversationProps.type = CONVERSATION.TYPE.ONE_TO_ONE
+ testPrivateReplyActionVisible(false)
+ })
+
+ test('hides private reply action for guest messages', async () => {
+ messageProps.actorId = 'guest-user'
+ messageProps.actorType = ATTENDEE.ACTOR_TYPE.GUESTS
+ testPrivateReplyActionVisible(false)
+ })
+
+ test('hides private reply action when current user is a guest', async () => {
+ messageProps.actorId = 'another-user'
+ getActorTypeMock.mockClear().mockReturnValue(() => ATTENDEE.ACTOR_TYPE.GUESTS)
+ testPrivateReplyActionVisible(false)
+ })
+ })
+
+ describe('delete action', () => {
+ test('emits delete event', async () => {
+ // need to mock the date to be within 6h
+ const mockDate = new Date('2020-05-07 10:00:00')
+ jest.spyOn(global.Date, 'now')
+ .mockImplementation(() => mockDate)
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ stubs: {
+ ActionButton,
+ },
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Delete')
+ expect(actionButton.exists()).toBe(true)
+
+ await actionButton.find('button').trigger('click')
+
+ expect(wrapper.emitted().delete).toBeTruthy()
+ })
+
+ /**
+ * @param {boolean} visible Whether or not the delete action is visible
+ * @param {Date} mockDate The message date (deletion only works within 6h)
+ * @param {number} participantType The participant type of the user
+ */
+ function testDeleteMessageVisible(visible, mockDate, participantType = PARTICIPANT.TYPE.USER) {
+ store = new Store(testStoreConfig)
+
+ // need to mock the date to be within 6h
+ if (!mockDate) {
+ mockDate = new Date('2020-05-07 10:00:00')
+ }
+
+ jest.spyOn(global.Date, 'now')
+ .mockImplementation(() => mockDate)
+
+ messageProps.participant.participantType = participantType
+
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ stubs: {
+ ActionButton,
+ },
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Delete')
+ expect(actionButton.exists()).toBe(visible)
+ }
+
+ test('hides delete action when message is older than 6 hours', () => {
+ testDeleteMessageVisible(false, new Date('2020-05-07 15:24:00'))
+ })
+
+ test('hides delete action when the conversation is read-only', () => {
+ conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY
+ testDeleteMessageVisible(false)
+ })
+
+ test('hides delete action for file messages', () => {
+ messageProps.message = '{file}'
+ messageProps.messageParameters.file = {}
+ testDeleteMessageVisible(false)
+ })
+
+ test('hides delete action on other people messages for non-moderators', () => {
+ messageProps.actorId = 'another-user'
+ conversationProps.type = CONVERSATION.TYPE.GROUP
+ testDeleteMessageVisible(false)
+ })
+
+ test('shows delete action on other people messages for moderators', () => {
+ messageProps.actorId = 'another-user'
+ conversationProps.type = CONVERSATION.TYPE.GROUP
+ testDeleteMessageVisible(true, null, PARTICIPANT.TYPE.MODERATOR)
+ })
+
+ test('shows delete action on other people messages for owner', () => {
+ messageProps.actorId = 'another-user'
+ conversationProps.type = CONVERSATION.TYPE.PUBLIC
+ testDeleteMessageVisible(true, null, PARTICIPANT.TYPE.OWNER)
+ })
+
+ test('does not show delete action even for guest moderators', () => {
+ messageProps.actorId = 'another-user'
+ conversationProps.type = CONVERSATION.TYPE.PUBLIC
+ testDeleteMessageVisible(false, null, PARTICIPANT.TYPE.GUEST_MODERATOR)
+ })
+
+ test('does not show delete action on other people messages in one to one conversations', () => {
+ messageProps.actorId = 'another-user'
+ conversationProps.type = CONVERSATION.TYPE.ONE_TO_ONE
+ testDeleteMessageVisible(false)
+ })
+ })
+
+ test('marks message as unread', async () => {
+ const updateLastReadMessageAction = jest.fn().mockResolvedValueOnce()
+ const fetchConversationAction = jest.fn().mockResolvedValueOnce()
+ testStoreConfig.modules.conversationsStore.actions.updateLastReadMessage = updateLastReadMessageAction
+ testStoreConfig.modules.conversationsStore.actions.fetchConversation = fetchConversationAction
+ store = new Store(testStoreConfig)
+
+ messageProps.previousMessageId = 100
+
+ // appears even with more restrictive conditions
+ conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY
+ messageProps.actorId = 'another-user'
+ messageProps.participant = {
+ actorId: 'guest-id-1',
+ actorType: ATTENDEE.ACTOR_TYPE.GUESTS,
+ participantType: PARTICIPANT.TYPE.GUEST,
+ }
+
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ stubs: {
+ ActionButton,
+ },
+
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Mark as unread')
+ expect(actionButton.exists()).toBe(true)
+
+ await actionButton.find('button').trigger('click')
+ // needs two updates...
+ await wrapper.vm.$nextTick()
+ await wrapper.vm.$nextTick()
+
+ expect(updateLastReadMessageAction).toHaveBeenCalledWith(expect.anything(), {
+ token: TOKEN,
+ id: 100,
+ updateVisually: true,
+ })
+
+ expect(fetchConversationAction).toHaveBeenCalledWith(expect.anything(), {
+ token: TOKEN,
+ })
+ })
+
+ test('copies message link', async () => {
+ const copyTextMock = jest.fn()
+
+ // appears even with more restrictive conditions
+ conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY
+ messageProps.actorId = 'another-user'
+ messageProps.participant = {
+ actorId: 'guest-id-1',
+ actorType: ATTENDEE.ACTOR_TYPE.GUESTS,
+ participantType: PARTICIPANT.TYPE.GUEST,
+ }
+
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ mocks: {
+ $copyText: copyTextMock,
+ },
+ stubs: {
+ ActionButton,
+ },
+
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'Copy message link')
+ expect(actionButton.exists()).toBe(true)
+
+ await actionButton.find('button').trigger('click')
+
+ expect(copyTextMock).toHaveBeenCalledWith('http://localhost/nc-webroot/call/XXTOKENXX#message_123')
+ })
+
+ test('renders clickable custom actions', async () => {
+ const handler = jest.fn()
+ const handler2 = jest.fn()
+ const actionsGetterMock = jest.fn().mockReturnValue([{
+ label: 'first action',
+ icon: 'some-icon',
+ callback: handler,
+ }, {
+ label: 'second action',
+ icon: 'some-icon2',
+ callback: handler2,
+ }])
+ testStoreConfig.modules.messageActionsStore.getters.messageActions = actionsGetterMock
+ testStoreConfig.modules.messagesStore.getters.message = jest.fn(() => () => messageProps)
+ store = new Store(testStoreConfig)
+ const wrapper = shallowMount(MessageButtonsBar, {
+ localVue,
+ store,
+ stubs: {
+ ActionButton,
+ },
+ propsData: messageProps,
+ })
+
+ const actionButton = findActionButton(wrapper, 'first action')
+ expect(actionButton.exists()).toBe(true)
+ await actionButton.find('button').trigger('click')
+
+ expect(handler).toHaveBeenCalledWith({
+ apiDummyData: 1,
+ },)
+
+ const actionButton2 = findActionButton(wrapper, 'second action')
+ expect(actionButton2.exists()).toBe(true)
+ await actionButton2.find('button').trigger('click')
+
+ expect(handler2).toHaveBeenCalledWith({
+ apiDummyData: 1,
+ })
+ })
+ })
+})
diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
new file mode 100644
index 000000000..d1dc56aeb
--- /dev/null
+++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue
@@ -0,0 +1,453 @@
+<!--
+ - @copyright Copyright (c) 2021 Marco Ambrosini <marcoambrosini@pm.me>
+ -
+ - @author Marco Ambrosini <marcoambrosini@pm.me>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - 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>
+ <!-- Message Actions -->
+ <div class="message-buttons-bar">
+ <template v-if="page === 0">
+ <Button type="tertiary"
+ @click="page = 1">
+ <template #icon>
+ <EmoticonOutline :size="20" />
+ </template>
+ </Button>
+ <Actions v-show="isReplyable">
+ <ActionButton icon="icon-reply"
+ @click.stop="handleReply">
+ {{ t('spreed', 'Reply') }}
+ </ActionButton>
+ </Actions>
+ <Actions :force-menu="true"
+ :container="container"
+ :boundaries-element="containerElement">
+ <ActionButton v-if="isPrivateReplyable"
+ icon="icon-user"
+ :close-after-click="true"
+ @click.stop="handlePrivateReply">
+ {{ t('spreed', 'Reply privately') }}
+ </ActionButton>
+ <ActionButton icon="icon-external"
+ :close-after-click="true"
+ @click.stop.prevent="handleCopyMessageLink">
+ {{ t('spreed', 'Copy message link') }}
+ </ActionButton>
+ <ActionButton :close-after-click="true"
+ @click.stop="handleMarkAsUnread">
+ <template #icon>
+ <EyeOffOutline decorative
+ title=""
+ :size="16" />
+ </template>
+ {{ t('spreed', 'Mark as unread') }}
+ </ActionButton>
+ <ActionLink v-if="linkToFile"
+ icon="icon-text"
+ :href="linkToFile">
+ {{ t('spreed', 'Go to file') }}
+ </ActionLink>
+ <ActionButton v-if="!isCurrentGuest && !isFileShare"
+ :close-after-click="true"
+ @click.stop="showForwarder = true">
+ <Share slot="icon"
+ :size="16"
+ decorative
+ title="" />
+ {{ t('spreed', 'Forward message') }}
+ </ActionButton>
+ <ActionSeparator v-if="messageActions.length > 0" />
+ <template v-for="action in messageActions">
+ <ActionButton :key="action.label"
+ :icon="action.icon"
+ :close-after-click="true"
+ @click="action.callback(messageApiData)">
+ {{ action.label }}
+ </ActionButton>
+ </template>
+ <template v-if="isDeleteable">
+ <ActionSeparator />
+ <ActionButton icon="icon-delete"
+ :close-after-click="true"
+ @click.stop="handleDelete">
+ {{ t('spreed', 'Delete') }}
+ </ActionButton>
+ </template>
+ </Actions>
+ </template>
+
+ <template v-if="page === 1">
+ <Button type="tertiary"
+ @click="page = 0">
+ <template #icon>
+ <ArrowLeft :size="20" />
+ </template>
+ </Button>
+ <Button type="tertiary"
+ @click="addReactionToMessage('👍')">
+ <template #icon>
+ <span>👍</span>
+ </template>
+ </Button>
+ <Button type="tertiary"
+ @click="addReactionToMessage('❤️')">
+ <template #icon>
+ <span>❤️</span>
+ </template>
+ </Button>
+ <EmojiPicker @select="addReactionToMessage">
+ <Button type="tertiary">
+ <template #icon>
+ <Plus :size="20" />
+ </template>
+ </Button>
+ </EmojiPicker>
+ </template>
+ <Forwarder v-if="showForwarder"
+ :message-object="messageObject"
+ @close="showForwarder = false" />
+ </div>
+</template>
+
+<script>
+import { PARTICIPANT, CONVERSATION, ATTENDEE } from '../../../../../constants'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import ActionLink from '@nextcloud/vue/dist/Components/ActionLink'
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionSeparator from '@nextcloud/vue/dist/Components/ActionSeparator'
+import EyeOffOutline from 'vue-material-design-icons/EyeOffOutline'
+import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue'
+import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
+import Plus from 'vue-material-design-icons/Plus.vue'
+import Share from 'vue-material-design-icons/Share'
+import moment from '@nextcloud/moment'
+import { EventBus } from '../../../../../services/EventBus'
+import { generateUrl } from '@nextcloud/router'
+import {
+ showError,
+ showSuccess,
+} from '@nextcloud/dialogs'
+import Forwarder from '../MessagePart/Forwarder'
+import Button from '@nextcloud/vue/dist/Components/Button'
+import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker'
+
+export default {
+ name: 'MessageButtonsBar',
+
+ components: {
+ Actions,
+ ActionButton,
+ ActionLink,
+ EyeOffOutline,
+ Share,
+ ActionSeparator,
+ Forwarder,
+ Button,
+ EmoticonOutline,
+ ArrowLeft,
+ Plus,
+ EmojiPicker,
+ },
+
+ props: {
+ token: {
+ type: String,
+ required: true,
+ },
+
+ previousMessageId: {
+ type: [String, Number],
+ required: true,
+ },
+
+ isReplyable: {
+ type: Boolean,
+ required: true,
+ },
+
+ messageObject: {
+ type: Object,
+ required: true,
+ },
+
+ actorId: {
+ type: String,
+ required: true,
+ },
+
+ actorType: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * The display name of the sender of the message.
+ */
+ actorDisplayName: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * The parameters of the rich object message
+ */
+ messageParameters: {
+ type: [Array, Object],
+ required: true,
+ },
+
+ /**
+ * The message timestamp.
+ */
+ timestamp: {
+ type: Number,
+ default: 0,
+ },
+
+ /**
+ * The message id.
+ */
+ id: {
+ type: [String, Number],
+ required: true,
+ },
+
+ /**
+ * The parent message's id.
+ */
+ parent: {
+ type: Number,
+ default: 0,
+ },
+
+ /**
+ * The message or quote text.
+ */
+ message: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * The type of system message
+ */
+ systemMessage: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * The type of the message.
+ */
+ messageType: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * The participant object.
+ */
+ participant: {
+ type: Object,
+ required: true,
+ },
+
+ messageApiData: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ // Shows/hides the message forwarder component
+ showForwarder: false,
+
+ // The pagination of the buttons menu
+ page: 0,
+ }
+ },
+
+ computed: {
+ conversation() {
+ return this.$store.getters.conversation(this.token)
+ },
+
+ container() {
+ return this.$store.getters.getMainContainerSelector()
+ },
+
+ containerElement() {
+ return document.querySelector(this.container)
+ },
+
+ isDeleteable() {
+ if (this.isConversationReadOnly) {
+ return false
+ }
+
+ const isObjectShare = this.message === '{object}'
+ && this.messageParameters?.object
+
+ return (moment(this.timestamp * 1000).add(6, 'h')) > moment()
+ && this.messageType === 'comment'
+ && !this.isDeleting
+ && !this.isFileShare
+ && !isObjectShare
+ && (this.isMyMsg
+ || (this.conversation.type !== CONVERSATION.TYPE.ONE_TO_ONE
+ && (this.participant.participantType === PARTICIPANT.TYPE.OWNER
+ || this.participant.participantType === PARTICIPANT.TYPE.MODERATOR)))
+ },
+
+ isPrivateReplyable() {
+ return this.isReplyable
+ && (this.conversation.type === CONVERSATION.TYPE.PUBLIC
+ || this.conversation.type === CONVERSATION.TYPE.GROUP)
+ && !this.isMyMsg
+ && this.actorType === ATTENDEE.ACTOR_TYPE.USERS
+ && this.$store.getters.getActorType() === ATTENDEE.ACTOR_TYPE.USERS
+ },
+
+ messageActions() {
+ return this.$store.getters.messageActions
+ },
+
+ linkToFile() {
+ if (this.isFileShare) {
+ return this.messageParameters?.file?.link
+ }
+ return ''
+ },
+
+ isFileShare() {
+ return this.message === '{file}' && this.messageParameters?.file
+ },
+
+ isCurrentGuest() {
+ return this.$store.getters.getActorType() === 'guests'
+ },
+
+ isMyMsg() {
+ return this.actorId === this.$store.getters.getActorId()
+ && this.actorType === this.$store.getters.getActorType()
+ },
+
+ isConversationReadOnly() {
+ return this.conversation.readOnly === CONVERSATION.STATE.READ_ONLY
+ },
+ },
+
+ methods: {
+ handleReply() {
+ this.$store.dispatch('addMessageToBeReplied', {
+ id: this.id,
+ actorId: this.actorId,
+ actorType: this.actorType,
+ actorDisplayName: this.actorDisplayName,
+ timestamp: this.timestamp,
+ systemMessage: this.systemMessage,
+ messageType: this.messageType,
+ message: this.message,
+ messageParameters: this.messageParameters,
+ token: this.token,
+ previousMessageId: this.previousMessageId,
+ })
+ 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)
+ this.$router.push({ name: 'conversation', params: { token: conversation.token } }).catch(err => console.debug(`Error while pushing the new conversation's route: ${err}`))
+ },
+
+ async handleCopyMessageLink() {
+ try {
+ const link = window.location.protocol + '//' + window.location.host + generateUrl('/call/' + this.token) + '#message_' + this.id
+ await this.$copyText(link)
+ showSuccess(t('spreed', 'Message link copied to clipboard.'))
+ } catch (error) {
+ console.error('Error copying link: ', error)
+ showError(t('spreed', 'The link could not be copied.'))
+ }
+ },
+
+ async handleMarkAsUnread() {
+ // update in backend + visually
+ await this.$store.dispatch('updateLastReadMessage', {
+ token: this.token,
+ id: this.previousMessageId,
+ updateVisually: true,
+ })
+
+ // reload conversation to update additional attributes that have computed values
+ await this.$store.dispatch('fetchConversation', { token: this.token })
+ },
+
+ 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')
+ }
+
+ },
+
+ handleDelete() {
+ this.$emit('delete')
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+@import '../../../../../assets/variables';
+
+.message-buttons-bar {
+ display: flex;
+ right: 14px;
+ bottom: -4px;
+ position: absolute;
+ z-index: 100000;
+ background-color: var(--color-main-background);
+ border-radius: calc($clickable-area / 2);
+ box-shadow: 0 0 4px 0px var(--color-box-shadow);
+
+ & h6 {
+ margin-left: auto;
+ }
+}
+
+</style>