Skip to content

Commit

Permalink
fix: multiple proteus 1:1 conversations with team member [WPB-9063] (#…
Browse files Browse the repository at this point in the history
…17403)

* fix: multiple proteus 1:1 conversations with team member

* chore: remove irrelevant test
  • Loading branch information
PatrykBuniX authored May 13, 2024
1 parent 31bdc09 commit 2625382
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 44 deletions.
40 changes: 40 additions & 0 deletions src/script/conversation/ConversationRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,46 @@ describe('ConversationRepository', () => {
expect(conversationEntity).toEqual(proteus1to1Conversation);
expect(conversationRepository['conversationService'].getMLS1to1Conversation).not.toHaveBeenCalled();
});

it('returns a selected proteus 1:1 conversation with a team member even if there are multiple conversations with the same user', async () => {
const conversationRepository = testFactory.conversation_repository!;

const teamId = 'teamId';
const domain = 'test-domain';

const otherUserId = {id: 'f718411c-3833-479d-bd80-af03f38416', domain};
const otherUser = new User(otherUserId.id, otherUserId.domain);
otherUser.teamId = teamId;

otherUser.supportedProtocols([ConversationProtocol.PROTEUS]);

conversationRepository['userState'].users.push(otherUser);

const selfUserId = {id: '109da91a-a495-47a870-9ffbe924b2d1', domain};
const selfUser = new User(selfUserId.id, selfUserId.domain);
selfUser.teamId = teamId;
selfUser.supportedProtocols([ConversationProtocol.PROTEUS]);
jest.spyOn(conversationRepository['userState'], 'self').mockReturnValue(selfUser);

const proteus1to1Conversation = _generateConversation({
type: CONVERSATION_TYPE.ONE_TO_ONE,
protocol: ConversationProtocol.PROTEUS,
users: [otherUser],
overwites: {team_id: teamId, domain},
});

const proteus1to1Conversation2 = _generateConversation({
type: CONVERSATION_TYPE.ONE_TO_ONE,
protocol: ConversationProtocol.PROTEUS,
users: [otherUser],
overwites: {team_id: teamId, domain},
});

conversationRepository['conversationState'].conversations.push(proteus1to1Conversation, proteus1to1Conversation2);

const conversationEntity = await conversationRepository.init1to1Conversation(proteus1to1Conversation2, true);
expect(conversationEntity).toEqual(proteus1to1Conversation2);
});
});

describe('getInitialised1To1Conversation', () => {
Expand Down
72 changes: 28 additions & 44 deletions src/script/conversation/ConversationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ export class ConversationRepository {
* @param userEntity User entity for whom to get the conversation
* @param isLiveUpdate Whether the conversation is being initialised because of a live update (e.g. some websocket event)
* @param shouldRefreshUser Whether the user should be refetched from backend before getting the conversation
* @param knownConversationId Known conversation ID - if provided, we will try to find the conversation with this exact ID (needed for proteus 1:1 conversation with a team member)
* @returns Resolves with the initialised 1:1 conversation with requested user
*/
public async getInitialised1To1Conversation(
Expand All @@ -1300,6 +1301,7 @@ export class ConversationRepository {
isLiveUpdate: false,
shouldRefreshUser: false,
},
knownConversationId?: QualifiedId,
): Promise<Conversation | null> {
const user = await this.userRepository.getUserById(userId);

Expand All @@ -1326,57 +1328,39 @@ export class ConversationRepository {
return this.initMLS1to1Conversation(userId, isMLSSupportedByTheOtherUser, shouldDelayMLSGroupEstablishment);
}

const proteusConversation = await this.getOrCreateProteus1To1Conversation(user);
// There's no connection so it's a proteus conversation with a team member
const selfUser = this.userState.self();
const inCurrentTeam = selfUser && selfUser.teamId && user.teamId === selfUser.teamId;

if (!proteusConversation) {
if (!inCurrentTeam) {
// It's not possible to create a 1:1 conversation with a user from another team without a connection
return null;
}

const proteusConversation = await this.getOrCreateProteusTeam1to1Conversation(user, knownConversationId);

return this.initProteus1to1Conversation(proteusConversation.qualifiedId, isProteusSupportedByTheOtherUser);
}

/**
* Get or create a proteus 1:1 conversation with a user.
* If a conversation does not exist, but user is in the current team, or there's a connection with this user, proteus 1:1 conversation will be created and saved.
* Get or create a proteus 1:1 conversation with a team member. If a conversation does not exist, it will be created.
* This is a legacy type of 1:1 conversation, which really is a group conversation with only two members.
* Due to some bug in the past, it's possible that there are multiple proteus 1:1 conversations with the same user,
* so we have to make sure we get the right one (with the knownConversationId parameter)
* @param userEntity User entity for whom to get the conversation
* @returns Resolves with the conversation with requested user (if in the current team or there's an existing connection with this user), otherwise `null`
* @param knownConversationId Known conversation ID - if provided, we will try to find the conversation with this exact ID
* @returns Resolves with the conversation with requested user
*/
private async getOrCreateProteus1To1Conversation(userEntity: User): Promise<Conversation | null> {
const selfUser = this.userState.self();
const inCurrentTeam = selfUser && selfUser.teamId && userEntity.teamId === selfUser.teamId;

if (inCurrentTeam) {
return this.getOrCreateProteusTeam1to1Conversation(userEntity);
}

const userConnection = userEntity.connection();

if (!userConnection) {
return null;
}
private async getOrCreateProteusTeam1to1Conversation(
userEntity: User,
knownConversationId?: QualifiedId,
): Promise<Conversation> {
const exactConversation = knownConversationId && this.conversationState.findConversation(knownConversationId);

const conversationId = userConnection.conversationId;
try {
const conversationEntity = await this.getConversationById(conversationId);
conversationEntity.connection(userConnection);
await this.updateParticipatingUserEntities(conversationEntity);
return conversationEntity;
} catch (error) {
const isConversationNotFound =
error instanceof ConversationError && error.type === ConversationError.TYPE.CONVERSATION_NOT_FOUND;
if (!isConversationNotFound) {
throw error;
}
return null;
if (exactConversation) {
return exactConversation;
}
}

/**
* Get or create a proteus team 1to1 conversation with a user. If a conversation does not exist, it will be created.
* @param userEntity User entity for whom to get the conversation
* @returns Resolves with the conversation with requested user
*/
private async getOrCreateProteusTeam1to1Conversation(userEntity: User): Promise<Conversation> {
const matchingConversationEntity = this.conversationState.conversations().find(conversationEntity => {
if (!conversationEntity.is1to1()) {
// Disregard conversations that are not 1:1
Expand Down Expand Up @@ -1826,15 +1810,15 @@ export class ConversationRepository {

const initialisedMLSConversation = await this.establishMLS1to1Conversation(mlsConversation, otherUserId);

if (shouldOpenMLS1to1Conversation) {
// If proteus conversation was previously active conversaiton, we want to make mls 1:1 conversation active.
amplify.publish(WebAppEvents.CONVERSATION.SHOW, initialisedMLSConversation, {});
}

// If mls is supported by the other user, we can establish the group and remove readonly state from the conversation.
initialisedMLSConversation.readOnlyState(null);
await this.update1To1ConversationParticipants(mlsConversation, otherUserId);
await this.saveConversation(initialisedMLSConversation);

if (shouldOpenMLS1to1Conversation) {
// If proteus conversation was previously active conversaiton, we want to make mls 1:1 conversation active.
amplify.publish(WebAppEvents.CONVERSATION.SHOW, initialisedMLSConversation, {});
}
return initialisedMLSConversation;
};

Expand Down Expand Up @@ -1944,7 +1928,7 @@ export class ConversationRepository {
);

try {
return await this.getInitialised1To1Conversation(otherUserId, {shouldRefreshUser});
return await this.getInitialised1To1Conversation(otherUserId, {shouldRefreshUser}, conversation.qualifiedId);
} catch {}

return conversation;
Expand Down
1 change: 1 addition & 0 deletions test/helper/ConversationGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export function generateConversation({

if (users) {
conversation.participating_user_ets(users);
conversation.participating_user_ids(users.map(user => user.qualifiedId));
}

return conversation;
Expand Down

0 comments on commit 2625382

Please sign in to comment.