diff --git a/src/script/connection/ConnectionRepository.test.ts b/src/script/connection/ConnectionRepository.test.ts index e74cd2a33ff..1d3d94c5118 100644 --- a/src/script/connection/ConnectionRepository.test.ts +++ b/src/script/connection/ConnectionRepository.test.ts @@ -25,6 +25,8 @@ import {StatusCodes} from 'http-status-codes'; import {WebAppEvents} from '@wireapp/webapp-events'; +import {SelfService} from 'src/script/self/SelfService'; +import {TeamService} from 'src/script/team/TeamService'; import {generateUser} from 'test/helper/UserGenerator'; import {createUuid} from 'Util/uuid'; @@ -39,9 +41,11 @@ import {UserRepository} from '../user/UserRepository'; function buildConnectionRepository() { const connectionState = new ConnectionState(); const connectionService = new ConnectionService(); + const selfService = new SelfService(); + const teamService = new TeamService(); const userRepository = {refreshUser: jest.fn()} as unknown as UserRepository; return [ - new ConnectionRepository(connectionService, userRepository, connectionState), + new ConnectionRepository(connectionService, userRepository, selfService, teamService, connectionState), {connectionState, userRepository, connectionService}, ] as const; } @@ -130,7 +134,7 @@ describe('ConnectionRepository', () => { jest.spyOn(connectionService, 'getConnections').mockResolvedValue([connectionRequest, connectionRequest]); - await connectionRepository.getConnections(); + await connectionRepository.getConnections([]); const storedConnection = connectionRepository.getConnectionByConversationId({ id: connectionRequest.conversation, diff --git a/src/script/connection/ConnectionRepository.ts b/src/script/connection/ConnectionRepository.ts index 3d221a8fb3c..fdb39054176 100644 --- a/src/script/connection/ConnectionRepository.ts +++ b/src/script/connection/ConnectionRepository.ts @@ -18,16 +18,18 @@ */ import {ConnectionStatus} from '@wireapp/api-client/lib/connection/'; -import {UserConnectionEvent, USER_EVENT} from '@wireapp/api-client/lib/event/'; -import type {BackendEventType} from '@wireapp/api-client/lib/event/BackendEvent'; +import {UserConnectionEvent, USER_EVENT, UserEvent} from '@wireapp/api-client/lib/event/'; import {BackendErrorLabel} from '@wireapp/api-client/lib/http/'; import {QualifiedId} from '@wireapp/api-client/lib/user'; -import type {UserConnectionData} from '@wireapp/api-client/lib/user/data/'; +import type {UserConnectionData, UserUpdateData} from '@wireapp/api-client/lib/user/data/'; import {amplify} from 'amplify'; import {container} from 'tsyringe'; import {WebAppEvents} from '@wireapp/webapp-events'; +import {SelfService} from 'src/script/self/SelfService'; +import {TeamService} from 'src/script/team/TeamService'; +import {UserState} from 'src/script/user/UserState'; import {replaceLink, t} from 'Util/LocalizerUtil'; import {getLogger, Logger} from 'Util/Logger'; import {matchQualifiedIds} from 'Util/QualifiedId'; @@ -54,16 +56,13 @@ export class ConnectionRepository { private readonly logger: Logger; private onDeleteConnectionRequestConversation?: (userId: QualifiedId) => Promise; - static get CONFIG(): Record { - return { - SUPPORTED_EVENTS: [USER_EVENT.CONNECTION], - }; - } - constructor( connectionService: ConnectionService, userRepository: UserRepository, + private readonly selfService: SelfService, + private readonly teamService: TeamService, private readonly connectionState = container.resolve(ConnectionState), + private readonly userState = container.resolve(UserState), ) { this.connectionService = connectionService; this.userRepository = userRepository; @@ -79,20 +78,27 @@ export class ConnectionRepository { * @param eventJson JSON data for event * @param source Source of event */ - private readonly onUserEvent = async (eventJson: UserConnectionEvent, source: EventSource) => { + private readonly onUserEvent = async (eventJson: UserEvent, source: EventSource) => { const eventType = eventJson.type; - const isSupportedType = ConnectionRepository.CONFIG.SUPPORTED_EVENTS.includes(eventType); - if (isSupportedType) { - this.logger.info(`User Event: '${eventType}' (Source: ${source})`); - - const isUserConnection = eventType === USER_EVENT.CONNECTION; - if (isUserConnection) { - await this.onUserConnection(eventJson, source); - } + switch (eventType) { + case USER_EVENT.CONNECTION: + await this.onUserConnection(eventJson as UserConnectionEvent, source); + break; + case USER_EVENT.UPDATE: + await this.onUserUpdate(eventJson); + break; } }; + private async onUserUpdate(eventJson: UserUpdateData) { + if (eventJson.user.id === this.userState.self()?.qualifiedId.id) { + await this.deletePendingConnectionsToSelfNewTeamMembers(); + return; + } + await this.deletePendingConnectionToNewTeamMember(eventJson); + } + /** * Convert a JSON event into an entity and get the matching conversation. * @@ -269,9 +275,17 @@ export class ConnectionRepository { * * @returns Promise that resolves when all connections have been retrieved and mapped */ - async getConnections(): Promise { + async getConnections(teamMembers: QualifiedId[]): Promise { const connectionData = await this.connectionService.getConnections(); - const connections = ConnectionMapper.mapConnectionsFromJson(connectionData); + + const acceptedConnectionsOrNoneTeamMembersConnections = connectionData.filter(connection => { + const isTeamMember = teamMembers.some(teamMemberQualifiedId => + matchQualifiedIds(connection.qualified_to, teamMemberQualifiedId), + ); + return !isTeamMember || connection.status === ConnectionStatus.ACCEPTED; + }); + + const connections = ConnectionMapper.mapConnectionsFromJson(acceptedConnectionsOrNoneTeamMembersConnections); this.connectionState.connections(connections); return connections; @@ -375,17 +389,17 @@ export class ConnectionRepository { } } - private async deleteConnectionWithUser(user: User) { + public async deleteConnectionWithUser(user: User) { const connection = this.connectionState .connections() .find(connection => matchQualifiedIds(connection.userId, user.qualifiedId)); + await this.onDeleteConnectionRequestConversation?.(user.qualifiedId); + if (connection) { this.connectionState.connections.remove(connection); user.connection(null); } - - await this.onDeleteConnectionRequestConversation?.(user.qualifiedId); } /** @@ -437,4 +451,58 @@ export class ConnectionRepository { amplify.publish(WebAppEvents.NOTIFICATION.NOTIFY, messageEntity, connectionEntity); } } + + async deletePendingConnectionsToSelfNewTeamMembers() { + const freshSelf = await this.selfService.getSelf([]); + const newTeamId = freshSelf.team; + + if (!newTeamId) { + return; + } + + const currentConnectionsUserIds = this.connectionState.connections().map(connection => connection.userId); + const currentConnectionsUsers = await this.userRepository.getUsersById(currentConnectionsUserIds); + + const teamMembersToDeletePendingConnectionsWith = await this.teamService.getTeamMembersByIds( + newTeamId, + currentConnectionsUsers.map(user => user.qualifiedId.id), + ); + + const currentUsersToDeleteConnectionWith = currentConnectionsUsers.filter(user => { + return teamMembersToDeletePendingConnectionsWith.some(member => member.user === user.qualifiedId.id); + }); + + for (const user of currentUsersToDeleteConnectionWith) { + await this.deleteConnectionWithUser(user); + } + } + + async deletePendingConnectionToNewTeamMember(event: UserUpdateData) { + const newlyJoinedUserId = event.user.id; + const selfUserDomain = this.userState.self()?.domain; + const newlyJoinedUserQualifiedId = { + id: newlyJoinedUserId, + /* + we can assume that the domain of the user is the same as the self user domain + because they have joined our team + */ + domain: selfUserDomain ?? '', + }; + + const newlyJoinedUser = await this.userRepository.getUserById(newlyJoinedUserQualifiedId); + const connectionWithNewlyJoinedUser = newlyJoinedUser.connection(); + const conversationIdWithNewlyJoinedUser = connectionWithNewlyJoinedUser?.conversationId; + + // If the connection is already accepted, we don't need to delete the conversation from our state + // we're gonna use the previous 1:1 conversation with the newly joined user + if ( + !connectionWithNewlyJoinedUser || + !conversationIdWithNewlyJoinedUser || + connectionWithNewlyJoinedUser?.status() === ConnectionStatus.ACCEPTED + ) { + return; + } + + await this.deleteConnectionWithUser(newlyJoinedUser); + } } diff --git a/src/script/conversation/ConversationState.ts b/src/script/conversation/ConversationState.ts index e73a5bafffb..0f4a0482cac 100644 --- a/src/script/conversation/ConversationState.ts +++ b/src/script/conversation/ConversationState.ts @@ -252,7 +252,11 @@ export class ConversationState { /** * Check whether conversation is currently displayed. */ - isActiveConversation(conversationEntity: Conversation): boolean { + isActiveConversation(conversationEntity?: Conversation): boolean { + if (!conversationEntity) { + return false; + } + const activeConversation = this.activeConversation(); return !!activeConversation && !!conversationEntity && matchQualifiedIds(activeConversation, conversationEntity); } diff --git a/src/script/main/app.ts b/src/script/main/app.ts index 2786ae1fed7..218a89a0bab 100644 --- a/src/script/main/app.ts +++ b/src/script/main/app.ts @@ -103,6 +103,7 @@ import {APIClient} from '../service/APIClientSingleton'; import {Core} from '../service/CoreSingleton'; import {StorageKey, StorageRepository, StorageService} from '../storage'; import {TeamRepository} from '../team/TeamRepository'; +import {TeamService} from '../team/TeamService'; import {AppInitStatisticsValue} from '../telemetry/app_init/AppInitStatisticsValue'; import {AppInitTelemetry} from '../telemetry/app_init/AppInitTelemetry'; import {AppInitTimingsStep} from '../telemetry/app_init/AppInitTimingsStep'; @@ -207,6 +208,7 @@ export class App { private _setupRepositories() { const repositories: ViewModelRepositories = {} as ViewModelRepositories; const selfService = new SelfService(); + const teamService = new TeamService(); repositories.asset = container.resolve(AssetRepository); @@ -228,11 +230,20 @@ export class App { serverTimeHandler, repositories.properties, ); - repositories.connection = new ConnectionRepository(new ConnectionService(), repositories.user); + repositories.connection = new ConnectionRepository( + new ConnectionService(), + repositories.user, + selfService, + teamService, + ); repositories.event = new EventRepository(this.service.event, this.service.notification, serverTimeHandler); repositories.search = new SearchRepository(repositories.user); - repositories.team = new TeamRepository(repositories.user, repositories.asset, () => - this.logout(SIGN_OUT_REASON.ACCOUNT_DELETED, true), + + repositories.team = new TeamRepository( + repositories.user, + repositories.asset, + () => this.logout(SIGN_OUT_REASON.ACCOUNT_DELETED, true), + teamService, ); repositories.message = new MessageRepository( @@ -455,7 +466,7 @@ export class App { onProgress(10); telemetry.timeStep(AppInitTimingsStep.INITIALIZED_CRYPTOGRAPHY); - const connections = await connectionRepository.getConnections(); + const connections = await connectionRepository.getConnections(teamMembers); telemetry.timeStep(AppInitTimingsStep.RECEIVED_USER_DATA); diff --git a/test/helper/TestFactory.js b/test/helper/TestFactory.js index fa81c8beb8f..fea3d480abd 100644 --- a/test/helper/TestFactory.js +++ b/test/helper/TestFactory.js @@ -182,7 +182,12 @@ export class TestFactory { await this.exposeUserActors(); this.connection_service = new ConnectionService(); - this.connection_repository = new ConnectionRepository(this.connection_service, this.user_repository); + this.connection_repository = new ConnectionRepository( + this.connection_service, + this.user_repository, + this.self_service, + this.team_service, + ); return this.connection_repository; } @@ -223,8 +228,10 @@ export class TestFactory { await this.exposeTeamActors(); await this.exposeClientActors(); + this.self_service = new SelfService(); + this.self_repository = new SelfRepository( - new SelfService(), + this.self_service, this.user_repository, this.team_repository, this.client_repository,