diff options
author | Dorra <dorra.jaoued7@gmail.com> | 2023-10-04 16:52:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-04 16:52:55 +0200 |
commit | 6159317f095e167a7f730608179894bd93e2523b (patch) | |
tree | 3147fd48fa926ce9b230438f7386d3fd26984759 | |
parent | 4df5ae8a4d5e9260681ec93cbeaa5a74e75ea741 (diff) | |
parent | 7f84892054742f7cb2fbbc26f185edcfbd83a275 (diff) |
Merge pull request #10603 from nextcloud/feat/10405/note-to-self
feat(conversation) - add Note to self conversation
-rw-r--r-- | docs/constants.md | 1 | ||||
-rw-r--r-- | src/components/ConversationSettings/ConversationSettingsDialog.vue | 17 | ||||
-rw-r--r-- | src/components/LeftSidebar/LeftSidebar.vue | 32 | ||||
-rw-r--r-- | src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue | 59 | ||||
-rw-r--r-- | src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue | 24 | ||||
-rw-r--r-- | src/components/RightSidebar/RightSidebar.vue | 16 | ||||
-rw-r--r-- | src/components/TopBar/CallButton.vue | 1 | ||||
-rw-r--r-- | src/constants.js | 1 | ||||
-rw-r--r-- | src/services/conversationsService.js | 9 | ||||
-rw-r--r-- | src/store/messagesStore.js | 65 | ||||
-rw-r--r-- | src/store/messagesStore.spec.js | 183 |
11 files changed, 325 insertions, 83 deletions
diff --git a/docs/constants.md b/docs/constants.md index f5c4b2105..92f2f4991 100644 --- a/docs/constants.md +++ b/docs/constants.md @@ -8,6 +8,7 @@ * `3` Public * `4` Changelog * `5` Former "One to one" (When a user is deleted from the server or removed from all their conversations, `1` "One to one" rooms are converted to this type) +* `6` Note to self ### Object types diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index 8a174ef30..2086b8021 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -36,7 +36,8 @@ <template v-if="!isBreakoutRoom"> <!-- Notifications settings and devices preview screen --> - <NcAppSettingsSection id="notifications" + <NcAppSettingsSection v-if="!isNoteToSelf" + id="notifications" :title="t('spreed', 'Personal')"> <NcCheckboxRadioSwitch :checked.sync="showMediaSettings" type="switch"> @@ -49,8 +50,8 @@ <NcAppSettingsSection v-if="canFullModerate" id="conversation-settings" :title="t('spreed', 'Moderation')"> - <ListableSettings :token="token" /> - <LinkShareSettings ref="linkShareSettings" /> + <ListableSettings v-if="!isNoteToSelf" :token="token" /> + <LinkShareSettings v-if="!isNoteToSelf" ref="linkShareSettings" /> <ExpirationSettings :token="token" can-full-moderate /> </NcAppSettingsSection> <NcAppSettingsSection v-else @@ -60,7 +61,7 @@ </NcAppSettingsSection> <!-- Meeting: lobby and sip --> - <NcAppSettingsSection v-if="canFullModerate" + <NcAppSettingsSection v-if="canFullModerate && !isNoteToSelf" id="meeting" :title="t('spreed', 'Meeting')"> <LobbySettings :token="token" /> @@ -68,7 +69,7 @@ </NcAppSettingsSection> <!-- Conversation permissions --> - <NcAppSettingsSection v-if="canFullModerate" + <NcAppSettingsSection v-if="canFullModerate && !isNoteToSelf" id="permissions" :title="t('spreed', 'Permissions')"> <ConversationPermissionsSettings :token="token" /> @@ -99,7 +100,7 @@ <NcAppSettingsSection v-if="canLeaveConversation || canDeleteConversation" id="dangerzone" :title="t('spreed', 'Danger zone')"> - <LockingSettings :token="token" /> + <LockingSettings v-if="canFullModerate && !isNoteToSelf" :token="token" /> <DangerZone :conversation="conversation" :can-leave-conversation="canLeaveConversation" :can-delete-conversation="canDeleteConversation" /> @@ -173,6 +174,10 @@ export default { return this.conversation.canEnableSIP }, + isNoteToSelf() { + return this.conversation.type === CONVERSATION.TYPE.NOTE_TO_SELF + }, + token() { return this.$store.getters.getConversationSettingsToken() || this.$store.getters.getToken() diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index 1fce07c42..f8ab62abd 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -90,6 +90,15 @@ {{ t('spreed','Create a new conversation') }} </NcActionButton> + <NcActionButton v-if="!hasNoteToSelf" + close-after-click + @click="restoreNoteToSelfConversation"> + <template #icon> + <Note :size="20" /> + </template> + {{ t('spreed','New personal note') }} + </NcActionButton> + <NcActionButton close-after-click @click="showModalListConversations"> <template #icon> @@ -239,6 +248,7 @@ import FilterIcon from 'vue-material-design-icons/Filter.vue' import FilterRemoveIcon from 'vue-material-design-icons/FilterRemove.vue' import List from 'vue-material-design-icons/FormatListBulleted.vue' import MessageBadge from 'vue-material-design-icons/MessageBadge.vue' +import Note from 'vue-material-design-icons/NoteEditOutline.vue' import Plus from 'vue-material-design-icons/Plus.vue' import { showError } from '@nextcloud/dialogs' @@ -267,6 +277,7 @@ import { CONVERSATION } from '../../constants.js' import BrowserStorage from '../../services/BrowserStorage.js' import { createPrivateConversation, + fetchNoteToSelfConversation, searchPossibleConversations, searchListedConversations, } from '../../services/conversationsService.js' @@ -303,6 +314,7 @@ export default { ChatPlus, List, DotsVertical, + Note, }, mixins: [ @@ -396,6 +408,10 @@ export default { return this.searchText !== '' }, + hasNoteToSelf() { + return this.conversationsList.find(conversation => conversation.type === CONVERSATION.TYPE.NOTE_TO_SELF) + }, + sourcesWithoutResults() { return !this.searchResultsUsers.length || !this.searchResultsGroups.length @@ -616,9 +632,7 @@ export default { } }, - async createConversation(name) { - const response = await createPrivateConversation(name) - const conversation = response.data.ocs.data + switchToConversation(conversation) { this.$store.dispatch('addConversation', conversation) this.abortSearch() this.$router.push({ @@ -627,6 +641,18 @@ export default { }).catch(err => console.debug(`Error while pushing the new conversation's route: ${err}`)) }, + async createConversation(name) { + const response = await createPrivateConversation(name) + const conversation = response.data.ocs.data + this.switchToConversation(conversation) + }, + + async restoreNoteToSelfConversation() { + const response = await fetchNoteToSelfConversation() + const conversation = response.data.ocs.data + this.switchToConversation(conversation) + }, + hasOneToOneConversationWith(userId) { return !!this.conversationsList.find(conversation => conversation.type === CONVERSATION.TYPE.ONE_TO_ONE && conversation.name === userId) }, diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue index e9f1e807f..e44e05051 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/Forwarder.vue @@ -68,7 +68,6 @@ </template> <script> -import cloneDeep from 'lodash/cloneDeep.js' import Check from 'vue-material-design-icons/Check.vue' @@ -128,70 +127,22 @@ export default { return this.$store.getters?.conversation(this.selectedConversationToken).displayName }, - /** - * Object containing all the mentions in the message that will be forwarded - * - * @return {object} mentions. - */ - mentions() { - const mentions = {} - for (const key in this.messageObject.messageParameters) { - if (key.startsWith('mention')) { - mentions[key] = this.messageObject.messageParameters[key] - } - } - return mentions - }, }, methods: { async setSelectedConversationToken(token) { this.selectedConversationToken = token - const messageToBeForwarded = cloneDeep(this.messageObject) - // Overwrite the selected conversation token - messageToBeForwarded.token = token - - if (messageToBeForwarded.parent) { - delete messageToBeForwarded.parent - } - - if (messageToBeForwarded.message === '{object}' && messageToBeForwarded.messageParameters.object) { - const richObject = messageToBeForwarded.messageParameters.object - try { - const response = await this.$store.dispatch('forwardRichObject', { - token, - richObject: { - objectId: richObject.id, - objectType: richObject.type, - metaData: JSON.stringify(richObject), - referenceId: '', - }, - }) - this.showForwardedConfirmation = true - this.forwardedMessageID = response.data.ocs.data.id - } catch (error) { - console.error('Error while forwarding message', error) - showError(t('spreed', 'Error while forwarding message')) - } - return - } - - // If there are mentions in the message to be forwarded, replace them in the message - // text. - if (this.mentions !== {}) { - for (const mention in this.mentions) { - messageToBeForwarded.message = messageToBeForwarded.message.replace(`{${mention}}`, '@' + this.mentions[mention].name) - } - } try { - const response = await this.$store.dispatch('forwardMessage', { messageToBeForwarded }) - this.showForwardedConfirmation = true + const response = await this.$store.dispatch('forwardMessage', { + targetToken: this.selectedConversationToken, + messageToBeForwarded: this.messageObject, + }) this.forwardedMessageID = response.data.ocs.data.id + this.showForwardedConfirmation = true } catch (error) { console.error('Error while forwarding message', error) showError(t('spreed', 'Error while forwarding message')) } - }, openConversation() { diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue index 61c5c10c7..d3af548ec 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.vue @@ -111,6 +111,14 @@ </template> {{ t('spreed', 'Go to file') }} </NcActionLink> + <NcActionButton v-if="canForwardMessage && !isInNoteToSelf" + close-after-click + @click="forwardToNote"> + <template #icon> + <Note :size="16" /> + </template> + {{ t('spreed', 'Note to self') }} + </NcActionButton> <NcActionButton v-if="canForwardMessage" close-after-click @click.stop="openForwarder"> @@ -256,6 +264,7 @@ import DeleteIcon from 'vue-material-design-icons/Delete.vue' import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline.vue' import EyeOffOutline from 'vue-material-design-icons/EyeOffOutline.vue' import File from 'vue-material-design-icons/File.vue' +import Note from 'vue-material-design-icons/NoteEditOutline.vue' import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue' import Plus from 'vue-material-design-icons/Plus.vue' import Reply from 'vue-material-design-icons/Reply.vue' @@ -311,6 +320,7 @@ export default { EmoticonOutline, EyeOffOutline, File, + Note, OpenInNewIcon, Plus, Reply, @@ -536,6 +546,10 @@ export default { && this.messageParameters?.object?.type === 'talk-poll' }, + isInNoteToSelf() { + return this.conversation.type === CONVERSATION.TYPE.NOTE_TO_SELF + }, + canForwardMessage() { return !this.isCurrentGuest && !this.isFileShare @@ -702,6 +716,16 @@ export default { this.$emit('update:isReactionsMenuOpen', true) }, + async forwardToNote() { + try { + await this.$store.dispatch('forwardMessage', { messageToBeForwarded: this.messageObject }) + showSuccess(t('spreed', 'Message forwarded to "Note to self"')) + } catch (error) { + console.error('Error while forwarding message to "Note to self"', error) + showError(t('spreed', 'Error while forwarding message to "Note to self"')) + } + }, + openForwarder() { this.$emit('update:isForwarderOpen', true) }, diff --git a/src/components/RightSidebar/RightSidebar.vue b/src/components/RightSidebar/RightSidebar.vue index 93e876896..97df245eb 100644 --- a/src/components/RightSidebar/RightSidebar.vue +++ b/src/components/RightSidebar/RightSidebar.vue @@ -41,7 +41,7 @@ </template> <ChatView :is-visible="opened" /> </NcAppSidebarTab> - <NcAppSidebarTab v-if="(getUserId || isModeratorOrUser) && !isOneToOne" + <NcAppSidebarTab v-if="showParticipantsTab" id="participants" ref="participantsTab" :order="2" @@ -266,6 +266,14 @@ export default { && (this.breakoutRoomsConfigured || this.conversation.breakoutRoomMode === CONVERSATION.BREAKOUT_ROOM_MODE.FREE || this.conversation.objectType === 'room') }, + showParticipantsTab() { + return (this.getUserId || this.isModeratorOrUser) && !this.isOneToOne && !this.isNoteToSelf + }, + + isNoteToSelf() { + return this.conversation.type === CONVERSATION.TYPE.NOTE_TO_SELF + }, + breakoutRoomsText() { return t('spreed', 'Breakout rooms') }, @@ -277,7 +285,7 @@ export default { this.conversationName = this.conversation.displayName } - if (newConversation.token === oldConversation.token || this.isOneToOne) { + if (newConversation.token === oldConversation.token || !this.showParticipantsTab) { return } @@ -294,10 +302,10 @@ export default { } }, - isOneToOne: { + showParticipantsTab: { immediate: true, handler(value) { - if (value) { + if (!value) { this.activeTab = 'shared-items' } }, diff --git a/src/components/TopBar/CallButton.vue b/src/components/TopBar/CallButton.vue index 22b3efb2f..de642fde3 100644 --- a/src/components/TopBar/CallButton.vue +++ b/src/components/TopBar/CallButton.vue @@ -254,6 +254,7 @@ export default { showStartCallButton() { return this.callEnabled + && this.conversation.type !== CONVERSATION.TYPE.NOTE_TO_SELF && this.conversation.readOnly === CONVERSATION.STATE.READ_WRITE && !this.isInCall }, diff --git a/src/constants.js b/src/constants.js index 60a9a55bf..7834b0526 100644 --- a/src/constants.js +++ b/src/constants.js @@ -72,6 +72,7 @@ export const CONVERSATION = { PUBLIC: 3, CHANGELOG: 4, ONE_TO_ONE_FORMER: 5, + NOTE_TO_SELF: 6, }, BREAKOUT_ROOM_MODE: { diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js index 2f84b4e92..28d5a7189 100644 --- a/src/services/conversationsService.js +++ b/src/services/conversationsService.js @@ -62,6 +62,14 @@ const searchListedConversations = async function({ searchText }, options) { } /** + * Generate note-to-self conversation + * + */ +const fetchNoteToSelfConversation = async function() { + return axios.get(generateOcsUrl('apps/spreed/api/v4/room/note-to-self')) +} + +/** * Fetch possible conversations * * @param {object} data the wrapping object; @@ -438,6 +446,7 @@ const deleteConversationAvatar = async function(token) { export { fetchConversations, fetchConversation, + fetchNoteToSelfConversation, searchListedConversations, searchPossibleConversations, createOneToOneConversation, diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js index 020300ad6..6dac72083 100644 --- a/src/store/messagesStore.js +++ b/src/store/messagesStore.js @@ -21,6 +21,7 @@ */ import Hex from 'crypto-js/enc-hex.js' import SHA256 from 'crypto-js/sha256.js' +import cloneDeep from 'lodash/cloneDeep.js' import Vue from 'vue' import { showError } from '@nextcloud/dialogs' @@ -28,7 +29,9 @@ import { showError } from '@nextcloud/dialogs' import { ATTENDEE, CHAT, + CONVERSATION, } from '../constants.js' +import { fetchNoteToSelfConversation } from '../services/conversationsService.js' import { deleteMessage, updateLastReadMessage, @@ -1256,30 +1259,60 @@ const actions = { }, /** - * Posts a simple text message to a room + * Forwards message to a conversation. By default , the message is forwarded to Note to self. * * @param {object} context default store context; * will be forwarded; * @param {object} data the wrapping object; + * @param {string} [data.targetToken] the conversation token to where the message will be forwarded; * @param {object} data.messageToBeForwarded the message object; */ - async forwardMessage(context, { messageToBeForwarded }) { - const response = await postNewMessage(messageToBeForwarded, { silent: false }) - return response - }, + async forwardMessage(context, { targetToken, messageToBeForwarded }) { + const message = cloneDeep(messageToBeForwarded) + + // when there is no token provided, the message will be forwarded to the Note to self conversation + if (!targetToken) { + let noteToSelf = context.getters.conversationsList.find(conversation => conversation.type === CONVERSATION.TYPE.NOTE_TO_SELF) + // If Note to self doesn't exist, it will be regenerated + if (!noteToSelf) { + const response = await fetchNoteToSelfConversation() + noteToSelf = response.data.ocs.data + context.dispatch('addConversation', noteToSelf) + } + targetToken = noteToSelf.token + } + // Overwrite with the target conversation token + message.token = targetToken + if (message.parent) { + delete message.parent + } + + if (message.message === '{object}' && message.messageParameters.object) { + const richObject = message.messageParameters.object + const response = await postRichObjectToConversation( + targetToken, + { + objectId: richObject.id, + objectType: richObject.type, + metaData: JSON.stringify(richObject), + referenceId: '', + }, + ) + return response + } - /** - * Posts a simple text message to a room - * - * @param {object} context default store context; - * will be forwarded; - * @param {object} data the wrapping object; - * @param {string} data.token token of the target conversation - * @param {object} data.richObject the rich object; - */ - async forwardRichObject(context, { token, richObject }) { - const response = await postRichObjectToConversation(token, richObject) + // If there are mentions in the message to be forwarded, replace them in the message + // text. + for (const key in message.messageParameters) { + if (key.startsWith('mention')) { + const mention = message.messageParameters[key] + message.message = message.message.replace(`{${key}}`, `@${mention.name}`) + } + } + + const response = await postNewMessage(message, { silent: false }) return response + }, /** diff --git a/src/store/messagesStore.spec.js b/src/store/messagesStore.spec.js index 1c0514dd2..44e00ba2b 100644 --- a/src/store/messagesStore.spec.js +++ b/src/store/messagesStore.spec.js @@ -11,12 +11,16 @@ import { ATTENDEE, CHAT, } from '../constants.js' import { + fetchNoteToSelfConversation, +} from '../services/conversationsService.js' +import { deleteMessage, updateLastReadMessage, fetchMessages, getMessageContext, lookForNewMessages, postNewMessage, + postRichObjectToConversation, } from '../services/messagesService.js' import { useGuestNameStore } from '../stores/guestName.js' import { generateOCSErrorResponse, generateOCSResponse } from '../test-helpers.js' @@ -31,6 +35,11 @@ jest.mock('../services/messagesService', () => ({ getMessageContext: jest.fn(), lookForNewMessages: jest.fn(), postNewMessage: jest.fn(), + postRichObjectToConversation: jest.fn(), +})) + +jest.mock('../services/conversationsService', () => ({ + fetchNoteToSelfConversation: jest.fn(), })) jest.mock('../utils/cancelableRequest') @@ -1778,4 +1787,178 @@ describe('messagesStore', () => { expect(store.getters.hasMoreMessagesToLoad(TOKEN)).toBe(false) }) }) + + describe('Forward a message', () => { + let conversations + let message1 + let messageToBeForwarded + let targetToken + let messageExpected + + beforeEach(() => { + localVue = createLocalVue() + localVue.use(Vuex) + + testStoreConfig = cloneDeep(storeConfig) + store = new Vuex.Store(testStoreConfig) + + message1 = { + id: 1, + token: TOKEN, + message: 'simple text message', + messageParameters: {}, + } + conversations = [ + { + token: TOKEN, + type: 3, + displayName: 'conversation 1', + }, + { + token: 'token-self', + type: 6, + displayName: 'Note to self', + }, + { + token: 'token-2', + type: 3, + displayName: 'conversation 2', + }, + ] + }) + + test('forwards a message to the conversation when a token is given', () => { + // Arrange + targetToken = 'token-2' + messageToBeForwarded = message1 + messageExpected = cloneDeep(message1) + messageExpected.token = targetToken + + // Act + store.dispatch('forwardMessage', { targetToken, messageToBeForwarded }) + + // Assert + expect(postNewMessage).toHaveBeenCalledWith(messageExpected, { silent: false }) + }) + test('forwards a message to Note to self when no token is given ', () => { + // Arrange + targetToken = 'token-self' + messageToBeForwarded = message1 + messageExpected = cloneDeep(message1) + messageExpected.token = targetToken + + store.dispatch('addConversation', conversations[1]) + + // Act + store.dispatch('forwardMessage', { messageToBeForwarded }) + + // Assert + expect(postNewMessage).toHaveBeenCalledWith(messageExpected, { silent: false }) + }) + + test('generates Note to self when it does not exist ', async () => { + // Arrange + messageToBeForwarded = message1 + messageExpected = cloneDeep(message1) + messageExpected.token = 'token-self' + + const response = { + data: { + ocs: { + data: conversations[1], + }, + }, + } + fetchNoteToSelfConversation.mockResolvedValueOnce(response) + + // Act + store.dispatch('forwardMessage', { messageToBeForwarded }) + await flushPromises() + + // Assert + expect(store.getters.conversationsList).toContain(conversations[1]) + expect(postNewMessage).toHaveBeenCalledWith(messageExpected, { silent: false }) + }) + |