diff --git a/src/lib/chat/matrix-client.ts b/src/lib/chat/matrix-client.ts index 60cc1a171..062fc4ddf 100644 --- a/src/lib/chat/matrix-client.ts +++ b/src/lib/chat/matrix-client.ts @@ -80,10 +80,15 @@ export class MatrixClient implements IChatClient { try { const userPresenceData = await this.matrix.getPresence(userId); - const isOnline = userPresenceData?.presence === 'online'; - const lastSeenAt = userPresenceData?.last_active_ago - ? new Date(Date.now() - userPresenceData.last_active_ago).toISOString() - : null; + + if (!userPresenceData) { + return { lastSeenAt: null, isOnline: false }; + } + + const { presence, last_active_ago } = userPresenceData; + + const isOnline = presence === 'online'; + const lastSeenAt = last_active_ago ? new Date(Date.now() - last_active_ago).toISOString() : null; return { lastSeenAt, isOnline }; } catch (error) { @@ -508,7 +513,7 @@ export class MatrixClient implements IChatClient { firstName: user?.displayName, lastName: '', profileId: '', - isOnline: user?.presence === 'online', + isOnline: false, profileImage: user?.avatarUrl, lastSeenAt: '', }; diff --git a/src/store/channels-list/saga.test.ts b/src/store/channels-list/saga.test.ts index 300badff2..5fbf2176a 100644 --- a/src/store/channels-list/saga.test.ts +++ b/src/store/channels-list/saga.test.ts @@ -16,6 +16,7 @@ import { otherUserJoinedChannel, otherUserLeftChannel, mapToZeroUsers, + updateUserPresence, } from './saga'; import { SagaActionTypes, setStatus } from '.'; @@ -56,6 +57,7 @@ const MOCK_CONVERSATIONS = [mockConversation('0001'), mockConversation('0002')]; const chatClient = { getChannels: () => MOCK_CHANNELS, getConversations: () => MOCK_CONVERSATIONS, + getUserPresence: () => {}, }; describe('channels list saga', () => { @@ -625,4 +627,106 @@ describe('channels list saga', () => { }); }); }); + + describe(updateUserPresence, () => { + function subject(conversations, provide = []) { + return expectSaga(updateUserPresence, conversations).provide([ + [matchers.call.fn(chat.get), chatClient], + ...provide, + ]); + } + + const mockOtherMembers = [{ matrixId: 'member_001' }, { matrixId: 'member_002' }, { matrixId: 'member_003' }]; + const mockConversations = [{ otherMembers: mockOtherMembers }]; + + it('exits early if feature flag is not enabled', async () => { + featureFlags.enableMatrix = false; + await subject(mockConversations).not.call(chat.get).run(); + }); + + it('fetches and updates user presence data', async () => { + featureFlags.enableMatrix = true; + + const mockPresenceData = { + lastSeenAt: '2023-01-01T00:00:00.000Z', + isOnline: true, + }; + + await subject(mockConversations, [ + [matchers.call([chatClient, chatClient.getUserPresence], 'member_001'), mockPresenceData], + [matchers.call([chatClient, chatClient.getUserPresence], 'member_002'), mockPresenceData], + [matchers.call([chatClient, chatClient.getUserPresence], 'member_003'), mockPresenceData], + ]) + .call(chat.get) + .call([chatClient, chatClient.getUserPresence], 'member_001') + .call([chatClient, chatClient.getUserPresence], 'member_002') + .call([chatClient, chatClient.getUserPresence], 'member_003') + .run(); + }); + + it('does not fail if member does not have matrixId', async () => { + featureFlags.enableMatrix = true; + const conversationsWithMissingMatrixId = [{ otherMembers: [{ matrixId: null }] }]; + + await subject(conversationsWithMissingMatrixId).call(chat.get).not.call(chatClient.getUserPresence).run(); + }); + + it('should set lastSeenAt, and isOnline to true if user is online', () => { + const mockConversations = [ + { + id: 'conversation_0001', + otherMembers: [ + { + userId: 'user_1', + matrixId: 'matrix_1', + lastSeenAt: '', + isOnline: false, + }, + ], + }, + ]; + + const mockPresenceData1 = { lastSeenAt: '2023-10-17T10:00:00.000Z', isOnline: true }; + + testSaga(updateUserPresence, mockConversations) + .next() + .call(chat.get) + .next(chatClient) + .call([chatClient, chatClient.getUserPresence], 'matrix_1') + .next(mockPresenceData1) + .isDone(); + + expect(mockConversations[0].otherMembers[0].lastSeenAt).toBe(mockPresenceData1.lastSeenAt); + expect(mockConversations[0].otherMembers[0].isOnline).toBe(mockPresenceData1.isOnline); + }); + + it('should set lastSeenAt to null and isOnline to false if user is offline', () => { + const mockConversations = [ + { + id: 'conversation_0001', + otherMembers: [ + { + userId: 'user_1', + matrixId: 'matrix_1', + lastSeenAt: '', + isOnline: false, + }, + ], + }, + ]; + + const mockPresenceData1 = { lastSeenAt: null, isOnline: false }; + + testSaga(updateUserPresence, mockConversations) + .next() + .call(chat.get) + .next(chatClient) + .call([chatClient, chatClient.getUserPresence], 'matrix_1') + .next(mockPresenceData1) + .isDone(); + + expect(mockConversations[0].otherMembers[0].lastSeenAt).toBe(mockPresenceData1.lastSeenAt); + expect(mockConversations[0].otherMembers[0].isOnline).toBe(mockPresenceData1.isOnline); + }); + }); }); diff --git a/src/store/channels-list/saga.ts b/src/store/channels-list/saga.ts index da097a425..aa454935e 100644 --- a/src/store/channels-list/saga.ts +++ b/src/store/channels-list/saga.ts @@ -63,20 +63,25 @@ export function* mapToZeroUsers(channels: any[]) { return; } -export function* updateOtherMembersLastSeenAt(conversations) { +export function* updateUserPresence(conversations) { if (!featureFlags.enableMatrix) { return; } const chatClient = yield call(chat.get); for (let conversation of conversations) { - const matrixId = conversation?.otherMembers?.[0]?.matrixId; + const { otherMembers } = conversation; + + for (let member of otherMembers) { + const matrixId = member?.matrixId; + if (!matrixId) continue; - if (conversation.isOneOnOne && matrixId) { const presenceData = yield call([chatClient, chatClient.getUserPresence], matrixId); - if (presenceData && presenceData.lastSeenAt) { - conversation.otherMembers[0].lastSeenAt = presenceData.lastSeenAt; - } + if (!presenceData) continue; + + const { lastSeenAt, isOnline } = presenceData; + member.lastSeenAt = lastSeenAt; + member.isOnline = isOnline; } } } @@ -113,7 +118,7 @@ export function* fetchConversations() { chatClient.getConversations, ]); yield call(mapToZeroUsers, conversations); - yield call(updateOtherMembersLastSeenAt, conversations); + yield call(updateUserPresence, conversations); const existingConversationList = yield select(denormalizeConversations); const optimisticConversationIds = existingConversationList