Merge branch 'develop' into feature/bca/rust_flavor

This commit is contained in:
valere 2022-12-13 10:11:21 +01:00
commit 3db82e629b
34 changed files with 532 additions and 55 deletions

1
changelog.d/7737.bugfix Normal file
View File

@ -0,0 +1 @@
Fix issue of Scan QR code button sometimes not showing when it should be available

1
changelog.d/7740.feature Normal file
View File

@ -0,0 +1 @@
Handle account data removal

1
changelog.d/7754.feature Normal file
View File

@ -0,0 +1 @@
Delete unused client information from account data

1
changelog.d/7770.bugfix Normal file
View File

@ -0,0 +1 @@
[Push Notifications] When push notification for threaded message is clicked, thread timeline will be opened instead of room's main timeline

View File

@ -125,12 +125,6 @@ interface AuthenticationService {
deviceId: String? = null deviceId: String? = null
): Session ): Session
/**
* @param homeServerConnectionConfig the information about the homeserver and other configuration
* Return true if qr code login is supported by the server, false otherwise.
*/
suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean
/** /**
* Authenticate using m.login.token method during sign in with QR code. * Authenticate using m.login.token method during sign in with QR code.
* @param homeServerConnectionConfig the information about the homeserver and other configuration * @param homeServerConnectionConfig the information about the homeserver and other configuration

View File

@ -22,5 +22,6 @@ data class LoginFlowResult(
val isLoginAndRegistrationSupported: Boolean, val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String, val homeServerUrl: String,
val isOutdatedHomeserver: Boolean, val isOutdatedHomeserver: Boolean,
val isLogoutDevicesSupported: Boolean val isLogoutDevicesSupported: Boolean,
val isLoginWithQrSupported: Boolean,
) )

View File

@ -63,4 +63,17 @@ interface SessionAccountDataService {
* Update the account data with the provided type and the provided account data content. * Update the account data with the provided type and the provided account data content.
*/ */
suspend fun updateUserAccountData(type: String, content: Content) suspend fun updateUserAccountData(type: String, content: Content)
/**
* Retrieve user account data list whose type starts with the given type.
* @param type the type or the starting part of a type
* @return list of account data whose type starts with the given type
*/
fun getUserAccountDataEventsStartWith(type: String): List<UserAccountDataEvent>
/**
* Deletes user account data of the given type.
* @param type the type to delete from user account data
*/
suspend fun deleteUserAccountData(type: String)
} }

View File

@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.failure.MatrixIdFailure
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -299,7 +298,8 @@ internal class DefaultAuthenticationService @Inject constructor(
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl = homeServerUrl, homeServerUrl = homeServerUrl,
isOutdatedHomeserver = !versions.isSupportedBySdk(), isOutdatedHomeserver = !versions.isSupportedBySdk(),
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices() isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices(),
isLoginWithQrSupported = versions.doesServerSupportQrCodeLogin(),
) )
} }
@ -408,20 +408,6 @@ internal class DefaultAuthenticationService @Inject constructor(
) )
} }
override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
val versions = runCatching {
executeRequest(null) {
authAPI.versions()
}
}
return if (versions.isSuccess) {
versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse()
} else {
false
}
}
override suspend fun loginUsingQrLoginToken( override suspend fun loginUsingQrLoginToken(
homeServerConnectionConfig: HomeServerConnectionConfig, homeServerConnectionConfig: HomeServerConnectionConfig,
loginToken: String, loginToken: String,

View File

@ -21,6 +21,7 @@ import io.realm.RealmQuery
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntityFields
@ -44,3 +45,11 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? =
internal fun RoomEntity.fastContains(eventId: String): Boolean { internal fun RoomEntity.fastContains(eventId: String): Boolean {
return EventEntity.where(realm, eventId = eventId).findFirst() != null return EventEntity.where(realm, eventId = eventId).findFirst() != null
} }
internal fun RoomEntity.removeAccountData(type: String) {
accountData
.where()
.equalTo(RoomAccountDataEntityFields.TYPE, type)
.findFirst()
?.deleteFromRealm()
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.query
import io.realm.Realm
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
/**
* Delete an account_data event.
*/
internal fun UserAccountDataEntity.Companion.delete(realm: Realm, type: String) {
realm
.where<UserAccountDataEntity>()
.equalTo(UserAccountDataEntityFields.TYPE, type)
.findFirst()
?.deleteFromRealm()
}

View File

@ -427,6 +427,19 @@ internal interface RoomAPI {
@Body content: JsonDict @Body content: JsonDict
) )
/**
* Remove an account_data event from the room.
* @param userId the user id
* @param roomId the room id
* @param type the type
*/
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/rooms/{roomId}/account_data/{type}")
suspend fun deleteRoomAccountData(
@Path("userId") userId: String,
@Path("roomId") roomId: String,
@Path("type") type: String
)
/** /**
* Upgrades the given room to a particular room version. * Upgrades the given room to a particular room version.
* Errors: * Errors:

View File

@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
import org.matrix.android.sdk.internal.database.model.deleteOnCascade import org.matrix.android.sdk.internal.database.model.deleteOnCascade
import org.matrix.android.sdk.internal.database.query.delete
import org.matrix.android.sdk.internal.database.query.findAllFrom import org.matrix.android.sdk.internal.database.query.findAllFrom
import org.matrix.android.sdk.internal.database.query.getDirectRooms import org.matrix.android.sdk.internal.database.query.getDirectRooms
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
@ -94,7 +95,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(
// If we get some direct chat invites, we synchronize the user account data including those. // If we get some direct chat invites, we synchronize the user account data including those.
suspend fun synchronizeWithServerIfNeeded(invites: Map<String, InvitedRoomSync>) { suspend fun synchronizeWithServerIfNeeded(invites: Map<String, InvitedRoomSync>) {
if (invites.isNullOrEmpty()) return if (invites.isEmpty()) return
val directChats = directChatsHelper.getLocalDirectMessages().toMutable() val directChats = directChatsHelper.getLocalDirectMessages().toMutable()
var hasUpdate = false var hasUpdate = false
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
@ -252,9 +253,17 @@ internal class UserAccountDataSyncHandler @Inject constructor(
} }
fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { fun handleGenericAccountData(realm: Realm, type: String, content: Content?) {
if (content.isNullOrEmpty()) {
// This is a response for a deleted account data according to
// https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
UserAccountDataEntity.delete(realm, type)
return
}
val existing = realm.where<UserAccountDataEntity>() val existing = realm.where<UserAccountDataEntity>()
.equalTo(UserAccountDataEntityFields.TYPE, type) .equalTo(UserAccountDataEntityFields.TYPE, type)
.findFirst() .findFirst()
if (existing != null) { if (existing != null) {
// Update current value // Update current value
existing.contentStr = ContentMapper.map(content) existing.contentStr = ContentMapper.map(content)

View File

@ -27,6 +27,7 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntity
import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.removeAccountData
import org.matrix.android.sdk.internal.session.room.read.FullyReadContent import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler
@ -56,6 +57,13 @@ internal class RoomSyncAccountDataHandler @Inject constructor(
} }
private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) { private fun handleGeneric(roomEntity: RoomEntity, content: JsonDict?, eventType: String) {
if (content.isNullOrEmpty()) {
// This is a response for a deleted account data according to
// https://github.com/ShadowJonathan/matrix-doc/blob/account-data-delete/proposals/3391-account-data-delete.md#sync
roomEntity.removeAccountData(eventType)
return
}
val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst() val existing = roomEntity.accountData.where().equalTo(RoomAccountDataEntityFields.TYPE, eventType).findFirst()
if (existing != null) { if (existing != null) {
existing.contentStr = ContentMapper.map(content) existing.contentStr = ContentMapper.map(content)

View File

@ -18,13 +18,14 @@ package org.matrix.android.sdk.internal.session.user.accountdata
import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
internal interface AccountDataAPI { internal interface AccountDataAPI {
/** /**
* Set some account_data for the client. * Set some account_data for the user.
* *
* @param userId the user id * @param userId the user id
* @param type the type * @param type the type
@ -36,4 +37,16 @@ internal interface AccountDataAPI {
@Path("type") type: String, @Path("type") type: String,
@Body params: Any @Body params: Any
) )
/**
* Remove an account_data for the user.
*
* @param userId the user id
* @param type the type
*/
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc3391/user/{userId}/account_data/{type}")
suspend fun deleteAccountData(
@Path("userId") userId: String,
@Path("type") type: String
)
} }

View File

@ -42,4 +42,7 @@ internal abstract class AccountDataModule {
@Binds @Binds
abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
@Binds
abstract fun bindDeleteUserAccountDataTask(task: DefaultDeleteUserAccountDataTask): DeleteUserAccountDataTask
} }

View File

@ -34,10 +34,11 @@ import javax.inject.Inject
internal class DefaultSessionAccountDataService @Inject constructor( internal class DefaultSessionAccountDataService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val deleteUserAccountDataTask: DeleteUserAccountDataTask,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val userAccountDataDataSource: UserAccountDataDataSource, private val userAccountDataDataSource: UserAccountDataDataSource,
private val roomAccountDataDataSource: RoomAccountDataDataSource, private val roomAccountDataDataSource: RoomAccountDataDataSource,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor,
) : SessionAccountDataService { ) : SessionAccountDataService {
override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? { override fun getUserAccountDataEvent(type: String): UserAccountDataEvent? {
@ -78,4 +79,12 @@ internal class DefaultSessionAccountDataService @Inject constructor(
userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) userAccountDataSyncHandler.handleGenericAccountData(realm, type, content)
} }
} }
override fun getUserAccountDataEventsStartWith(type: String): List<UserAccountDataEvent> {
return userAccountDataDataSource.getAccountDataEventsStartWith(type)
}
override suspend fun deleteUserAccountData(type: String) {
deleteUserAccountDataTask.execute(DeleteUserAccountDataTask.Params(type))
}
} }

View File

@ -0,0 +1,43 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.user.accountdata
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface DeleteUserAccountDataTask : Task<DeleteUserAccountDataTask.Params, Unit> {
data class Params(
val type: String,
)
}
internal class DefaultDeleteUserAccountDataTask @Inject constructor(
private val accountDataApi: AccountDataAPI,
@UserId private val userId: String,
private val globalErrorReceiver: GlobalErrorReceiver,
) : DeleteUserAccountDataTask {
override suspend fun execute(params: DeleteUserAccountDataTask.Params) {
return executeRequest(globalErrorReceiver) {
accountDataApi.deleteAccountData(userId, params.type)
}
}
}

View File

@ -60,6 +60,16 @@ internal class UserAccountDataDataSource @Inject constructor(
) )
} }
fun getAccountDataEventsStartWith(type: String): List<UserAccountDataEvent> {
return realmSessionProvider.withRealm { realm ->
realm
.where(UserAccountDataEntity::class.java)
.beginsWith(UserAccountDataEntityFields.TYPE, type)
.findAll()
.map(accountDataMapper::map)
}
}
private fun accountDataEventsQuery(realm: Realm, types: Set<String>): RealmQuery<UserAccountDataEntity> { private fun accountDataEventsQuery(realm: Realm, types: Set<String>): RealmQuery<UserAccountDataEntity> {
val query = realm.where(UserAccountDataEntity::class.java) val query = realm.where(UserAccountDataEntity::class.java)
if (types.isNotEmpty()) { if (types.isNotEmpty()) {

View File

@ -0,0 +1,53 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.user.accountdata
import io.mockk.coVerify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.android.sdk.test.fakes.FakeAccountDataApi
import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
private const val A_TYPE = "a-type"
private const val A_USER_ID = "a-user-id"
@ExperimentalCoroutinesApi
class DefaultDeleteUserAccountDataTaskTest {
private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver()
private val fakeAccountDataApi = FakeAccountDataApi()
private val deleteUserAccountDataTask = DefaultDeleteUserAccountDataTask(
accountDataApi = fakeAccountDataApi.instance,
userId = A_USER_ID,
globalErrorReceiver = fakeGlobalErrorReceiver
)
@Test
fun `given parameters when executing the task then api is called`() = runTest {
// Given
val params = DeleteUserAccountDataTask.Params(type = A_TYPE)
fakeAccountDataApi.givenParamsToDeleteAccountData(A_USER_ID, A_TYPE)
// When
deleteUserAccountDataTask.execute(params)
// Then
coVerify { fakeAccountDataApi.instance.deleteAccountData(A_USER_ID, A_TYPE) }
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2022 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.test.fakes
import io.mockk.coEvery
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI
internal class FakeAccountDataApi {
val instance: AccountDataAPI = mockk()
fun givenParamsToDeleteAccountData(userId: String, type: String) {
coEvery { instance.deleteAccountData(userId, type) } just runs
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.session.clientinfo
import im.vector.app.core.di.ActiveSessionHolder
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import javax.inject.Inject
class DeleteUnusedClientInformationUseCase @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
suspend fun execute(deviceInfoList: List<DeviceInfo>): Result<Unit> = runCatching {
// A defensive approach against local storage reports an empty device list (although it is not a seen situation).
if (deviceInfoList.isEmpty()) return Result.success(Unit)
val expectedClientInfoKeyList = deviceInfoList.map { MATRIX_CLIENT_INFO_KEY_PREFIX + it.deviceId }
activeSessionHolder
.getSafeActiveSession()
?.accountDataService()
?.getUserAccountDataEventsStartWith(MATRIX_CLIENT_INFO_KEY_PREFIX)
?.map { it.type }
?.subtract(expectedClientInfoKeyList.toSet())
?.forEach { userAccountDataKeyToDelete ->
activeSessionHolder.getSafeActiveSession()?.accountDataService()?.deleteUserAccountData(userAccountDataKeyToDelete)
}
}
}

View File

@ -31,10 +31,10 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.session.clientinfo.DeleteUnusedClientInformationUseCase
import im.vector.app.core.time.Clock import im.vector.app.core.time.Clock
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
@ -67,6 +67,7 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase, private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase,
private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase, private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase,
private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase, private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase,
private val deleteUnusedClientInformationUseCase: DeleteUnusedClientInformationUseCase,
) : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) { ) : VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction { sealed class Action : VectorViewModelAction {
@ -103,6 +104,9 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
) { cryptoList, infoList, pInfo -> ) { cryptoList, infoList, pInfo ->
// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}")
// Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
deleteUnusedClientInformation(infoList)
infoList infoList
.filter { info -> .filter { info ->
// filter out verified sessions or those which do not support encryption (i.e. without crypto info) // filter out verified sessions or those which do not support encryption (i.e. without crypto info)
@ -146,6 +150,12 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(
} }
} }
private fun deleteUnusedClientInformation(deviceFullInfoList: List<DeviceInfo>) {
viewModelScope.launch {
deleteUnusedClientInformationUseCase.execute(deviceFullInfoList)
}
}
override fun handle(action: Action) { override fun handle(action: Action) {
when (action) { when (action) {
is Action.IgnoreDevice -> { is Action.IgnoreDevice -> {

View File

@ -26,6 +26,7 @@ import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityThreadsBinding import im.vector.app.databinding.ActivityThreadsBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.extensions.toAnalyticsInteraction
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
@ -143,13 +144,20 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
context: Context, context: Context,
threadTimelineArgs: ThreadTimelineArgs?, threadTimelineArgs: ThreadTimelineArgs?,
threadListArgs: ThreadListArgs?, threadListArgs: ThreadListArgs?,
eventIdToNavigate: String? = null eventIdToNavigate: String? = null,
firstStartMainActivity: Boolean = false
): Intent { ): Intent {
return Intent(context, ThreadsActivity::class.java).apply { val intent = Intent(context, ThreadsActivity::class.java).apply {
putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs) putExtra(THREAD_TIMELINE_ARGS, threadTimelineArgs)
putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate) putExtra(THREAD_EVENT_ID_TO_NAVIGATE, eventIdToNavigate)
putExtra(THREAD_LIST_ARGS, threadListArgs) putExtra(THREAD_LIST_ARGS, threadListArgs)
} }
return if (firstStartMainActivity) {
MainActivity.getIntentWithNextIntent(context, intent)
} else {
intent
}
} }
} }

View File

@ -60,6 +60,8 @@ import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.HomeActivity import im.vector.app.features.home.HomeActivity
import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.arguments.TimelineArgs import im.vector.app.features.home.room.detail.arguments.TimelineArgs
import im.vector.app.features.home.room.threads.ThreadsActivity
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver import im.vector.app.features.settings.troubleshoot.TestNotificationReceiver
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -574,6 +576,7 @@ class NotificationUtils @Inject constructor(
fun buildMessagesListNotification( fun buildMessagesListNotification(
messageStyle: NotificationCompat.MessagingStyle, messageStyle: NotificationCompat.MessagingStyle,
roomInfo: RoomEventGroupInfo, roomInfo: RoomEventGroupInfo,
threadId: String?,
largeIcon: Bitmap?, largeIcon: Bitmap?,
lastMessageTimestamp: Long, lastMessageTimestamp: Long,
senderDisplayNameForReplyCompat: String?, senderDisplayNameForReplyCompat: String?,
@ -581,7 +584,11 @@ class NotificationUtils @Inject constructor(
): Notification { ): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color) val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked // Build the pending intent for when the notification is clicked
val openRoomIntent = buildOpenRoomIntent(roomInfo.roomId) val openIntent = when {
threadId != null && vectorPreferences.areThreadMessagesEnabled() -> buildOpenThreadIntent(roomInfo, threadId)
else -> buildOpenRoomIntent(roomInfo.roomId)
}
val smallIcon = R.drawable.ic_notification val smallIcon = R.drawable.ic_notification
val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
@ -666,8 +673,8 @@ class NotificationUtils @Inject constructor(
} }
} }
if (openRoomIntent != null) { if (openIntent != null) {
setContentIntent(openRoomIntent) setContentIntent(openIntent)
} }
if (largeIcon != null) { if (largeIcon != null) {
@ -826,6 +833,45 @@ class NotificationUtils @Inject constructor(
) )
} }
private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: String?): PendingIntent? {
val threadTimelineArgs = ThreadTimelineArgs(
startsThread = false,
roomId = roomInfo.roomId,
rootThreadEventId = threadId,
showKeyboard = false,
displayName = roomInfo.roomDisplayName,
avatarUrl = null,
roomEncryptionTrustLevel = null,
)
val threadIntentTap = ThreadsActivity.newIntent(
context = context,
threadTimelineArgs = threadTimelineArgs,
threadListArgs = null,
firstStartMainActivity = true,
)
threadIntentTap.action = actionIds.tapToView
// pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
threadIntentTap.data = createIgnoredUri("openThread?$threadId")
val roomIntent = RoomDetailActivity.newIntent(
context = context,
timelineArgs = TimelineArgs(
roomId = roomInfo.roomId,
switchToParentSpace = true
),
firstStartMainActivity = false
)
// Recreate the back stack
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false))
.addNextIntentWithParentStack(roomIntent)
.addNextIntent(threadIntentTap)
.getPendingIntent(
clock.epochMillis().toInt(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
}
private fun buildOpenHomePendingIntentForSummary(): PendingIntent { private fun buildOpenHomePendingIntentForSummary(): PendingIntent {
val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true) val intent = HomeActivity.newIntent(context, firstStartMainActivity = false, clearNotification = true)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP

View File

@ -33,14 +33,14 @@ class RoomGroupMessageCreator @Inject constructor(
) { ) {
fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message { fun createRoomMessage(events: List<NotifiableMessageEvent>, roomId: String, userDisplayName: String, userAvatarUrl: String?): RoomNotification.Message {
val firstKnownRoomEvent = events[0] val lastKnownRoomEvent = events.last()
val roomName = firstKnownRoomEvent.roomName ?: firstKnownRoomEvent.senderName ?: "" val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: ""
val roomIsGroup = !firstKnownRoomEvent.roomIsDirect val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
val style = NotificationCompat.MessagingStyle( val style = NotificationCompat.MessagingStyle(
Person.Builder() Person.Builder()
.setName(userDisplayName) .setName(userDisplayName)
.setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) .setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
.setKey(firstKnownRoomEvent.matrixID) .setKey(lastKnownRoomEvent.matrixID)
.build() .build()
).also { ).also {
it.conversationTitle = roomName.takeIf { roomIsGroup } it.conversationTitle = roomName.takeIf { roomIsGroup }
@ -75,6 +75,7 @@ class RoomGroupMessageCreator @Inject constructor(
it.customSound = events.last().soundName it.customSound = events.last().soundName
it.isUpdated = events.last().isUpdated it.isUpdated = events.last().isUpdated
}, },
threadId = lastKnownRoomEvent.threadId,
largeIcon = largeBitmap, largeIcon = largeBitmap,
lastMessageTimestamp, lastMessageTimestamp,
userDisplayName, userDisplayName,

View File

@ -118,7 +118,7 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private suspend fun checkQrCodeLoginCapability(config: HomeServerConnectionConfig) { private fun checkQrCodeLoginCapability() {
if (!vectorFeatures.isQrCodeLoginEnabled()) { if (!vectorFeatures.isQrCodeLoginEnabled()) {
setState { setState {
copy( copy(
@ -133,11 +133,9 @@ class OnboardingViewModel @AssistedInject constructor(
) )
} }
} else { } else {
// check if selected server supports MSC3882 first
val canLoginWithQrCode = authenticationService.isQrLoginSupported(config)
setState { setState {
copy( copy(
canLoginWithQrCode = canLoginWithQrCode canLoginWithQrCode = selectedHomeserver.isLoginWithQrSupported
) )
} }
} }
@ -705,7 +703,10 @@ class OnboardingViewModel @AssistedInject constructor(
// This is invalid // This is invalid
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig"))) _viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
} else { } else {
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction) startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, suspend {
checkQrCodeLoginCapability()
postAction()
})
} }
} }
@ -764,8 +765,6 @@ class OnboardingViewModel @AssistedInject constructor(
_viewEvents.post(OnboardingViewEvents.OutdatedHomeserver) _viewEvents.post(OnboardingViewEvents.OutdatedHomeserver)
} }
checkQrCodeLoginCapability(config)
when (trigger) { when (trigger) {
is OnboardingAction.HomeServerChange.SelectHomeServer -> { is OnboardingAction.HomeServerChange.SelectHomeServer -> {
onHomeServerSelected(config, serverTypeOverride, authResult) onHomeServerSelected(config, serverTypeOverride, authResult)

View File

@ -76,6 +76,7 @@ data class SelectedHomeserverState(
val preferredLoginMode: LoginMode = LoginMode.Unknown, val preferredLoginMode: LoginMode = LoginMode.Unknown,
val supportedLoginTypes: List<String> = emptyList(), val supportedLoginTypes: List<String> = emptyList(),
val isLogoutDevicesSupported: Boolean = false, val isLogoutDevicesSupported: Boolean = false,
val isLoginWithQrSupported: Boolean = false,
) : Parcelable ) : Parcelable
@Parcelize @Parcelize

View File

@ -47,7 +47,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
upstreamUrl = authFlow.homeServerUrl, upstreamUrl = authFlow.homeServerUrl,
preferredLoginMode = preferredLoginMode, preferredLoginMode = preferredLoginMode,
supportedLoginTypes = authFlow.supportedLoginTypes, supportedLoginTypes = authFlow.supportedLoginTypes,
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported,
isLoginWithQrSupported = authFlow.isLoginWithQrSupported,
) )
private fun LoginFlowResult.findPreferredLoginMode() = when { private fun LoginFlowResult.findPreferredLoginMode() = when {

View File

@ -112,6 +112,7 @@ class DevicesViewModel @AssistedInject constructor(
val deviceFullInfoList = async.invoke() val deviceFullInfoList = async.invoke()
val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() } val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() }
val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive } val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive }
copy( copy(
devices = async, devices = async,
unverifiedSessionsCount = unverifiedSessionsCount, unverifiedSessionsCount = unverifiedSessionsCount,

View File

@ -0,0 +1,136 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.session.clientinfo
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.coVerify
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
private const val A_CURRENT_DEVICE_ID = "current-device-id"
private const val A_DEVICE_ID_1 = "a-device-id-1"
private const val A_DEVICE_ID_2 = "a-device-id-2"
private const val A_DEVICE_ID_3 = "a-device-id-3"
private const val A_DEVICE_ID_4 = "a-device-id-4"
private val A_DEVICE_INFO_1 = DeviceInfo(deviceId = A_DEVICE_ID_1)
private val A_DEVICE_INFO_2 = DeviceInfo(deviceId = A_DEVICE_ID_2)
class DeleteUnusedClientInformationUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val deleteUnusedClientInformationUseCase = DeleteUnusedClientInformationUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
)
@Before
fun setup() {
fakeActiveSessionHolder.fakeSession.givenSessionId(A_CURRENT_DEVICE_ID)
}
@Test
fun `given a device list that account data has all of them and extra devices then use case deletes the unused ones`() = runTest {
// Given
val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
val userAccountDataEventList = listOf(
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3, mapOf("key" to "value")),
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4, mapOf("key" to "value")),
)
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
userAccountDataEventList = userAccountDataEventList,
)
// When
deleteUnusedClientInformationUseCase.execute(devices)
// Then
coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_3) }
coVerify { fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_4) }
}
@Test
fun `given a device list that account data has exactly all of them then use case does nothing`() = runTest {
// Given
val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
val userAccountDataEventList = listOf(
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
)
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
userAccountDataEventList = userAccountDataEventList,
)
// When
deleteUnusedClientInformationUseCase.execute(devices)
// Then
coVerify(exactly = 0) {
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
}
}
@Test
fun `given a device list that account data has missing some of them then use case does nothing`() = runTest {
// Given
val devices = listOf(A_DEVICE_INFO_1, A_DEVICE_INFO_2)
val userAccountDataEventList = listOf(
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
)
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
userAccountDataEventList = userAccountDataEventList,
)
// When
deleteUnusedClientInformationUseCase.execute(devices)
// Then
coVerify(exactly = 0) {
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
}
}
@Test
fun `given an empty device list that account data has some devices then use case does nothing`() = runTest {
// Given
val devices = emptyList<DeviceInfo>()
val userAccountDataEventList = listOf(
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_1, mapOf("key" to "value")),
UserAccountDataEvent(type = MATRIX_CLIENT_INFO_KEY_PREFIX + A_DEVICE_ID_2, mapOf("key" to "value")),
)
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.givenGetUserAccountDataEventsStartWith(
type = MATRIX_CLIENT_INFO_KEY_PREFIX,
userAccountDataEventList = userAccountDataEventList,
)
// When
deleteUnusedClientInformationUseCase.execute(devices)
// Then
coVerify(exactly = 0) {
fakeActiveSessionHolder.fakeSession.fakeSessionAccountDataService.deleteUserAccountData(any())
}
}
}

View File

@ -83,6 +83,7 @@ private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL) private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password, userFacingUrl = A_HOMESERVER_URL)
private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true) private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true)
private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL) private val DEFAULT_SELECTED_HOMESERVER_STATE = SELECTED_HOMESERVER_STATE.copy(userFacingUrl = A_DEFAULT_HOMESERVER_URL)
private val DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED = DEFAULT_SELECTED_HOMESERVER_STATE.copy(isLoginWithQrSupported = true)
private const val AN_EMAIL = "hello@example.com" private const val AN_EMAIL = "hello@example.com"
private const val A_PASSWORD = "a-password" private const val A_PASSWORD = "a-password"
private const val A_USERNAME = "hello-world" private const val A_USERNAME = "hello-world"
@ -164,7 +165,7 @@ class OnboardingViewModelTest {
fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest { fun `given combined login enabled, when handling sign in splash action, then emits OpenCombinedLogin with default homeserver qrCode supported`() = runTest {
val test = viewModel.test() val test = viewModel.test()
fakeVectorFeatures.givenCombinedLoginEnabled() fakeVectorFeatures.givenCombinedLoginEnabled()
givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE, canLoginWithQrCode = true) givenCanSuccessfullyUpdateHomeserver(A_DEFAULT_HOMESERVER_URL, DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED)
viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn)) viewModel.handle(OnboardingAction.SplashAction.OnIAlreadyHaveAnAccount(OnboardingFlow.SignIn))
@ -173,9 +174,9 @@ class OnboardingViewModelTest {
initialState, initialState,
{ copy(onboardingFlow = OnboardingFlow.SignIn) }, { copy(onboardingFlow = OnboardingFlow.SignIn) },
{ copy(isLoading = true) }, { copy(isLoading = true) },
{ copy(canLoginWithQrCode = true) }, { copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE_WITH_QR_SUPPORTED) },
{ copy(selectedHomeserver = DEFAULT_SELECTED_HOMESERVER_STATE) },
{ copy(signMode = SignMode.SignIn) }, { copy(signMode = SignMode.SignIn) },
{ copy(canLoginWithQrCode = true) },
{ copy(isLoading = false) } { copy(isLoading = false) }
) )
.assertEvents(OnboardingViewEvents.OpenCombinedLogin) .assertEvents(OnboardingViewEvents.OpenCombinedLogin)
@ -1174,13 +1175,11 @@ class OnboardingViewModelTest {
resultingState: SelectedHomeserverState, resultingState: SelectedHomeserverState,
config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG, config: HomeServerConnectionConfig = A_HOMESERVER_CONFIG,
fingerprint: Fingerprint? = null, fingerprint: Fingerprint? = null,
canLoginWithQrCode: Boolean = false,
) { ) {
fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config) fakeHomeServerConnectionConfigFactory.givenConfigFor(homeserverUrl, fingerprint, config)
fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) fakeStartAuthenticationFlowUseCase.givenResult(config, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.StartRegistration)
fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString()) fakeHomeServerHistoryService.expectUrlToBeAdded(config.homeServerUri.toString())
fakeAuthenticationService.givenIsQrLoginSupported(config, canLoginWithQrCode)
} }
private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) { private fun givenUpdatingHomeserverErrors(homeserverUrl: String, resultingState: SelectedHomeserverState, error: Throwable) {
@ -1188,7 +1187,6 @@ class OnboardingViewModelTest {
fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState)) fakeStartAuthenticationFlowUseCase.givenResult(A_HOMESERVER_CONFIG, StartAuthenticationResult(isHomeserverOutdated = false, resultingState))
givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error)) givenRegistrationResultFor(RegisterAction.StartRegistration, RegistrationActionHandler.Result.Error(error))
fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString()) fakeHomeServerHistoryService.expectUrlToBeAdded(A_HOMESERVER_CONFIG.homeServerUri.toString())
fakeAuthenticationService.givenIsQrLoginSupported(A_HOMESERVER_CONFIG, false)
} }
private fun givenUserNameIsAvailable(userName: String) { private fun givenUserNameIsAvailable(userName: String) {

View File

@ -140,7 +140,8 @@ class StartAuthenticationFlowUseCaseTest {
isLoginAndRegistrationSupported = true, isLoginAndRegistrationSupported = true,
homeServerUrl = A_DECLARED_HOMESERVER_URL, homeServerUrl = A_DECLARED_HOMESERVER_URL,
isOutdatedHomeserver = false, isOutdatedHomeserver = false,
isLogoutDevicesSupported = false isLogoutDevicesSupported = false,
isLoginWithQrSupported = false
) )
private fun expectedResult( private fun expectedResult(

View File

@ -58,10 +58,6 @@ class FakeAuthenticationService : AuthenticationService by mockk() {
coEvery { getWellKnownData(matrixId, config) } returns result coEvery { getWellKnownData(matrixId, config) } returns result
} }
fun givenIsQrLoginSupported(config: HomeServerConnectionConfig, result: Boolean) {
coEvery { isQrLoginSupported(config) } returns result
}
fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) { fun givenWellKnownThrows(matrixId: String, config: HomeServerConnectionConfig?, cause: Throwable) {
coEvery { getWellKnownData(matrixId, config) } throws cause coEvery { getWellKnownData(matrixId, config) } throws cause
} }

View File

@ -54,4 +54,8 @@ class FakeSessionAccountDataService : SessionAccountDataService by mockk(relaxed
fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) { fun verifyUpdateUserAccountDataEventSucceeds(type: String, content: Content, inverse: Boolean = false) {
coVerify(inverse = inverse) { updateUserAccountData(type, content) } coVerify(inverse = inverse) { updateUserAccountData(type, content) }
} }
fun givenGetUserAccountDataEventsStartWith(type: String, userAccountDataEventList: List<UserAccountDataEvent>) {
every { getUserAccountDataEventsStartWith(type) } returns userAccountDataEventList
}
} }