mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Merge branch 'develop' into feature/fga/user_status_last_pieces
This commit is contained in:
@@ -36,5 +36,5 @@ dependencies {
|
||||
implementation(libs.matrix.emojibase.bindings)
|
||||
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testCommonDependencies(libs)
|
||||
testCommonDependencies(libs, includeTestComposeView = true)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+72
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -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('/')}"
|
||||
|
||||
+13
@@ -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>
|
||||
}
|
||||
+2
@@ -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(
|
||||
|
||||
+6
-1
@@ -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
|
||||
|
||||
+36
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -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,
|
||||
|
||||
+7
-1
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -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()
|
||||
}
|
||||
}
|
||||
+11
-7
@@ -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)
|
||||
|
||||
+10
-8
@@ -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)
|
||||
|
||||
+4
-4
@@ -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,
|
||||
)
|
||||
|
||||
+9
-7
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user