diff options
author | Maksim Sukharev <antreesy.web@gmail.com> | 2023-04-24 13:35:02 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-24 13:35:02 +0200 |
commit | b61866d3ec2a80b7eea038f7b8538c4db666b644 (patch) | |
tree | 98169a56fddafe9b7602680af874ae212697c838 | |
parent | 26f74213b2a04d4c52c4a8e5b1a86c2952a36fb1 (diff) | |
parent | 82d3a6e3df552432ed39f658cd0a286c7a853486 (diff) |
Merge pull request #9346 from nextcloud/fix/6808/mark-conversation-unread
Allow to mark conversation as unread
-rw-r--r-- | src/components/LeftSidebar/ConversationsList/Conversation.spec.js | 12 | ||||
-rw-r--r-- | src/components/LeftSidebar/ConversationsList/Conversation.vue | 24 | ||||
-rw-r--r-- | src/services/conversationsService.js | 15 | ||||
-rw-r--r-- | src/store/conversationsStore.js | 122 | ||||
-rw-r--r-- | src/store/conversationsStore.spec.js | 28 |
5 files changed, 141 insertions, 60 deletions
diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js index a6434108e..5a6ab3ad4 100644 --- a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js +++ b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js @@ -530,10 +530,22 @@ describe('Conversation.vue', () => { expect(toggleFavoriteAction).toHaveBeenCalledWith(expect.anything(), item) }) + test('marks conversation as unread', async () => { + const markConversationUnreadAction = jest.fn().mockResolvedValueOnce() + testStoreConfig.modules.conversationsStore.actions.markConversationUnread = markConversationUnreadAction + + const action = shallowMountAndGetAction('Mark as unread') + expect(action.exists()).toBe(true) + + await action.find('button').trigger('click') + + expect(markConversationUnreadAction).toHaveBeenCalledWith(expect.anything(), { token: item.token }) + }) test('marks conversation as read', async () => { const clearLastReadMessageAction = jest.fn().mockResolvedValueOnce() testStoreConfig.modules.conversationsStore.actions.clearLastReadMessage = clearLastReadMessageAction + item.unreadMessages = 1 const action = shallowMountAndGetAction('Mark as read') expect(action.exists()).toBe(true) diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.vue b/src/components/LeftSidebar/ConversationsList/Conversation.vue index 94224fd84..1376e4997 100644 --- a/src/components/LeftSidebar/ConversationsList/Conversation.vue +++ b/src/components/LeftSidebar/ConversationsList/Conversation.vue @@ -62,13 +62,22 @@ @click.stop.prevent="handleCopyLink"> {{ t('spreed', 'Copy link') }} </NcActionButton> - <NcActionButton :close-after-click="true" + <NcActionButton v-if="item.unreadMessages" + :close-after-click="true" @click.prevent.exact="markConversationAsRead"> <template #icon> <EyeOutline :size="16" /> </template> {{ t('spreed', 'Mark as read') }} </NcActionButton> + <NcActionButton v-else + :close-after-click="true" + @click.prevent.exact="markConversationAsUnread"> + <template #icon> + <EyeOffOutline :size="16" /> + </template> + {{ t('spreed', 'Mark as unread') }} + </NcActionButton> <NcActionButton :close-after-click="true" @click.prevent.exact="showConversationSettings"> <Cog slot="icon" @@ -116,6 +125,7 @@ import ArrowRight from 'vue-material-design-icons/ArrowRight.vue' import Cog from 'vue-material-design-icons/Cog.vue' import Delete from 'vue-material-design-icons/Delete.vue' import ExitToApp from 'vue-material-design-icons/ExitToApp.vue' +import EyeOffOutline from 'vue-material-design-icons/EyeOffOutline.vue' import EyeOutline from 'vue-material-design-icons/EyeOutline.vue' import Star from 'vue-material-design-icons/Star.vue' @@ -134,14 +144,16 @@ export default { name: 'Conversation', components: { + ConversationIcon, + NcActionButton, + NcListItem, + // Icons ArrowRight, Cog, - ConversationIcon, Delete, ExitToApp, + EyeOffOutline, EyeOutline, - NcActionButton, - NcListItem, Star, }, @@ -350,6 +362,10 @@ export default { this.$store.dispatch('clearLastReadMessage', { token: this.item.token }) }, + markConversationAsUnread() { + this.$store.dispatch('markConversationUnread', { token: this.item.token }) + }, + showConversationSettings() { emit('show-conversation-settings', { token: this.item.token }) }, diff --git a/src/services/conversationsService.js b/src/services/conversationsService.js index 12c754aaf..05547a5a0 100644 --- a/src/services/conversationsService.js +++ b/src/services/conversationsService.js @@ -207,6 +207,20 @@ const clearConversationHistory = async function(token) { } /** + * Set conversation as unread + * + * @param {string} token The token of the conversation to be set as unread + */ +const setConversationUnread = async function(token) { + try { + const response = axios.delete(generateOcsUrl('apps/spreed/api/v1/chat/{token}/read', { token })) + return response + } catch (error) { + console.debug('Error while setting the conversation as unread: ', error) + } +} + +/** * Add a conversation to the favorites * * @param {string} token The token of the conversation to be favorites @@ -441,6 +455,7 @@ export { setConversationName, setConversationDescription, clearConversationHistory, + setConversationUnread, setConversationPermissions, setCallPermissions, setMessageExpiration, diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js index 69c199232..34743d6e4 100644 --- a/src/store/conversationsStore.js +++ b/src/store/conversationsStore.js @@ -46,6 +46,7 @@ import { setConversationDescription, deleteConversation, clearConversationHistory, + setConversationUnread, setNotificationLevel, setNotificationCalls, setConversationPermissions, @@ -123,13 +124,13 @@ const mutations = { * Deletes a conversation from the store. * * @param {object} state current store state; - * @param {object} token the token of the conversation to delete; + * @param {string} token the token of the conversation to delete; */ deleteConversation(state, token) { Vue.delete(state.conversations, token) }, /** - * Resets the store to it's original state + * Resets the store to its original state * * @param {object} state current store state; */ @@ -200,7 +201,7 @@ const mutations = { const actions = { /** - * Add a conversation to the store and index the displayname. + * Add a conversation to the store and index the displayName. * * @param {object} context default store context; * @param {object} conversation the conversation; @@ -213,8 +214,8 @@ const actions = { displayName: context.getters.getDisplayName(), } - // Fallback to getCurrentUser() only if if has not been set yet (as - // getCurrentUser() needs to be overriden in public share pages as it + // Fallback to getCurrentUser() only if it has not been set yet (as + // getCurrentUser() needs to be overridden in public share pages as it // always returns an anonymous user). if (!currentUser.uid) { currentUser = getCurrentUser() @@ -252,7 +253,7 @@ const actions = { * * @param {object} context default store context; * @param {object} data the wrapping object; - * @param {object} data.token the token of the conversation to be deleted; + * @param {string} data.token the token of the conversation to be deleted; */ async deleteConversationFromServer(context, { token }) { await deleteConversation(token) @@ -265,7 +266,7 @@ const actions = { * * @param {object} context default store context; * @param {object} data the wrapping object; - * @param {object} data.token the token of the conversation whose history is + * @param {string} data.token the token of the conversation whose history is * to be cleared; */ async clearConversationHistory(context, { token }) { @@ -281,7 +282,7 @@ const actions = { }, /** - * Resets the store to it's original state. + * Resets the store to its original state. * * @param {object} context default store context; */ @@ -291,11 +292,11 @@ const actions = { }, async toggleGuests({ commit, getters }, { token, allowGuests }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } + const conversation = Object.assign({}, getters.conversations[token]) if (allowGuests) { await makePublic(token) conversation.type = CONVERSATION.TYPE.PUBLIC @@ -308,8 +309,7 @@ const actions = { }, async toggleFavorite({ commit, getters }, { token, isFavorite }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } @@ -319,17 +319,18 @@ const actions = { } else { await addToFavorites(token) } - conversation.isFavorite = !isFavorite + + const conversation = Object.assign({}, getters.conversations[token], { isFavorite: !isFavorite }) commit('addConversation', conversation) }, async toggleLobby({ commit, getters }, { token, enableLobby }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } + const conversation = Object.assign({}, getters.conversations[token]) if (enableLobby) { await changeLobbyState(token, WEBINAR.LOBBY.NON_MODERATORS) conversation.lobbyState = WEBINAR.LOBBY.NON_MODERATORS @@ -342,13 +343,13 @@ const actions = { }, async setConversationName({ commit, getters }, { token, name }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } await setConversationName(token, name) - conversation.displayName = name + + const conversation = Object.assign({}, getters.conversations[token], { displayName: name }) commit('addConversation', conversation) }, @@ -368,102 +369,107 @@ const actions = { }, async setReadOnlyState({ commit, getters }, { token, readOnly }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } await changeReadOnlyState(token, readOnly) - conversation.readOnly = readOnly + + const conversation = Object.assign({}, getters.conversations[token], { readOnly }) commit('addConversation', conversation) }, async setListable({ commit, getters }, { token, listable }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } await changeListable(token, listable) - conversation.listable = listable + + const conversation = Object.assign({}, getters.conversations[token], { listable }) commit('addConversation', conversation) }, async setLobbyTimer({ commit, getters }, { token, timestamp }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } + const conversation = Object.assign({}, getters.conversations[token], { lobbyTimer: timestamp }) + // The backend requires the state and timestamp to be set together. await changeLobbyState(token, conversation.lobbyState, timestamp) - conversation.lobbyTimer = timestamp commit('addConversation', conversation) }, async setSIPEnabled({ commit, getters }, { token, state }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } await setSIPEnabled(token, state) - conversation.sipEnabled = state + + const conversation = Object.assign({}, getters.conversations[token], { sipEnabled: state }) commit('addConversation', conversation) }, async setConversationProperties({ commit, getters }, { token, properties }) { - let conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } - conversation = Object.assign(conversation, properties) + const conversation = Object.assign({}, getters.conversations[token], properties) commit('addConversation', conversation) }, async markConversationRead({ commit, getters }, token) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } - conversation.unreadMessages = 0 - conversation.unreadMention = false + commit('updateUnreadMessages', { token, unreadMessages: 0, unreadMention: false }) + }, - commit('addConversation', conversation) + async markConversationUnread({ commit, dispatch, getters }, { token }) { + if (!getters.conversations[token]) { + return + } + + await setConversationUnread(token) + commit('updateUnreadMessages', { token, unreadMessages: 1 }) + await dispatch('fetchConversation', { token }) }, async updateLastCommonReadMessage({ commit, getters }, { token, lastCommonReadMessage }) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } - conversation.lastCommonReadMessage = lastCommonReadMessage + const conversation = Object.assign({}, getters.conversations[token], { lastCommonReadMessage }) commit('addConversation', conversation) }, async updateConversationLastActive({ commit, getters }, token) { - const conversation = Object.assign({}, getters.conversations[token]) - if (!conversation) { + if (!getters.conversations[token]) { return } - conversation.lastActivity = (new Date().getTime()) / 1000 + const conversation = Object.assign({}, getters.conversations[token], { + lastActivity: (new Date().getTime()) / 1000, + }) commit('addConversation', conversation) }, async updateConversationLastMessage({ commit }, { token, lastMessage }) { /** - * Only use the last message as lastmessage when: + * Only use the last message as lastMessage when: * 1. It's not a command reply * 2. It's not a temporary message starting with "/" which is a user posting a command * 3. It's not a reaction or deletion of a reaction @@ -485,13 +491,14 @@ const actions = { async updateConversationLastMessageFromNotification({ getters, commit }, { notification }) { const [token, messageId] = notification.objectId.split('/') - const conversation = { ...getters.conversation(token) } - if (!conversation) { + if (!getters.conversations[token]) { // Conversation not loaded yet, skipping return } + const conversation = Object.assign({}, getters.conversations[token]) + const actor = notification.subjectRichParameters.user || notification.subjectRichParameters.guest || { type: 'guest', id: 'unknown', @@ -549,29 +556,32 @@ const actions = { async updateCallStateFromNotification({ getters, commit }, { notification }) { const token = notification.objectId - const conversation = { ...getters.conversation(token) } - if (!conversation) { + if (!getters.conversations[token]) { // Conversation not loaded yet, skipping return } - conversation.hasCall = true - conversation.callFlag = PARTICIPANT.CALL_FLAG.WITH_VIDEO - conversation.activeSince = (new Date(notification.datetime)).getTime() / 1000 - conversation.lastActivity = conversation.activeSince - conversation.callStartTime = conversation.activeSince + const activeSince = (new Date(notification.datetime)).getTime() / 1000 + + const conversation = Object.assign({}, getters.conversations[token], { + hasCall: true, + callFlag: PARTICIPANT.CALL_FLAG.WITH_VIDEO, + activeSince, + lastActivity: activeSince, + callStartTime: activeSince, + }) // Inaccurate but best effort from here on: const lastMessage = { token, - id: 'temp' + conversation.activeSince, + id: 'temp' + activeSince, actorType: 'guests', actorId: 'unknown', actorDisplayName: t('spreed', 'Guest'), message: notification.subjectRich, messageParameters: notification.subjectRichParameters, - timestamp: conversation.activeSince, + timestamp: activeSince, messageType: 'system', systemMessage: 'call_started', expirationTimestamp: 0, diff --git a/src/store/conversationsStore.spec.js b/src/store/conversationsStore.spec.js index 741fd6c6f..54a7ecde2 100644 --- a/src/store/conversationsStore.spec.js +++ b/src/store/conversationsStore.spec.js @@ -1,4 +1,5 @@ import { createLocalVue } from '@vue/test-utils' +import flushPromises from 'flush-promises' import { cloneDeep } from 'lodash' import Vuex from 'vuex' @@ -26,6 +27,7 @@ import { deleteConversation, setConversationPermissions, setCallPermissions, + setConversationUnread, } from '../services/conversationsService.js' import storeConfig from './storeConfig.js' @@ -47,6 +49,7 @@ jest.mock('../services/conversationsService', () => ({ deleteConversation: jest.fn(), setConversationPermissions: jest.fn(), setCallPermissions: jest.fn(), + setConversationUnread: jest.fn(), })) describe('conversationsStore', () => { @@ -583,6 +586,31 @@ describe('conversationsStore', () => { expect(changedConversation.unreadMention).toBe(false) }) + test('marks conversation as unread', async () => { + testConversation.unreadMessages = 0 + + const response = { + data: { + ocs: { + data: { ...testConversation, unreadMessages: 1 }, + }, + }, + } + fetchConversation.mockResolvedValue(response) + + store.dispatch('addConversation', testConversation) + + store.dispatch('markConversationUnread', { token: testToken }) + + await flushPromises() + + expect(setConversationUnread).toHaveBeenCalled() + expect(fetchConversation).toHaveBeenCalled() + + const changedConversation = store.getters.conversation(testToken) + expect(changedConversation.unreadMessages).toBe(1) + }) + test('updates last common read message', () => { testConversation.lastCommonReadMessage = { id: 999, |