Merge branch 'develop' into feature/fga/user_status_last_pieces

This commit is contained in:
ganfra
2026-08-11 10:04:18 +02:00
56 changed files with 715 additions and 115 deletions
+1 -1
View File
@@ -36,5 +36,5 @@ dependencies {
implementation(libs.matrix.emojibase.bindings)
testImplementation(projects.libraries.matrix.test)
testCommonDependencies(libs)
testCommonDependencies(libs, includeTestComposeView = true)
}
@@ -62,7 +62,7 @@ internal fun EmojiPickerView(
Column(modifier) {
SearchBar(
modifier = Modifier.padding(bottom = 10.dp),
modifier = Modifier.padding(bottom = if (state.isSearchActive) 0.dp else 10.dp),
queryState = state.searchQuery,
resultState = state.searchResults,
active = state.isSearchActive,
@@ -0,0 +1,72 @@
/*
* 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.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.libraries.emoji.impl.picker
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsEqualTo
import androidx.compose.ui.test.getBoundsInRoot
import androidx.compose.ui.test.hasScrollToIndexAction
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.compose.ui.unit.dp
import io.element.android.emojibasebindings.Emoji
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import org.junit.Test
import org.robolectric.annotation.Config
private val PICKER_HEIGHT = 600.dp
class EmojiPickerViewTest : RobolectricTest() {
@Config(qualifiers = "w400dp-h800dp")
@Test
fun `the search results reach the bottom of the picker`() = runAndroidComposeUiTest<ComponentActivity> {
setEmojiPickerView()
onNode(hasScrollToIndexAction())
.getBoundsInRoot()
.bottom
.assertIsEqualTo(PICKER_HEIGHT, "emoji grid bottom")
}
}
private fun AndroidComposeUiTest<ComponentActivity>.setEmojiPickerView() {
setContent {
Box(Modifier.size(400.dp, PICKER_HEIGHT)) {
EmojiPickerView(
state = aDefaultEmojiPickerState(
isSearchActive = true,
searchQuery = "smile",
searchResults = SearchBarResultState.Results(
persistentListOf(
Emoji(
hexcode = "0x00",
label = "grinning face",
tags = persistentListOf("grinning"),
shortcodes = persistentListOf("smile"),
unicode = "😀",
skins = null,
),
)
),
),
onSelectEmoji = {},
selectedEmojis = persistentSetOf(),
modifier = Modifier.fillMaxSize(),
)
}
}
}
@@ -11,6 +11,8 @@ package io.element.android.libraries.matrix.api.auth
import io.element.android.libraries.matrix.api.BuildConfig
object OAuthConfig {
// This *can* be null if the build time config field is not set
@Suppress("UNNECESSARY_SAFE_CALL")
val CLIENT_URI = BuildConfig.OAUTH_CLIENT_URI_PATH?.let {
// Concatenate the base URI and the path, ensuring there is exactly one '/' between them
"${BuildConfig.CLIENT_URI.trimEnd('/')}/${it.trimStart('/')}"
@@ -0,0 +1,13 @@
/*
* 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.libraries.matrix.api.linknewdevice
interface ContinuationMessageSender {
suspend fun cancel(): Result<Unit>
suspend fun confirm(): Result<Unit>
}
@@ -18,8 +18,10 @@ interface LinkDesktopHandler {
sealed interface LinkDesktopStep {
data object Uninitialized : LinkDesktopStep
data object Starting : LinkDesktopStep
data class WaitingForAuth(
val verificationUri: String,
val continuationMessageSender: ContinuationMessageSender,
) : LinkDesktopStep
data class EstablishingSecureChannel(
@@ -22,7 +22,12 @@ sealed interface LinkMobileStep {
data object Starting : LinkMobileStep
data class QrReady(val data: String) : LinkMobileStep
data object QrRotating : LinkMobileStep
data class WaitingForAuth(val verificationUri: String) : LinkMobileStep
data class WaitingForAuth(
val verificationUri: String,
val continuationMessageSender: ContinuationMessageSender,
) : LinkMobileStep
data class QrScanned(val checkCodeSender: CheckCodeSender) : LinkMobileStep
data class Error(val errorType: ErrorType) : LinkMobileStep
data object SyncingSecrets : LinkMobileStep
@@ -0,0 +1,36 @@
/*
* 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.libraries.matrix.impl.linknewdevice
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.linknewdevice.ContinuationMessageSender
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import org.matrix.rustcomponents.sdk.ContinuationMessageSender as FfiContinuationMessageSender
class RustContinuationMessageSender(
private val inner: FfiContinuationMessageSender,
private val sessionDispatcher: CoroutineDispatcher,
) : ContinuationMessageSender {
override suspend fun cancel(): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
inner.cancel()
}.onFailure {
Timber.e(it, "Error cancelling continuation message")
}
}
override suspend fun confirm(): Result<Unit> = withContext(sessionDispatcher) {
runCatchingExceptions {
inner.confirm()
}.onFailure {
Timber.e(it, "Error confirming continuation message")
}
}
}
@@ -75,6 +75,10 @@ class RustLinkDesktopHandler(
GrantQrLoginProgress.SyncingSecrets -> LinkDesktopStep.SyncingSecrets
is GrantQrLoginProgress.WaitingForAuth -> LinkDesktopStep.WaitingForAuth(
verificationUri = verificationUri,
continuationMessageSender = RustContinuationMessageSender(
inner = continuationSender,
sessionDispatcher = sessionDispatcher,
)
)
is GrantQrLoginProgress.EstablishingSecureChannel -> LinkDesktopStep.EstablishingSecureChannel(
checkCode = checkCode,
@@ -84,7 +84,13 @@ class RustLinkMobileHandler(
)
GrantGeneratedQrLoginProgress.Starting -> LinkMobileStep.Starting
GrantGeneratedQrLoginProgress.SyncingSecrets -> LinkMobileStep.SyncingSecrets
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(verificationUri)
is GrantGeneratedQrLoginProgress.WaitingForAuth -> LinkMobileStep.WaitingForAuth(
verificationUri = verificationUri,
continuationMessageSender = RustContinuationMessageSender(
inner = continuationSender,
sessionDispatcher = sessionDispatcher,
)
)
}
}
}
@@ -0,0 +1,25 @@
/*
* 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.libraries.matrix.impl.linknewdevice
import io.element.android.libraries.matrix.api.linknewdevice.ContinuationMessageSender
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeContinuationMessageSender(
private val cancelResult: () -> Result<Unit> = { lambdaError() },
private val confirmResult: () -> Result<Unit> = { lambdaError() },
) : ContinuationMessageSender {
override suspend fun cancel(): Result<Unit> = simulateLongTask {
cancelResult()
}
override suspend fun confirm(): Result<Unit> = simulateLongTask {
confirmResult()
}
}
@@ -16,6 +16,8 @@ import io.element.android.libraries.matrix.api.linknewdevice.LinkDesktopStep
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuationMessageSender
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
import io.element.android.libraries.matrix.test.QR_CODE_DATA
import io.element.android.tests.testutils.ExpectedResult
import io.element.android.tests.testutils.match
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@@ -50,16 +52,18 @@ class RustLinkDesktopHandlerTest {
runCurrent()
// progress from the handler is mapped and emitted
listOf(
GrantQrLoginProgress.Starting to LinkDesktopStep.Starting,
GrantQrLoginProgress.SyncingSecrets to LinkDesktopStep.SyncingSecrets,
GrantQrLoginProgress.Starting to ExpectedResult(LinkDesktopStep.Starting),
GrantQrLoginProgress.SyncingSecrets to ExpectedResult(LinkDesktopStep.SyncingSecrets),
GrantQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
to LinkDesktopStep.WaitingForAuth("aVerificationUri"),
to ExpectedResult(
resultClass = LinkDesktopStep.WaitingForAuth::class.java,
),
GrantQrLoginProgress.EstablishingSecureChannel(1.toUByte(), "1")
to LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1"),
GrantQrLoginProgress.Done to LinkDesktopStep.Done,
).forEach { (progress, expectedStep) ->
to ExpectedResult(LinkDesktopStep.EstablishingSecureChannel(1.toUByte(), "1")),
GrantQrLoginProgress.Done to ExpectedResult(LinkDesktopStep.Done),
).forEach { (progress, expectedResult) ->
handler.emitScanProgress(progress)
assertThat(awaitItem()).isEqualTo(expectedStep)
assertThat(awaitItem()).match(expectedResult)
}
// scan returns, no new event is emitted
completable.complete(Unit)
@@ -18,6 +18,8 @@ import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiContinuati
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiGrantLoginWithQrCodeHandler
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData
import io.element.android.libraries.matrix.test.QR_CODE_DATA_RECIPROCATE
import io.element.android.tests.testutils.ExpectedResult
import io.element.android.tests.testutils.match
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@@ -51,18 +53,18 @@ class RustLinkMobileHandlerTest {
runCurrent()
// progress from the handler is mapped and emitted
listOf(
GrantGeneratedQrLoginProgress.Starting to LinkMobileStep.Starting::class.java,
GrantGeneratedQrLoginProgress.SyncingSecrets to LinkMobileStep.SyncingSecrets::class.java,
GrantGeneratedQrLoginProgress.Starting to ExpectedResult(LinkMobileStep.Starting),
GrantGeneratedQrLoginProgress.SyncingSecrets to ExpectedResult(LinkMobileStep.SyncingSecrets),
GrantGeneratedQrLoginProgress.WaitingForAuth("aVerificationUri", FakeFfiContinuationMessageSender())
to LinkMobileStep.WaitingForAuth::class.java,
to ExpectedResult(LinkMobileStep.WaitingForAuth::class.java),
GrantGeneratedQrLoginProgress.QrScanned(FakeFfiCheckCodeSender())
to LinkMobileStep.QrScanned::class.java,
to ExpectedResult(LinkMobileStep.QrScanned::class.java),
GrantGeneratedQrLoginProgress.QrReady(FakeFfiQrCodeData(toBytesResult = { QR_CODE_DATA_RECIPROCATE }))
to LinkMobileStep.QrReady::class.java,
GrantGeneratedQrLoginProgress.Done to LinkMobileStep.Done::class.java,
).forEach { (progress, expectedStepClass) ->
to ExpectedResult(LinkMobileStep.QrReady::class.java),
GrantGeneratedQrLoginProgress.Done to ExpectedResult(LinkMobileStep.Done),
).forEach { (progress, expectedResult) ->
handler.emitGenerateProgress(progress)
assertThat(awaitItem()).isInstanceOf(expectedStepClass)
assertThat(awaitItem()).match(expectedResult)
}
// generate returns, no new event is emitted
completable.complete(Unit)
@@ -8,12 +8,12 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.selection.toggleable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
@@ -40,8 +40,8 @@ fun CheckableUserRow(
Row(
modifier = modifier
.fillMaxWidth()
.clickable(role = Role.Checkbox, enabled = enabled) {
onCheckedChange(!checked)
.toggleable(value = checked, role = Role.Checkbox, enabled = enabled) {
onCheckedChange(it)
},
verticalAlignment = Alignment.CenterVertically,
) {
@@ -66,7 +66,7 @@ fun CheckableUserRow(
}
}
Checkbox(
onCheckedChange = onCheckedChange,
onCheckedChange = null,
checked = checked,
enabled = enabled,
)
@@ -27,7 +27,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
@Composable
internal fun ContentErrorView(
title: String,
message: String,
message: String?,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues.Zero,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
@@ -51,12 +51,14 @@ internal fun ContentErrorView(
color = ElementTheme.colors.textCriticalPrimary,
style = ElementTheme.typography.fontBodyMdMedium,
)
Text(
text = message,
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodySmRegular,
onTextLayout = onTextLayout,
)
if (message != null) {
Text(
text = message,
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodySmRegular,
onTextLayout = onTextLayout,
)
}
}
}
}
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -45,6 +44,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentErrorView
import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue
import io.element.android.libraries.matrix.ui.media.contentvalidation.InvalidContentView
import io.element.android.libraries.matrix.ui.media.contentvalidation.NotFoundContentView
@@ -77,7 +77,7 @@ fun InReplyToView(
else -> ReplyToLoadingContent(modifier = modifier)
}
is InReplyToDetails.Error ->
ReplyToErrorContent(data = inReplyTo, maxLines = maxLines, modifier = modifier)
ReplyToErrorContent(modifier = modifier)
is InReplyToDetails.Loading ->
ReplyToLoadingContent(modifier = modifier)
}
@@ -150,24 +150,15 @@ private fun ReplyToLoadingContent(
@Composable
private fun ReplyToErrorContent(
data: InReplyToDetails.Error,
maxLines: Int,
modifier: Modifier = Modifier,
) {
val paddings = PaddingValues(horizontal = 12.dp, vertical = 4.dp)
Row(
modifier
.background(ElementTheme.colors.bgCanvasDefault)
.padding(paddings)
) {
Text(
text = data.message,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textCriticalPrimary,
maxLines = maxLines,
overflow = TextOverflow.Ellipsis,
)
}
ContentErrorView(
title = stringResource(CommonStrings.error_message_not_found),
message = null,
modifier = modifier,
contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp),
onTextLayout = null,
)
}
@Composable