From 7469bbaee038715ccce05e2850b10d82fc9f41b5 Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Thu, 28 Sep 2023 13:47:28 -0600 Subject: [PATCH 1/2] fetch existing conversations for set of users via chat client --- src/lib/chat/index.ts | 5 +++++ src/lib/chat/matrix-client.ts | 4 ++++ src/lib/chat/sendbird-client.ts | 6 ++++++ src/store/channels-list/api.ts | 5 ----- src/store/create-conversation/saga.test.ts | 25 ++++++++++++++-------- src/store/create-conversation/saga.ts | 5 +++-- 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/lib/chat/index.ts b/src/lib/chat/index.ts index 8542df840..d109304a5 100644 --- a/src/lib/chat/index.ts +++ b/src/lib/chat/index.ts @@ -51,6 +51,7 @@ export interface IChatClient { file?: FileUploadResult, optimisticId?: string ) => Promise; + fetchConversationsWithUsers: (userIds: string[]) => Promise; } export class Chat { @@ -101,6 +102,10 @@ export class Chat { return this.client.sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId); } + async fetchConversationsWithUsers(userIds: string[]): Promise { + return this.client.fetchConversationsWithUsers(userIds); + } + initChat(events: RealtimeChatEvents): void { this.client.init(events); } diff --git a/src/lib/chat/matrix-client.ts b/src/lib/chat/matrix-client.ts index d2abb2f74..3b7613774 100644 --- a/src/lib/chat/matrix-client.ts +++ b/src/lib/chat/matrix-client.ts @@ -199,6 +199,10 @@ export class MatrixClient implements IChatClient { }; } + async fetchConversationsWithUsers(_userIds: string[]) { + return []; + } + get isDisconnected() { return this.connectionStatus === ConnectionStatus.Disconnected; } diff --git a/src/lib/chat/sendbird-client.ts b/src/lib/chat/sendbird-client.ts index 3ab8760d1..8fdfa2d0c 100644 --- a/src/lib/chat/sendbird-client.ts +++ b/src/lib/chat/sendbird-client.ts @@ -14,6 +14,7 @@ import { uploadImage, createConversation as createConversationMessageApi } from import { MemberNetworks } from '../../store/users/types'; import { DirectMessage } from '../../store/channels-list/types'; import { MentionableUser } from '../../store/channels/api'; +import { Channel } from '../../store/channels'; export class SendbirdClient implements IChatClient { sendbird: SendbirdGroupChat = null; @@ -143,6 +144,11 @@ export class SendbirdClient implements IChatClient { return sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId); } + async fetchConversationsWithUsers(userIds: string[]): Promise { + const response = await get('/conversations', { userIds }); + return response.body; + } + private initSessionHandler(events: RealtimeChatEvents) { const sessionHandler = new SessionHandler({ onSessionClosed: () => { diff --git a/src/store/channels-list/api.ts b/src/store/channels-list/api.ts index 4f322808c..056ada7d5 100644 --- a/src/store/channels-list/api.ts +++ b/src/store/channels-list/api.ts @@ -27,11 +27,6 @@ export async function createConversation( return directMessages.body; } -export async function fetchConversationsWithUsers(userIds: string[]): Promise { - const response = await get('/conversations', { userIds }); - return response.body; -} - interface ImageApiUploadResponse { apiUrl: string; query: string; diff --git a/src/store/create-conversation/saga.test.ts b/src/store/create-conversation/saga.test.ts index ae3fe0fd1..37da43b3a 100644 --- a/src/store/create-conversation/saga.test.ts +++ b/src/store/create-conversation/saga.test.ts @@ -12,9 +12,9 @@ import { import { setGroupCreating, Stage, setFetchingConversations, setStage } from '.'; import { channelsReceived, createConversation as performCreateConversation } from '../channels-list/saga'; -import { fetchConversationsWithUsers } from '../channels-list/api'; import { rootReducer } from '../reducer'; import { StoreBuilder } from '../test/store'; +import { chat } from '../../lib/chat'; describe('create conversation saga', () => { describe('startConversation', () => { @@ -77,32 +77,39 @@ describe('create conversation saga', () => { }); describe(performGroupMembersSelected, () => { + const chatClient = { + fetchConversationsWithUsers: () => [], + }; + function subject(...args: Parameters) { return expectSaga(...args) - .provide([[matchers.call.fn(channelsReceived), null]]) + .provide([ + [matchers.call.fn(channelsReceived), null], + [matchers.call.fn(chat.get), chatClient], + [matchers.call.fn(chatClient.fetchConversationsWithUsers), []], + ]) .withReducer(rootReducer, defaultState()); } it('includes current user when fetching conversations', async () => { const initialState = new StoreBuilder().withCurrentUser({ id: 'current-user-id' }); - return expectSaga(performGroupMembersSelected, [{ value: 'other-user-id' }] as any) - .provide([[matchers.call.fn(fetchConversationsWithUsers), []]]) + return subject(performGroupMembersSelected, [{ value: 'other-user-id' }] as any) .withReducer(rootReducer, initialState.build()) - .call(fetchConversationsWithUsers, ['current-user-id', 'other-user-id']) + .call([chatClient, chatClient.fetchConversationsWithUsers], ['current-user-id', 'other-user-id']) .run(); }); it('saves first existing conversation', async () => { await subject(performGroupMembersSelected, []) - .provide([[matchers.call.fn(fetchConversationsWithUsers), [{ id: 'convo-1' }, { id: 'convo-2' }]]]) + .provide([[matchers.call.fn(chatClient.fetchConversationsWithUsers), [{ id: 'convo-1' }, { id: 'convo-2' }]]]) .call(channelsReceived, { payload: { channels: [{ id: 'convo-1' }] } }) .run(); }); it('opens the existing conversation', async () => { const { storeState } = await subject(performGroupMembersSelected, []) - .provide([[matchers.call.fn(fetchConversationsWithUsers), [{ id: 'convo-1' }]]]) + .provide([[matchers.call.fn(chatClient.fetchConversationsWithUsers), [{ id: 'convo-1' }]]]) .run(); expect(storeState.chat.activeConversationId).toBe('convo-1'); @@ -112,7 +119,7 @@ describe('create conversation saga', () => { const initialState = defaultState({ stage: Stage.StartGroupChat }); const { returnValue } = await subject(performGroupMembersSelected, []) - .provide([[matchers.call.fn(fetchConversationsWithUsers), [{ id: 'convo-1' }]]]) + .provide([[matchers.call.fn(chatClient.fetchConversationsWithUsers), [{ id: 'convo-1' }]]]) .withReducer(rootReducer, initialState) .run(); @@ -124,7 +131,7 @@ describe('create conversation saga', () => { const initialState = defaultState({ stage: Stage.StartGroupChat }); const { returnValue, storeState } = await subject(performGroupMembersSelected, users) - .provide([[matchers.call.fn(fetchConversationsWithUsers), []]]) + .provide([[matchers.call.fn(chatClient.fetchConversationsWithUsers), []]]) .withReducer(rootReducer, initialState) .run(); diff --git a/src/store/create-conversation/saga.ts b/src/store/create-conversation/saga.ts index c5f55090b..206a81d59 100644 --- a/src/store/create-conversation/saga.ts +++ b/src/store/create-conversation/saga.ts @@ -1,10 +1,10 @@ import { put, call, select, race, take, fork, spawn } from 'redux-saga/effects'; import { SagaActionTypes, Stage, setFetchingConversations, setGroupCreating, setGroupUsers, setStage } from '.'; import { channelsReceived, createConversation as performCreateConversation } from '../channels-list/saga'; -import { fetchConversationsWithUsers } from '../channels-list/api'; import { setactiveConversationId } from '../chat'; import { Events, getAuthChannel } from '../authentication/channels'; import { currentUserSelector } from '../authentication/selectors'; +import { Chat, chat } from '../../lib/chat'; export function* reset() { yield put(setGroupUsers([])); @@ -30,7 +30,8 @@ export function* performGroupMembersSelected(users: { value: string; label: stri currentUser.id, ...users.map((o) => o.value), ]; - const existingConversations = yield call(fetchConversationsWithUsers, userIds); + const chatClient: Chat = yield call(chat.get); + const existingConversations = yield call([chatClient, chatClient.fetchConversationsWithUsers], userIds); if (existingConversations.length === 0) { yield put(setGroupUsers(users)); From 673704a231d854e7bfcc0f31357dddd06ca866a8 Mon Sep 17 00:00:00 2001 From: Dale Fukami Date: Tue, 3 Oct 2023 07:20:35 -0600 Subject: [PATCH 2/2] Fetch existing conversations with users via Matrix simplified --- src/lib/chat/index.ts | 6 ++--- src/lib/chat/matrix-client.ts | 26 ++++++++++++++++++++-- src/lib/chat/sendbird-client.ts | 3 ++- src/store/create-conversation/saga.test.ts | 12 +++++++--- src/store/create-conversation/saga.ts | 11 +++++---- src/store/test/store.ts | 1 + 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/lib/chat/index.ts b/src/lib/chat/index.ts index d109304a5..77cbfa8d4 100644 --- a/src/lib/chat/index.ts +++ b/src/lib/chat/index.ts @@ -51,7 +51,7 @@ export interface IChatClient { file?: FileUploadResult, optimisticId?: string ) => Promise; - fetchConversationsWithUsers: (userIds: string[]) => Promise; + fetchConversationsWithUsers: (users: User[]) => Promise[]>; } export class Chat { @@ -102,8 +102,8 @@ export class Chat { return this.client.sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId); } - async fetchConversationsWithUsers(userIds: string[]): Promise { - return this.client.fetchConversationsWithUsers(userIds); + async fetchConversationsWithUsers(users: User[]): Promise { + return this.client.fetchConversationsWithUsers(users); } initChat(events: RealtimeChatEvents): void { diff --git a/src/lib/chat/matrix-client.ts b/src/lib/chat/matrix-client.ts index 3b7613774..f688b3107 100644 --- a/src/lib/chat/matrix-client.ts +++ b/src/lib/chat/matrix-client.ts @@ -199,8 +199,30 @@ export class MatrixClient implements IChatClient { }; } - async fetchConversationsWithUsers(_userIds: string[]) { - return []; + async fetchConversationsWithUsers(users: User[]) { + const userMatrixIds = users.map((u) => u.matrixId); + const rooms = await this.getFilteredRooms(this.isConversation); + const matches = []; + for (const room of rooms) { + const roomMembers = room + .getMembers() + .filter((m) => m.membership === 'join' || m.membership === 'invite') + .map((m) => m.userId); + if (this.arraysMatch(roomMembers, userMatrixIds)) { + matches.push(room); + } + } + return matches.map(this.mapConversation); + } + + arraysMatch(a, b) { + if (a.length !== b.length) { + return false; + } + + a.sort(); + b.sort(); + return a.every((val, idx) => val === b[idx]); } get isDisconnected() { diff --git a/src/lib/chat/sendbird-client.ts b/src/lib/chat/sendbird-client.ts index 8fdfa2d0c..30ec41580 100644 --- a/src/lib/chat/sendbird-client.ts +++ b/src/lib/chat/sendbird-client.ts @@ -144,7 +144,8 @@ export class SendbirdClient implements IChatClient { return sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId); } - async fetchConversationsWithUsers(userIds: string[]): Promise { + async fetchConversationsWithUsers(users: User[]): Promise { + const userIds = users.map((u) => u.userId); const response = await get('/conversations', { userIds }); return response.body; } diff --git a/src/store/create-conversation/saga.test.ts b/src/store/create-conversation/saga.test.ts index 37da43b3a..0f5d33e24 100644 --- a/src/store/create-conversation/saga.test.ts +++ b/src/store/create-conversation/saga.test.ts @@ -91,12 +91,18 @@ describe('create conversation saga', () => { .withReducer(rootReducer, defaultState()); } - it('includes current user when fetching conversations', async () => { - const initialState = new StoreBuilder().withCurrentUser({ id: 'current-user-id' }); + it('calls the chat api with all users', async () => { + const initialState = new StoreBuilder() + .withCurrentUser({ id: 'current-user-id' }) + .withUsers({ userId: 'other-user-id' }); return subject(performGroupMembersSelected, [{ value: 'other-user-id' }] as any) .withReducer(rootReducer, initialState.build()) - .call([chatClient, chatClient.fetchConversationsWithUsers], ['current-user-id', 'other-user-id']) + .call.like({ + context: chatClient, + fn: chatClient.fetchConversationsWithUsers, + args: [[{ userId: 'current-user-id' }, { userId: 'other-user-id' }]], + }) .run(); }); diff --git a/src/store/create-conversation/saga.ts b/src/store/create-conversation/saga.ts index 206a81d59..e9e82c6e7 100644 --- a/src/store/create-conversation/saga.ts +++ b/src/store/create-conversation/saga.ts @@ -5,6 +5,7 @@ import { setactiveConversationId } from '../chat'; import { Events, getAuthChannel } from '../authentication/channels'; import { currentUserSelector } from '../authentication/selectors'; import { Chat, chat } from '../../lib/chat'; +import { denormalize as denormalizeUsers } from '../users'; export function* reset() { yield put(setGroupUsers([])); @@ -24,17 +25,19 @@ export function* groupMembersSelected(action) { } } -export function* performGroupMembersSelected(users: { value: string; label: string; image?: string }[]) { +export function* performGroupMembersSelected(userSelections: { value: string; label: string; image?: string }[]) { const currentUser = yield select(currentUserSelector); const userIds = [ currentUser.id, - ...users.map((o) => o.value), + ...userSelections.map((o) => o.value), ]; + const users = yield select((state) => denormalizeUsers(userIds, state)); + const chatClient: Chat = yield call(chat.get); - const existingConversations = yield call([chatClient, chatClient.fetchConversationsWithUsers], userIds); + const existingConversations = yield call([chatClient, chatClient.fetchConversationsWithUsers], users); if (existingConversations.length === 0) { - yield put(setGroupUsers(users)); + yield put(setGroupUsers(userSelections)); return Stage.GroupDetails; } else { const selectedConversation = existingConversations[0]; diff --git a/src/store/test/store.ts b/src/store/test/store.ts index 98af658d8..53743c032 100644 --- a/src/store/test/store.ts +++ b/src/store/test/store.ts @@ -52,6 +52,7 @@ export class StoreBuilder { withCurrentUser(user: Partial) { this.currentUser = { ...DEFAULT_USER_ATTRS, ...user }; + this.users.push({ userId: user.id }); return this; }