diff options
author | Maksim Sukharev <antreesy.web@gmail.com> | 2024-07-03 16:01:11 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-03 16:01:11 +0200 |
commit | 372d728d3d9eba80c6f3402b1bb2d4429004e116 (patch) | |
tree | 869a49d5588d3799595c457d6ad6ebde02b95949 | |
parent | a3c275cacab1b70b949895fefbdc4876fd2afc04 (diff) | |
parent | 4b5daa2d45fb28fd01a038b3af2084665e2a0a9d (diff) |
Merge pull request #12632 from nextcloud/feat/noid/delete-message-reactive
fix(chat): promptly update messages list in sidebar, store and chat
-rw-r--r-- | src/components/MessagesList/MessagesList.vue | 40 | ||||
-rw-r--r-- | src/store/messagesStore.js | 21 | ||||
-rw-r--r-- | src/store/messagesStore.spec.js | 125 |
3 files changed, 62 insertions, 124 deletions
diff --git a/src/components/MessagesList/MessagesList.vue b/src/components/MessagesList/MessagesList.vue index 16cd6b972..6abf5cbc5 100644 --- a/src/components/MessagesList/MessagesList.vue +++ b/src/components/MessagesList/MessagesList.vue @@ -406,49 +406,31 @@ export default { }, softUpdateAuthorGroups(oldGroups, newGroups, dateTimestamp) { - const newGroupIdSet = new Set(this.messagesList.map(message => message.id)) - const newGroupsKeys = Object.keys(newGroups) + const groupIds = new Set([...Object.keys(oldGroups), ...Object.keys(newGroups)]) - Object.entries(oldGroups).forEach(([id, oldGroup]) => { - if (newGroupsKeys.includes(id)) { - // Group is presented, will be handled in the next loop - return - } - - if (oldGroup.messages.some(message => !newGroupIdSet.has(message.id))) { - // Delete groups of normal and temporary messages, - // if at least one message from the group is no longer in the store + groupIds.forEach(id => { + if (oldGroups[id] && !newGroups[id]) { + // group no longer exists, remove this.$delete(this.messagesGroupedByDateByAuthor[dateTimestamp], id) - } - }) - Object.entries(newGroups).forEach(([id, newGroup]) => { - if (!oldGroups[id]) { - const oldId = Object.keys(oldGroups) - .find(key => +id < +key && oldGroups[key].nextMessageId <= newGroup.nextMessageId) - if (oldId) { - // newGroup includes oldGroup and more old messages, remove oldGroup - this.$delete(this.messagesGroupedByDateByAuthor[dateTimestamp], oldId) - } - // newGroup is not presented in the list, add it - this.$set(this.messagesGroupedByDateByAuthor[dateTimestamp], id, newGroup) - } else if (!this.areGroupsIdentical(newGroup, oldGroups[id])) { - // newGroup includes oldGroup and more recent messages - this.$set(this.messagesGroupedByDateByAuthor[dateTimestamp], id, newGroup) + } else if ((newGroups[id] && !oldGroups[id]) || !this.areGroupsIdentical(newGroups[id], oldGroups[id])) { + // group did not exist before, or group differs from previous state, add + this.$set(this.messagesGroupedByDateByAuthor[dateTimestamp], id, newGroups[id]) } }) }, areGroupsIdentical(group1, group2) { + // Compare plain values if (group1.messages.length !== group2.messages.length - || JSON.stringify(group1.messages) !== JSON.stringify(group2.messages) || group1.dateSeparator !== group2.dateSeparator || group1.previousMessageId !== group2.previousMessageId || group1.nextMessageId !== group2.nextMessageId) { return false } - // Check for temporary messages, replaced with messages from server - return group1.messages.every((message, index) => group2.messages[index].id === message.id) + // Compare ids and stringified messages (look for temporary, edited, deleted messages, replaced from server) + return group1.messages.every((message, index) => group2.messages[index].id === message.id + && JSON.stringify(group2.messages[index]) === JSON.stringify(message)) }, removeExpiredMessagesFromStore() { diff --git a/src/store/messagesStore.js b/src/store/messagesStore.js index 7895a2cea..84ff44445 100644 --- a/src/store/messagesStore.js +++ b/src/store/messagesStore.js @@ -290,13 +290,7 @@ const mutations = { if (!state.messages[token]) { Vue.set(state.messages, token, {}) } - if (state.messages[token][message.id]) { - Vue.set(state.messages[token], message.id, - Object.assign({}, state.messages[token][message.id], message) - ) - } else { - Vue.set(state.messages[token], message.id, message) - } + Vue.set(state.messages[token], message.id, message) }, /** * Deletes a message from the store. @@ -541,15 +535,6 @@ const actions = { context.commit('addMessage', { token, message: message.parent }) } - // update conversation lastMessage, if it was edited - if (message.systemMessage === 'message_edited' - && message.parent.id === context.getters.conversation(token).lastMessage.id) { - context.dispatch('updateConversationLastMessage', { - token, - lastMessage: message.parent, - }) - } - const reactionsStore = useReactionsStore() if (message.systemMessage === 'message_deleted') { reactionsStore.resetReactions(token, message.parent.id) @@ -558,6 +543,10 @@ const actions = { } if (message.systemMessage === 'message_edited' || message.systemMessage === 'message_deleted') { + // update conversation lastMessage, if it was edited or deleted + if (message.parent.id === context.getters.conversation(token).lastMessage.id) { + context.dispatch('updateConversationLastMessage', { token, lastMessage: message.parent }) + } // Check existing messages for having a deleted / edited message as parent, and update them context.getters.messagesList(token) .filter(storedMessage => storedMessage.parent?.id === message.parent.id && JSON.stringify(storedMessage.parent) !== JSON.stringify(message.parent)) diff --git a/src/store/messagesStore.spec.js b/src/store/messagesStore.spec.js index e2ce0b247..7d230381d 100644 --- a/src/store/messagesStore.spec.js +++ b/src/store/messagesStore.spec.js @@ -68,9 +68,23 @@ jest.mock('@nextcloud/capabilities', () => ({ describe('messagesStore', () => { const TOKEN = 'XXTOKENXX' + const conversation = { + token: TOKEN, + lastMessage: { + id: 123, + }, + } + let localVue = null let testStoreConfig let store = null + let getActorIdMock + let getUserIdMock + let getActorTypeMock + let getDisplayNameMock + let conversationMock + let updateConversationLastMessageMock + let updateConversationLastReadMessageMock let updateConversationLastActiveAction let reactionsStore @@ -80,10 +94,26 @@ describe('messagesStore', () => { setActivePinia(createPinia()) reactionsStore = useReactionsStore() - testStoreConfig = cloneDeep(messagesStore) + testStoreConfig = cloneDeep(storeConfig) + getActorIdMock = jest.fn().mockReturnValue(() => 'actor-id-1') + getUserIdMock = jest.fn().mockReturnValue(() => 'actor-id-1') + getActorTypeMock = jest.fn().mockReturnValue(() => ATTENDEE.ACTOR_TYPE.USERS) + getDisplayNameMock = jest.fn().mockReturnValue(() => 'actor-display-name-1') + conversationMock = jest.fn().mockReturnValue(conversation) + updateConversationLastMessageMock = jest.fn() + updateConversationLastReadMessageMock = jest.fn() updateConversationLastActiveAction = jest.fn() - testStoreConfig.actions.updateConversationLastActive = updateConversationLastActiveAction + + testStoreConfig.modules.actorStore.getters.getActorId = getActorIdMock + testStoreConfig.modules.actorStore.getters.getUserId = getUserIdMock + testStoreConfig.modules.actorStore.getters.getActorType = getActorTypeMock + testStoreConfig.modules.actorStore.getters.getDisplayName = getDisplayNameMock + testStoreConfig.modules.conversationsStore.getters.conversation = jest.fn().mockReturnValue(conversationMock) + testStoreConfig.modules.conversationsStore.actions.updateConversationLastMessage = updateConversationLastMessageMock + testStoreConfig.modules.conversationsStore.actions.updateConversationLastReadMessage = updateConversationLastReadMessageMock + testStoreConfig.modules.conversationsStore.actions.updateConversationLastActive = updateConversationLastActiveAction + testStoreConfig.modules.pollStore.getters.debounceGetPollData = jest.fn() store = new Vuex.Store(testStoreConfig) }) @@ -104,11 +134,8 @@ describe('messagesStore', () => { }) test('doesn\'t add specific messages to the store', () => { - testStoreConfig = cloneDeep(storeConfig) - testStoreConfig.modules.pollStore.getters.debounceGetPollData = jest.fn() reactionsStore.resetReactions = jest.fn() reactionsStore.processReaction = jest.fn() - store = new Vuex.Store(testStoreConfig) const messages = [{ id: 2, @@ -229,9 +256,7 @@ describe('messagesStore', () => { let message beforeEach(() => { - testStoreConfig = cloneDeep(storeConfig) reactionsStore.resetReactions = jest.fn() - store = new Vuex.Store(testStoreConfig) message = { id: 10, @@ -361,24 +386,8 @@ describe('messagesStore', () => { describe('edit message', () => { let message - let conversationMock - let updateConversationLastMessageMock beforeEach(() => { - const conversation = { - token: TOKEN, - lastMessage: { - id: 10, - }, - } - - testStoreConfig = cloneDeep(messagesStore) - conversationMock = jest.fn().mockReturnValue(conversation) - updateConversationLastMessageMock = jest.fn() - testStoreConfig.getters.conversation = jest.fn().mockReturnValue(conversationMock) - testStoreConfig.actions.updateConversationLastMessage = updateConversationLastMessageMock - store = new Vuex.Store(testStoreConfig) - message = { id: 10, token: TOKEN, @@ -470,28 +479,13 @@ describe('messagesStore', () => { describe('temporary messages', () => { let mockDate - let getActorIdMock - let getActorTypeMock - let getDisplayNameMock let chatExtraStore beforeEach(() => { mockDate = new Date('2020-01-01 20:00:00') jest.spyOn(global, 'Date') .mockImplementation(() => mockDate) - - testStoreConfig = cloneDeep(messagesStore) chatExtraStore = useChatExtrasStore() - - getActorIdMock = jest.fn().mockReturnValue(() => 'actor-id-1') - getActorTypeMock = jest.fn().mockReturnValue(() => ATTENDEE.ACTOR_TYPE.USERS) - getDisplayNameMock = jest.fn().mockReturnValue(() => 'actor-display-name-1') - testStoreConfig.getters.getActorId = getActorIdMock - testStoreConfig.getters.getActorType = getActorTypeMock - testStoreConfig.getters.getDisplayName = getDisplayNameMock - testStoreConfig.actions.updateConversationLastActive = updateConversationLastActiveAction - - store = new Vuex.Store(testStoreConfig) }) test('creates temporary message', async () => { @@ -640,8 +634,10 @@ describe('messagesStore', () => { expect(updateConversationLastActiveAction).toHaveBeenCalledWith(expect.anything(), TOKEN) // add again just replaces it - temporaryMessage.message = 'replaced' - store.dispatch('addTemporaryMessage', { token: TOKEN, message: temporaryMessage }) + store.dispatch('addTemporaryMessage', { + token: TOKEN, + message: { ...temporaryMessage, message: 'replaced' } + }) expect(store.getters.messagesList(TOKEN)).toMatchObject([{ id: 'temp-1577908800000', @@ -757,31 +753,9 @@ describe('messagesStore', () => { }) describe('last read message markers', () => { - let conversationMock - let getUserIdMock - let updateConversationLastReadMessageMock - beforeEach(() => { - const conversation = { - lastMessage: { - id: 123, - }, - } - - testStoreConfig = cloneDeep(messagesStore) - - getUserIdMock = jest.fn() - conversationMock = jest.fn().mockReturnValue(conversation) - updateConversationLastReadMessageMock = jest.fn() - testStoreConfig.getters.conversation = jest.fn().mockReturnValue(conversationMock) - testStoreConfig.getters.getUserId = jest.fn().mockReturnValue(getUserIdMock) - testStoreConfig.actions.updateConversationLastReadMessage = updateConversationLastReadMessageMock - testStoreConfig.actions.addConversation = jest.fn() - const response = generateOCSResponse({ payload: conversation }) updateLastReadMessage.mockResolvedValue(response) - - store = new Vuex.Store(testStoreConfig) }) test('stores visual last read message id per token', () => { @@ -793,7 +767,7 @@ describe('messagesStore', () => { }) test('clears last read message', async () => { - getUserIdMock.mockReturnValue('user-1') + getUserIdMock.mockReturnValue(() => 'user-1') store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) await store.dispatch('clearLastReadMessage', { @@ -813,7 +787,7 @@ describe('messagesStore', () => { }) test('clears last read message for federated conversation', async () => { - getUserIdMock.mockReturnValue('federated-user-1') + getUserIdMock.mockReturnValue(() => 'federated-user-1') conversationMock.mockReturnValue({ lastMessage: {}, remoteServer: 'nextcloud.com', @@ -835,7 +809,7 @@ describe('messagesStore', () => { }) test('clears last read message and update visually', async () => { - getUserIdMock.mockReturnValue('user-1') + getUserIdMock.mockReturnValue(() => 'user-1') store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) await store.dispatch('clearLastReadMessage', { @@ -855,7 +829,7 @@ describe('messagesStore', () => { }) test('clears last read message for guests', async () => { - getUserIdMock.mockReturnValue(null) + getUserIdMock.mockReturnValue(() => null) store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) await store.dispatch('clearLastReadMessage', { @@ -875,7 +849,7 @@ describe('messagesStore', () => { }) test('updates last read message', async () => { - getUserIdMock.mockReturnValue('user-1') + getUserIdMock.mockReturnValue(() => 'user-1') const response = generateOCSResponse({ payload: { unreadMessages: 0, @@ -903,7 +877,7 @@ describe('messagesStore', () => { }) test('updates last read message and update visually', async () => { - getUserIdMock.mockReturnValue('user-1') + getUserIdMock.mockReturnValue(() => 'user-1') const response = generateOCSResponse({ payload: { unreadMessages: 0, @@ -931,7 +905,7 @@ describe('messagesStore', () => { }) test('updates last read message for guests', async () => { - getUserIdMock.mockReturnValue(null) + getUserIdMock.mockReturnValue(() => null) store.dispatch('setVisualLastReadMessageId', { token: TOKEN, id: 100 }) await store.dispatch('updateLastReadMessage', { @@ -1790,7 +1764,7 @@ describe('messagesStore', () => { lastMessage: { id: 100 }, lastReadMessage: 50, }) - getUserIdMock.mockReturnValue('current-user') + getUserIdMock.mockReturnValue(() => 'current-user') const temporaryMessage = { id: 'temp-123', @@ -1985,11 +1959,10 @@ describe('messagesStore', () => { */ function setupWithValues(lastKnownMessageId, lastConversationMessageId) { store.dispatch('setLastKnownMessageId', { token: TOKEN, id: 123 }) - const conversationMock = jest.fn().mockReturnValue({ + conversationMock.mockReturnValue({ + token: TOKEN, lastMessage: { id: lastConversationMessageId }, }) - testStoreConfig.getters.conversation = jest.fn().mockReturnValue(conversationMock) - store = new Vuex.Store(testStoreConfig) store.dispatch('setLastKnownMessageId', { token: TOKEN, id: lastKnownMessageId }) } @@ -2015,12 +1988,6 @@ describe('messagesStore', () => { let messageExpected beforeEach(() => { - localVue = createLocalVue() - localVue.use(Vuex) - - testStoreConfig = cloneDeep(storeConfig) - store = new Vuex.Store(testStoreConfig) - message1 = { id: 1, token: TOKEN, |