From cb025dde869654ecb1226e3d8dcfab7bf5b251a4 Mon Sep 17 00:00:00 2001 From: dominic Date: Mon, 16 Oct 2023 08:54:34 +0100 Subject: [PATCH 1/5] feat: set isOnline status with user presence data --- src/lib/chat/matrix-client.ts | 2 +- src/store/channels-list/saga.ts | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/lib/chat/matrix-client.ts b/src/lib/chat/matrix-client.ts index 60cc1a171..1ea4388fa 100644 --- a/src/lib/chat/matrix-client.ts +++ b/src/lib/chat/matrix-client.ts @@ -508,7 +508,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.ts b/src/store/channels-list/saga.ts index da097a425..1c9b0d9a2 100644 --- a/src/store/channels-list/saga.ts +++ b/src/store/channels-list/saga.ts @@ -63,19 +63,26 @@ 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 { isOneOnOne, otherMembers } = conversation; + const matrixId = otherMembers?.[0]?.matrixId; - if (conversation.isOneOnOne && matrixId) { + if (isOneOnOne && matrixId) { const presenceData = yield call([chatClient, chatClient.getUserPresence], matrixId); - if (presenceData && presenceData.lastSeenAt) { - conversation.otherMembers[0].lastSeenAt = presenceData.lastSeenAt; + + if (presenceData) { + if (presenceData.lastSeenAt) { + conversation.otherMembers[0].lastSeenAt = presenceData.lastSeenAt; + } + if (presenceData.isOnline) { + conversation.otherMembers[0].isOnline = presenceData.isOnline; + } } } } @@ -113,7 +120,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 From 04488f9a42d64377a95c8a21f3d10b31f0281006 Mon Sep 17 00:00:00 2001 From: dominic Date: Tue, 17 Oct 2023 13:08:15 +0100 Subject: [PATCH 2/5] fix: ensure presence is fetched for multiple other members in a conversation --- src/lib/chat/matrix-client.ts | 13 +++++++++---- src/store/channels-list/saga.ts | 23 +++++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/lib/chat/matrix-client.ts b/src/lib/chat/matrix-client.ts index 1ea4388fa..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) { diff --git a/src/store/channels-list/saga.ts b/src/store/channels-list/saga.ts index 1c9b0d9a2..e2cb1bc8a 100644 --- a/src/store/channels-list/saga.ts +++ b/src/store/channels-list/saga.ts @@ -70,19 +70,22 @@ export function* updateUserPresence(conversations) { const chatClient = yield call(chat.get); for (let conversation of conversations) { - const { isOneOnOne, otherMembers } = conversation; - const matrixId = otherMembers?.[0]?.matrixId; + const { otherMembers } = conversation; + + for (let member of otherMembers) { + const matrixId = member?.matrixId; + if (!matrixId) continue; - if (isOneOnOne && matrixId) { const presenceData = yield call([chatClient, chatClient.getUserPresence], matrixId); + if (!presenceData) continue; + + const { lastSeenAt, isOnline } = presenceData; - if (presenceData) { - if (presenceData.lastSeenAt) { - conversation.otherMembers[0].lastSeenAt = presenceData.lastSeenAt; - } - if (presenceData.isOnline) { - conversation.otherMembers[0].isOnline = presenceData.isOnline; - } + if (lastSeenAt) { + member.lastSeenAt = lastSeenAt; + } + if (isOnline) { + member.isOnline = isOnline; } } } From 30cd94e47f0a7aba914d2a6973b97d14b5c1bcc8 Mon Sep 17 00:00:00 2001 From: dominic Date: Tue, 17 Oct 2023 13:22:31 +0100 Subject: [PATCH 3/5] feat: add test coverage for updateUserPresence --- src/store/channels-list/saga.test.ts | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/store/channels-list/saga.test.ts b/src/store/channels-list/saga.test.ts index 300badff2..7d547ebab 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,48 @@ 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(); + }); + }); }); From 9acc958288e91ca57e00c3a685b29c793549a3e9 Mon Sep 17 00:00:00 2001 From: dominic Date: Tue, 17 Oct 2023 13:42:42 +0100 Subject: [PATCH 4/5] feat: directly update isOnline and lastSeenAt --- src/store/channels-list/saga.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/store/channels-list/saga.ts b/src/store/channels-list/saga.ts index e2cb1bc8a..aa454935e 100644 --- a/src/store/channels-list/saga.ts +++ b/src/store/channels-list/saga.ts @@ -80,13 +80,8 @@ export function* updateUserPresence(conversations) { if (!presenceData) continue; const { lastSeenAt, isOnline } = presenceData; - - if (lastSeenAt) { - member.lastSeenAt = lastSeenAt; - } - if (isOnline) { - member.isOnline = isOnline; - } + member.lastSeenAt = lastSeenAt; + member.isOnline = isOnline; } } } From 358aff578d0c8245134f23dc5b74a66b79374732 Mon Sep 17 00:00:00 2001 From: dominic Date: Tue, 17 Oct 2023 13:48:28 +0100 Subject: [PATCH 5/5] feat: add test coverage for setting lastSeenAt and isOnline --- src/store/channels-list/saga.test.ts | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/store/channels-list/saga.test.ts b/src/store/channels-list/saga.test.ts index 7d547ebab..5fbf2176a 100644 --- a/src/store/channels-list/saga.test.ts +++ b/src/store/channels-list/saga.test.ts @@ -670,5 +670,63 @@ describe('channels list saga', () => { 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); + }); }); });