Merge branch 'develop' into hughns/qr-grant-error-mapping

This commit is contained in:
Hugh Nimmo-Smith 2026-04-28 17:04:08 +01:00 committed by GitHub
commit 055e5fddd3
29 changed files with 265 additions and 44 deletions

View file

@ -17,6 +17,7 @@ jobs:
permissions: permissions:
# Need write permissions on PRs to remove the label "Record-Screenshots" # Need write permissions on PRs to remove the label "Record-Screenshots"
pull-requests: write pull-requests: write
contents: write
name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }} name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots' if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots'

View file

@ -27,6 +27,7 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.features.linknewdevice.impl.screens.confirmation.CodeConfirmationNode
import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode
import io.element.android.features.linknewdevice.impl.screens.error.ErrorNode import io.element.android.features.linknewdevice.impl.screens.error.ErrorNode
import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType import io.element.android.features.linknewdevice.impl.screens.error.ErrorScreenType
@ -107,6 +108,11 @@ class LinkNewDeviceFlowNode(
val data: String, val data: String,
) : NavTarget ) : NavTarget
@Parcelize
data class CodeConfirmation(
val code: String,
) : NavTarget
@Parcelize @Parcelize
data object MobileEnterNumber : NavTarget data object MobileEnterNumber : NavTarget
@ -166,7 +172,9 @@ class LinkNewDeviceFlowNode(
is LinkDesktopStep.Error -> { is LinkDesktopStep.Error -> {
navigateToError(linkDesktopStep.errorType) navigateToError(linkDesktopStep.errorType)
} }
is LinkDesktopStep.EstablishingSecureChannel -> Unit is LinkDesktopStep.EstablishingSecureChannel -> {
backstack.push(NavTarget.CodeConfirmation(linkDesktopStep.checkCodeString))
}
is LinkDesktopStep.InvalidQrCode -> { is LinkDesktopStep.InvalidQrCode -> {
// This error will be handled by the ScanQrCodeNode // This error will be handled by the ScanQrCodeNode
} }
@ -247,6 +255,18 @@ class LinkNewDeviceFlowNode(
} }
createNode<EnterNumberNode>(buildContext, listOf(callback)) createNode<EnterNumberNode>(buildContext, listOf(callback))
} }
is NavTarget.CodeConfirmation -> {
val callback = object : CodeConfirmationNode.Callback {
override fun onCancel() {
// Push error
backstack.push(NavTarget.Error(ErrorScreenType.Cancelled))
}
}
val inputs = CodeConfirmationNode.Inputs(
code = navTarget.code,
)
createNode<CodeConfirmationNode>(buildContext, listOf(inputs, callback))
}
is NavTarget.MobileShowQrCode -> { is NavTarget.MobileShowQrCode -> {
val callback = object : ShowQrCodeNode.Callback { val callback = object : ShowQrCodeNode.Callback {
override fun navigateBack() { override fun navigateBack() {

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.linknewdevice.impl.screens.confirmation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
@AssistedInject
class CodeConfirmationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
) : Node(buildContext = buildContext, plugins = plugins) {
interface Callback : Plugin {
fun onCancel()
}
data class Inputs(
val code: String,
) : NodeInputs
private val callback: Callback = callback()
private val input = inputs<Inputs>()
@Composable
override fun View(modifier: Modifier) {
CodeConfirmationView(
code = input.code,
onCancel = callback::onCancel,
)
}
}

View file

@ -0,0 +1,134 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.linknewdevice.impl.screens.confirmation
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.linknewdevice.impl.R
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun CodeConfirmationView(
code: String,
onCancel: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(onBack = onCancel)
FlowStepPage(
modifier = modifier,
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
title = stringResource(R.string.screen_qr_code_login_device_code_title),
subTitle = stringResource(R.string.screen_qr_code_login_device_code_subtitle),
content = { Content(code = code) },
buttons = { Buttons(onCancel = onCancel) }
)
}
@Composable
private fun Content(code: String) {
Column(
modifier = Modifier.padding(top = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Digits(code = code)
Spacer(modifier = Modifier.height(32.dp))
WaitingForOtherDevice()
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Digits(code: String) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
code.forEach {
Text(
modifier = Modifier
.padding(horizontal = 6.dp, vertical = 4.dp)
.clip(RoundedCornerShape(4.dp))
.background(ElementTheme.colors.bgActionSecondaryPressed)
.padding(horizontal = 16.dp, vertical = 17.dp),
text = it.toString()
)
}
}
}
@Composable
private fun WaitingForOtherDevice() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
CircularProgressIndicator(
modifier = Modifier
.size(20.dp)
.padding(2.dp),
strokeWidth = 2.dp,
)
Text(
text = stringResource(R.string.screen_qr_code_login_verify_code_loading),
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun Buttons(
onCancel: () -> Unit,
) {
Column(modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_cancel),
onClick = onCancel,
)
}
}
@PreviewsDayNight
@Composable
internal fun CodeConfirmationViewPreview() {
ElementPreview {
CodeConfirmationView(
code = "67",
onCancel = {},
)
}
}

View file

@ -34,6 +34,8 @@
<string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_item_2">"If you encounter the same problem, try a different wifi network or use your mobile data instead of wifi"</string>
<string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesnt work, sign in manually"</string> <string name="screen_qr_code_login_connection_note_secure_state_list_item_3">"If that doesnt work, sign in manually"</string>
<string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string> <string name="screen_qr_code_login_connection_note_secure_state_title">"Connection not secure"</string>
<string name="screen_qr_code_login_device_code_subtitle">"Youll be asked to enter the two digits shown on this device."</string>
<string name="screen_qr_code_login_device_code_title">"Enter the number below on your other device"</string>
<string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string> <string name="screen_qr_code_login_error_cancelled_subtitle">"The sign in was cancelled on the other device."</string>
<string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string> <string name="screen_qr_code_login_error_cancelled_title">"Sign in request cancelled"</string>
<string name="screen_qr_code_login_error_declined_subtitle">"The sign in was declined on the other device."</string> <string name="screen_qr_code_login_error_declined_subtitle">"The sign in was declined on the other device."</string>
@ -54,4 +56,5 @@ Try signing in manually, or scan the QR code with another device."</string>
<string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your devices camera in order to continue."</string> <string name="screen_qr_code_login_no_camera_permission_state_description">"You need to give permission for %1$s to use your devices camera in order to continue."</string>
<string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string> <string name="screen_qr_code_login_no_camera_permission_state_title">"Allow camera access to scan the QR code"</string>
<string name="screen_qr_code_login_unknown_error_description">"An unexpected error occurred. Please try again."</string> <string name="screen_qr_code_login_unknown_error_description">"An unexpected error occurred. Please try again."</string>
<string name="screen_qr_code_login_verify_code_loading">"Waiting for your other device"</string>
</resources> </resources>

View file

@ -178,7 +178,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version
# https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt
# All new features should not be implemented in the pull request that upgrades the version, developers should # All new features should not be implemented in the pull request that upgrades the version, developers should
# only fix API breaks and may add some TODOs. # only fix API breaks and may add some TODOs.
matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.21" matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.04.27"
# Others # Others
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }

View file

@ -196,9 +196,9 @@ interface JoinedRoom : BaseRoom {
/** /**
* Start sharing live location in this room. * Start sharing live location in this room.
* @param durationMillis How long to share location (in milliseconds). * @param durationMillis How long to share location (in milliseconds).
* @return Result indicating success or failure. * @return Result containing the [EventId] of the beacon state event on success or an error on failure.
*/ */
suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit> suspend fun startLiveLocationShare(durationMillis: Long): Result<EventId>
/** /**
* Stop sharing live location in this room. * Stop sharing live location in this room.

View file

@ -214,5 +214,5 @@ fun SessionData.toSession() = Session(
deviceId = deviceId, deviceId = deviceId,
homeserverUrl = homeserverUrl, homeserverUrl = homeserverUrl,
slidingSyncVersion = SlidingSyncVersion.NATIVE, slidingSyncVersion = SlidingSyncVersion.NATIVE,
oidcData = oidcData, oauthData = oidcData,
) )

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.matrix.rustcomponents.sdk.ClientBuildException import org.matrix.rustcomponents.sdk.ClientBuildException
import org.matrix.rustcomponents.sdk.OidcException import org.matrix.rustcomponents.sdk.OAuthException
fun Throwable.mapAuthenticationException(): AuthenticationException { fun Throwable.mapAuthenticationException(): AuthenticationException {
return when (this) { return when (this) {
@ -29,12 +29,12 @@ fun Throwable.mapAuthenticationException(): AuthenticationException {
is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message)
is ClientBuildException.EventCache -> AuthenticationException.Generic(message) is ClientBuildException.EventCache -> AuthenticationException.Generic(message)
} }
is OidcException -> when (this) { is OAuthException -> when (this) {
is OidcException.Generic -> AuthenticationException.Oidc(message) is OAuthException.Generic -> AuthenticationException.Oidc(message)
is OidcException.CallbackUrlInvalid -> AuthenticationException.Oidc(message) is OAuthException.CallbackUrlInvalid -> AuthenticationException.Oidc(message)
is OidcException.Cancelled -> AuthenticationException.Oidc(message) is OAuthException.Cancelled -> AuthenticationException.Oidc(message)
is OidcException.MetadataInvalid -> AuthenticationException.Oidc(message) is OAuthException.MetadataInvalid -> AuthenticationException.Oidc(message)
is OidcException.NotSupported -> AuthenticationException.Oidc(message) is OAuthException.NotSupported -> AuthenticationException.Oidc(message)
} }
else -> AuthenticationException.Generic(message) else -> AuthenticationException.Generic(message)
} }

View file

@ -15,6 +15,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
MatrixHomeServerDetails( MatrixHomeServerDetails(
url = url(), url = url(),
supportsPasswordLogin = supportsPasswordLogin(), supportsPasswordLogin = supportsPasswordLogin(),
supportsOidcLogin = supportsOidcLogin(), supportsOidcLogin = supportsOauthLogin(),
) )
} }

View file

@ -12,14 +12,14 @@ import dev.zacsweers.metro.Inject
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.auth.OidcConfig import io.element.android.libraries.matrix.api.auth.OidcConfig
import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider import io.element.android.libraries.matrix.api.auth.OidcRedirectUrlProvider
import org.matrix.rustcomponents.sdk.OidcConfiguration import org.matrix.rustcomponents.sdk.OAuthConfiguration
@Inject @Inject
class OidcConfigurationProvider( class OidcConfigurationProvider(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val oidcRedirectUrlProvider: OidcRedirectUrlProvider, private val oidcRedirectUrlProvider: OidcRedirectUrlProvider,
) { ) {
fun get(): OidcConfiguration = OidcConfiguration( fun get(): OAuthConfiguration = OAuthConfiguration(
clientName = buildMeta.applicationName, clientName = buildMeta.applicationName,
redirectUri = oidcRedirectUrlProvider.provide(), redirectUri = oidcRedirectUrlProvider.provide(),
clientUri = OidcConfig.CLIENT_URI, clientUri = OidcConfig.CLIENT_URI,

View file

@ -9,7 +9,7 @@
package io.element.android.libraries.matrix.impl.auth package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.api.auth.OidcPrompt import io.element.android.libraries.matrix.api.auth.OidcPrompt
import org.matrix.rustcomponents.sdk.OidcPrompt as RustOidcPrompt import org.matrix.rustcomponents.sdk.OAuthPrompt as RustOidcPrompt
internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt { internal fun OidcPrompt.toRustPrompt(): RustOidcPrompt {
return when (this) { return when (this) {

View file

@ -31,8 +31,8 @@ class RustHomeServerLoginCompatibilityChecker(
it.homeserverLoginDetails() it.homeserverLoginDetails()
} }
.use { .use {
Timber.d("Homeserver $url | OIDC: ${it.supportsOidcLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}") Timber.d("Homeserver $url | OIDC: ${it.supportsOauthLogin()} | Password: ${it.supportsPasswordLogin()} | SSO: ${it.supportsSsoLogin()}")
it.supportsOidcLogin() || it.supportsPasswordLogin() it.supportsOauthLogin() || it.supportsPasswordLogin()
} }
} }
} }

View file

@ -260,8 +260,8 @@ class RustMatrixAuthenticationService(
return withContext(coroutineDispatchers.io) { return withContext(coroutineDispatchers.io) {
runCatchingExceptions { runCatchingExceptions {
val client = currentClient ?: error("You need to call `setHomeserver()` first") val client = currentClient ?: error("You need to call `setHomeserver()` first")
val oAuthAuthorizationData = client.urlForOidc( val oAuthAuthorizationData = client.urlForOauth(
oidcConfiguration = oidcConfigurationProvider.get(), oauthConfiguration = oidcConfigurationProvider.get(),
prompt = prompt.toRustPrompt(), prompt = prompt.toRustPrompt(),
loginHint = loginHint, loginHint = loginHint,
// If we want to restore a previous session for which we have encryption keys, we can pass the deviceId here. At the moment, we don't // If we want to restore a previous session for which we have encryption keys, we can pass the deviceId here. At the moment, we don't
@ -282,7 +282,7 @@ class RustMatrixAuthenticationService(
return withContext(coroutineDispatchers.io) { return withContext(coroutineDispatchers.io) {
runCatchingExceptions { runCatchingExceptions {
pendingOAuthAuthorizationData?.use { pendingOAuthAuthorizationData?.use {
currentClient?.abortOidcAuth(it) currentClient?.abortOauthAuth(it)
} }
pendingOAuthAuthorizationData = null pendingOAuthAuthorizationData = null
}.mapFailure { failure -> }.mapFailure { failure ->
@ -304,7 +304,7 @@ class RustMatrixAuthenticationService(
runCatchingExceptions { runCatchingExceptions {
val client = currentClient ?: error("You need to call `setHomeserver()` first") val client = currentClient ?: error("You need to call `setHomeserver()` first")
val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first") val currentSessionPaths = sessionPaths ?: error("You need to call `setHomeserver()` first")
client.loginWithOidcCallback( client.loginWithOauthCallback(
callbackUrl = callbackUrl, callbackUrl = callbackUrl,
) )
// Free the pending data since we won't use it to abort the flow anymore // Free the pending data since we won't use it to abort the flow anymore
@ -368,7 +368,7 @@ class RustMatrixAuthenticationService(
qrCodeData = sdkQrCodeLoginData, qrCodeData = sdkQrCodeLoginData,
) )
client.newLoginWithQrCodeHandler( client.newLoginWithQrCodeHandler(
oidcConfiguration = oidcConfiguration, oauthConfiguration = oidcConfiguration,
).use { ).use {
it.scan( it.scan(
qrCodeData = qrCodeData.rustQrCodeData, qrCodeData = qrCodeData.rustQrCodeData,

View file

@ -42,7 +42,7 @@ object QrErrorMapper {
is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn is RustHumanQrLoginException.OtherDeviceNotSignedIn -> QrLoginException.OtherDeviceNotSignedIn
is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported is RustHumanQrLoginException.LinkingNotSupported -> QrLoginException.LinkingNotSupported
is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown is RustHumanQrLoginException.Unknown -> QrLoginException.Unknown
is RustHumanQrLoginException.OidcMetadataInvalid -> QrLoginException.OidcMetadataInvalid is RustHumanQrLoginException.OAuthMetadataInvalid -> QrLoginException.OidcMetadataInvalid
is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable is RustHumanQrLoginException.SlidingSyncNotAvailable -> QrLoginException.SlidingSyncNotAvailable
is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent is RustHumanQrLoginException.CheckCodeAlreadySent -> QrLoginException.CheckCodeAlreadySent
is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent is RustHumanQrLoginException.CheckCodeCannotBeSent -> QrLoginException.CheckCodeCannotBeSent

View file

@ -25,7 +25,7 @@ object RustIdentityResetHandleFactory {
return runCatchingExceptions { return runCatchingExceptions {
identityResetHandle?.let { identityResetHandle?.let {
when (val authType = identityResetHandle.authType()) { when (val authType = identityResetHandle.authType()) {
is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) is CrossSigningResetAuthType.OAuth -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl)
// User interactive authentication (user + password) // User interactive authentication (user + password)
CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle) CrossSigningResetAuthType.Uiaa -> RustPasswordIdentityResetHandle(userId, identityResetHandle)
} }

View file

@ -27,7 +27,7 @@ internal fun Session.toSessionData(
accessToken = accessToken, accessToken = accessToken,
refreshToken = refreshToken, refreshToken = refreshToken,
homeserverUrl = homeserverUrl ?: this.homeserverUrl, homeserverUrl = homeserverUrl ?: this.homeserverUrl,
oidcData = oidcData, oidcData = oauthData,
loginTimestamp = Date(), loginTimestamp = Date(),
isTokenValid = isTokenValid, isTokenValid = isTokenValid,
loginType = loginType, loginType = loginType,

View file

@ -516,10 +516,10 @@ class JoinedRustRoom(
return innerRoom.liveLocationSharesFlow().timedByExpiry(systemClock::epochMillis) return innerRoom.liveLocationSharesFlow().timedByExpiry(systemClock::epochMillis)
} }
override suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit> = withContext(roomDispatcher) { override suspend fun startLiveLocationShare(durationMillis: Long): Result<EventId> = withContext(roomDispatcher) {
runCatchingExceptions { runCatchingExceptions {
innerRoom.startLiveLocationShare(durationMillis.toULong()) innerRoom.startLiveLocationShare(durationMillis.toULong())
} }.map(::EventId)
} }
override suspend fun stopLiveLocationShare(): Result<Unit> = withContext(roomDispatcher) { override suspend fun stopLiveLocationShare(): Result<Unit> = withContext(roomDispatcher) {
@ -538,7 +538,7 @@ class JoinedRustRoom(
override fun destroy() { override fun destroy() {
baseRoom.destroy() baseRoom.destroy()
liveInnerTimeline.destroy() liveTimeline.close()
threadsListService.destroy() threadsListService.destroy()
Timber.d("Room $roomId destroyed") Timber.d("Room $roomId destroyed")
} }

View file

@ -16,8 +16,8 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import org.matrix.rustcomponents.sdk.LiveLocationShareListener
import org.matrix.rustcomponents.sdk.LiveLocationShareUpdate import org.matrix.rustcomponents.sdk.LiveLocationShareUpdate
import org.matrix.rustcomponents.sdk.LiveLocationsListener
import org.matrix.rustcomponents.sdk.RoomInterface import org.matrix.rustcomponents.sdk.RoomInterface
import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare import org.matrix.rustcomponents.sdk.LiveLocationShare as RustLiveLocationShare
@ -41,9 +41,9 @@ fun RoomInterface.liveLocationSharesFlow(): Flow<List<LiveLocationShare>> {
} }
} }
return callbackFlow { return callbackFlow {
val liveLocationShares = liveLocationShares() val liveLocationShares = liveLocationsObserver()
val shares: MutableList<LiveLocationShare> = ArrayList() val shares: MutableList<LiveLocationShare> = ArrayList()
val taskHandle = liveLocationShares.subscribe(object : LiveLocationShareListener { val taskHandle = liveLocationShares.subscribe(object : LiveLocationsListener {
override fun onUpdate(updates: List<LiveLocationShareUpdate>) { override fun onUpdate(updates: List<LiveLocationShareUpdate>) {
for (update in updates) { for (update in updates) {
shares.applyUpdate(update) shares.applyUpdate(update)

View file

@ -13,7 +13,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.AuthenticationException
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.ClientBuildException import org.matrix.rustcomponents.sdk.ClientBuildException
import org.matrix.rustcomponents.sdk.OidcException import org.matrix.rustcomponents.sdk.OAuthException
class AuthenticationExceptionMappingTest { class AuthenticationExceptionMappingTest {
@Test @Test
@ -65,15 +65,15 @@ class AuthenticationExceptionMappingTest {
@Test @Test
fun `mapping Oidc exceptions map to the Oidc Kotlin`() { fun `mapping Oidc exceptions map to the Oidc Kotlin`() {
assertThat(OidcException.Generic("Generic").mapAuthenticationException()) assertThat(OAuthException.Generic("Generic").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("Generic") .isException<AuthenticationException.Oidc>("Generic")
assertThat(OidcException.CallbackUrlInvalid("CallbackUrlInvalid").mapAuthenticationException()) assertThat(OAuthException.CallbackUrlInvalid("CallbackUrlInvalid").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("CallbackUrlInvalid") .isException<AuthenticationException.Oidc>("CallbackUrlInvalid")
assertThat(OidcException.Cancelled("Cancelled").mapAuthenticationException()) assertThat(OAuthException.Cancelled("Cancelled").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("Cancelled") .isException<AuthenticationException.Oidc>("Cancelled")
assertThat(OidcException.MetadataInvalid("MetadataInvalid").mapAuthenticationException()) assertThat(OAuthException.MetadataInvalid("MetadataInvalid").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("MetadataInvalid") .isException<AuthenticationException.Oidc>("MetadataInvalid")
assertThat(OidcException.NotSupported("NotSupported").mapAuthenticationException()) assertThat(OAuthException.NotSupported("NotSupported").mapAuthenticationException())
.isException<AuthenticationException.Oidc>("NotSupported") .isException<AuthenticationException.Oidc>("NotSupported")
} }

View file

@ -32,7 +32,7 @@ class QrErrorMapperTest {
assertThat(QrErrorMapper.map(RustHumanQrLoginException.OtherDeviceNotSignedIn())).isEqualTo(QrLoginException.OtherDeviceNotSignedIn) assertThat(QrErrorMapper.map(RustHumanQrLoginException.OtherDeviceNotSignedIn())).isEqualTo(QrLoginException.OtherDeviceNotSignedIn)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.LinkingNotSupported())).isEqualTo(QrLoginException.LinkingNotSupported) assertThat(QrErrorMapper.map(RustHumanQrLoginException.LinkingNotSupported())).isEqualTo(QrLoginException.LinkingNotSupported)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.Unknown())).isEqualTo(QrLoginException.Unknown) assertThat(QrErrorMapper.map(RustHumanQrLoginException.Unknown())).isEqualTo(QrLoginException.Unknown)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.OidcMetadataInvalid())).isEqualTo(QrLoginException.OidcMetadataInvalid) assertThat(QrErrorMapper.map(RustHumanQrLoginException.OAuthMetadataInvalid())).isEqualTo(QrLoginException.OidcMetadataInvalid)
assertThat(QrErrorMapper.map(RustHumanQrLoginException.SlidingSyncNotAvailable())).isEqualTo(QrLoginException.SlidingSyncNotAvailable) assertThat(QrErrorMapper.map(RustHumanQrLoginException.SlidingSyncNotAvailable())).isEqualTo(QrLoginException.SlidingSyncNotAvailable)
} }
} }

View file

@ -9,6 +9,7 @@
package io.element.android.libraries.matrix.impl.fixtures.factories package io.element.android.libraries.matrix.impl.fixtures.factories
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEvent import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEvent
import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
@ -68,6 +69,8 @@ internal fun aRustNotificationRoomInfo(
isDirect: Boolean = false, isDirect: Boolean = false,
joinRule: JoinRule? = null, joinRule: JoinRule? = null,
isSpace: Boolean = false, isSpace: Boolean = false,
serviceMembers: List<UserId> = emptyList(),
activeServiceMemberCount: Int = 0,
) = NotificationRoomInfo( ) = NotificationRoomInfo(
displayName = displayName, displayName = displayName,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
@ -78,6 +81,8 @@ internal fun aRustNotificationRoomInfo(
isDirect = isDirect, isDirect = isDirect,
joinRule = joinRule, joinRule = joinRule,
isSpace = isSpace, isSpace = isSpace,
serviceMembers = serviceMembers.map { it.value },
activeServiceMembersCount = activeServiceMemberCount.toULong(),
) )
internal fun aRustNotificationEventTimeline( internal fun aRustNotificationEventTimeline(

View file

@ -62,6 +62,7 @@ internal fun aRustRoomInfo(
serviceMembers: List<String> = emptyList(), serviceMembers: List<String> = emptyList(),
isLowPriority: Boolean = false, isLowPriority: Boolean = false,
activeRoomCallConsensusIntent: RtcCallIntentConsensus = RtcCallIntentConsensus.None, activeRoomCallConsensusIntent: RtcCallIntentConsensus = RtcCallIntentConsensus.None,
activeServiceMembersCount: Int = 0,
) = RoomInfo( ) = RoomInfo(
id = id, id = id,
displayName = displayName, displayName = displayName,
@ -101,4 +102,5 @@ internal fun aRustRoomInfo(
serviceMembers = serviceMembers, serviceMembers = serviceMembers,
isLowPriority = isLowPriority, isLowPriority = isLowPriority,
activeRoomCallConsensusIntent = activeRoomCallConsensusIntent, activeRoomCallConsensusIntent = activeRoomCallConsensusIntent,
activeServiceMembersCount = activeServiceMembersCount.toULong(),
) )

View file

@ -24,6 +24,6 @@ internal fun aRustSession(
userId = A_USER_ID.value, userId = A_USER_ID.value,
deviceId = A_DEVICE_ID.value, deviceId = A_DEVICE_ID.value,
homeserverUrl = A_HOMESERVER_URL, homeserverUrl = A_HOMESERVER_URL,
oidcData = null, oauthData = null,
slidingSyncVersion = proxy, slidingSyncVersion = proxy,
) )

View file

@ -18,7 +18,7 @@ class FakeFfiHomeserverLoginDetails(
private val supportsSsoLogin: Boolean = false, private val supportsSsoLogin: Boolean = false,
) : HomeserverLoginDetails(NoHandle) { ) : HomeserverLoginDetails(NoHandle) {
override fun url(): String = url override fun url(): String = url
override fun supportsOidcLogin(): Boolean = supportsOidcLogin override fun supportsOauthLogin(): Boolean = supportsOidcLogin
override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin override fun supportsPasswordLogin(): Boolean = supportsPasswordLogin
override fun supportsSsoLogin(): Boolean = supportsSsoLogin override fun supportsSsoLogin(): Boolean = supportsSsoLogin
} }

View file

@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService import io.element.android.libraries.matrix.test.room.threads.FakeThreadsListService
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
@ -238,8 +239,8 @@ class FakeJoinedRoom(
return liveLocationSharesFlow return liveLocationSharesFlow
} }
override suspend fun startLiveLocationShare(durationMillis: Long): Result<Unit> = simulateLongTask { override suspend fun startLiveLocationShare(durationMillis: Long): Result<EventId> = simulateLongTask {
startLiveLocationShareResult(durationMillis) startLiveLocationShareResult(durationMillis).map { AN_EVENT_ID }
} }
override suspend fun stopLiveLocationShare(): Result<Unit> = simulateLongTask { override suspend fun stopLiveLocationShare(): Result<Unit> = simulateLongTask {

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c8dab1dc964cea9a76dc7130d8ac2bfe5d3c866fd6d8d969101eb50e828775d3
size 31915

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec67e3fef25c57331cb2425f8fc46a733e16a98c9945d0a4e5b950843c42fa34
size 31087

View file

@ -163,7 +163,9 @@
"screen_qr_code_login_connection_note_secure_state.*", "screen_qr_code_login_connection_note_secure_state.*",
"screen_qr_code_login_unknown_error_description", "screen_qr_code_login_unknown_error_description",
"screen_qr_code_login_invalid_scan_state_.*", "screen_qr_code_login_invalid_scan_state_.*",
"screen_qr_code_login_no_camera_permission_state_.*" "screen_qr_code_login_no_camera_permission_state_.*",
"screen_qr_code_login_device_code_.*",
"screen_qr_code_login_verify_code_loading"
] ]
}, },
{ {