Skip to content

Commit

Permalink
Matrix: Create group conversation (#1065)
Browse files Browse the repository at this point in the history
  • Loading branch information
dalefukami authored Oct 3, 2023
1 parent d0f9316 commit 12dbfca
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 21 deletions.
5 changes: 5 additions & 0 deletions src/lib/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface IChatClient {
file?: FileUploadResult,
optimisticId?: string
) => Promise<MessagesResponse>;
fetchConversationsWithUsers: (users: User[]) => Promise<Partial<Channel>[]>;
}

export class Chat {
Expand Down Expand Up @@ -101,6 +102,10 @@ export class Chat {
return this.client.sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId);
}

async fetchConversationsWithUsers(users: User[]): Promise<any[]> {
return this.client.fetchConversationsWithUsers(users);
}

initChat(events: RealtimeChatEvents): void {
this.client.init(events);
}
Expand Down
26 changes: 26 additions & 0 deletions src/lib/chat/matrix-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/chat/sendbird-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,6 +144,12 @@ export class SendbirdClient implements IChatClient {
return sendMessagesByChannelId(channelId, message, mentionedUserIds, parentMessage, file, optimisticId);
}

async fetchConversationsWithUsers(users: User[]): Promise<any[]> {
const userIds = users.map((u) => u.userId);
const response = await get<Channel[]>('/conversations', { userIds });
return response.body;
}

private initSessionHandler(events: RealtimeChatEvents) {
const sessionHandler = new SessionHandler({
onSessionClosed: () => {
Expand Down
5 changes: 0 additions & 5 deletions src/store/channels-list/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ export async function createConversation(
return directMessages.body;
}

export async function fetchConversationsWithUsers(userIds: string[]): Promise<any[]> {
const response = await get<Channel[]>('/conversations', { userIds });
return response.body;
}

interface ImageApiUploadResponse {
apiUrl: string;
query: string;
Expand Down
35 changes: 24 additions & 11 deletions src/store/create-conversation/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -77,32 +77,45 @@ describe('create conversation saga', () => {
});

describe(performGroupMembersSelected, () => {
const chatClient = {
fetchConversationsWithUsers: () => [],
};

function subject(...args: Parameters<typeof expectSaga>) {
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');
Expand All @@ -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();

Expand All @@ -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();

Expand Down
14 changes: 9 additions & 5 deletions src/store/create-conversation/saga.ts
Original file line number Diff line number Diff line change
@@ -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([]));
Expand All @@ -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];
Expand Down
1 change: 1 addition & 0 deletions src/store/test/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export class StoreBuilder {

withCurrentUser(user: Partial<AuthenticatedUser>) {
this.currentUser = { ...DEFAULT_USER_ATTRS, ...user };
this.users.push({ userId: user.id });
return this;
}

Expand Down

0 comments on commit 12dbfca

Please sign in to comment.