Skip to content

Commit

Permalink
feat: fetch the users presence asynchronously (isOnline) (#1101)
Browse files Browse the repository at this point in the history
* feat: set isOnline status with user presence data

* fix: ensure presence is fetched for multiple other members in a conversation

* feat: add test coverage for updateUserPresence

* feat: directly update isOnline and lastSeenAt

* feat: add test coverage for setting lastSeenAt and isOnline
  • Loading branch information
domw30 authored Oct 17, 2023
1 parent d61983e commit 0924ea3
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 12 deletions.
15 changes: 10 additions & 5 deletions src/lib/chat/matrix-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -508,7 +513,7 @@ export class MatrixClient implements IChatClient {
firstName: user?.displayName,
lastName: '',
profileId: '',
isOnline: user?.presence === 'online',
isOnline: false,
profileImage: user?.avatarUrl,
lastSeenAt: '',
};
Expand Down
104 changes: 104 additions & 0 deletions src/store/channels-list/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
otherUserJoinedChannel,
otherUserLeftChannel,
mapToZeroUsers,
updateUserPresence,
} from './saga';

import { SagaActionTypes, setStatus } from '.';
Expand Down Expand Up @@ -56,6 +57,7 @@ const MOCK_CONVERSATIONS = [mockConversation('0001'), mockConversation('0002')];
const chatClient = {
getChannels: () => MOCK_CHANNELS,
getConversations: () => MOCK_CONVERSATIONS,
getUserPresence: () => {},
};

describe('channels list saga', () => {
Expand Down Expand Up @@ -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);
});
});
});
19 changes: 12 additions & 7 deletions src/store/channels-list/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 0924ea3

Please sign in to comment.