diff --git a/src/components/message/styles.scss b/src/components/message/styles.scss index a9326c03d..156000520 100644 --- a/src/components/message/styles.scss +++ b/src/components/message/styles.scss @@ -312,7 +312,7 @@ display: flex; align-items: center; justify-content: center; - font-size: 10px; + font-size: 12px; line-height: 14px; } } diff --git a/src/lib/chat/matrix-client.ts b/src/lib/chat/matrix-client.ts index d138b3c3d..3b160a0bb 100644 --- a/src/lib/chat/matrix-client.ts +++ b/src/lib/chat/matrix-client.ts @@ -477,6 +477,11 @@ export class MatrixClient implements IChatClient { hasMore = await this.matrix.paginateEventTimeline(liveTimeline, { backwards: true, limit: 50 }); } + const isEncrypted = room?.hasEncryptionStateEvent(); + if (isEncrypted) { + await room.decryptAllEvents(); + } + const effectiveEvents = events.map((event) => event.getEffectiveEvent()); const messages = await this.getAllChatMessagesFromRoom(effectiveEvents); @@ -551,7 +556,7 @@ export class MatrixClient implements IChatClient { const events = room.getLiveTimeline().getEvents(); const result = events - .filter((event) => event.getType() === MatrixConstants.REACTION) + .filter((event) => event.getType() === MatrixConstants.REACTION && !event?.event?.unsigned?.redacted_because) .map((event) => { const content = event.getContent(); const relatesTo = content[MatrixConstants.RELATES_TO]; diff --git a/src/store/messages/saga.fetch.test.ts b/src/store/messages/saga.fetch.test.ts index 9fc5b746c..ae7bb84db 100644 --- a/src/store/messages/saga.fetch.test.ts +++ b/src/store/messages/saga.fetch.test.ts @@ -53,7 +53,10 @@ describe(fetch, () => { const messageResponse = { hasMore: false, messages: [] }; const { storeState } = await subject(fetch, { payload: { channelId: channel.id } }) - .provide([[matchers.call.fn(chatClient.getMessagesByChannelId), messageResponse]]) + .provide([ + [matchers.call.fn(chatClient.getMessagesByChannelId), messageResponse], + [matchers.call.fn(mapMessagesAndPreview), messageResponse.messages], + ]) .withReducer(rootReducer, initialChannelState(channel)) .run(); @@ -62,8 +65,12 @@ 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(); @@ -86,6 +93,7 @@ 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 5d3782355..d705827c5 100644 --- a/src/store/messages/saga.receiveNewMessage.test.ts +++ b/src/store/messages/saga.receiveNewMessage.test.ts @@ -10,6 +10,7 @@ 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) { @@ -29,7 +30,11 @@ 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); @@ -45,6 +50,9 @@ 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(); @@ -60,6 +68,7 @@ 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()) @@ -69,6 +78,25 @@ 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' }); @@ -89,6 +117,9 @@ 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(); @@ -102,6 +133,9 @@ 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(); @@ -125,6 +159,9 @@ describe(receiveNewMessage, () => { }); const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) + .provide([ + stubResponse(call(getMessageEmojiReactions, channelId), [{}]), + ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -149,6 +186,9 @@ describe(receiveNewMessage, () => { }); const { storeState } = await subject(receiveNewMessage, { payload: { channelId, message } }) + .provide([ + stubResponse(call(getMessageEmojiReactions, channelId), [{}]), + ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -177,6 +217,9 @@ 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(); @@ -202,6 +245,10 @@ describe(receiveNewMessage, () => { ); const { storeState } = await subject(batchedReceiveNewMessage, eventPayloads) + .provide([ + stubResponse(call(getMessageEmojiReactions, channelId1), [{}]), + stubResponse(call(getMessageEmojiReactions, channelId2), [{}]), + ]) .withReducer(rootReducer, initialState.build()) .run(); @@ -226,6 +273,9 @@ 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 0401a9ea0..b3ba13f8c 100644 --- a/src/store/messages/saga.test.ts +++ b/src/store/messages/saga.test.ts @@ -9,7 +9,6 @@ import { sendBrowserNotification, receiveUpdateMessage, replaceOptimisticMessage, - applyEmojiReactions, onMessageEmojiReactionChange, updateMessageEmojiReaction, sendEmojiReaction, @@ -289,7 +288,7 @@ describe(receiveUpdateMessage, () => { const { storeState } = await expectSaga(receiveUpdateMessage, { payload: { channelId: 'channel-1', message: editedMessage }, }) - .provide([...successResponses()]) + .provide([...successResponses(), [call(getMessageEmojiReactions, 'channel-1'), [{}]]]) .withReducer(rootReducer, initialState.build()) .run(); @@ -300,6 +299,33 @@ describe(receiveUpdateMessage, () => { const message = { id: 8667728016, message: 'original message' }; const editedMessage = { id: 8667728016, message: 'edited message: www.example.com' }; const preview = { id: 'fdf2ce2b-062e-4a83-9c27-03f36c81c0c0', type: 'link' }; + 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'), [{}]], + [call(getPreview, editedMessage.message), preview], + + ...successResponses(), + ]) + .withReducer(rootReducer, initialState.build()) + .run(); + 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 }); @@ -307,13 +333,13 @@ describe(receiveUpdateMessage, () => { payload: { channelId: 'channel-1', message: editedMessage }, }) .provide([ - [call(getPreview, editedMessage.message), preview], + [call(getMessageEmojiReactions, 'channel-1'), reactions], ...successResponses(), ]) .withReducer(rootReducer, initialState.build()) .run(); - expect(storeState.normalized.messages[message.id]).toEqual({ ...editedMessage, preview }); + expect(storeState.normalized.messages[message.id]).toEqual({ ...editedMessage, reactions: expectedReactions }); }); function successResponses() { @@ -392,91 +418,6 @@ 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 536067655..bd5117ca2 100644 --- a/src/store/messages/saga.ts +++ b/src/store/messages/saga.ts @@ -10,7 +10,6 @@ import { MediaType, MessageSendStatus, MediaDownloadStatus, - Message, } from '.'; import { receive as receiveMessage } from './'; import { ConversationStatus, MessagesFetchState, DefaultRoomLabels } from '../channels'; @@ -121,6 +120,8 @@ 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); @@ -133,6 +134,23 @@ export function* mapMessagesAndPreview(messages, channelId) { if (preview) { message.preview = preview; } + + if (channelId === '!OhPCRBVfMZkCQRIBQX:zero-synapse-development.zer0.io') { + console.log('XXXX MESSAAGEGEGE 1 1 1 1 1 1 ', message); + } + + const relatedReactions = reactions.filter((reaction) => reaction.eventId === message.id); + if (relatedReactions.length > 0) { + message.reactions = relatedReactions.reduce((acc, reaction) => { + if (!reaction.key) return acc; // Skip if key is undefined + acc[reaction.key] = (acc[reaction.key] || 0) + 1; + return acc; + }, message.reactions || {}); + } + } + + if (channelId === '!OhPCRBVfMZkCQRIBQX:zero-synapse-development.zer0.io') { + console.log('XXXX MESSAAGEGEGE 2 2 2 2 2 2 ', messages); } return messages; @@ -166,10 +184,6 @@ 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, @@ -461,7 +475,6 @@ export function* batchedReceiveNewMessage(batchedPayloads) { if (!channel) { continue; } - const mappedMessages = yield call(mapMessagesAndPreview, byChannelId[channelId], channelId); yield receiveBatchedMessages(channelId, mappedMessages); @@ -701,23 +714,3 @@ 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 || {}); - } - }); -}