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

feat: support remote search by handle #2470

Merged
merged 2 commits into from
Feb 9, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.SelfTeamIdProvider
import com.wire.kalium.logic.data.id.toDao
import com.wire.kalium.logic.data.id.toModel
import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails
import com.wire.kalium.logic.data.publicuser.model.UserSearchResult
import com.wire.kalium.logic.data.user.ConnectionStateMapper
Expand Down Expand Up @@ -62,6 +61,11 @@ internal interface SearchUserRepository {
excludeMembersOfConversation: ConversationId?
): Either<StorageFailure, List<UserSearchDetails>>

suspend fun searchLocalByHandle(
handle: String,
excludeMembersOfConversation: ConversationId?
): Either<StorageFailure, List<UserSearchDetails>>

}

data class SearchUsersOptions(
Expand Down Expand Up @@ -131,18 +135,8 @@ internal class SearchUserRepositoryImpl(
} else {
searchDAO.getKnownContactsExcludingAConversation(excludeConversation.toDao())
}
}.map { searchEntityList ->
searchEntityList.map {
UserSearchDetails(
id = it.id.toModel(),
name = it.name,
completeAssetId = it.completeAssetId?.toModel(),
type = userTypeMapper.fromUserTypeEntity(it.type),
previewAssetId = it.previewAssetId?.toModel(),
connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(it.connectionStatus),
handle = it.handle
)
}
}.map {
it.map(userMapper::fromSearchEntityToUserSearchDetails)
}

override suspend fun searchLocalByName(
Expand All @@ -155,16 +149,23 @@ internal class SearchUserRepositoryImpl(
searchDAO.searchListExcludingAConversation(excludeMembersOfConversation.toDao(), name)
}
}.map {
it.map { searchEntity ->
UserSearchDetails(
id = searchEntity.id.toModel(),
name = searchEntity.name,
completeAssetId = searchEntity.completeAssetId?.toModel(),
previewAssetId = searchEntity.previewAssetId?.toModel(),
type = userTypeMapper.fromUserTypeEntity(searchEntity.type),
connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(searchEntity.connectionStatus),
handle = searchEntity.handle
)
it.map(userMapper::fromSearchEntityToUserSearchDetails)
}

override suspend fun searchLocalByHandle(
handle: String,
excludeMembersOfConversation: ConversationId?
): Either<StorageFailure, List<UserSearchDetails>> = if (excludeMembersOfConversation == null) {
wrapStorageRequest {
searchDAO.handleSearch(handle)
}.map {
it.map(userMapper::fromSearchEntityToUserSearchDetails)
}
} else {
wrapStorageRequest {
searchDAO.handleSearchExcludingAConversation(handle, excludeMembersOfConversation.toDao())
}.map {
it.map(userMapper::fromSearchEntityToUserSearchDetails)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.wire.kalium.logic.data.id.TeamId
import com.wire.kalium.logic.data.id.toDao
import com.wire.kalium.logic.data.id.toModel
import com.wire.kalium.logic.data.message.UserSummary
import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails
import com.wire.kalium.logic.data.user.type.DomainUserTypeMapper
import com.wire.kalium.logic.data.user.type.UserEntityTypeMapper
import com.wire.kalium.logic.di.MapperProvider
Expand All @@ -46,6 +47,7 @@ import com.wire.kalium.persistence.dao.UserAvailabilityStatusEntity
import com.wire.kalium.persistence.dao.UserDetailsEntity
import com.wire.kalium.persistence.dao.UserEntity
import com.wire.kalium.persistence.dao.UserEntityMinimized
import com.wire.kalium.persistence.dao.UserSearchEntity
import com.wire.kalium.persistence.dao.UserTypeEntity
import kotlinx.datetime.toInstant

Expand Down Expand Up @@ -90,6 +92,7 @@ interface UserMapper {
fun fromUserProfileDtoToOtherUser(userProfile: UserProfileDTO, selfUserId: UserId, selfTeamId: TeamId?): OtherUser

fun fromFailedUserToEntity(userId: NetworkQualifiedId): UserEntity
fun fromSearchEntityToUserSearchDetails(searchEntity: UserSearchEntity): UserSearchDetails
}

@Suppress("TooManyFunctions")
Expand Down Expand Up @@ -397,6 +400,16 @@ internal class UserMapperImpl(
activeOneOnOneConversationId = null
)
}

override fun fromSearchEntityToUserSearchDetails(searchEntity: UserSearchEntity) = UserSearchDetails(
id = searchEntity.id.toModel(),
name = searchEntity.name,
completeAssetId = searchEntity.completeAssetId?.toModel(),
previewAssetId = searchEntity.previewAssetId?.toModel(),
type = domainUserTypeMapper.fromUserTypeEntity(searchEntity.type),
connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(searchEntity.connectionStatus),
handle = searchEntity.handle
)
}

fun SupportedProtocol.toApi() = when (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1753,7 +1753,8 @@ class UserSessionScope internal constructor(
get() = SearchScope(
searchUserRepository = searchUserRepository,
selfUserId = userId,
sessionRepository = globalScope.sessionRepository
sessionRepository = globalScope.sessionRepository,
kaliumConfigs = kaliumConfigs
)

private val clearUserData: ClearUserDataUseCase get() = ClearUserDataUseCaseImpl(userStorage)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.feature.search

import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.publicuser.ConversationMemberExcludedOptions
import com.wire.kalium.logic.data.publicuser.SearchUserRepository
import com.wire.kalium.logic.data.publicuser.SearchUsersOptions
import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.functional.getOrElse
import com.wire.kalium.logic.functional.map
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope

/**
* Result of a search by handle.
*/
class SearchByHandleUseCase internal constructor(
private val searchUserRepository: SearchUserRepository,
private val selfUserId: UserId,
private val maxRemoteSearchResultCount: Int
) {
suspend operator fun invoke(
searchHandle: String,
excludingConversation: ConversationId?,
customDomain: String?
): SearchUserResult = coroutineScope {
val cleanSearchQuery = searchHandle
.trim()
.removePrefix("@")
.lowercase()

if (cleanSearchQuery.isBlank()) {
return@coroutineScope SearchUserResult(emptyList(), emptyList())
}

val remoteResultsDeferred = async {
searchUserRepository.searchUserRemoteDirectory(
cleanSearchQuery,
customDomain ?: selfUserId.domain,
maxRemoteSearchResultCount,
SearchUsersOptions(
conversationExcluded = excludingConversation?.let { ConversationMemberExcludedOptions.ConversationExcluded(it) }
?: ConversationMemberExcludedOptions.None,
selfUserIncluded = false
)
).map { userSearchResult ->
userSearchResult.result.map {
UserSearchDetails(
id = it.id,
name = it.name,
completeAssetId = it.completePicture,
previewAssetId = it.previewPicture,
type = it.userType,
connectionStatus = it.connectionStatus,
handle = it.handle
)
}
}.getOrElse(emptyList())
.associateBy { it.id }
.toMutableMap()
}

val localSearchResultDeferred = async {
searchUserRepository.searchLocalByHandle(
cleanSearchQuery,
excludingConversation
).getOrElse(emptyList())
.associateBy { it.id }
.toMutableMap()
}

val remoteResults = remoteResultsDeferred.await()
val localSearchResult = localSearchResultDeferred.await()

SearchUserResult.resolveLocalAndRemoteResult(localSearchResult, remoteResults)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,26 @@ package com.wire.kalium.logic.feature.search
import com.wire.kalium.logic.data.publicuser.SearchUserRepository
import com.wire.kalium.logic.data.session.SessionRepository
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.featureFlags.KaliumConfigs

class SearchScope internal constructor(
private val searchUserRepository: SearchUserRepository,
private val sessionRepository: SessionRepository,
private val selfUserId: UserId
private val selfUserId: UserId,
private val kaliumConfigs: KaliumConfigs
) {
val searchUsersUseCase: SearchUsersUseCase get() = SearchUsersUseCase(searchUserRepository, selfUserId)
val searchUsers: SearchUsersUseCase
get() = SearchUsersUseCase(
searchUserRepository,
selfUserId,
kaliumConfigs.maxRemoteSearchResultCount
)

val searchByHandle: SearchByHandleUseCase
get() = SearchByHandleUseCase(
searchUserRepository,
selfUserId,
kaliumConfigs.maxRemoteSearchResultCount
)
val federatedSearchParser: FederatedSearchParser get() = FederatedSearchParser(sessionRepository, selfUserId)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.feature.search

import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserId

data class SearchUserResult(
val connected: List<UserSearchDetails>,
val notConnected: List<UserSearchDetails>
) {
internal companion object {
inline fun resolveLocalAndRemoteResult(
localResult: MutableMap<UserId, UserSearchDetails>,
remoteSearch: MutableMap<UserId, UserSearchDetails>
): SearchUserResult {
val updatedUser = mutableListOf<UserId>()
remoteSearch.forEach { (userId, remoteUser) ->
if (localResult.contains(userId) || (remoteUser.connectionStatus == ConnectionState.ACCEPTED)) {
localResult[userId] = remoteUser
updatedUser.add(userId)
}
}

updatedUser.forEach { userId ->
remoteSearch.remove(userId)
}

return SearchUserResult(
connected = localResult.values.toList(),
notConnected = remoteSearch.values.toList()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import com.wire.kalium.logic.data.publicuser.ConversationMemberExcludedOptions
import com.wire.kalium.logic.data.publicuser.SearchUserRepository
import com.wire.kalium.logic.data.publicuser.SearchUsersOptions
import com.wire.kalium.logic.data.publicuser.model.UserSearchDetails
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.functional.getOrElse
import com.wire.kalium.logic.functional.map
Expand All @@ -37,15 +36,16 @@ import kotlinx.coroutines.coroutineScope
*/
class SearchUsersUseCase internal constructor(
private val searchUserRepository: SearchUserRepository,
private val selfUserId: UserId
private val selfUserId: UserId,
private val maxRemoteSearchResultCount: Int
) {
suspend operator fun invoke(
searchQuery: String,
excludingMembersOfConversation: ConversationId?,
customDomain: String?
): Result {
): SearchUserResult {
return if (searchQuery.isBlank()) {
Result(
SearchUserResult(
connected = searchUserRepository.getKnownContacts(excludingMembersOfConversation).getOrElse(emptyList()),
notConnected = emptyList()
)
Expand All @@ -58,14 +58,14 @@ class SearchUsersUseCase internal constructor(
searchQuery: String,
excludingConversation: ConversationId?,
customDomain: String?
): Result = coroutineScope {
): SearchUserResult = coroutineScope {
val cleanSearchQuery = searchQuery.trim().lowercase()

val remoteResultsDeferred = async {
searchUserRepository.searchUserRemoteDirectory(
cleanSearchQuery,
customDomain ?: selfUserId.domain,
MAX_SEARCH_RESULTS,
maxRemoteSearchResultCount,
SearchUsersOptions(
conversationExcluded = excludingConversation?.let { ConversationMemberExcludedOptions.ConversationExcluded(it) }
?: ConversationMemberExcludedOptions.None,
Expand Down Expand Up @@ -98,37 +98,6 @@ class SearchUsersUseCase internal constructor(
val remoteResults = remoteResultsDeferred.await()
val localSearchResult = localSearchResultDeferred.await()

resolveLocalAndRemoteResult(localSearchResult, remoteResults)
}

private inline fun resolveLocalAndRemoteResult(
localResult: MutableMap<UserId, UserSearchDetails>,
remoteSearch: MutableMap<UserId, UserSearchDetails>
): Result {
val updatedUser = mutableListOf<UserId>()
remoteSearch.forEach { (userId, remoteUser) ->
if (localResult.contains(userId) || (remoteUser.connectionStatus == ConnectionState.ACCEPTED)) {
localResult[userId] = remoteUser
updatedUser.add(userId)
}
}

updatedUser.forEach { userId ->
remoteSearch.remove(userId)
}

return Result(
connected = localResult.values.toList(),
notConnected = remoteSearch.values.toList()
)
}

data class Result(
val connected: List<UserSearchDetails>,
val notConnected: List<UserSearchDetails>
)

private companion object {
private const val MAX_SEARCH_RESULTS = 30
SearchUserResult.resolveLocalAndRemoteResult(localSearchResult, remoteResults)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ data class KaliumConfigs(
// Interval between attempts to advance the proteus to MLS migration
val mlsMigrationInterval: Duration = 24.hours,
val fetchAllTeamMembersEagerly: Boolean = false,
val maxRemoteSearchResultCount: Int = 30
)

sealed interface BuildFileRestrictionState {
Expand Down
Loading
Loading