diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js index 526c419667d..5bd27f0b1c1 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js @@ -541,6 +541,7 @@ describe('Message.vue', () => { test('renders author if first message', async () => { messageProps.isFirstMessage = true + messageProps.showAuthor = true const wrapper = shallowMount(Message, { localVue, store, diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.vue b/src/components/MessagesList/MessagesGroup/Message/Message.vue index 31c630b1739..f31fd184dad 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.vue +++ b/src/components/MessagesList/MessagesGroup/Message/Message.vue @@ -38,7 +38,9 @@ the main body of the message as well as a quote. @animationend="isHighlighted = false" @mouseover="handleMouseover" @mouseleave="handleMouseleave"> -
+
+ + + +
@@ -858,17 +902,18 @@ export default { diff --git a/src/components/MessagesList/MessagesGroup/MessagesGroup.spec.js b/src/components/MessagesList/MessagesGroup/MessagesGroup.spec.js index 98f77a57692..8e4955a1db2 100644 --- a/src/components/MessagesList/MessagesGroup/MessagesGroup.spec.js +++ b/src/components/MessagesList/MessagesGroup/MessagesGroup.spec.js @@ -3,6 +3,7 @@ import { cloneDeep } from 'lodash' import Vuex from 'vuex' import MessagesGroup from './MessagesGroup.vue' +import MessagesSystemGroup from './MessagesSystemGroup.vue' import { ATTENDEE } from '../../../constants.js' import storeConfig from '../../../store/storeConfig.js' @@ -123,7 +124,33 @@ describe('MessagesGroup.vue', () => { }) test('renders grouped system messages', () => { - const wrapper = shallowMount(MessagesGroup, { + const MESSAGES = [{ + id: 100, + token: TOKEN, + actorId: 'actor-1', + actorDisplayName: 'actor one', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'Actor entered the scene', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'call_started', + timestamp: 100, + isReplyable: false, + }, { + id: 110, + token: TOKEN, + actorId: 'actor-1', + actorDisplayName: 'actor one', + actorType: ATTENDEE.ACTOR_TYPE.USERS, + message: 'Actor left the scene', + messageType: 'comment', + messageParameters: {}, + systemMessage: 'call_stopped', + timestamp: 200, + isReplyable: false, + }] + + const wrapper = shallowMount(MessagesSystemGroup, { localVue, store, propsData: { @@ -132,31 +159,7 @@ describe('MessagesGroup.vue', () => { dateSeparator: '', previousMessageId: 90, nextMessageId: 200, - messages: [{ - id: 100, - token: TOKEN, - actorId: 'actor-1', - actorDisplayName: 'actor one', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - message: 'Actor entered the scene', - messageType: 'comment', - messageParameters: {}, - systemMessage: 'call_started', - timestamp: 100, - isReplyable: false, - }, { - id: 110, - token: TOKEN, - actorId: 'actor-1', - actorDisplayName: 'actor one', - actorType: ATTENDEE.ACTOR_TYPE.USERS, - message: 'Actor left the scene', - messageType: 'comment', - messageParameters: {}, - systemMessage: 'call_stopped', - timestamp: 200, - isReplyable: false, - }], + messages: MESSAGES, }, }) @@ -169,24 +172,24 @@ describe('MessagesGroup.vue', () => { const messagesEl = wrapper.findAllComponents({ name: 'Message' }) // TODO: date separator let message = messagesEl.at(0) - expect(message.attributes('id')).toBe('100') - expect(message.attributes('message')).toBe('Actor entered the scene') - expect(message.attributes('actorid')).toBe('actor-1') - expect(message.attributes('actordisplayname')).toBe('actor one') - expect(message.attributes('previousmessageid')).toBe('90') - expect(message.attributes('nextmessageid')).toBe('110') - expect(message.attributes('isfirstmessage')).toBe('true') - expect(message.attributes('showauthor')).not.toBeDefined() + expect(message.props('id')).toBe(MESSAGES[0].id) + expect(message.props('message')).toBe(MESSAGES[0].message) + expect(message.props('actorid')).toBe(MESSAGES[0].actorid) + expect(message.props('actordisplayname')).toBe(MESSAGES[0].actordisplayname) + expect(message.props('previousmessageid')).toBe(MESSAGES[0].previousmessageid) + expect(message.props('nextmessageid')).toBe(MESSAGES[0].nextmessageid) + expect(message.props('isfirstmessage')).toBe(MESSAGES[0].isfirstmessage) + expect(message.props('showauthor')).not.toBeDefined() message = messagesEl.at(1) - expect(message.attributes('id')).toBe('110') - expect(message.attributes('message')).toBe('Actor left the scene') - expect(message.attributes('actorid')).toBe('actor-1') - expect(message.attributes('actordisplayname')).toBe('actor one') - expect(message.attributes('previousmessageid')).toBe('100') - expect(message.attributes('nextmessageid')).toBe('200') - expect(message.attributes('isfirstmessage')).not.toBeDefined() - expect(message.attributes('showauthor')).not.toBeDefined() + expect(message.props('id')).toBe(MESSAGES[1].id) + expect(message.props('message')).toBe(MESSAGES[1].message) + expect(message.props('actorid')).toBe(MESSAGES[1].actorid) + expect(message.props('actordisplayname')).toBe(MESSAGES[1].actordisplayname) + expect(message.props('previousmessageid')).toBe(MESSAGES[1].previousmessageid) + expect(message.props('nextmessageid')).toBe(MESSAGES[1].nextmessageid) + expect(message.props('isfirstmessage')).not.toBeDefined() + expect(message.props('showauthor')).not.toBeDefined() }) test('renders guest display name', () => { diff --git a/src/components/MessagesList/MessagesGroup/MessagesGroup.vue b/src/components/MessagesList/MessagesGroup/MessagesGroup.vue index ab70e0609d5..76b78d20621 100644 --- a/src/components/MessagesList/MessagesGroup/MessagesGroup.vue +++ b/src/components/MessagesList/MessagesGroup/MessagesGroup.vue @@ -27,9 +27,8 @@ role="heading" aria-level="3">{{ dateSeparator }}
-
-
+
+
@@ -40,13 +39,13 @@ ref="message" v-bind="message" :is-first-message="index === 0" + :is-temporary="message.timestamp === 0" :next-message-id="(messages[index + 1] && messages[index + 1].id) || nextMessageId" :previous-message-id="(index > 0 && messages[index - 1].id) || previousMessageId" :actor-type="actorType" :actor-id="actorId" :actor-display-name="actorDisplayName" - :show-author="!isSystemMessage" - :is-temporary="message.timestamp === 0" /> + show-author />
@@ -138,14 +137,6 @@ export default { return displayName }, - /** - * Whether the given message is a system message - * - * @return {boolean} - */ - isSystemMessage() { - return this.messages[0].systemMessage.length !== 0 - }, }, methods: { @@ -188,9 +179,6 @@ export default { display: flex; margin: auto; padding: 0; - &--system { - padding-left: $clickable-area + 8px; - } &:focus { background-color: rgba(47, 47, 47, 0.068); } diff --git a/src/components/MessagesList/MessagesGroup/MessagesSystemGroup.vue b/src/components/MessagesList/MessagesGroup/MessagesSystemGroup.vue new file mode 100644 index 00000000000..f30bb8730dd --- /dev/null +++ b/src/components/MessagesList/MessagesGroup/MessagesSystemGroup.vue @@ -0,0 +1,337 @@ + + + + + + + diff --git a/src/components/MessagesList/MessagesList.spec.js b/src/components/MessagesList/MessagesList.spec.js index d3515a36349..95c68065c55 100644 --- a/src/components/MessagesList/MessagesList.spec.js +++ b/src/components/MessagesList/MessagesList.spec.js @@ -270,7 +270,7 @@ describe('MessagesList.vue', () => { }, }) - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + const groups = wrapper.findAllComponents({ ref: 'messagesGroup' }) expect(groups.exists()).toBe(true) @@ -298,7 +298,7 @@ describe('MessagesList.vue', () => { }, }) - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + const groups = wrapper.findAllComponents({ ref: 'messagesGroup' }) expect(groups.exists()).toBe(true) diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue index bd4721de6fd..a44aac58019 100644 --- a/src/components/MessagesList/MessagesList.vue +++ b/src/components/MessagesList/MessagesList.vue @@ -33,7 +33,8 @@ get the messagesList array and loop through the list to generate the messages. :class="{'scroller--chatScrolledToBottom': isChatScrolledToBottom}" @scroll="debounceHandleScroll">
- diff --git a/src/composables/useCombinedSystemMessage.js b/src/composables/useCombinedSystemMessage.js new file mode 100644 index 00000000000..4baafc8def5 --- /dev/null +++ b/src/composables/useCombinedSystemMessage.js @@ -0,0 +1,319 @@ +/* + * @copyright Copyright (c) 2023 Maksim Sukharev + * + * @author Maksim Sukharev + * + * @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 . + */ + +import cloneDeep from 'lodash/cloneDeep.js' + +import { useStore } from './useStore.js' + +/** + * Create combined system message from the passed object + * + */ +export function useCombinedSystemMessage() { + const store = useStore() + + /** + * + * @param {object} message message to check for + * @return {boolean} + */ + function checkIfSelfIsActor(message) { + return message.actorId === store.getters.getActorId() + && message.actorType === store.getters.getActorType() + } + + /** + * + * @param {object} message message to check for + * @return {boolean} + */ + function checkIfSelfIsOneOfActors(message) { + return message.messageParameters.actor.id === store.getters.getActorId() + && message.messageParameters.actor.type + 's' === store.getters.getActorType() + } + + /** + * + * @param {object} message message to check for + * @return {boolean} + */ + function checkIfSelfIsOneOfUsers(message) { + return message.messageParameters.user.id === store.getters.getActorId() + && message.messageParameters.user.type + 's' === store.getters.getActorType() + } + + /** + * + * @param {object} group object representing the group of system messages + * @param {number} group.id id of the group + * @param {Array} group.messages array of grouped messages + * @param {string} group.type combination type + * @param {boolean} group.collapsed collapsed state + * @return {object} + */ + function createCombinedSystemMessage({ id, messages, type, collapsed }) { + const combinedMessage = cloneDeep(messages[0]) + + // Handle cases when users reconnected to the call + if (type === 'call_reconnected') { + if (checkIfSelfIsOneOfActors(combinedMessage)) { + combinedMessage.message = t('spreed', 'You reconnected to the call') + } else { + combinedMessage.message = t('spreed', '{actor} reconnected to the call') + } + + return combinedMessage + } + + // clear messageParameters to be filled later + const actor = messages[0].messageParameters.actor + combinedMessage.messageParameters = { actor } + const actorIsAdministrator = actor.id === 'guest/cli' && actor.type === 'guest' + + // usersCounter should be equal at least 2, as we're using method only for groups + let usersCounter = 0 + let selfIsUser = false + let referenceIndex = 0 + + // Handle cases when actor added users to conversation (when populate on creation, for example) + if (type === 'user_added') { + messages.forEach(message => { + if (checkIfSelfIsOneOfUsers(message)) { + selfIsUser = true + } else { + combinedMessage.messageParameters[`user${referenceIndex}`] = message.messageParameters.user + referenceIndex++ + } + usersCounter++ + }) + + if (checkIfSelfIsActor(combinedMessage)) { + if (usersCounter === 2) { + combinedMessage.message = t('spreed', 'You added {user0} and {user1}') + } else { + combinedMessage.message = n('spreed', + 'You added {user0}, {user1} and %n more participant', + 'You added {user0}, {user1} and %n more participants', usersCounter - 2) + } + } else if (selfIsUser) { + if (usersCounter === 2) { + combinedMessage.message = actorIsAdministrator + ? t('spreed', 'An administrator added you and {user0}') + : t('spreed', '{actor} added you and {user0}') + } else { + combinedMessage.message = actorIsAdministrator + ? n('spreed', + 'An administrator added you, {user0} and %n more participant', + 'An administrator added you, {user0} and %n more participants', usersCounter - 2) + : n('spreed', + '{actor} added you, {user0} and %n more participant', + '{actor} added you, {user0} and %n more participants', usersCounter - 2) + } + } else { + if (usersCounter === 2) { + combinedMessage.message = actorIsAdministrator + ? t('spreed', 'An administrator added {user0} and {user1}') + : t('spreed', '{actor} added {user0} and {user1}') + } else { + combinedMessage.message = actorIsAdministrator + ? n('spreed', + 'An administrator added {user0}, {user1} and %n more participant', + 'An administrator added {user0}, {user1} and %n more participants', usersCounter - 2) + : n('spreed', + '{actor} added {user0}, {user1} and %n more participant', + '{actor} added {user0}, {user1} and %n more participants', usersCounter - 2) + } + } + } + + // Handle cases when users joined or left the call + if (type === 'call_joined' || type === 'call_left') { + const storedUniqueUsers = [] + + messages.forEach(message => { + const actorReference = `${message.messageParameters.actor.id}_${message.messageParameters.actor.type}` + if (storedUniqueUsers.includes(actorReference)) { + return + } + if (checkIfSelfIsOneOfActors(message)) { + selfIsUser = true + } else { + combinedMessage.messageParameters[`user${referenceIndex}`] = message.messageParameters.actor + storedUniqueUsers.push(actorReference) + referenceIndex++ + } + usersCounter++ + }) + + if (usersCounter === 1) { + combinedMessage.message = messages[0].message + return combinedMessage + } + + if (type === 'call_joined') { + if (selfIsUser) { + if (usersCounter === 2) { + combinedMessage.message = t('spreed', 'You and {user0} joined the call') + } else { + combinedMessage.message = n('spreed', + 'You, {user0} and %n more participant joined the call', + 'You, {user0} and %n more participants joined the call', usersCounter - 2) + } + } else { + if (usersCounter === 2) { + combinedMessage.message = t('spreed', '{user0} and {user1} joined the call') + } else { + combinedMessage.message = n('spreed', + '{user0}, {user1} and %n more participant joined the call', + '{user0}, {user1} and %n more participants joined the call', usersCounter - 2) + } + } + } else if (type === 'call_left') { + if (selfIsUser) { + if (usersCounter === 2) { + combinedMessage.message = t('spreed', 'You and {user0} left the call') + } else { + combinedMessage.message = n('spreed', + 'You, {user0} and %n more participant left the call', + 'You, {user0} and %n more participants left the call', usersCounter - 2) + } + } else { + if (usersCounter === 2) { + combinedMessage.message = t('spreed', '{user0} and {user1} left the call') + } else { + combinedMessage.message = n('spreed', + '{user0}, {user1} and %n more participant left the call', + '{user0}, {user1} and %n more participants left the call', usersCounter - 2) + } + } + } + + } + + // Handle cases when actor promoted several users to moderators + if (type === 'moderator_promoted') { + messages.forEach(message => { + if (checkIfSelfIsOneOfUsers(message)) { + selfIsUser = true + } else { + combinedMessage.messageParameters[`user${referenceIndex}`] = message.messageParameters.user + referenceIndex++ + } + usersCounter++ + }) + + if (checkIfSelfIsActor(combinedMessage)) { + if (usersCounter === 2) { + combinedMessage.message = t('spreed', 'You promoted {user0} and {user1} to moderators') + } else { + combinedMessage.message = n('spreed', + 'You promoted {user0}, {user1} and %n more participant to moderators', + 'You promoted {user0}, {user1} and %n more participants to moderators', usersCounter - 2) + } + } else if (selfIsUser) { + if (usersCounter === 2) { + combinedMessage.message = actorIsAdministrator + ? t('spreed', 'An administrator promoted you and {user0} to moderators') + : t('spreed', '{actor} promoted you and {user0} to moderators') + } else { + combinedMessage.message = actorIsAdministrator + ? n('spreed', + 'An administrator promoted you, {user0} and %n more participant to moderators', + 'An administrator promoted you, {user0} and %n more participants to moderators', usersCounter - 2) + : n('spreed', + '{actor} promoted you, {user0} and %n more participant to moderators', + '{actor} promoted you, {user0} and %n more participants to moderators', usersCounter - 2) + } + } else { + if (usersCounter === 2) { + combinedMessage.message = actorIsAdministrator + ? t('spreed', 'An administrator promoted {user0} and {user1} to moderators') + : t('spreed', '{actor} promoted {user0} and {user1} to moderators') + } else { + combinedMessage.message = actorIsAdministrator + ? n('spreed', + 'An administrator promoted {user0}, {user1} and %n more participant to moderators', + 'An administrator promoted {user0}, {user1} and %n more participants to moderators', usersCounter - 2) + : n('spreed', + '{actor} promoted {user0}, {user1} and %n more participant to moderators', + '{actor} promoted {user0}, {user1} and %n more participants to moderators', usersCounter - 2) + } + } + } + + // Handle cases when actor demoted several users from moderators + if (type === 'moderator_demoted') { + messages.forEach(message => { + if (checkIfSelfIsOneOfUsers(message)) { + selfIsUser = true + } else { + combinedMessage.messageParameters[`user${referenceIndex}`] = message.messageParameters.user + referenceIndex++ + } + usersCounter++ + }) + + if (checkIfSelfIsActor(combinedMessage)) { + if (usersCounter === 2) { + combinedMessage.message = t('spreed', 'You demoted {user0} and {user1} from moderators') + } else { + combinedMessage.message = n('spreed', + 'You demoted {user0}, {user1} and %n more participant from moderators', + 'You demoted {user0}, {user1} and %n more participants from moderators', usersCounter - 2) + } + } else if (selfIsUser) { + if (usersCounter === 2) { + combinedMessage.message = actorIsAdministrator + ? t('spreed', 'An administrator demoted you and {user0} from moderators') + : t('spreed', '{actor} demoted you and {user0} from moderators') + } else { + combinedMessage.message = actorIsAdministrator + ? n('spreed', + 'An administrator demoted you, {user0} and %n more participant from moderators', + 'An administrator demoted you, {user0} and %n more participants from moderators', usersCounter - 2) + : n('spreed', + '{actor} demoted you, {user0} and %n more participant from moderators', + '{actor} demoted you, {user0} and %n more participants from moderators', usersCounter - 2) + } + } else { + if (usersCounter === 2) { + combinedMessage.message = actorIsAdministrator + ? t('spreed', 'An administrator demoted {user0} and {user1} from moderators') + : t('spreed', '{actor} demoted {user0} and {user1} from moderators') + } else { + combinedMessage.message = actorIsAdministrator + ? n('spreed', + 'An administrator demoted {user0}, {user1} and %n more participant from moderators', + 'An administrator demoted {user0}, {user1} and %n more participants from moderators', usersCounter - 2) + : n('spreed', + '{actor} demoted {user0}, {user1} and %n more participant from moderators', + '{actor} demoted {user0}, {user1} and %n more participants from moderators', usersCounter - 2) + } + } + } + + return combinedMessage + } + + return { + createCombinedSystemMessage, + } +}