Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matrix: Create group conversation #1065

Merged
merged 2 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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