diff --git a/src/store/messages/saga.fetch.test.ts b/src/store/messages/saga.fetch.test.ts index ae7bb84db..9fc5b746c 100644 --- a/src/store/messages/saga.fetch.test.ts +++ b/src/store/messages/saga.fetch.test.ts @@ -53,10 +53,7 @@ describe(fetch, () => { const messageResponse = { hasMore: false, messages: [] }; const { storeState } = await subject(fetch, { payload: { channelId: channel.id } }) - .provide([ - [matchers.call.fn(chatClient.getMessagesByChannelId), messageResponse], - [matchers.call.fn(mapMessagesAndPreview), messageResponse.messages], - ]) + .provide([[matchers.call.fn(chatClient.getMessagesByChannelId), messageResponse]]) .withReducer(rootReducer, initialChannelState(channel)) .run(); @@ -65,12 +62,8 @@ describe(fetch, () => { it('sets hasLoadedMessages on channel', async () => { const channel = { id: 'channel-id', hasLoadedMessages: false }; - const messageResponse = { hasMore: false, messages: [] }; const { storeState } = await subject(fetch, { payload: { channelId: channel.id } }) - .provide([ - [matchers.call.fn(mapMessagesAndPreview), messageResponse.messages], - ]) .withReducer(rootReducer, initialChannelState(channel)) .run(); @@ -93,7 +86,6 @@ describe(fetch, () => { .withReducer(rootReducer, initialState as any) .provide([ [call([chatClient, chatClient.getMessagesByChannelId], channel.id, referenceTimestamp), messageResponse], - [matchers.call.fn(mapMessagesAndPreview), messageResponse.messages], ]) .run(); diff --git a/src/store/messages/saga.receiveNewMessage.test.ts b/src/store/messages/saga.receiveNewMessage.test.ts index 0aa2e2e56..5d3782355 100644 --- a/src/store/messages/saga.receiveNewMessage.test.ts +++ b/src/store/messages/saga.receiveNewMessage.test.ts @@ -10,7 +10,6 @@ import { denormalize as denormalizeChannel } from '../channels'; import { expectSaga, stubResponse } from '../../test/saga'; import { markConversationAsRead } from '../channels/saga'; import { StoreBuilder } from '../test/store'; -import { getMessageEmojiReactions } from '../../lib/chat'; describe(receiveNewMessage, () => { function subject(...args: Parameters) { @@ -30,11 +29,7 @@ describe(receiveNewMessage, () => { const initialState = new StoreBuilder().withConversationList({ id: channelId, messages: existingMessages }); const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), - ]) .withReducer(rootReducer, initialState.build()) - .run(); const channel = denormalizeChannel(channelId, storeState); @@ -50,9 +45,6 @@ describe(receiveNewMessage, () => { .withUsers({ userId: 'user-1', matrixId: 'matrix-id', firstName: 'the real user' }); const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), - ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -68,7 +60,6 @@ describe(receiveNewMessage, () => { const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), stubResponse(call(getPreview, 'www.google.com'), stubPreview), ]) .withReducer(rootReducer, initialState.build()) @@ -78,25 +69,6 @@ describe(receiveNewMessage, () => { expect(channel.messages[0].preview).toEqual(stubPreview); }); - it('adds the reactions to the message', async () => { - const channelId = 'channel-id'; - const message = { id: 'message-id', message: 'www.google.com' }; - const stubPreview = { id: 'simulated-preview' }; - const stubReactions = [{ eventId: 'message-id', key: '😂' }]; - const initialState = new StoreBuilder().withConversationList({ id: channelId }); - - const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), stubReactions), - stubResponse(call(getPreview, 'www.google.com'), stubPreview), - ]) - .withReducer(rootReducer, initialState.build()) - .run(); - - const channel = denormalizeChannel(channelId, storeState); - expect(channel.messages[0].reactions).toEqual({ '😂': 1 }); - }); - it('does nothing if the channel does not exist', async () => { const channelId = 'non-existing-channel-id'; const initialState = new StoreBuilder().withConversationList({ id: 'other-channel' }); @@ -110,7 +82,6 @@ describe(receiveNewMessage, () => { it('favors the new version if message already exists', async () => { const channelId = 'channel-id'; const message = { id: 'new-message', message: 'the new message' }; - const existingMessages = [ { id: 'new-message', message: 'message_0001' }, { id: 'other-message', message: 'message_0002' }, @@ -118,9 +89,6 @@ describe(receiveNewMessage, () => { const initialState = new StoreBuilder().withConversationList({ id: channelId, messages: existingMessages }); const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), - ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -134,9 +102,6 @@ describe(receiveNewMessage, () => { const conversationState = new StoreBuilder().withConversationList({ id: 'channel-id' }); await subject(receiveNewMessage, { payload: { channelId: 'channel-id', message } }) - .provide([ - stubResponse(call(getMessageEmojiReactions, 'channel-id'), [{}]), - ]) .withReducer(rootReducer, conversationState.build()) .not.call(markConversationAsRead, 'channel-id') .run(); @@ -160,9 +125,6 @@ describe(receiveNewMessage, () => { }); const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), - ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -187,9 +149,6 @@ describe(receiveNewMessage, () => { }); const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), - ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -218,9 +177,6 @@ describe(receiveNewMessage, () => { const initialState = new StoreBuilder().withConversationList({ id: channelId, messages: existingMessages }); const { storeState } = await subject(batchedReceiveNewMessage, eventPayloads) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), - ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -246,10 +202,6 @@ describe(receiveNewMessage, () => { ); const { storeState } = await subject(batchedReceiveNewMessage, eventPayloads) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId1), [{}]), - stubResponse(call(getMessageEmojiReactions, channelId2), [{}]), - ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -274,9 +226,6 @@ describe(receiveNewMessage, () => { const initialState = new StoreBuilder().withConversationList({ id: channelId, messages: existingMessages }); const { storeState } = await subject(batchedReceiveNewMessage, eventPayloads) - .provide([ - stubResponse(call(getMessageEmojiReactions, channelId), [{}]), - ]) .withReducer(rootReducer, initialState.build()) .run(); diff --git a/src/store/messages/saga.test.ts b/src/store/messages/saga.test.ts index c28ff3ad0..0401a9ea0 100644 --- a/src/store/messages/saga.test.ts +++ b/src/store/messages/saga.test.ts @@ -9,6 +9,7 @@ import { sendBrowserNotification, receiveUpdateMessage, replaceOptimisticMessage, + applyEmojiReactions, onMessageEmojiReactionChange, updateMessageEmojiReaction, sendEmojiReaction, @@ -288,7 +289,7 @@ describe(receiveUpdateMessage, () => { const { storeState } = await expectSaga(receiveUpdateMessage, { payload: { channelId: 'channel-1', message: editedMessage }, }) - .provide([...successResponses(), [call(getMessageEmojiReactions, 'channel-1'), [{}]]]) + .provide([...successResponses()]) .withReducer(rootReducer, initialState.build()) .run(); @@ -306,9 +307,7 @@ describe(receiveUpdateMessage, () => { payload: { channelId: 'channel-1', message: editedMessage }, }) .provide([ - [call(getMessageEmojiReactions, 'channel-1'), [{}]], [call(getPreview, editedMessage.message), preview], - ...successResponses(), ]) .withReducer(rootReducer, initialState.build()) @@ -317,34 +316,6 @@ describe(receiveUpdateMessage, () => { expect(storeState.normalized.messages[message.id]).toEqual({ ...editedMessage, preview }); }); - it('adds the reactions if they exist', async () => { - const message = { id: 8667728016, message: 'original message' }; - const editedMessage = { id: 8667728016, message: 'edited message with reaction' }; - const reactions = [ - { eventId: 8667728016, key: '😂' }, - { eventId: 8667728016, key: '👍' }, - ]; - - const expectedReactions = { - '😂': 1, - '👍': 1, - }; - - const initialState = new StoreBuilder().withConversationList({ id: 'channel-1', messages: [message] as any }); - - const { storeState } = await expectSaga(receiveUpdateMessage, { - payload: { channelId: 'channel-1', message: editedMessage }, - }) - .provide([ - [call(getMessageEmojiReactions, 'channel-1'), reactions], - ...successResponses(), - ]) - .withReducer(rootReducer, initialState.build()) - .run(); - - expect(storeState.normalized.messages[message.id]).toEqual({ ...editedMessage, reactions: expectedReactions }); - }); - function successResponses() { return [ [ @@ -421,6 +392,91 @@ describe(replaceOptimisticMessage, () => { }); }); +describe('applyEmojiReactions', () => { + it('applies emoji reactions to messages correctly', async () => { + const roomId = 'room-id'; + const messages = [ + { id: 'message-1', reactions: {} }, + { id: 'message-2', reactions: {} }, + ] as any; + + const reactions = [ + { eventId: 'message-1', key: '😲' }, + { eventId: 'message-1', key: '❤️' }, + { eventId: 'message-2', key: '😂' }, + { eventId: 'message-2', key: '❤️' }, + ]; + + await expectSaga(applyEmojiReactions, roomId, messages) + .provide([[call(getMessageEmojiReactions, roomId), reactions]]) + .run(); + + expect(messages).toEqual([ + { id: 'message-1', reactions: { '😲': 1, '❤️': 1 } }, + { id: 'message-2', reactions: { '😂': 1, '❤️': 1 } }, + ]); + }); + + it('does not modify messages without reactions', async () => { + const roomId = 'room-id'; + const messages = [ + { id: 'message-1', reactions: {} }, + { id: 'message-2', reactions: {} }, + ] as any; + + const reactions = []; + + await expectSaga(applyEmojiReactions, roomId, messages) + .provide([[call(getMessageEmojiReactions, roomId), reactions]]) + .run(); + + expect(messages).toEqual([ + { id: 'message-1', reactions: {} }, + { id: 'message-2', reactions: {} }, + ]); + }); + + it('accumulates reactions for the same key', async () => { + const roomId = 'room-id'; + const messages = [ + { id: 'message-1', reactions: {} }, + ] as any; + + const reactions = [ + { eventId: 'message-1', key: '❤️' }, + { eventId: 'message-1', key: '❤️' }, + { eventId: 'message-1', key: '😂' }, + ]; + + await expectSaga(applyEmojiReactions, roomId, messages) + .provide([[call(getMessageEmojiReactions, roomId), reactions]]) + .run(); + + expect(messages).toEqual([ + { id: 'message-1', reactions: { '❤️': 2, '😂': 1 } }, + ]); + }); + + it('handles reactions when there are no matching messages', async () => { + const roomId = 'room-id'; + const messages = [ + { id: 'message-1', reactions: {} }, + ] as any; + + const reactions = [ + { eventId: 'message-2', key: '❤️' }, + ]; + + await expectSaga(applyEmojiReactions, roomId, messages) + .provide([[call(getMessageEmojiReactions, roomId), reactions]]) + .run(); + + expect(messages).toEqual([ + { id: 'message-1', reactions: {} }, + ]); + }); +}); + describe('onMessageEmojiReactionChange', () => { it('calls updateMessageEmojiReaction with the correct arguments', async () => { const roomId = 'room-id'; diff --git a/src/store/messages/saga.ts b/src/store/messages/saga.ts index c88e5f6b8..536067655 100644 --- a/src/store/messages/saga.ts +++ b/src/store/messages/saga.ts @@ -10,6 +10,7 @@ import { MediaType, MessageSendStatus, MediaDownloadStatus, + Message, } from '.'; import { receive as receiveMessage } from './'; import { ConversationStatus, MessagesFetchState, DefaultRoomLabels } from '../channels'; @@ -120,8 +121,6 @@ export function* getLocalZeroUsersMap() { } export function* mapMessagesAndPreview(messages, channelId) { - const reactions = yield call(getMessageEmojiReactions, channelId); - const zeroUsersMap = yield call(mapMessageSenders, messages, channelId); yield call(mapAdminUserIdToZeroUserId, [{ messages }], zeroUsersMap); @@ -134,14 +133,6 @@ export function* mapMessagesAndPreview(messages, channelId) { if (preview) { message.preview = preview; } - - const relatedReactions = reactions.filter((reaction) => reaction.eventId === message.id); - if (relatedReactions.length > 0) { - message.reactions = relatedReactions.reduce((acc, reaction) => { - acc[reaction.key] = (acc[reaction.key] || 0) + 1; - return acc; - }, message.reactions || {}); - } } return messages; @@ -175,6 +166,10 @@ export function* fetch(action) { messages = [...messagesResponse.messages, ...existingMessages]; messages = uniqBy(messages, (m) => m.id ?? m); + if (yield select(_isActive(channelId))) { + yield call(applyEmojiReactions, channelId, messages); + } + yield call(receiveChannel, { id: channelId, messages, @@ -706,3 +701,23 @@ export function* updateMessageEmojiReaction(roomId, { eventId, key }) { yield call(receiveChannel, { id: roomId, messages: updatedMessages }); } } + +export function* applyEmojiReactions(roomId: string, messages: Message[]): Generator { + const reactions = yield call(getMessageEmojiReactions, roomId); + + messages.forEach((message) => { + const relatedReactions = reactions.filter((reaction) => { + const messageId = message?.id?.toString(); + const eventId = reaction.eventId.toString(); + return eventId === messageId; + }); + + if (relatedReactions.length > 0) { + message.reactions = relatedReactions.reduce((acc, reaction) => { + const key = reaction.key; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, message.reactions || {}); + } + }); +}