diff --git a/src/lib/chat/index.ts b/src/lib/chat/index.ts index 8542df840..77cbfa8d4 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: (users: User[]) => Promise[]>; } export class Chat { @@ -101,6 +102,10 @@ export class Chat { return this.client.sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId); } + async fetchConversationsWithUsers(users: User[]): Promise { + return this.client.fetchConversationsWithUsers(users); + } + 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..f688b3107 100644 --- a/src/lib/chat/matrix-client.ts +++ b/src/lib/chat/matrix-client.ts @@ -199,6 +199,32 @@ export class MatrixClient implements IChatClient { }; } + 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() { return this.connectionStatus === ConnectionStatus.Disconnected; } diff --git a/src/lib/chat/sendbird-client.ts b/src/lib/chat/sendbird-client.ts index 3ab8760d1..30ec41580 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,12 @@ export class SendbirdClient implements IChatClient { return sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId); } + async fetchConversationsWithUsers(users: User[]): Promise { + const userIds = users.map((u) => u.userId); + 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..0f5d33e24 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,45 @@ 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' }); + it('calls the chat api with all users', async () => { + const initialState = new StoreBuilder() + .withCurrentUser({ id: 'current-user-id' }) + .withUsers({ userId: 'other-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.like({ + context: chatClient, + fn: chatClient.fetchConversationsWithUsers, + args: [[{ userId: 'current-user-id' }, { userId: '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 +125,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 +137,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..e9e82c6e7 100644 --- a/src/store/create-conversation/saga.ts +++ b/src/store/create-conversation/saga.ts @@ -1,10 +1,11 @@ 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'; +import { denormalize as denormalizeUsers } from '../users'; export function* reset() { yield put(setGroupUsers([])); @@ -24,16 +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 existingConversations = yield call(fetchConversationsWithUsers, userIds); + const users = yield select((state) => denormalizeUsers(userIds, state)); + + const chatClient: Chat = yield call(chat.get); + 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; }