Merge branch 'release/0.7.0' into main

This commit is contained in:
Benoit Marty 2024-10-10 20:23:52 +02:00
commit 90f704b8ae
197 changed files with 1305 additions and 875 deletions

3
.gitignore vendored
View file

@ -65,6 +65,9 @@ captures/
.idea/shelf .idea/shelf
.idea/sonarlint .idea/sonarlint
# .kotlin folder
.kotlin
# Keystore files # Keystore files
# Uncomment the following lines if you do not want to check your keystore files in. # Uncomment the following lines if you do not want to check your keystore files in.
#*.jks #*.jks

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.25" /> <option name="version" value="2.0.20" />
</component> </component>
</project> </project>

View file

@ -1,3 +1,77 @@
Changes in Element X v0.6.5 (2024-10-09)
========================================
## What's Changed
### ✨ Features
* Add developer setting to hide images in the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/3592
* Warn the user when unverified user has changed their identity by @bmarty in https://github.com/element-hq/element-x-android/pull/3621
### 🙌 Improvements
* Handle no network error when starting Element Call. by @bmarty in https://github.com/element-hq/element-x-android/pull/3527
### 🐛 Bugfixes
* Fix room settings not treating unencrypted DMs as DMs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3545
* Fix crash when aspectRatio is null. by @bmarty in https://github.com/element-hq/element-x-android/pull/3561
* Don't delete uploaded logs by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3540
* Don't display security banner for unknown RecoveryState by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3579
* Fix the logic of the room list banner state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3615
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/3560
* Sync Strings - import translations to Persian by @ElementBot in https://github.com/element-hq/element-x-android/pull/3612
### 🧱 Build
* Introduce ModulesConfig by @bmarty in https://github.com/element-hq/element-x-android/pull/3530
* Centralise the DI code generation logic by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3562
* Update Gradle impl module template with `setupAnvil()` call by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3563
* Use Anvil KSP instead of the Square KAPT one by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3564
* Upgrade the used JDK in the project to v21 by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3582
* Merge unit, screenshot tests and coverage in a single CI call by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3593
* Disable configuration cache in the CI by default by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3601
* Fix screenshot recording in CI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/3607
* Ensure the CI compile and execute all the unit tests. by @bmarty in https://github.com/element-hq/element-x-android/pull/3617
### Dependency upgrades
* Update dependency androidx.compose:compose-bom to v2024.09.00 by @renovate in https://github.com/element-hq/element-x-android/pull/3399
* Update dependency androidx.compose:compose-bom to v2024.09.02 by @renovate in https://github.com/element-hq/element-x-android/pull/3544
* Update dependency io.element.android:compound-android to v0.1.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3524
* Update dependency com.google.firebase:firebase-bom to v33.3.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3549
* Update dependency org.maplibre.gl:android-sdk to v11.5.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3550
* Update dependency org.maplibre.gl:android-plugin-annotation-v9 to v3.0.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3505
* Update dependency androidx.webkit:webkit to v1.12.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3520
* Update dependency com.posthog:posthog-android to v3.7.5 by @renovate in https://github.com/element-hq/element-x-android/pull/3546
* Update gradle-update/update-gradle-wrapper-action action to v2 by @renovate in https://github.com/element-hq/element-x-android/pull/3551
* Update dependency com.lemonappdev:konsist to v0.16.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3371
* Update android.gradle.plugin to v8.6.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3504
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.49 by @renovate in https://github.com/element-hq/element-x-android/pull/3553
* Update lifecycle to v2.8.6 by @renovate in https://github.com/element-hq/element-x-android/pull/3398
* Update dependency com.google.accompanist:accompanist-permissions to v0.36.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3400
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.50 by @renovate in https://github.com/element-hq/element-x-android/pull/3565
* Update dependency com.google.firebase:firebase-bom to v33.4.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3578
* Update android.gradle.plugin to v8.7.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3577
* Update dependency com.posthog:posthog-android to v3.8.0 by @renovate in https://github.com/element-hq/element-x-android/pull/3591
* dependency: Bump rust sdk to 0.2.51 by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/3602
* chore(deps): update dependencyanalysis to v2.1.3 by @renovate in https://github.com/element-hq/element-x-android/pull/3559
* Update wysiwyg to v2.37.13 by @renovate in https://github.com/element-hq/element-x-android/pull/3596
* fix(deps): update dependency io.nlopez.compose.rules:detekt to v0.4.15 by @renovate in https://github.com/element-hq/element-x-android/pull/3595
* fix(deps): update dependency com.google.testparameterinjector:test-parameter-injector to v1.18 by @renovate in https://github.com/element-hq/element-x-android/pull/3606
* fix(deps): update dependency com.squareup:kotlinpoet-ksp to v1.18.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3580
* Update dependency org.matrix.rustcomponents:sdk-android to v0.2.52 by @renovate in https://github.com/element-hq/element-x-android/pull/3619
* SDK 0.2.53 19b9a73ecc3e31d502dbf0c5850bfdfaddf02afe by @bmarty in https://github.com/element-hq/element-x-android/pull/3622
* Update dependency org.maplibre.gl:android-sdk to v11.5.1 by @renovate in https://github.com/element-hq/element-x-android/pull/3608
### Others
* rename invisible flag to onlySignedDeviceIsolation flag by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/3542
* Fix image viewer glitch by @ganfra in https://github.com/element-hq/element-x-android/pull/3537
* Prefix message sent by the current user by `You` instead of the sender name. by @bmarty in https://github.com/element-hq/element-x-android/pull/3547
* timeline : remove animateItem by @ganfra in https://github.com/element-hq/element-x-android/pull/3548
* Fix a couple of build-time warnings in Gradle output by @frebib in https://github.com/element-hq/element-x-android/pull/3349
* Use MSC2530 filename when loading media by @frebib in https://github.com/element-hq/element-x-android/pull/3567
* Prevent crash with duplicate room suggestion by @frebib in https://github.com/element-hq/element-x-android/pull/3576
* Add unit tests on TimelineItemsSubscriber by @bmarty in https://github.com/element-hq/element-x-android/pull/3554
* Fix tests on develop by @bmarty in https://github.com/element-hq/element-x-android/pull/3585
* Timeline better jump to behaviours by @ganfra in https://github.com/element-hq/element-x-android/pull/3597
* Fix building the app using a local SDK. by @bmarty in https://github.com/element-hq/element-x-android/pull/3604
* crypto: Use OnlySigned isolation flag to setup decryption trust req. by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/3569
* Fix black-on-black status bars with hidden media by @frebib in https://github.com/element-hq/element-x-android/pull/3611
* Remove supportSlidingSync boolean. by @bmarty in https://github.com/element-hq/element-x-android/pull/3609
* Ensure that `Presenter`s do not depend on other presenters. by @bmarty in https://github.com/element-hq/element-x-android/pull/3618
* Do not render pin violation in clear rooms. by @bmarty in https://github.com/element-hq/element-x-android/pull/3630
Changes in Element X v0.6.4 (2024-09-25) Changes in Element X v0.6.4 (2024-09-25)
======================================== ========================================

View file

@ -8,6 +8,7 @@
package io.element.android.appconfig package io.element.android.appconfig
object LearnMoreConfig { object LearnMoreConfig {
const val ENCRYPTION_URL: String = "https://element.io/help#encryption"
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5" const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18" const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
} }

View file

@ -45,6 +45,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.getRoomInfoFlow
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -120,12 +122,9 @@ class RoomFlowNode @AssistedInject constructor(
} }
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) { private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
val roomInfoFlow = client.getRoomInfoFlow( val roomInfoFlow = client.getRoomInfoFlow(roomIdOrAlias = roomId.toRoomIdOrAlias())
roomId = roomId val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged()
).map { it.getOrNull() } val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged()
val isSpaceFlow = roomInfoFlow.map { it?.isSpace.orFalse() }.distinctUntilChanged()
val currentMembershipFlow = roomInfoFlow.map { it?.currentUserMembership }.distinctUntilChanged()
combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace -> combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace ->
Timber.d("Room membership: $membership") Timber.d("Room membership: $membership")
when (membership) { when (membership) {

View file

@ -478,7 +478,7 @@ class LoggedInPresenterTest {
distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")), distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")),
currentDistributor = { null }, currentDistributor = { null },
), ),
registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ -> registerWithLambda: (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
Result.success(Unit) Result.success(Unit)
}, },
selectPushProviderLambda: (MatrixClient, PushProvider) -> Unit = { _, _ -> lambdaError() }, selectPushProviderLambda: (MatrixClient, PushProvider) -> Unit = { _, _ -> lambdaError() },

View file

@ -18,6 +18,7 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ksp) apply false alias(libs.plugins.ksp) apply false
alias(libs.plugins.anvil) apply false alias(libs.plugins.anvil) apply false
alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.jvm) apply false
@ -82,20 +83,15 @@ allprojects {
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
// Warnings are potential errors, so stop ignoring them compilerOptions {
// This is disabled by default, but the CI will enforce this. // Warnings are potential errors, so stop ignoring them
// You can override by passing `-PallWarningsAsErrors=true` in the command line // This is disabled by default, but the CI will enforce this.
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file // You can override by passing `-PallWarningsAsErrors=true` in the command line
kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true" // Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
kotlinOptions {
/*
// Uncomment to suppress Compose Kotlin compiler compatibility warning // Uncomment to suppress Compose Kotlin compiler compatibility warning
freeCompilerArgs += listOf( // freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
)
*/
} }
} }
} }
@ -192,19 +188,23 @@ subprojects {
subprojects { subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions { compilerOptions {
if (project.findProperty("composeCompilerReports") == "true") { if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += listOf( freeCompilerArgs.addAll(
"-P", listOf(
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + "-P",
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler" "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
)
) )
} }
if (project.findProperty("composeCompilerMetrics") == "true") { if (project.findProperty("composeCompilerMetrics") == "true") {
freeCompilerArgs += listOf( freeCompilerArgs.addAll(
"-P", listOf(
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + "-P",
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler" "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
)
) )
} }
} }

View file

@ -0,0 +1,2 @@
Main changes in this version: bug fixes and performance improvement.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2023, 2024 New Vector Ltd.
* *
@ -7,13 +9,14 @@
plugins { plugins {
id("io.element.android-library") id("io.element.android-library")
alias(libs.plugins.anvil)
} }
android { android {
namespace = "io.element.android.features.cachecleaner.api" namespace = "io.element.android.features.cachecleaner.api"
} }
setupAnvil()
dependencies { dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(libs.androidx.startup) implementation(libs.androidx.startup)

View file

@ -8,7 +8,9 @@
package io.element.android.features.call.impl.ui package io.element.android.features.call.impl.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.ConsoleMessage
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
@ -43,6 +45,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import timber.log.Timber
typealias RequestPermissionCallback = (Array<String>) -> Unit typealias RequestPermissionCallback = (Array<String>) -> Unit
@ -189,6 +192,25 @@ private fun WebView.setup(
override fun onPermissionRequest(request: PermissionRequest) { override fun onPermissionRequest(request: PermissionRequest) {
onPermissionsRequested(request) onPermissionsRequested(request)
} }
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
val priority = when (consoleMessage.messageLevel()) {
ConsoleMessage.MessageLevel.ERROR -> Log.ERROR
ConsoleMessage.MessageLevel.WARNING -> Log.WARN
else -> Log.DEBUG
}
Timber.tag("WebView").log(
priority = priority,
message = buildString {
append(consoleMessage.sourceId())
append(":")
append(consoleMessage.lineNumber())
append(" ")
append(consoleMessage.message())
},
)
return true
}
} }
} }

View file

@ -109,12 +109,7 @@ class CallScreenPresenterTest {
assertThat(initialState.isInWidgetMode).isTrue() assertThat(initialState.isInWidgetMode).isTrue()
assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1) assertThat(widgetDriver.runCalledCount).isEqualTo(1)
// Called several times because of the recomposition analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall))
analyticsLambda.assertions().isCalledExactly(2)
.withSequence(
listOf(value(MobileScreen.ScreenName.RoomCall)),
listOf(value(MobileScreen.ScreenName.RoomCall))
)
sendCallNotificationIfNeededLambda.assertions().isCalledOnce() sendCallNotificationIfNeededLambda.assertions().isCalledOnce()
} }
} }

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2024 New Vector Ltd. * Copyright 2024 New Vector Ltd.
* *
@ -6,13 +8,14 @@
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-library")
alias(libs.plugins.anvil)
} }
android { android {
namespace = "io.element.android.features.enterprise.impl" namespace = "io.element.android.features.enterprise.impl"
} }
setupAnvil()
dependencies { dependencies {
implementation(projects.anvilannotations) implementation(projects.anvilannotations)
api(projects.features.enterprise.api) api(projects.features.enterprise.api)

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2024 New Vector Ltd. * Copyright 2024 New Vector Ltd.
* *
@ -7,7 +9,6 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
} }
@ -15,6 +16,8 @@ android {
namespace = "io.element.android.features.ftue.test" namespace = "io.element.android.features.ftue.test"
} }
setupAnvil()
dependencies { dependencies {
implementation(projects.features.ftue.api) implementation(projects.features.ftue.api)
implementation(projects.tests.testutils) implementation(projects.tests.testutils)

View file

@ -99,7 +99,7 @@ class AcceptDeclineInvitePresenterTest {
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
) )
} }
skipItems(1) skipItems(2)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink( state.eventSink(
@ -147,7 +147,7 @@ class AcceptDeclineInvitePresenterTest {
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
) )
} }
skipItems(1) skipItems(2)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java) assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
} }

View file

@ -33,6 +33,8 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.getRoomInfoFlow
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
@ -70,7 +72,7 @@ class JoinRoomPresenter @AssistedInject constructor(
override fun present(): JoinRoomState { override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var retryCount by remember { mutableIntStateOf(0) } var retryCount by remember { mutableIntStateOf(0) }
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty())
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val contentState by produceState<ContentState>( val contentState by produceState<ContentState>(
@ -204,7 +206,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
name = name, name = name,
topic = topic, topic = topic,
alias = canonicalAlias, alias = canonicalAlias,
numberOfMembers = activeMembersCount, numberOfMembers = activeMembersCount.toLong(),
isDm = isDm, isDm = isDm,
roomType = if (isSpace) RoomType.Space else RoomType.Room, roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,

View file

@ -19,7 +19,6 @@ import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.RoomType import io.element.android.libraries.matrix.api.room.RoomType
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.ui.model.InviteSender import io.element.android.libraries.matrix.ui.model.InviteSender
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> { open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {

View file

@ -32,8 +32,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -67,10 +67,10 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is joined then content state is filled with his data`() = runTest { fun `present - when room is joined then content state is filled with his data`() = runTest {
val roomInfo = aRoomInfo() val roomSummary = aRoomSummary()
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -81,22 +81,22 @@ class JoinRoomPresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
val contentState = state.contentState as ContentState.Loaded val contentState = state.contentState as ContentState.Loaded
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
assertThat(contentState.name).isEqualTo(roomInfo.name) assertThat(contentState.name).isEqualTo(roomSummary.info.name)
assertThat(contentState.topic).isEqualTo(roomInfo.topic) assertThat(contentState.topic).isEqualTo(roomSummary.info.topic)
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias) assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias)
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount) assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.activeMembersCount)
assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect) assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect)
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl)
} }
} }
} }
@Test @Test
fun `present - when room is invited then join authorization is equal to invited`() = runTest { fun `present - when room is invited then join authorization is equal to invited`() = runTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -114,13 +114,13 @@ class JoinRoomPresenterTest {
fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest { fun `present - when room is invited then join authorization is equal to invited, an inviter is provided`() = runTest {
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob") val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
val expectedInviteSender = inviter.toInviteSender() val expectedInviteSender = inviter.toInviteSender()
val roomInfo = aRoomInfo( val roomSummary = aRoomSummary(
currentUserMembership = CurrentUserMembership.INVITED, currentUserMembership = CurrentUserMembership.INVITED,
inviter = inviter, inviter = inviter,
) )
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -140,10 +140,10 @@ class JoinRoomPresenterTest {
val acceptDeclinePresenter = Presenter { val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder) anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
} }
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -224,10 +224,10 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -243,10 +243,10 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest { fun `present - when room is left and not public then join authorization is equal to unknown`() = runTest {
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false) val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.LEFT, isPublic = false)
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient().apply {
getRoomInfoFlowLambda = { _ -> getRoomSummaryFlowLambda = { _ ->
flowOf(Optional.of(roomInfo)) flowOf(Optional.of(roomSummary))
} }
} }
val presenter = createJoinRoomPresenter( val presenter = createJoinRoomPresenter(
@ -338,11 +338,15 @@ class JoinRoomPresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
state.eventSink(JoinRoomEvents.KnockRoom) state.eventSink(JoinRoomEvents.KnockRoom)
} }
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit)) assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
fakeKnockRoom.lambda = knockRoomFailure fakeKnockRoom.lambda = knockRoomFailure
state.eventSink(JoinRoomEvents.KnockRoom) state.eventSink(JoinRoomEvents.KnockRoom)
} }
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.knockAction).isInstanceOf(AsyncAction.Failure::class.java) assertThat(state.knockAction).isInstanceOf(AsyncAction.Failure::class.java)
} }

View file

@ -10,7 +10,6 @@ import extension.setupAnvil
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
id("kotlin-parcelize") id("kotlin-parcelize")
alias(libs.plugins.anvil)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
} }

View file

@ -51,7 +51,7 @@ class WebViewMessageInterceptor(
} }
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
request ?: return super.shouldOverrideUrlLoading(view, request) request ?: return false
// Load the URL in a Chrome Custom Tab, and return true to cancel the load // Load the URL in a Chrome Custom Tab, and return true to cancel the load
onOpenExternalUrl(request.url.toString()) onOpenExternalUrl(request.url.toString())
return true return true

View file

@ -30,7 +30,6 @@ import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
import io.element.android.libraries.permissions.api.PermissionsView import io.element.android.libraries.permissions.api.PermissionsView
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
@ -51,6 +50,7 @@ fun QrCodeIntroView(
onBackClick = onBackClick, onBackClick = onBackClick,
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()), iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
title = stringResource(id = R.string.screen_qr_code_login_initial_state_title, state.desktopAppName), title = stringResource(id = R.string.screen_qr_code_login_initial_state_title, state.desktopAppName),
subTitle = stringResource(id = R.string.screen_qr_code_login_initial_state_subtitle),
content = { Content(state = state) }, content = { Content(state = state) },
buttons = { Buttons(state = state) } buttons = { Buttons(state = state) }
) )
@ -87,7 +87,7 @@ private fun ColumnScope.Buttons(
state: QrCodeIntroState, state: QrCodeIntroState,
) { ) {
Button( Button(
text = stringResource(id = CommonStrings.action_continue), text = stringResource(id = R.string.screen_qr_code_login_initial_state_button_title),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = { onClick = {
state.eventSink.invoke(QrCodeIntroEvents.Continue) state.eventSink.invoke(QrCodeIntroEvents.Continue)

View file

@ -60,6 +60,7 @@ Try signing in manually, or scan the QR code with another device."</string>
<string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string> <string name="screen_qr_code_login_initial_state_item_3">"Select %1$s"</string>
<string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string> <string name="screen_qr_code_login_initial_state_item_3_action">"“Link new device”"</string>
<string name="screen_qr_code_login_initial_state_item_4">"Scan the QR code with this device"</string> <string name="screen_qr_code_login_initial_state_item_4">"Scan the QR code with this device"</string>
<string name="screen_qr_code_login_initial_state_subtitle">"Only available if your account provider supports it."</string>
<string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string> <string name="screen_qr_code_login_initial_state_title">"Open %1$s on another device to get the QR code"</string>
<string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string> <string name="screen_qr_code_login_invalid_scan_state_description">"Use the QR code shown on the other device."</string>
<string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string> <string name="screen_qr_code_login_invalid_scan_state_retry_button">"Try again"</string>

View file

@ -11,7 +11,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.features.login.impl.R
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.clickOn
@ -61,12 +61,12 @@ class QrCodeIntroViewTest {
} }
@Test @Test
fun `on continue button clicked - emits the Continue event`() { fun `on submit button clicked - emits the Continue event`() {
val eventRecorder = EventsRecorder<QrCodeIntroEvents>() val eventRecorder = EventsRecorder<QrCodeIntroEvents>()
rule.setQrCodeIntroView( rule.setQrCodeIntroView(
state = aQrCodeIntroState(eventSink = eventRecorder), state = aQrCodeIntroState(eventSink = eventRecorder),
) )
rule.clickOn(CommonStrings.action_continue) rule.clickOn(R.string.screen_qr_code_login_initial_state_button_title)
eventRecorder.assertSingle(QrCodeIntroEvents.Continue) eventRecorder.assertSingle(QrCodeIntroEvents.Continue)
} }

View file

@ -15,6 +15,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -31,7 +33,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -40,6 +41,7 @@ import javax.inject.Inject
class IdentityChangeStatePresenter @Inject constructor( class IdentityChangeStatePresenter @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val featureFlagService: FeatureFlagService,
) : Presenter<IdentityChangeState> { ) : Presenter<IdentityChangeState> {
@Composable @Composable
override fun present(): IdentityChangeState { override fun present(): IdentityChangeState {
@ -62,14 +64,18 @@ class IdentityChangeStatePresenter @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange() { private fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange() {
room.syncUpdateFlow featureFlagService.isFeatureEnabledFlow(FeatureFlags.IdentityPinningViolationNotifications)
.filter { it }
.flatMapLatest {
room.syncUpdateFlow
}
.filter { .filter {
// Room cannot become unencrypted, so we can just apply a filter here. // Room cannot become unencrypted, so we can just apply a filter here.
room.isEncrypted room.isEncrypted
} }
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { .flatMapLatest {
combine(room.identityStateChangesFlow, room.membersStateFlow,) { identityStateChanges, membersState -> combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState ->
identityStateChanges.map { identityStateChange -> identityStateChanges.map { identityStateChange ->
val member = membersState.roomMembers() val member = membersState.roomMembers()
?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId } ?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }

View file

@ -31,6 +31,7 @@ import im.vector.app.features.analytics.plan.Interaction
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
import io.element.android.features.messages.impl.draft.ComposerDraftService import io.element.android.features.messages.impl.draft.ComposerDraftService
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor import io.element.android.features.messages.impl.messagecomposer.suggestions.SuggestionsProcessor
import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.features.messages.impl.utils.TextPillificationHelper

View file

@ -5,20 +5,22 @@
* Please see LICENSE in the repository root for full details. * Please see LICENSE in the repository root for full details.
*/ */
package io.element.android.features.messages.impl.messagecomposer package io.element.android.features.messages.impl.messagecomposer.suggestions
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
data class RoomAliasSuggestion( data class RoomAliasSuggestion(
val roomAlias: RoomAlias, val roomAlias: RoomAlias,
val roomSummary: RoomSummary, val roomId: RoomId,
val roomName: String?,
val roomAvatarUrl: String?,
) )
interface RoomAliasSuggestionsDataSource { interface RoomAliasSuggestionsDataSource {
@ -32,14 +34,16 @@ class DefaultRoomAliasSuggestionsDataSource @Inject constructor(
override fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>> { override fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>> {
return roomListService return roomListService
.allRooms .allRooms
.filteredSummaries .summaries
.map { roomSummaries -> .map { roomSummaries ->
roomSummaries roomSummaries
.mapNotNull { roomSummary -> .mapNotNull { roomSummary ->
roomSummary.canonicalAlias?.let { roomAlias -> roomSummary.info.canonicalAlias?.let { roomAlias ->
RoomAliasSuggestion( RoomAliasSuggestion(
roomAlias = roomAlias, roomAlias = roomAlias,
roomSummary = roomSummary, roomId = roomSummary.roomId,
roomName = roomSummary.info.name,
roomAvatarUrl = roomSummary.info.avatarUrl,
) )
} }
} }

View file

@ -36,7 +36,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -60,7 +59,7 @@ fun SuggestionsPickerView(
when (suggestion) { when (suggestion) {
is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.AtRoom -> "@room"
is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value is ResolvedSuggestion.Member -> suggestion.roomMember.userId.value
is ResolvedSuggestion.Alias -> suggestion.roomSummary.roomId.value is ResolvedSuggestion.Alias -> suggestion.roomId.value
} }
} }
) { ) {
@ -96,12 +95,12 @@ private fun SuggestionItemView(
val avatarData = when (suggestion) { val avatarData = when (suggestion) {
is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize) is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, avatarSize)
is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize) is ResolvedSuggestion.Member -> suggestion.roomMember.getAvatarData(avatarSize)
is ResolvedSuggestion.Alias -> suggestion.roomSummary.getAvatarData(avatarSize) is ResolvedSuggestion.Alias -> suggestion.getAvatarData(avatarSize)
} }
val title = when (suggestion) { val title = when (suggestion) {
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title) is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
is ResolvedSuggestion.Alias -> suggestion.roomSummary.name is ResolvedSuggestion.Alias -> suggestion.roomName
} }
val subtitle = when (suggestion) { val subtitle = when (suggestion) {
is ResolvedSuggestion.AtRoom -> "@room" is ResolvedSuggestion.AtRoom -> "@room"
@ -152,11 +151,6 @@ internal fun SuggestionsPickerViewPreview() {
role = RoomMember.Role.USER, role = RoomMember.Role.USER,
) )
val anAlias = remember { RoomAlias("#room:domain.org") } val anAlias = remember { RoomAlias("#room:domain.org") }
val roomSummaryDetails = remember {
aRoomSummaryDetails(
name = "My room",
)
}
SuggestionsPickerView( SuggestionsPickerView(
roomId = RoomId("!room:matrix.org"), roomId = RoomId("!room:matrix.org"),
roomName = "Room", roomName = "Room",
@ -166,8 +160,10 @@ internal fun SuggestionsPickerViewPreview() {
ResolvedSuggestion.Member(roomMember), ResolvedSuggestion.Member(roomMember),
ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")), ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
ResolvedSuggestion.Alias( ResolvedSuggestion.Alias(
anAlias, roomAlias = anAlias,
roomSummaryDetails, roomId = RoomId("!room:matrix.org"),
roomName = "My room",
roomAvatarUrl = null,
) )
), ),
onSelectSuggestion = {} onSelectSuggestion = {}

View file

@ -7,7 +7,6 @@
package io.element.android.features.messages.impl.messagecomposer.suggestions package io.element.android.features.messages.impl.messagecomposer.suggestions
import io.element.android.features.messages.impl.messagecomposer.RoomAliasSuggestion
import io.element.android.libraries.core.data.filterUpTo import io.element.android.libraries.core.data.filterUpTo
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
@ -55,7 +54,14 @@ class SuggestionsProcessor @Inject constructor() {
SuggestionType.Room -> { SuggestionType.Room -> {
roomAliasSuggestions roomAliasSuggestions
.filter { it.roomAlias.value.contains(suggestion.text, ignoreCase = true) } .filter { it.roomAlias.value.contains(suggestion.text, ignoreCase = true) }
.map { ResolvedSuggestion.Alias(it.roomAlias, it.roomSummary) } .map {
ResolvedSuggestion.Alias(
roomAlias = it.roomAlias,
roomId = it.roomId,
roomName = it.roomName,
roomAvatarUrl = it.roomAvatarUrl,
)
}
} }
SuggestionType.Command, SuggestionType.Command,
is SuggestionType.Custom -> { is SuggestionType.Custom -> {

View file

@ -9,6 +9,9 @@ package io.element.android.features.messages.impl.crypto.identity
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
@ -65,6 +68,43 @@ class IdentityChangeStatePresenterTest {
} }
} }
@Test
fun `present - when the room emits identity change, but the feature is disabled, the presenter emits new state`() = runTest {
val room = FakeMatrixRoom(
isEncrypted = true,
)
val featureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.IdentityPinningViolationNotifications.key to false,
)
)
val presenter = createIdentityChangeStatePresenter(
room = room,
featureFlagService = featureFlagService,
)
presenter.test {
val initialState = awaitItem()
assertThat(initialState.roomMemberIdentityStateChanges).isEmpty()
room.emitIdentityStateChanges(
listOf(
IdentityStateChange(
userId = A_USER_ID_2,
identityState = IdentityState.PinViolation,
),
)
)
// No item emitted.
expectNoEvents()
// Enable the feature
featureFlagService.setFeatureEnabled(FeatureFlags.IdentityPinningViolationNotifications, true)
val finalItem = awaitItem()
assertThat(finalItem.roomMemberIdentityStateChanges).hasSize(1)
val value = finalItem.roomMemberIdentityStateChanges.first()
assertThat(value.identityRoomMember.userId).isEqualTo(A_USER_ID_2)
assertThat(value.identityState).isEqualTo(IdentityState.PinViolation)
}
}
@Test @Test
fun `present - when the clear room emits identity change, the presenter does not emits new state`() = runTest { fun `present - when the clear room emits identity change, the presenter does not emits new state`() = runTest {
val room = FakeMatrixRoom(isEncrypted = false) val room = FakeMatrixRoom(isEncrypted = false)
@ -147,10 +187,16 @@ class IdentityChangeStatePresenterTest {
private fun createIdentityChangeStatePresenter( private fun createIdentityChangeStatePresenter(
room: MatrixRoom = FakeMatrixRoom(), room: MatrixRoom = FakeMatrixRoom(),
encryptionService: EncryptionService = FakeEncryptionService(), encryptionService: EncryptionService = FakeEncryptionService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(
initialState = mapOf(
FeatureFlags.IdentityPinningViolationNotifications.key to true,
)
),
): IdentityChangeStatePresenter { ): IdentityChangeStatePresenter {
return IdentityChangeStatePresenter( return IdentityChangeStatePresenter(
room = room, room = room,
encryptionService = encryptionService, encryptionService = encryptionService,
featureFlagService = featureFlagService,
) )
} }
} }

View file

@ -9,6 +9,8 @@ package io.element.android.features.messages.impl.messagecomposer
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.messagecomposer.suggestions.DefaultRoomAliasSuggestionsDataSource
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestion
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
@ -38,7 +40,9 @@ class DefaultRoomAliasSuggestionsDataSourceTest {
listOf( listOf(
RoomAliasSuggestion( RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS, roomAlias = A_ROOM_ALIAS,
roomSummary = aRoomSummaryWithAnAlias roomId = aRoomSummaryWithAnAlias.roomId,
roomName = aRoomSummaryWithAnAlias.info.name,
roomAvatarUrl = aRoomSummaryWithAnAlias.info.avatarUrl
) )
) )
) )

View file

@ -7,6 +7,8 @@
package io.element.android.features.messages.impl.messagecomposer package io.element.android.features.messages.impl.messagecomposer
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestion
import io.element.android.features.messages.impl.messagecomposer.suggestions.RoomAliasSuggestionsDataSource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow

View file

@ -8,7 +8,6 @@
package io.element.android.features.messages.impl.messagecomposer.suggestions package io.element.android.features.messages.impl.messagecomposer.suggestions
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.messagecomposer.RoomAliasSuggestion
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
@ -110,13 +109,25 @@ class SuggestionsProcessorTest {
val result = suggestionsProcessor.process( val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("ALI"), suggestion = aRoomSuggestion("ALI"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(RoomAliasSuggestion(A_ROOM_ALIAS, aRoomSummary)), roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID, currentUserId = A_USER_ID,
canSendRoomMention = { true }, canSendRoomMention = { true },
) )
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
listOf( listOf(
ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary) ResolvedSuggestion.Alias(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
) )
) )
} }
@ -127,13 +138,25 @@ class SuggestionsProcessorTest {
val result = suggestionsProcessor.process( val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("ali"), suggestion = aRoomSuggestion("ali"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(RoomAliasSuggestion(A_ROOM_ALIAS, aRoomSummary)), roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID, currentUserId = A_USER_ID,
canSendRoomMention = { true }, canSendRoomMention = { true },
) )
assertThat(result).isEqualTo( assertThat(result).isEqualTo(
listOf( listOf(
ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary) ResolvedSuggestion.Alias(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
) )
) )
} }
@ -144,7 +167,14 @@ class SuggestionsProcessorTest {
val result = suggestionsProcessor.process( val result = suggestionsProcessor.process(
suggestion = aRoomSuggestion("tot"), suggestion = aRoomSuggestion("tot"),
roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()), roomMembersState = MatrixRoomMembersState.Ready(persistentListOf()),
roomAliasSuggestions = listOf(RoomAliasSuggestion(A_ROOM_ALIAS, aRoomSummary)), roomAliasSuggestions = listOf(
RoomAliasSuggestion(
roomAlias = A_ROOM_ALIAS,
roomId = aRoomSummary.roomId,
roomName = aRoomSummary.info.name,
roomAvatarUrl = aRoomSummary.info.avatarUrl,
)
),
currentUserId = A_USER_ID, currentUserId = A_USER_ID,
canSendRoomMention = { true }, canSendRoomMention = { true },
) )

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2024 New Vector Ltd. * Copyright 2024 New Vector Ltd.
* *
@ -7,13 +9,14 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
} }
android { android {
namespace = "io.element.android.features.migration.impl" namespace = "io.element.android.features.migration.impl"
} }
setupAnvil()
dependencies { dependencies {
implementation(projects.features.migration.api) implementation(projects.features.migration.api)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)

View file

@ -21,11 +21,12 @@ import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@ -40,7 +41,6 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
private val notificationSettingsService: NotificationSettingsService, private val notificationSettingsService: NotificationSettingsService,
@Assisted private val isOneToOne: Boolean, @Assisted private val isOneToOne: Boolean,
private val roomListService: RoomListService, private val roomListService: RoomListService,
private val matrixClient: MatrixClient,
) : Presenter<EditDefaultNotificationSettingState> { ) : Presenter<EditDefaultNotificationSettingState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -57,8 +57,8 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val roomsWithUserDefinedMode: MutableState<List<RoomSummary>> = remember { val roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>> = remember {
mutableStateOf(listOf()) mutableStateOf(emptyList())
} }
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
@ -106,31 +106,37 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
.launchIn(this) .launchIn(this)
} }
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary>>) { private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>) {
roomListService.allRooms roomListService.allRooms
.summaries .summaries
.onEach { .onEach { roomSummaries ->
updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode) updateRoomsWithUserDefinedMode(roomSummaries, roomsWithUserDefinedMode)
} }
.launchIn(this) .launchIn(this)
} }
private fun CoroutineScope.updateRoomsWithUserDefinedMode( private suspend fun updateRoomsWithUserDefinedMode(
summaries: List<RoomSummary>, summaries: List<RoomSummary>,
roomsWithUserDefinedMode: MutableState<List<RoomSummary>> roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>
) = launch { ) {
val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrThrow().toSet() val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet()
roomsWithUserDefinedMode.value = summaries
val sortedSummaries = summaries .filter { roomSummary ->
.filterIsInstance<RoomSummary>() roomWithUserDefinedRules.contains(roomSummary.roomId.value) && roomSummary.isOneToOne == isOneToOne
.filter { }
val room = matrixClient.getRoom(it.roomId) ?: return@filter false .map { roomSummary ->
roomWithUserDefinedRules.contains(it.roomId.value) && isOneToOne == room.isOneToOne EditNotificationSettingRoomInfo(
roomId = roomSummary.roomId,
name = roomSummary.info.name,
heroesAvatar = roomSummary.info.heroes.map { hero ->
hero.getAvatarData(AvatarSize.CustomRoomNotificationSetting)
}.toImmutableList(),
avatarData = roomSummary.info.getAvatarData(AvatarSize.CustomRoomNotificationSetting),
notificationMode = roomSummary.info.userDefinedNotificationMode,
)
} }
// locale sensitive sorting // locale sensitive sorting
.sortedWith(compareBy(Collator.getInstance()) { it.name }) .sortedWith(compareBy(Collator.getInstance()) { roomSummary -> roomSummary.name })
roomsWithUserDefinedMode.value = sortedSummaries
} }
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch { private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {

View file

@ -9,13 +9,12 @@ package io.element.android.features.preferences.impl.notifications.edit
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class EditDefaultNotificationSettingState( data class EditDefaultNotificationSettingState(
val isOneToOne: Boolean, val isOneToOne: Boolean,
val mode: RoomNotificationMode?, val mode: RoomNotificationMode?,
val roomsWithUserDefinedMode: ImmutableList<RoomSummary>, val roomsWithUserDefinedMode: ImmutableList<EditNotificationSettingRoomInfo>,
val changeNotificationSettingAction: AsyncAction<Unit>, val changeNotificationSettingAction: AsyncAction<Unit>,
val displayMentionsOnlyDisclaimer: Boolean, val displayMentionsOnlyDisclaimer: Boolean,
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,

View file

@ -9,9 +9,10 @@ package io.element.android.features.preferences.impl.notifications.edit
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> { open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
@ -33,21 +34,25 @@ private fun anEditDefaultNotificationSettingsState(
isOneToOne = isOneToOne, isOneToOne = isOneToOne,
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
roomsWithUserDefinedMode = persistentListOf( roomsWithUserDefinedMode = persistentListOf(
aRoomSummary("Room"), anEditNotificationSettingRoomInfo("Room"),
aRoomSummary(null), anEditNotificationSettingRoomInfo(null),
), ),
changeNotificationSettingAction = changeNotificationSettingAction, changeNotificationSettingAction = changeNotificationSettingAction,
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer, displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
eventSink = {} eventSink = {}
) )
private fun aRoomSummary( private fun anEditNotificationSettingRoomInfo(
name: String?, name: String?,
) = aRoomSummaryDetails( ) = EditNotificationSettingRoomInfo(
roomId = RoomId("!roomId:domain"), roomId = RoomId("!roomId:domain"),
name = name, name = name,
avatarUrl = null, avatarData = AvatarData(
isDirect = false, id = "!roomId:domain",
lastMessage = null, name = name,
url = null,
size = AvatarSize.CustomRoomNotificationSetting,
),
heroesAvatar = persistentListOf(),
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY, notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
) )

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar import io.element.android.libraries.designsystem.components.avatar.CompositeAvatar
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
@ -27,9 +26,7 @@ import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toPersistentList
/** /**
* A view that allows a user to edit the default notification setting for rooms. This can be set separately * A view that allows a user to edit the default notification setting for rooms. This can be set separately
@ -80,7 +77,7 @@ fun EditDefaultNotificationSettingView(
if (state.roomsWithUserDefinedMode.isNotEmpty()) { if (state.roomsWithUserDefinedMode.isNotEmpty()) {
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) { PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
state.roomsWithUserDefinedMode.forEach { summary -> state.roomsWithUserDefinedMode.forEach { summary ->
val subtitle = when (summary.userDefinedNotificationMode) { val subtitle = when (summary.notificationMode) {
RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages) RoomNotificationMode.ALL_MESSAGES -> stringResource(id = R.string.screen_notification_settings_edit_mode_all_messages)
RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> { RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY -> {
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords) stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
@ -101,10 +98,8 @@ fun EditDefaultNotificationSettingView(
}, },
leadingContent = ListItemContent.Custom { leadingContent = ListItemContent.Custom {
CompositeAvatar( CompositeAvatar(
avatarData = summary.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting), avatarData = summary.avatarData,
heroes = summary.heroes.map { user -> heroes = summary.heroesAvatar,
user.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting)
}.toPersistentList()
) )
}, },
onClick = { onClick = {

View file

@ -0,0 +1,21 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.preferences.impl.notifications.edit
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import kotlinx.collections.immutable.ImmutableList
data class EditNotificationSettingRoomInfo(
val roomId: RoomId,
val name: String?,
val heroesAvatar: ImmutableList<AvatarData>,
val avatarData: AvatarData,
val notificationMode: RoomNotificationMode?
)

View file

@ -14,11 +14,8 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
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.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitLastSequentialItem
@ -49,24 +46,20 @@ class EditDefaultNotificationSettingsPresenterTest {
@Test @Test
fun `present - ensure list of rooms with user defined mode`() = runTest { fun `present - ensure list of rooms with user defined mode`() = runTest {
val room = FakeMatrixRoom()
val notificationSettingsService = FakeNotificationSettingsService( val notificationSettingsService = FakeNotificationSettingsService(
initialRoomMode = RoomNotificationMode.ALL_MESSAGES, initialRoomMode = RoomNotificationMode.ALL_MESSAGES,
initialRoomModeIsDefault = false initialRoomModeIsDefault = false
) )
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService).apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService, matrixClient) val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
roomListService.postAllRooms(listOf(aRoomSummary(notificationMode = RoomNotificationMode.ALL_MESSAGES))) roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
val loadedState = consumeItemsUntilPredicate { state -> val loadedState = consumeItemsUntilPredicate { state ->
state.roomsWithUserDefinedMode.any { it.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES } state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
}.last() }.last()
assertThat(loadedState.roomsWithUserDefinedMode.any { it.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue() assertThat(loadedState.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }).isTrue()
} }
} }
@ -121,13 +114,11 @@ class EditDefaultNotificationSettingsPresenterTest {
private fun createEditDefaultNotificationSettingPresenter( private fun createEditDefaultNotificationSettingPresenter(
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(), notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
roomListService: FakeRoomListService = FakeRoomListService(), roomListService: FakeRoomListService = FakeRoomListService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
): EditDefaultNotificationSettingPresenter { ): EditDefaultNotificationSettingPresenter {
return EditDefaultNotificationSettingPresenter( return EditDefaultNotificationSettingPresenter(
notificationSettingsService = notificationSettingsService, notificationSettingsService = notificationSettingsService,
isOneToOne = false, isOneToOne = false,
roomListService = roomListService, roomListService = roomListService,
matrixClient = matrixClient
) )
} }
} }

View file

@ -9,7 +9,6 @@ import extension.setupAnvil
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
id("kotlin-parcelize") id("kotlin-parcelize")
} }

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
@ -24,34 +25,35 @@ class RoomListRoomSummaryFactory @Inject constructor(
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter,
) { ) {
fun create(details: RoomSummary): RoomListRoomSummary { fun create(roomSummary: RoomSummary): RoomListRoomSummary {
val avatarData = details.getAvatarData(size = AvatarSize.RoomListItem) val roomInfo = roomSummary.info
val avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomListItem)
return RoomListRoomSummary( return RoomListRoomSummary(
id = details.roomId.value, id = roomSummary.roomId.value,
roomId = details.roomId, roomId = roomSummary.roomId,
name = details.name, name = roomInfo.name,
numberOfUnreadMessages = details.numUnreadMessages, numberOfUnreadMessages = roomInfo.numUnreadMessages,
numberOfUnreadMentions = details.numUnreadMentions, numberOfUnreadMentions = roomInfo.numUnreadMentions,
numberOfUnreadNotifications = details.numUnreadNotifications, numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
isMarkedUnread = details.isMarkedUnread, isMarkedUnread = roomInfo.isMarkedUnread,
timestamp = lastMessageTimestampFormatter.format(details.lastMessageTimestamp), timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp),
lastMessage = details.lastMessage?.let { message -> lastMessage = roomSummary.lastMessage?.let { message ->
roomLastMessageFormatter.format(message.event, details.isDm) roomLastMessageFormatter.format(message.event, roomInfo.isDm)
}.orEmpty(), }.orEmpty(),
avatarData = avatarData, avatarData = avatarData,
userDefinedNotificationMode = details.userDefinedNotificationMode, userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = details.hasRoomCall, hasRoomCall = roomInfo.hasRoomCall,
isDirect = details.isDirect, isDirect = roomInfo.isDirect,
isFavorite = details.isFavorite, isFavorite = roomInfo.isFavorite,
inviteSender = details.inviter?.toInviteSender(), inviteSender = roomInfo.inviter?.toInviteSender(),
isDm = details.isDm, isDm = roomInfo.isDm,
canonicalAlias = details.canonicalAlias, canonicalAlias = roomInfo.canonicalAlias,
displayType = if (details.currentUserMembership == CurrentUserMembership.INVITED) { displayType = if (roomInfo.currentUserMembership == CurrentUserMembership.INVITED) {
RoomSummaryDisplayType.INVITE RoomSummaryDisplayType.INVITE
} else { } else {
RoomSummaryDisplayType.ROOM RoomSummaryDisplayType.ROOM
}, },
heroes = details.heroes.map { user -> heroes = roomInfo.heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomListItem) user.getAvatarData(size = AvatarSize.RoomListItem)
}.toImmutableList(), }.toImmutableList(),
) )

View file

@ -22,9 +22,9 @@ data class RoomListRoomSummary(
val roomId: RoomId, val roomId: RoomId,
val name: String?, val name: String?,
val canonicalAlias: RoomAlias?, val canonicalAlias: RoomAlias?,
val numberOfUnreadMessages: Int, val numberOfUnreadMessages: Long,
val numberOfUnreadMentions: Int, val numberOfUnreadMentions: Long,
val numberOfUnreadNotifications: Int, val numberOfUnreadNotifications: Long,
val isMarkedUnread: Boolean, val isMarkedUnread: Boolean,
val timestamp: String?, val timestamp: String?,
val lastMessage: CharSequence?, val lastMessage: CharSequence?,

View file

@ -119,9 +119,9 @@ internal fun anInviteSender(
internal fun aRoomListRoomSummary( internal fun aRoomListRoomSummary(
id: String = "!roomId:domain", id: String = "!roomId:domain",
name: String? = "Room name", name: String? = "Room name",
numberOfUnreadMessages: Int = 0, numberOfUnreadMessages: Long = 0,
numberOfUnreadMentions: Int = 0, numberOfUnreadMentions: Long = 0,
numberOfUnreadNotifications: Int = 0, numberOfUnreadNotifications: Long = 0,
isMarkedUnread: Boolean = false, isMarkedUnread: Boolean = false,
lastMessage: String? = "Last message", lastMessage: String? = "Last message",
timestamp: String? = lastMessage?.let { "88:88" }, timestamp: String? = lastMessage?.let { "88:88" },

View file

@ -396,7 +396,7 @@ class RoomListPresenterTest {
val notificationSettingsService = FakeNotificationSettingsService() val notificationSettingsService = FakeNotificationSettingsService()
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
roomListService.postAllRooms(listOf(aRoomSummary(notificationMode = userDefinedMode))) roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode)))
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomListService = roomListService, roomListService = roomListService,
notificationSettingsService = notificationSettingsService notificationSettingsService = notificationSettingsService

View file

@ -76,9 +76,9 @@ class RoomListRoomSummaryTest {
} }
internal fun createRoomListRoomSummary( internal fun createRoomListRoomSummary(
numberOfUnreadMentions: Int = 0, numberOfUnreadMentions: Long = 0,
numberOfUnreadMessages: Int = 0, numberOfUnreadMessages: Long = 0,
numberOfUnreadNotifications: Int = 0, numberOfUnreadNotifications: Long = 0,
isMarkedUnread: Boolean = false, isMarkedUnread: Boolean = false,
userDefinedNotificationMode: RoomNotificationMode? = null, userDefinedNotificationMode: RoomNotificationMode? = null,
isFavorite: Boolean = false, isFavorite: Boolean = false,

View file

@ -31,7 +31,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
@ -50,7 +49,7 @@ fun ResetIdentityRootView(
buttons = { buttons = {
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(id = CommonStrings.action_continue), text = stringResource(id = R.string.screen_encryption_reset_action_continue_reset),
onClick = { state.eventSink(ResetIdentityRootEvent.Continue) }, onClick = { state.eventSink(ResetIdentityRootEvent.Continue) },
destructive = true, destructive = true,
) )
@ -98,9 +97,9 @@ private fun Content() {
iconComposable = { iconComposable = {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Info(),
contentDescription = null, contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary, tint = ElementTheme.colors.iconSecondary,
) )
}, },
), ),
@ -109,9 +108,9 @@ private fun Content() {
iconComposable = { iconComposable = {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Info(),
contentDescription = null, contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary, tint = ElementTheme.colors.iconSecondary,
) )
}, },
), ),

View file

@ -18,7 +18,7 @@
<string name="screen_create_new_recovery_key_title">"Reset the encryption for your account using another device"</string> <string name="screen_create_new_recovery_key_title">"Reset the encryption for your account using another device"</string>
<string name="screen_encryption_reset_action_continue_reset">"Continue reset"</string> <string name="screen_encryption_reset_action_continue_reset">"Continue reset"</string>
<string name="screen_encryption_reset_bullet_1">"Your account details, contacts, preferences, and chat list will be kept"</string> <string name="screen_encryption_reset_bullet_1">"Your account details, contacts, preferences, and chat list will be kept"</string>
<string name="screen_encryption_reset_bullet_2">"You will lose your existing message history unless it is stored on another device"</string> <string name="screen_encryption_reset_bullet_2">"You will lose any message history thats stored only on the server"</string>
<string name="screen_encryption_reset_bullet_3">"You will need to verify all your existing devices and contacts again"</string> <string name="screen_encryption_reset_bullet_3">"You will need to verify all your existing devices and contacts again"</string>
<string name="screen_encryption_reset_footer">"Only reset your identity if you dont have access to another signed-in device and youve lost your recovery key."</string> <string name="screen_encryption_reset_footer">"Only reset your identity if you dont have access to another signed-in device and youve lost your recovery key."</string>
<string name="screen_encryption_reset_title">"Can\'t confirm? Youll need to reset your identity."</string> <string name="screen_encryption_reset_title">"Can\'t confirm? Youll need to reset your identity."</string>

View file

@ -60,7 +60,7 @@ class ResetIdentityRootViewTest {
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder),
) )
rule.clickOn(CommonStrings.action_continue) rule.clickOn(R.string.screen_encryption_reset_action_continue_reset)
eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue)
} }

View file

@ -23,6 +23,8 @@ android {
setupAnvil() setupAnvil()
dependencies { dependencies {
implementation(projects.appconfig)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)

View file

@ -18,9 +18,11 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.api.util.onSuccessLogout import io.element.android.features.logout.api.util.onSuccessLogout
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -36,6 +38,10 @@ class VerifySelfSessionNode @AssistedInject constructor(
showDeviceVerifiedScreen = inputs<VerifySessionEntryPoint.Params>().showDeviceVerifiedScreen, showDeviceVerifiedScreen = inputs<VerifySessionEntryPoint.Params>().showDeviceVerifiedScreen,
) )
private fun onLearnMoreClick(activity: Activity, dark: Boolean) {
activity.openUrlInChromeCustomTab(null, dark, LearnMoreConfig.ENCRYPTION_URL)
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
@ -44,6 +50,9 @@ class VerifySelfSessionNode @AssistedInject constructor(
VerifySelfSessionView( VerifySelfSessionView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onLearnMoreClick = {
onLearnMoreClick(activity, isDark)
},
onEnterRecoveryKey = callback::onEnterRecoveryKey, onEnterRecoveryKey = callback::onEnterRecoveryKey,
onResetKey = callback::onResetKey, onResetKey = callback::onResetKey,
onFinish = callback::onDone, onFinish = callback::onDone,

View file

@ -23,6 +23,8 @@ data class VerifySelfSessionState(
@Stable @Stable
sealed interface VerificationStep { sealed interface VerificationStep {
data object Loading : VerificationStep data object Loading : VerificationStep
// FIXME canEnterRecoveryKey value is never read.
data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : VerificationStep data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : VerificationStep
data object Canceled : VerificationStep data object Canceled : VerificationStep
data object AwaitingOtherDeviceResponse : VerificationStep data object AwaitingOtherDeviceResponse : VerificationStep

View file

@ -9,13 +9,13 @@ package io.element.android.features.verifysession.impl
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -64,6 +64,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver
@Composable @Composable
fun VerifySelfSessionView( fun VerifySelfSessionView(
state: VerifySelfSessionState, state: VerifySelfSessionState,
onLearnMoreClick: () -> Unit,
onEnterRecoveryKey: () -> Unit, onEnterRecoveryKey: () -> Unit,
onResetKey: () -> Unit, onResetKey: () -> Unit,
onFinish: () -> Unit, onFinish: () -> Unit,
@ -140,7 +141,10 @@ fun VerifySelfSessionView(
) )
} }
) { ) {
Content(flowState = verificationFlowStep) Content(
flowState = verificationFlowStep,
onLearnMoreClick = onLearnMoreClick,
)
} }
} }
@ -203,38 +207,68 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
} }
@Composable @Composable
private fun Content(flowState: FlowStep) { private fun Content(
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { flowState: FlowStep,
if (flowState is FlowStep.Verifying) { onLearnMoreClick: () -> Unit,
) {
when (flowState) {
is VerifySelfSessionState.VerificationStep.Initial -> {
ContentInitial(onLearnMoreClick)
}
is FlowStep.Verifying -> {
ContentVerifying(flowState) ContentVerifying(flowState)
} }
else -> Unit
}
}
@Composable
private fun ContentInitial(
onLearnMoreClick: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier
.clickable { onLearnMoreClick() }
.padding(vertical = 4.dp, horizontal = 16.dp),
text = stringResource(CommonStrings.action_learn_more),
style = ElementTheme.typography.fontBodyLgMedium
)
} }
} }
@Composable @Composable
private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) { private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) {
when (verificationFlowStep.data) { Box(
is SessionVerificationData.Decimals -> { modifier = Modifier.fillMaxSize(),
val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() } contentAlignment = Alignment.Center
Text( ) {
modifier = Modifier.fillMaxWidth(), when (verificationFlowStep.data) {
text = text, is SessionVerificationData.Decimals -> {
style = ElementTheme.typography.fontHeadingLgBold, val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() }
color = MaterialTheme.colorScheme.primary, Text(
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(),
) text = text,
} style = ElementTheme.typography.fontHeadingLgBold,
is SessionVerificationData.Emojis -> { color = MaterialTheme.colorScheme.primary,
// We want each row to have up to 4 emojis textAlign = TextAlign.Center,
val rows = verificationFlowStep.data.emojis.chunked(4) )
Column( }
modifier = Modifier.fillMaxWidth(), is SessionVerificationData.Emojis -> {
verticalArrangement = Arrangement.spacedBy(40.dp), // We want each row to have up to 4 emojis
) { val rows = verificationFlowStep.data.emojis.chunked(4)
rows.forEach { emojis -> Column(
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { modifier = Modifier.fillMaxWidth(),
for (emoji in emojis) { verticalArrangement = Arrangement.spacedBy(40.dp),
EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp)) ) {
rows.forEach { emojis ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
for (emoji in emojis) {
EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp))
}
} }
} }
} }
@ -292,14 +326,14 @@ private fun BottomMenu(
text = stringResource(R.string.screen_identity_use_another_device), text = stringResource(R.string.screen_identity_use_another_device),
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
) )
OutlinedButton( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_enter_recovery_key), text = stringResource(R.string.screen_session_verification_enter_recovery_key),
onClick = onEnterRecoveryKey, onClick = onEnterRecoveryKey,
) )
} }
// This option should always be displayed // This option should always be displayed
TextButton( OutlinedButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
onClick = onResetKey, onClick = onResetKey,
@ -402,6 +436,7 @@ private fun BottomMenu(
internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview { internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview {
VerifySelfSessionView( VerifySelfSessionView(
state = state, state = state,
onLearnMoreClick = {},
onEnterRecoveryKey = {}, onEnterRecoveryKey = {},
onResetKey = {}, onResetKey = {},
onFinish = {}, onFinish = {},

View file

@ -343,6 +343,7 @@ class VerifySelfSessionPresenterTest {
skipItems(1) skipItems(1)
val initialItem = awaitItem() val initialItem = awaitItem()
initialItem.eventSink(VerifySelfSessionViewEvents.SignOut) initialItem.eventSink(VerifySelfSessionViewEvents.SignOut)
assertThat(awaitItem().signOutAction.isLoading()).isTrue()
val finalItem = awaitItem() val finalItem = awaitItem()
assertThat(finalItem.signOutAction.isSuccess()).isTrue() assertThat(finalItem.signOutAction.isSuccess()).isTrue()
assertThat(finalItem.signOutAction.dataOrNull()).isEqualTo("aUrl") assertThat(finalItem.signOutAction.dataOrNull()).isEqualTo("aUrl")

View file

@ -146,6 +146,22 @@ class VerifySelfSessionViewTest {
} }
} }
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on learn more invokes the expected callback`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
eventSink = eventsRecorder
),
onLearnMoreClick = callback,
)
rule.clickOn(CommonStrings.action_learn_more)
}
}
@Test @Test
fun `clicking on they match emits the expected event`() { fun `clicking on they match emits the expected event`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
@ -222,6 +238,7 @@ class VerifySelfSessionViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView(
state: VerifySelfSessionState, state: VerifySelfSessionState,
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(),
onFinished: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(),
onResetKey: () -> Unit = EnsureNeverCalled(), onResetKey: () -> Unit = EnsureNeverCalled(),
@ -230,6 +247,7 @@ class VerifySelfSessionViewTest {
setContent { setContent {
VerifySelfSessionView( VerifySelfSessionView(
state = state, state = state,
onLearnMoreClick = onLearnMoreClick,
onEnterRecoveryKey = onEnterRecoveryKey, onEnterRecoveryKey = onEnterRecoveryKey,
onFinish = onFinished, onFinish = onFinished,
onResetKey = onResetKey, onResetKey = onResetKey,

View file

@ -4,9 +4,9 @@
[versions] [versions]
# Project # Project
android_gradle_plugin = "8.7.0" android_gradle_plugin = "8.7.0"
kotlin = "1.9.25" kotlin = "2.0.20"
kotlinpoetKsp = "1.18.1" kotlinpoetKsp = "1.18.1"
ksp = "1.9.25-1.0.20" ksp = "2.0.20-1.0.25"
firebaseAppDistribution = "5.0.0" firebaseAppDistribution = "5.0.0"
# AndroidX # AndroidX
@ -25,7 +25,7 @@ media3 = "1.4.1"
camera = "1.3.4" camera = "1.3.4"
# Compose # Compose
compose_bom = "2024.09.02" compose_bom = "2024.09.03"
composecompiler = "1.5.15" composecompiler = "1.5.15"
# Coroutines # Coroutines
@ -40,7 +40,7 @@ test_core = "1.6.1"
#other #other
coil = "2.7.0" coil = "2.7.0"
datetime = "0.6.0" datetime = "0.6.0"
dependencyAnalysis = "2.1.3" dependencyAnalysis = "2.1.4"
serialization_json = "1.6.3" serialization_json = "1.6.3"
showkase = "1.0.3" showkase = "1.0.3"
appyx = "1.4.0" appyx = "1.4.0"
@ -62,6 +62,7 @@ kover = "0.8.3"
[libraries] [libraries]
# Project # Project
android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" }
compose_compiler_plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
# https://developer.android.com/studio/write/java8-support#library-desugaring-versions # https://developer.android.com/studio/write/java8-support#library-desugaring-versions
android_desugar = "com.android.tools:desugar_jdk_libs:2.1.2" android_desugar = "com.android.tools:desugar_jdk_libs:2.1.2"
anvil_gradle_plugin = { module = "dev.zacsweers.anvil:gradle-plugin", version.ref = "anvil" } anvil_gradle_plugin = { module = "dev.zacsweers.anvil:gradle-plugin", version.ref = "anvil" }
@ -103,7 +104,7 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re
androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx_startup = "androidx.startup:startup-runtime:1.2.0" androidx_startup = "androidx.startup:startup-runtime:1.2.0"
androidx_preference = "androidx.preference:preference:1.2.1" androidx_preference = "androidx.preference:preference:1.2.1"
androidx_webkit = "androidx.webkit:webkit:1.12.0" androidx_webkit = "androidx.webkit:webkit:1.12.1"
androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" }
androidx_compose_material3 = { module = "androidx.compose.material3:material3" } androidx_compose_material3 = { module = "androidx.compose.material3:material3" }
@ -143,7 +144,7 @@ test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" }
test_arch_core = "androidx.arch.core:core-testing:2.2.0" test_arch_core = "androidx.arch.core:core-testing:2.2.0"
test_junit = "junit:junit:4.13.2" test_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.6.2" test_runner = "androidx.test:runner:1.6.2"
test_mockk = "io.mockk:mockk:1.13.12" test_mockk = "io.mockk:mockk:1.13.13"
test_konsist = "com.lemonappdev:konsist:0.16.1" test_konsist = "com.lemonappdev:konsist:0.16.1"
test_turbine = "app.cash.turbine:turbine:1.1.0" test_turbine = "app.cash.turbine:turbine:1.1.0"
test_truth = "com.google.truth:truth:1.4.4" test_truth = "com.google.truth:truth:1.4.4"
@ -189,13 +190,13 @@ kotlinpoet = "com.squareup:kotlinpoet:1.18.1"
zxing_cpp = "io.github.zxing-cpp:android:2.2.0" zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
# Analytics # Analytics
posthog = "com.posthog:posthog-android:3.8.0" posthog = "com.posthog:posthog-android:3.8.1"
sentry = "io.sentry:sentry-android:7.14.0" sentry = "io.sentry:sentry-android:7.14.0"
# main branch can be tested replacing the version with main-SNAPSHOT # main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.25.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.25.0"
# Emojibase # Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"
sigpwned_emoji4j = "com.sigpwned:emoji4j-core:15.1.2" sigpwned_emoji4j = "com.sigpwned:emoji4j-core:15.1.2"
# Di # Di
@ -238,3 +239,4 @@ firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" } knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
sonarqube = "org.sonarqube:5.1.0.4882" sonarqube = "org.sonarqube:5.1.0.4882"
licensee = "app.cash.licensee:1.11.0" licensee = "app.cash.licensee:1.11.0"
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.preview.PreviewGroup
@ -24,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
fun LinearProgressIndicator( fun LinearProgressIndicator(
progress: () -> Float, progress: () -> Float,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
gapSize: Dp = 0.dp,
color: Color = ProgressIndicatorDefaults.linearColor, color: Color = ProgressIndicatorDefaults.linearColor,
trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap, strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
@ -31,9 +33,11 @@ fun LinearProgressIndicator(
androidx.compose.material3.LinearProgressIndicator( androidx.compose.material3.LinearProgressIndicator(
modifier = modifier, modifier = modifier,
progress = progress, progress = progress,
gapSize = gapSize,
color = color, color = color,
trackColor = trackColor, trackColor = trackColor,
strokeCap = strokeCap, strokeCap = strokeCap,
drawStopIndicator = {},
) )
} }
@ -41,6 +45,7 @@ fun LinearProgressIndicator(
fun LinearProgressIndicator( fun LinearProgressIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
color: Color = ProgressIndicatorDefaults.linearColor, color: Color = ProgressIndicatorDefaults.linearColor,
gapSize: Dp = 0.dp,
trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap, strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
) { ) {
@ -49,14 +54,17 @@ fun LinearProgressIndicator(
androidx.compose.material3.LinearProgressIndicator( androidx.compose.material3.LinearProgressIndicator(
modifier = modifier, modifier = modifier,
progress = { 0.75F }, progress = { 0.75F },
gapSize = gapSize,
color = color, color = color,
trackColor = trackColor, trackColor = trackColor,
strokeCap = strokeCap, strokeCap = strokeCap,
drawStopIndicator = {},
) )
} else { } else {
androidx.compose.material3.LinearProgressIndicator( androidx.compose.material3.LinearProgressIndicator(
modifier = modifier, modifier = modifier,
color = color, color = color,
gapSize = gapSize,
trackColor = trackColor, trackColor = trackColor,
strokeCap = strokeCap, strokeCap = strokeCap,
) )

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.designsystem.theme.components.bottomsheet package io.element.android.libraries.designsystem.theme.components.bottomsheet
import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.SpringSpec import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.exponentialDecay import androidx.compose.animation.core.exponentialDecay
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@ -296,13 +295,9 @@ internal object AnchoredDraggableDefaults {
/** /**
* The default animation used by [AnchoredDraggableState]. * The default animation used by [AnchoredDraggableState].
*/ */
@get:ExperimentalMaterial3Api
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@ExperimentalMaterial3Api @ExperimentalMaterial3Api
val SnapAnimationSpec = SpringSpec<Float>() val SnapAnimationSpec = SpringSpec<Float>()
@get:ExperimentalMaterial3Api
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
@ExperimentalMaterial3Api @ExperimentalMaterial3Api
val DecayAnimationSpec = exponentialDecay<Float>() val DecayAnimationSpec = exponentialDecay<Float>()
} }

View file

@ -9,7 +9,6 @@ package io.element.android.libraries.featureflag.api
import io.element.android.appconfig.OnBoardingConfig import io.element.android.appconfig.OnBoardingConfig
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
/** /**
* To enable or disable a FeatureFlags, change the `defaultValue` value. * To enable or disable a FeatureFlags, change the `defaultValue` value.
@ -93,13 +92,7 @@ enum class FeatureFlags(
key = "feature.qrCodeLogin", key = "feature.qrCodeLogin",
title = "Enable login using QR code", title = "Enable login using QR code",
description = "Allow the user to login using the QR code flow", description = "Allow the user to login using the QR code flow",
defaultValue = { buildMeta -> defaultValue = { OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE },
when (buildMeta.buildType) {
// TODO remove once the feature is ready to publish
BuildType.RELEASE -> false
else -> OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE
}
},
isFinished = false, isFinished = false,
), ),
IncomingShare( IncomingShare(
@ -132,4 +125,17 @@ enum class FeatureFlags(
defaultValue = { false }, defaultValue = { false },
isFinished = false, isFinished = false,
), ),
IdentityPinningViolationNotifications(
key = "feature.identityPinningViolationNotifications",
title = "Identity pinning violation notifications",
description = null,
defaultValue = { buildMeta ->
when (buildMeta.buildType) {
// Do not enable this feature in release builds
BuildType.RELEASE -> false
else -> true
}
},
isFinished = false,
),
} }

View file

@ -38,6 +38,8 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.io.Closeable import java.io.Closeable
import java.util.Optional import java.util.Optional
@ -94,7 +96,13 @@ interface MatrixClient : Closeable {
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun roomMembershipObserver(): RoomMembershipObserver fun roomMembershipObserver(): RoomMembershipObserver
fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>>
/**
* Get a room summary flow for a given room ID or alias.
* The flow will emit a new value whenever the room summary is updated.
* The flow will emit Optional.empty item if the room is not found.
*/
fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<RoomSummary>>
fun isMe(userId: UserId?) = userId == sessionId fun isMe(userId: UserId?) = userId == sessionId
@ -142,3 +150,14 @@ interface MatrixClient : Closeable {
fun canDeactivateAccount(): Boolean fun canDeactivateAccount(): Boolean
suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit> suspend fun deactivateAccount(password: String, eraseData: Boolean): Result<Unit>
} }
/**
* Get a room info flow for a given room ID or alias.
* The flow will emit a new value whenever the room info is updated.
* The flow will emit Optional.empty item if the room is not found.
*/
fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<MatrixRoomInfo>> {
return getRoomSummaryFlow(roomIdOrAlias)
.map { roomSummary -> roomSummary.map { it.info } }
.distinctUntilChanged()
}

View file

@ -33,6 +33,13 @@ data class MatrixRoomInfo(
val canonicalAlias: RoomAlias?, val canonicalAlias: RoomAlias?,
val alternativeAliases: ImmutableList<RoomAlias>, val alternativeAliases: ImmutableList<RoomAlias>,
val currentUserMembership: CurrentUserMembership, val currentUserMembership: CurrentUserMembership,
/**
* Member who invited the current user to a room that's in the invited
* state.
*
* Can be missing if the room membership invite event is missing from the
* store.
*/
val inviter: RoomMember?, val inviter: RoomMember?,
val activeMembersCount: Long, val activeMembersCount: Long,
val invitedMembersCount: Long, val invitedMembersCount: Long,
@ -43,7 +50,26 @@ data class MatrixRoomInfo(
val userDefinedNotificationMode: RoomNotificationMode?, val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean, val hasRoomCall: Boolean,
val activeRoomCallParticipants: ImmutableList<UserId>, val activeRoomCallParticipants: ImmutableList<UserId>,
val isMarkedUnread: Boolean,
/**
* "Interesting" messages received in that room, independently of the
* notification settings.
*/
val numUnreadMessages: Long,
/**
* Events that will notify the user, according to their
* notification settings.
*/
val numUnreadNotifications: Long,
/**
* Events causing mentions/highlights for the user, according to their
* notification settings.
*/
val numUnreadMentions: Long,
val heroes: ImmutableList<MatrixUser>, val heroes: ImmutableList<MatrixUser>,
val pinnedEventIds: ImmutableList<EventId>, val pinnedEventIds: ImmutableList<EventId>,
val creator: UserId?, val creator: UserId?,
) ) {
val aliases: List<RoomAlias>
get() = listOfNotNull(canonicalAlias) + alternativeAliases
}

View file

@ -7,35 +7,14 @@
package io.element.android.libraries.matrix.api.roomlist package io.element.android.libraries.matrix.api.roomlist
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.user.MatrixUser
data class RoomSummary( data class RoomSummary(
val roomId: RoomId, val info: MatrixRoomInfo,
val name: String?,
val canonicalAlias: RoomAlias?,
val alternativeAliases: List<RoomAlias>,
val isDirect: Boolean,
val avatarUrl: String?,
val lastMessage: RoomMessage?, val lastMessage: RoomMessage?,
val numUnreadMessages: Int,
val numUnreadMentions: Int,
val numUnreadNotifications: Int,
val isMarkedUnread: Boolean,
val inviter: RoomMember?,
val userDefinedNotificationMode: RoomNotificationMode?,
val hasRoomCall: Boolean,
val isDm: Boolean,
val isFavorite: Boolean,
val currentUserMembership: CurrentUserMembership,
val heroes: List<MatrixUser>,
) { ) {
val roomId = info.id
val lastMessageTimestamp = lastMessage?.originServerTs val lastMessageTimestamp = lastMessage?.originServerTs
val aliases: List<RoomAlias> val isOneToOne get() = info.activeMembersCount == 2L
get() = listOfNotNull(canonicalAlias) + alternativeAliases
} }

View file

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.InvitedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -84,8 +83,8 @@ import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -105,8 +104,8 @@ import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.Optional import java.util.Optional
import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.INFINITE
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
@ -262,21 +261,14 @@ class RustMatrixClient(
* @param timeout the timeout to wait for the room to be available * @param timeout the timeout to wait for the room to be available
* @throws TimeoutCancellationException if the room is not available after the timeout * @throws TimeoutCancellationException if the room is not available after the timeout
*/ */
private suspend fun awaitJoinedRoom(roomIdOrAlias: RoomIdOrAlias, timeout: Duration): RoomSummary { private suspend fun awaitJoinedRoom(
val predicate: (List<RoomSummary>) -> Boolean = when (roomIdOrAlias) { roomIdOrAlias: RoomIdOrAlias,
is RoomIdOrAlias.Alias -> { roomSummaries: List<RoomSummary> -> timeout: Duration
val found = roomSummaries.find { it.aliases.contains(roomIdOrAlias.roomAlias) } ): RoomSummary {
found != null && found.currentUserMembership == CurrentUserMembership.JOINED
}
is RoomIdOrAlias.Id -> { roomSummaries: List<RoomSummary> ->
val found = roomSummaries.find { it.roomId == roomIdOrAlias.roomId }
found != null && found.currentUserMembership == CurrentUserMembership.JOINED
}
}
return withTimeout(timeout) { return withTimeout(timeout) {
roomListService.allRooms.summaries getRoomSummaryFlow(roomIdOrAlias)
.filter(predicate) .mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() }
.first() .filter { roomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.JOINED }
.first() .first()
// Ensure that the room is ready // Ensure that the room is ready
.also { client.awaitRoomRemoteEcho(it.roomId.value) } .also { client.awaitRoomRemoteEcho(it.roomId.value) }
@ -568,20 +560,21 @@ class RustMatrixClient(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
override fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>> { override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<RoomSummary>> {
return flow { val predicate: (RoomSummary) -> Boolean = when (roomIdOrAlias) {
var room = getRoom(roomId) is RoomIdOrAlias.Alias -> { roomSummary ->
if (room == null) { roomSummary.info.aliases.contains(roomIdOrAlias.roomAlias)
emit(Optional.empty())
awaitJoinedRoom(roomId.toRoomIdOrAlias(), INFINITE)
room = getRoom(roomId)
} }
room?.use { is RoomIdOrAlias.Id -> { roomSummary ->
room.roomInfoFlow roomSummary.roomId == roomIdOrAlias.roomId
.map { roomInfo -> Optional.of(roomInfo) }
.collect(this)
} }
}.distinctUntilChanged() }
return roomListService.allRooms.summaries
.map { roomSummaries ->
val roomSummary = roomSummaries.firstOrNull(predicate)
Optional.ofNullable(roomSummary)
}
.distinctUntilChanged()
} }
override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = withContext(sessionDispatcher) { override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = withContext(sessionDispatcher) {

View file

@ -53,6 +53,10 @@ class MatrixRoomInfoMapper {
activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(), activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(),
heroes = it.elementHeroes().toImmutableList(), heroes = it.elementHeroes().toImmutableList(),
pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(), pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(),
isMarkedUnread = it.isMarkedUnread,
numUnreadMessages = it.numUnreadMessages.toLong(),
numUnreadMentions = it.numUnreadMentions.toLong(),
numUnreadNotifications = it.numUnreadNotifications.toLong(),
) )
} }
} }

View file

@ -31,7 +31,7 @@ internal class RoomListFactory(
private val innerRoomListService: RoomListService, private val innerRoomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
) { ) {
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory() private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory()
/** /**
* Creates a room list that can be used to load more rooms and filter them dynamically. * Creates a room list that can be used to load more rooms and filter them dynamically.

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.roomlist package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
@ -17,19 +18,19 @@ val RoomListFilter.predicate
is RoomListFilter.Any -> { _: RoomSummary -> true } is RoomListFilter.Any -> { _: RoomSummary -> true }
RoomListFilter.None -> { _: RoomSummary -> false } RoomListFilter.None -> { _: RoomSummary -> false }
RoomListFilter.Category.Group -> { roomSummary: RoomSummary -> RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
!roomSummary.isDm && !roomSummary.isInvited() !roomSummary.info.isDm && !roomSummary.isInvited()
} }
RoomListFilter.Category.People -> { roomSummary: RoomSummary -> RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
roomSummary.isDm && !roomSummary.isInvited() roomSummary.info.isDm && !roomSummary.isInvited()
} }
RoomListFilter.Favorite -> { roomSummary: RoomSummary -> RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
roomSummary.isFavorite && !roomSummary.isInvited() roomSummary.info.isFavorite && !roomSummary.isInvited()
} }
RoomListFilter.Unread -> { roomSummary: RoomSummary -> RoomListFilter.Unread -> { roomSummary: RoomSummary ->
!roomSummary.isInvited() && (roomSummary.numUnreadNotifications > 0 || roomSummary.isMarkedUnread) !roomSummary.isInvited() && (roomSummary.info.numUnreadNotifications > 0 || roomSummary.info.isMarkedUnread)
} }
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary -> is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
roomSummary.name.orEmpty().contains(pattern, ignoreCase = true) roomSummary.info.name.orEmpty().contains(pattern, ignoreCase = true)
} }
RoomListFilter.Invite -> { roomSummary: RoomSummary -> RoomListFilter.Invite -> { roomSummary: RoomSummary ->
roomSummary.isInvited() roomSummary.isInvited()
@ -50,4 +51,4 @@ fun List<RoomSummary>.filter(filter: RoomListFilter): List<RoomSummary> {
} }
} }
private fun RoomSummary.isInvited() = currentUserMembership == CurrentUserMembership.INVITED private fun RoomSummary.isInvited() = info.currentUserMembership == CurrentUserMembership.INVITED

View file

@ -1,51 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.isDm
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
import io.element.android.libraries.matrix.impl.room.elementHeroes
import io.element.android.libraries.matrix.impl.room.map
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryDetailsFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo()
val latestRoomMessage = roomListItem.latestEvent().use { event ->
roomMessageFactory.create(event)
}
return RoomSummary(
roomId = RoomId(roomInfo.id),
name = roomInfo.displayName,
canonicalAlias = roomInfo.canonicalAlias?.let(::RoomAlias),
alternativeAliases = roomInfo.alternativeAliases.map(::RoomAlias),
isDirect = roomInfo.isDirect,
avatarUrl = roomInfo.avatarUrl,
numUnreadMentions = roomInfo.numUnreadMentions.toInt(),
numUnreadMessages = roomInfo.numUnreadMessages.toInt(),
numUnreadNotifications = roomInfo.numUnreadNotifications.toInt(),
isMarkedUnread = roomInfo.isMarkedUnread,
lastMessage = latestRoomMessage,
inviter = roomInfo.inviter?.let(RoomMemberMapper::map),
userDefinedNotificationMode = roomInfo.cachedUserDefinedNotificationMode?.let(RoomNotificationSettingsMapper::mapMode),
hasRoomCall = roomInfo.hasRoomCall,
isDm = isDm(isDirect = roomInfo.isDirect, activeMembersCount = roomInfo.activeMembersCount.toInt()),
isFavorite = roomInfo.isFavourite,
currentUserMembership = roomInfo.membership.map(),
heroes = roomInfo.elementHeroes(),
)
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.impl.room.MatrixRoomInfoMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.use
class RoomSummaryFactory(
private val roomMessageFactory: RoomMessageFactory = RoomMessageFactory(),
private val roomInfoMapper: MatrixRoomInfoMapper = MatrixRoomInfoMapper(),
) {
suspend fun create(roomListItem: RoomListItem): RoomSummary {
val roomInfo = roomListItem.roomInfo().let(roomInfoMapper::map)
val latestRoomMessage = roomListItem.latestEvent().use { event ->
roomMessageFactory.create(event)
}
return RoomSummary(
info = roomInfo,
lastMessage = latestRoomMessage,
)
}
}

View file

@ -23,9 +23,8 @@ class RoomSummaryListProcessor(
private val roomSummaries: MutableSharedFlow<List<RoomSummary>>, private val roomSummaries: MutableSharedFlow<List<RoomSummary>>,
private val roomListService: RoomListServiceInterface, private val roomListService: RoomListServiceInterface,
private val coroutineContext: CoroutineContext, private val coroutineContext: CoroutineContext,
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory(),
) { ) {
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
private val mutex = Mutex() private val mutex = Mutex()
suspend fun postUpdate(updates: List<RoomListEntriesUpdate>) { suspend fun postUpdate(updates: List<RoomListEntriesUpdate>) {
@ -40,7 +39,7 @@ class RoomSummaryListProcessor(
suspend fun rebuildRoomSummaries() { suspend fun rebuildRoomSummaries() {
updateRoomSummaries { updateRoomSummaries {
forEachIndexed { i, summary -> forEachIndexed { i, summary ->
val result = buildAndCacheRoomSummaryForIdentifier(summary.roomId.value) val result = buildRoomSummaryForIdentifier(summary.roomId.value)
if (result != null) { if (result != null) {
this[i] = result this[i] = result
} }
@ -97,23 +96,17 @@ class RoomSummaryListProcessor(
} }
private suspend fun buildSummaryForRoomListEntry(entry: RoomListItem): RoomSummary { private suspend fun buildSummaryForRoomListEntry(entry: RoomListItem): RoomSummary {
return buildAndCacheRoomSummaryForRoomListItem(entry) return buildRoomSummaryForRoomListItem(entry)
} }
private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary? { private suspend fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary? {
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem -> return roomListService.roomOrNull(identifier)?.use { roomListItem ->
buildAndCacheRoomSummaryForRoomListItem(roomListItem) buildRoomSummaryForRoomListItem(roomListItem)
} }
if (builtRoomSummary == null) {
roomSummariesByIdentifier.remove(identifier)
}
return builtRoomSummary
} }
private suspend fun buildAndCacheRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary { private suspend fun buildRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary {
val builtRoomSummary = roomSummaryDetailsFactory.create(roomListItem = roomListItem) return roomSummaryDetailsFactory.create(roomListItem = roomListItem)
roomSummariesByIdentifier[builtRoomSummary.roomId.value] = builtRoomSummary
return builtRoomSummary
} }
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) { private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {

View file

@ -105,6 +105,10 @@ class MatrixRoomInfoMapperTest {
).toImmutableList(), ).toImmutableList(),
pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(), pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(),
creator = A_USER_ID, creator = A_USER_ID,
isMarkedUnread = false,
numUnreadMessages = 12L,
numUnreadNotifications = 13L,
numUnreadMentions = 14L,
) )
) )
} }
@ -174,6 +178,10 @@ class MatrixRoomInfoMapperTest {
heroes = emptyList<MatrixUser>().toImmutableList(), heroes = emptyList<MatrixUser>().toImmutableList(),
pinnedEventIds = emptyList<EventId>().toPersistentList(), pinnedEventIds = emptyList<EventId>().toPersistentList(),
creator = null, creator = null,
isMarkedUnread = true,
numUnreadMessages = 12L,
numUnreadNotifications = 13L,
numUnreadMentions = 14L,
) )
) )
} }

View file

@ -19,7 +19,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.A_SERVER_LIST
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
@ -29,7 +29,7 @@ import org.junit.Test
class DefaultJoinRoomTest { class DefaultJoinRoomTest {
@Test @Test
fun `when using roomId and there is no server names, the classic join room API is used`() = runTest { fun `when using roomId and there is no server names, the classic join room API is used`() = runTest {
val roomSummary = aRoomSummaryFilled() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom()
@ -64,7 +64,7 @@ class DefaultJoinRoomTest {
@Test @Test
fun `when using roomId and server names are available, joinRoomByIdOrAlias API is used`() = runTest { fun `when using roomId and server names are available, joinRoomByIdOrAlias API is used`() = runTest {
val roomSummary = aRoomSummaryFilled() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom()
@ -100,7 +100,7 @@ class DefaultJoinRoomTest {
@Test @Test
fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest { fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest {
val roomSummary = aRoomSummaryFilled() val roomSummary = aRoomSummary()
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) } val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
val roomResult = FakeMatrixRoom() val roomResult = FakeMatrixRoom()

View file

@ -16,10 +16,11 @@ import org.junit.Test
class RoomListFilterTest { class RoomListFilterTest {
private val regularRoom = aRoomSummary( private val regularRoom = aRoomSummary(
isDm = false isDirect = false,
) )
private val dmRoom = aRoomSummary( private val dmRoom = aRoomSummary(
isDm = true isDirect = true,
activeMembersCount = 2
) )
private val favoriteRoom = aRoomSummary( private val favoriteRoom = aRoomSummary(
isFavorite = true isFavorite = true

View file

@ -15,7 +15,6 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID_3 import io.element.android.libraries.matrix.test.A_ROOM_ID_3
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -40,7 +39,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PushBack adds a new entry at the end of the list`() = runTest { fun `PushBack adds a new entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2)))) processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
@ -50,7 +49,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PushFront inserts a new entry at the start of the list`() = runTest { fun `PushFront inserts a new entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2)))) processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
@ -60,7 +59,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Set replaces an entry at some index`() = runTest { fun `Set replaces an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -72,7 +71,7 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Insert inserts a new entry at the provided index`() = runTest { fun `Insert inserts a new entry at the provided index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled()) summaries.value = listOf(aRoomSummary())
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -84,7 +83,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Remove removes an entry at some index`() = runTest { fun `Remove removes an entry at some index`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -96,7 +98,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PopBack removes an entry at the end of the list`() = runTest { fun `PopBack removes an entry at the end of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -108,7 +113,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `PopFront removes an entry at the start of the list`() = runTest { fun `PopFront removes an entry at the start of the list`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -120,7 +128,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Clear removes all the entries`() = runTest { fun `Clear removes all the entries`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear)) processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
@ -130,7 +141,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Truncate removes all entries after the provided length`() = runTest { fun `Truncate removes all entries after the provided length`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -142,7 +156,10 @@ class RoomSummaryListProcessorTest {
@Test @Test
fun `Reset removes all entries and add the provided ones`() = runTest { fun `Reset removes all entries and add the provided ones`() = runTest {
summaries.value = listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(A_ROOM_ID_2)) summaries.value = listOf(
aRoomSummary(roomId = A_ROOM_ID),
aRoomSummary(A_ROOM_ID_2)
)
val processor = createProcessor() val processor = createProcessor()
val index = 0 val index = 0
@ -156,6 +173,6 @@ class RoomSummaryListProcessorTest {
summaries, summaries,
FakeRustRoomListService(), FakeRustRoomListService(),
coroutineContext = StandardTestDispatcher(testScheduler), coroutineContext = StandardTestDispatcher(testScheduler),
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(), roomSummaryDetailsFactory = RoomSummaryFactory(),
) )
} }

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.InvitedRoom import io.element.android.libraries.matrix.api.room.InvitedRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -118,8 +117,8 @@ class FakeMatrixClient(
var knockRoomLambda: (RoomId) -> Result<Unit> = { var knockRoomLambda: (RoomId) -> Result<Unit> = {
Result.success(Unit) Result.success(Unit)
} }
var getRoomInfoFlowLambda = { _: RoomId -> var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty()) flowOf<Optional<RoomSummary>>(Optional.empty())
} }
var logoutLambda: (Boolean, Boolean) -> String? = { _, _ -> var logoutLambda: (Boolean, Boolean) -> String? = { _, _ ->
null null
@ -316,7 +315,7 @@ class FakeMatrixClient(
return Result.success(visitedRoomsId) return Result.success(visitedRoomsId)
} }
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomSummaryFlowLambda(roomIdOrAlias)
var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean -> var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean ->
// no-op // no-op

View file

@ -24,7 +24,6 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
@ -32,7 +31,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState import io.element.android.libraries.matrix.api.room.MatrixRoomNotificationSettingsState
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.location.AssetType
@ -40,21 +38,15 @@ import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerL
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
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.user.MatrixUser
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_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
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.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -527,62 +519,6 @@ class FakeMatrixRoom(
} }
} }
fun aRoomInfo(
id: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
rawName: String? = name,
topic: String? = "A topic",
avatarUrl: String? = AN_AVATAR_URL,
isDirect: Boolean = false,
isPublic: Boolean = true,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavorite: Boolean = false,
canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
invitedMembersCount: Long = 0,
joinedMembersCount: Long = 1,
highlightCount: Long = 0,
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null,
) = MatrixRoomInfo(
id = id,
name = name,
rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases.toImmutableList(),
currentUserMembership = currentUserMembership,
inviter = inviter,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
highlightCount = highlightCount,
notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
userPowerLevels = userPowerLevels,
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
heroes = heroes.toImmutableList(),
pinnedEventIds = pinnedEventIds.toImmutableList(),
creator = roomCreator,
)
fun defaultRoomPowerLevels() = MatrixRoomPowerLevels( fun defaultRoomPowerLevels() = MatrixRoomPowerLevels(
ban = 50, ban = 50,
invite = 0, invite = 0,

View file

@ -0,0 +1,90 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_ROOM_RAW_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
fun aRoomInfo(
id: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
rawName: String? = A_ROOM_RAW_NAME,
topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = AN_AVATAR_URL,
isDirect: Boolean = false,
isPublic: Boolean = true,
isSpace: Boolean = false,
isTombstoned: Boolean = false,
isFavorite: Boolean = false,
canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(),
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
invitedMembersCount: Long = 0,
joinedMembersCount: Long = 1,
highlightCount: Long = 0,
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null,
isMarkedUnread: Boolean = false,
numUnreadMessages: Long = 0,
numUnreadNotifications: Long = 0,
numUnreadMentions: Long = 0,
) = MatrixRoomInfo(
id = id,
name = name,
rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases.toImmutableList(),
currentUserMembership = currentUserMembership,
inviter = inviter,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
highlightCount = highlightCount,
notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
userPowerLevels = userPowerLevels,
activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
heroes = heroes.toImmutableList(),
pinnedEventIds = pinnedEventIds.toImmutableList(),
creator = roomCreator,
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions,
)

View file

@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.room.message.RoomMessage
@ -21,69 +22,88 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
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_ROOM_RAW_NAME
import io.element.android.libraries.matrix.test.A_ROOM_TOPIC
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentList
fun aRoomSummaryFilled( fun aRoomSummary(
roomId: RoomId = A_ROOM_ID, info: MatrixRoomInfo = aRoomInfo(),
name: String = A_ROOM_NAME,
isDirect: Boolean = false,
avatarUrl: String? = null,
lastMessage: RoomMessage? = aRoomMessage(), lastMessage: RoomMessage? = aRoomMessage(),
numUnreadMentions: Int = 0, ) = RoomSummary(
numUnreadMessages: Int = 0, info = info,
notificationMode: RoomNotificationMode? = null,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
) = aRoomSummary(
roomId = roomId,
name = name,
isDirect = isDirect,
avatarUrl = avatarUrl,
lastMessage = lastMessage, lastMessage = lastMessage,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
notificationMode = notificationMode,
currentUserMembership = currentUserMembership,
) )
fun aRoomSummary( fun aRoomSummary(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME, name: String? = A_ROOM_NAME,
isDirect: Boolean = false, rawName: String? = A_ROOM_RAW_NAME,
topic: String? = A_ROOM_TOPIC,
avatarUrl: String? = null, avatarUrl: String? = null,
lastMessage: RoomMessage? = aRoomMessage(), isDirect: Boolean = false,
numUnreadMentions: Int = 0, isPublic: Boolean = true,
numUnreadMessages: Int = 0, isSpace: Boolean = false,
numUnreadNotifications: Int = 0, isTombstoned: Boolean = false,
isMarkedUnread: Boolean = false, isFavorite: Boolean = false,
notificationMode: RoomNotificationMode? = null,
inviter: RoomMember? = null,
canonicalAlias: RoomAlias? = null, canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(), alternativeAliases: List<RoomAlias> = emptyList(),
hasRoomCall: Boolean = false,
isDm: Boolean = false,
isFavorite: Boolean = false,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED, currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
inviter: RoomMember? = null,
activeMembersCount: Long = 1,
invitedMembersCount: Long = 0,
joinedMembersCount: Long = 1,
highlightCount: Long = 0,
notificationCount: Long = 0,
userDefinedNotificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
userPowerLevels: ImmutableMap<UserId, Long> = persistentMapOf(),
activeRoomCallParticipants: List<UserId> = emptyList(),
heroes: List<MatrixUser> = emptyList(), heroes: List<MatrixUser> = emptyList(),
pinnedEventIds: List<EventId> = emptyList(),
roomCreator: UserId? = null,
isMarkedUnread: Boolean = false,
numUnreadMessages: Long = 0,
numUnreadNotifications: Long = 0,
numUnreadMentions: Long = 0,
lastMessage: RoomMessage? = aRoomMessage(),
) = RoomSummary( ) = RoomSummary(
roomId = roomId, info = MatrixRoomInfo(
name = name, id = roomId,
isDirect = isDirect, name = name,
avatarUrl = avatarUrl, rawName = rawName,
topic = topic,
avatarUrl = avatarUrl,
isDirect = isDirect,
isPublic = isPublic,
isSpace = isSpace,
isTombstoned = isTombstoned,
isFavorite = isFavorite,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases.toPersistentList(),
currentUserMembership = currentUserMembership,
inviter = inviter,
activeMembersCount = activeMembersCount,
invitedMembersCount = invitedMembersCount,
joinedMembersCount = joinedMembersCount,
userPowerLevels = userPowerLevels,
highlightCount = highlightCount,
notificationCount = notificationCount,
userDefinedNotificationMode = userDefinedNotificationMode,
hasRoomCall = hasRoomCall,
activeRoomCallParticipants = activeRoomCallParticipants.toPersistentList(),
heroes = heroes.toPersistentList(),
pinnedEventIds = pinnedEventIds.toPersistentList(),
creator = roomCreator,
isMarkedUnread = isMarkedUnread,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
numUnreadMentions = numUnreadMentions,
),
lastMessage = lastMessage, lastMessage = lastMessage,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
userDefinedNotificationMode = notificationMode,
inviter = inviter,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases,
hasRoomCall = hasRoomCall,
isDm = isDm,
isFavorite = isFavorite,
currentUserMembership = currentUserMembership,
heroes = heroes,
) )
fun aRoomMessage( fun aRoomMessage(

View file

@ -1,66 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.user.MatrixUser
open class RoomSummaryDetailsProvider : PreviewParameterProvider<RoomSummary> {
override val values: Sequence<RoomSummary>
get() = sequenceOf(
aRoomSummaryDetails(),
aRoomSummaryDetails(name = null),
)
}
fun aRoomSummaryDetails(
roomId: RoomId = RoomId("!room:domain"),
name: String? = "roomName",
canonicalAlias: RoomAlias? = null,
alternativeAliases: List<RoomAlias> = emptyList(),
isDirect: Boolean = true,
avatarUrl: String? = null,
lastMessage: RoomMessage? = null,
inviter: RoomMember? = null,
notificationMode: RoomNotificationMode? = null,
hasRoomCall: Boolean = false,
isDm: Boolean = false,
numUnreadMentions: Int = 0,
numUnreadMessages: Int = 0,
numUnreadNotifications: Int = 0,
isMarkedUnread: Boolean = false,
isFavorite: Boolean = false,
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
heroes: List<MatrixUser> = emptyList(),
) = RoomSummary(
roomId = roomId,
name = name,
canonicalAlias = canonicalAlias,
alternativeAliases = alternativeAliases,
isDirect = isDirect,
avatarUrl = avatarUrl,
lastMessage = lastMessage,
inviter = inviter,
userDefinedNotificationMode = notificationMode,
hasRoomCall = hasRoomCall,
isDm = isDm,
numUnreadMentions = numUnreadMentions,
numUnreadMessages = numUnreadMessages,
numUnreadNotifications = numUnreadNotifications,
isMarkedUnread = isMarkedUnread,
isFavorite = isFavorite,
currentUserMembership = currentUserMembership,
heroes = heroes,
)

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.components
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
class SelectRoomInfoProvider : PreviewParameterProvider<SelectRoomInfo> {
override val values: Sequence<SelectRoomInfo>
get() = sequenceOf(
aSelectRoomInfo(roomId = RoomId("!room1:domain")),
aSelectRoomInfo(roomId = RoomId("!room2:domain"), name = "Room with a name"),
aSelectRoomInfo(roomId = RoomId("!room3:domain"), name = "Room with a name and alias", canonicalAlias = RoomAlias("#alias:domain")),
)
}
fun aSelectRoomInfo(
roomId: RoomId,
name: String? = null,
canonicalAlias: RoomAlias? = null,
avatarUrl: String? = null,
heroes: ImmutableList<MatrixUser> = persistentListOf(),
) = SelectRoomInfo(
roomId = roomId,
name = name,
canonicalAlias = canonicalAlias,
avatarUrl = avatarUrl,
heroes = heroes,
)

View file

@ -34,15 +34,15 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun SelectedRoom( fun SelectedRoom(
roomSummary: RoomSummary, roomInfo: SelectRoomInfo,
onRemoveRoom: (RoomSummary) -> Unit, onRemoveRoom: (SelectRoomInfo) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box( Box(
@ -53,14 +53,12 @@ fun SelectedRoom(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
CompositeAvatar( CompositeAvatar(
avatarData = roomSummary.getAvatarData(size = AvatarSize.SelectedRoom), avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom),
heroes = roomSummary.heroes.map { user -> heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
user.getAvatarData(size = AvatarSize.SelectedRoom)
}.toImmutableList()
) )
Text( Text(
// If name is null, we do not have space to render "No room name", so just use `#` here. // If name is null, we do not have space to render "No room name", so just use `#` here.
text = roomSummary.name ?: "#", text = roomInfo.name ?: "#",
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
@ -69,14 +67,14 @@ fun SelectedRoom(
Surface( Surface(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(20.dp) .size(20.dp)
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.clickable( .clickable(
indication = ripple(), indication = ripple(),
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
onClick = { onRemoveRoom(roomSummary) } onClick = { onRemoveRoom(roomInfo) }
), ),
) { ) {
Icon( Icon(
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Close(),
@ -91,10 +89,10 @@ fun SelectedRoom(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun SelectedRoomPreview( internal fun SelectedRoomPreview(
@PreviewParameter(RoomSummaryDetailsProvider::class) roomSummary: RoomSummary @PreviewParameter(SelectRoomInfoProvider::class) roomInfo: SelectRoomInfo
) = ElementPreview { ) = ElementPreview {
SelectedRoom( SelectedRoom(
roomSummary = roomSummary, roomInfo = roomInfo,
onRemoveRoom = {}, onRemoveRoom = {},
) )
} }

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import android.content.Context
import coil.ImageLoader import coil.ImageLoader
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.request.Options import coil.request.Options
@ -15,7 +14,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
internal class AvatarDataFetcherFactory( internal class AvatarDataFetcherFactory(
private val context: Context,
private val client: MatrixClient private val client: MatrixClient
) : Fetcher.Factory<AvatarData> { ) : Fetcher.Factory<AvatarData> {
override fun create( override fun create(
@ -24,7 +22,6 @@ internal class AvatarDataFetcherFactory(
imageLoader: ImageLoader imageLoader: ImageLoader
): Fetcher { ): Fetcher {
return CoilMediaFetcher( return CoilMediaFetcher(
scalingFunction = { context.resources.displayMetrics.density * it },
mediaLoader = client.mediaLoader, mediaLoader = client.mediaLoader,
mediaData = data.toMediaRequestData(), mediaData = data.toMediaRequestData(),
options = options options = options

View file

@ -9,11 +9,25 @@ package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import kotlin.math.roundToLong
/**
* The size in pixel of the thumbnail to generate for the avatar.
* This is not the size of the avatar displayed in the UI but the size to get from the servers.
* Servers SHOULD produce thumbnails with the following dimensions and methods:
*
* 32x32, crop
* 96x96, crop
* 320x240, scale
* 640x480, scale
* 800x600, scale
*
* Let's always use the same size so coil caching works properly.
*/
const val AVATAR_THUMBNAIL_SIZE_IN_PIXEL = 240L
internal fun AvatarData.toMediaRequestData(): MediaRequestData { internal fun AvatarData.toMediaRequestData(): MediaRequestData {
return MediaRequestData( return MediaRequestData(
source = url?.let { MediaSource(it) }, source = url?.let { MediaSource(it) },
kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong()) kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
) )
} }

View file

@ -20,10 +20,8 @@ import okio.Buffer
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import timber.log.Timber import timber.log.Timber
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.math.roundToLong
internal class CoilMediaFetcher( internal class CoilMediaFetcher(
private val scalingFunction: (Float) -> Float,
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val mediaData: MediaRequestData, private val mediaData: MediaRequestData,
private val options: Options private val options: Options
@ -74,8 +72,8 @@ internal class CoilMediaFetcher(
private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? { private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? {
return mediaLoader.loadMediaThumbnail( return mediaLoader.loadMediaThumbnail(
source = mediaSource, source = mediaSource,
width = scalingFunction(kind.width.toFloat()).roundToLong(), width = kind.width,
height = scalingFunction(kind.height.toFloat()).roundToLong(), height = kind.height,
).map { byteArray -> ).map { byteArray ->
byteArray.asSourceResult(options) byteArray.asSourceResult(options)
}.onFailure { }.onFailure {

View file

@ -43,8 +43,8 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor(
} }
add(AvatarDataKeyer()) add(AvatarDataKeyer())
add(MediaRequestDataKeyer()) add(MediaRequestDataKeyer())
add(AvatarDataFetcherFactory(context, matrixClient)) add(AvatarDataFetcherFactory(matrixClient))
add(MediaRequestDataFetcherFactory(context, matrixClient)) add(MediaRequestDataFetcherFactory(matrixClient))
} }
.build() .build()
} }

View file

@ -7,14 +7,12 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import android.content.Context
import coil.ImageLoader import coil.ImageLoader
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.request.Options import coil.request.Options
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
internal class MediaRequestDataFetcherFactory( internal class MediaRequestDataFetcherFactory(
private val context: Context,
private val client: MatrixClient private val client: MatrixClient
) : Fetcher.Factory<MediaRequestData> { ) : Fetcher.Factory<MediaRequestData> {
override fun create( override fun create(
@ -23,7 +21,6 @@ internal class MediaRequestDataFetcherFactory(
imageLoader: ImageLoader imageLoader: ImageLoader
): Fetcher { ): Fetcher {
return CoilMediaFetcher( return CoilMediaFetcher(
scalingFunction = { context.resources.displayMetrics.density * it },
mediaLoader = client.mediaLoader, mediaLoader = client.mediaLoader,
mediaData = data, mediaData = data,
options = options options = options

View file

@ -9,10 +9,10 @@ package io.element.android.libraries.matrix.ui.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
fun RoomSummary.getAvatarData(size: AvatarSize) = AvatarData( fun MatrixRoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
id = roomId.value, id = id.value,
name = name, name = name,
url = avatarUrl, url = avatarUrl,
size = size, size = size,

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.ui.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
data class SelectRoomInfo(
val roomId: RoomId,
val name: String?,
val canonicalAlias: RoomAlias?,
val avatarUrl: String?,
val heroes: ImmutableList<MatrixUser>,
) {
fun getAvatarData(size: AvatarSize) = AvatarData(
id = roomId.value,
name = name,
url = avatarUrl,
size = size,
)
}
fun RoomSummary.toSelectRoomInfo() = SelectRoomInfo(
roomId = roomId,
name = info.name,
avatarUrl = info.avatarUrl,
heroes = info.heroes,
canonicalAlias = info.canonicalAlias,
)

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2023, 2024 New Vector Ltd.
* *
@ -7,9 +9,10 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
} }
setupAnvil()
android { android {
namespace = "io.element.android.libraries.mediapickers.api" namespace = "io.element.android.libraries.mediapickers.api"

View file

@ -1,3 +1,5 @@
import extension.setupAnvil
/* /*
* Copyright 2023, 2024 New Vector Ltd. * Copyright 2023, 2024 New Vector Ltd.
* *
@ -7,9 +9,10 @@
plugins { plugins {
id("io.element.android-compose-library") id("io.element.android-compose-library")
alias(libs.plugins.anvil)
} }
setupAnvil()
android { android {
namespace = "io.element.android.libraries.mediapickers.test" namespace = "io.element.android.libraries.mediapickers.test"

View file

@ -19,6 +19,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
@ -45,7 +46,7 @@ class DefaultNotificationBitmapLoader @Inject constructor(
private suspend fun loadRoomBitmap(path: String, imageLoader: ImageLoader): Bitmap? { private suspend fun loadRoomBitmap(path: String, imageLoader: ImageLoader): Bitmap? {
return try { return try {
val imageRequest = ImageRequest.Builder(context) val imageRequest = ImageRequest.Builder(context)
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)))
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.build() .build()
val result = imageLoader.execute(imageRequest) val result = imageLoader.execute(imageRequest)
@ -73,7 +74,7 @@ class DefaultNotificationBitmapLoader @Inject constructor(
private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? { private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? {
return try { return try {
val imageRequest = ImageRequest.Builder(context) val imageRequest = ImageRequest.Builder(context)
.data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)))
.transformations(CircleCropTransformation()) .transformations(CircleCropTransformation())
.build() .build()
val result = imageLoader.execute(imageRequest) val result = imageLoader.execute(imageRequest)

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.A_TIMESTAMP
import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AVATAR_THUMBNAIL_SIZE_IN_PIXEL
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator import io.element.android.libraries.push.impl.notifications.factories.createNotificationCreator
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
@ -84,7 +85,7 @@ class DefaultRoomGroupMessageCreatorTest {
expectedCoilRequests = listOf( expectedCoilRequests = listOf(
MediaRequestData( MediaRequestData(
source = MediaSource(url = A_ROOM_AVATAR), source = MediaSource(url = A_ROOM_AVATAR),
kind = MediaRequestData.Kind.Thumbnail(1024) kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
) )
) )
) )
@ -98,15 +99,15 @@ class DefaultRoomGroupMessageCreatorTest {
expectedCoilRequests = listOf( expectedCoilRequests = listOf(
MediaRequestData( MediaRequestData(
source = MediaSource(url = A_USER_AVATAR_1), source = MediaSource(url = A_USER_AVATAR_1),
kind = MediaRequestData.Kind.Thumbnail(1024) kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
), ),
MediaRequestData( MediaRequestData(
source = MediaSource(url = A_USER_AVATAR_2), source = MediaSource(url = A_USER_AVATAR_2),
kind = MediaRequestData.Kind.Thumbnail(1024) kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
), ),
MediaRequestData( MediaRequestData(
source = MediaSource(url = A_ROOM_AVATAR), source = MediaSource(url = A_ROOM_AVATAR),
kind = MediaRequestData.Kind.Thumbnail(1024) kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
), ),
) )
) )

View file

@ -10,7 +10,7 @@ package io.element.android.libraries.pushproviders.unifiedpush
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
class FakeUnifiedPushNewGatewayHandler( class FakeUnifiedPushNewGatewayHandler(
private val handleResult: suspend (String, String, String) -> Result<Unit> = { _, _, _ -> lambdaError() }, private val handleResult: (String, String, String) -> Result<Unit> = { _, _, _ -> lambdaError() },
) : UnifiedPushNewGatewayHandler { ) : UnifiedPushNewGatewayHandler {
override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> { override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> {
return handleResult(endpoint, pushGateway, clientSecret) return handleResult(endpoint, pushGateway, clientSecret)

View file

@ -7,10 +7,10 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
sealed interface RoomSelectEvents { sealed interface RoomSelectEvents {
data class SetSelectedRoom(val room: RoomSummary) : RoomSelectEvents data class SetSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvents
// TODO remove to restore multi-selection // TODO remove to restore multi-selection
data object RemoveSelectedRoom : RoomSelectEvents data object RemoveSelectedRoom : RoomSelectEvents

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.roomselect.impl
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -20,8 +21,9 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -36,7 +38,7 @@ class RoomSelectPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): RoomSelectState { override fun present(): RoomSelectState {
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummary>()) } var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
var searchQuery by remember { mutableStateOf("") } var searchQuery by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) } var isSearchActive by remember { mutableStateOf(false) }
@ -48,9 +50,9 @@ class RoomSelectPresenter @AssistedInject constructor(
dataSource.setSearchQuery(searchQuery) dataSource.setSearchQuery(searchQuery)
} }
val roomSummaryDetailsList by dataSource.roomSummaries.collectAsState(initial = persistentListOf()) val roomSummaryDetailsList by dataSource.roomInfoList.collectAsState(initial = persistentListOf())
val searchResults by remember { val searchResults by remember<State<SearchBarResultState<ImmutableList<SelectRoomInfo>>>> {
derivedStateOf { derivedStateOf {
when { when {
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList()) roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())

View file

@ -12,8 +12,9 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.roomlist.RoomList import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -38,11 +39,12 @@ class RoomSelectSearchDataSource @Inject constructor(
source = RoomList.Source.All, source = RoomList.Source.All,
) )
val roomSummaries: Flow<PersistentList<RoomSummary>> = roomList.filteredSummaries val roomInfoList: Flow<PersistentList<SelectRoomInfo>> = roomList.filteredSummaries
.map { roomSummaries -> .map { roomSummaries ->
roomSummaries roomSummaries
.filter { it.currentUserMembership == CurrentUserMembership.JOINED } .filter { it.info.currentUserMembership == CurrentUserMembership.JOINED }
.distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received .distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received
.map { roomSummary -> roomSummary.toSelectRoomInfo() }
.toPersistentList() .toPersistentList()
} }
.flowOn(coroutineDispatchers.computation) .flowOn(coroutineDispatchers.computation)

View file

@ -8,15 +8,15 @@
package io.element.android.libraries.roomselect.impl package io.element.android.libraries.roomselect.impl
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class RoomSelectState( data class RoomSelectState(
val mode: RoomSelectMode, val mode: RoomSelectMode,
val resultState: SearchBarResultState<ImmutableList<RoomSummary>>, val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
val query: String, val query: String,
val isSearchActive: Boolean, val isSearchActive: Boolean,
val selectedRooms: ImmutableList<RoomSummary>, val selectedRooms: ImmutableList<SelectRoomInfo>,
val eventSink: (RoomSelectEvents) -> Unit val eventSink: (RoomSelectEvents) -> Unit
) )

View file

@ -11,8 +11,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -32,7 +32,7 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
resultState = SearchBarResultState.Results(aRoomSelectRoomList()), resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
query = "Test", query = "Test",
isSearchActive = true, isSearchActive = true,
selectedRooms = persistentListOf(aRoomSummaryDetails(roomId = RoomId("!room2:domain"))) selectedRooms = aRoomSelectRoomList().subList(0, 1),
), ),
aRoomSelectState( aRoomSelectState(
mode = RoomSelectMode.Share, mode = RoomSelectMode.Share,
@ -43,10 +43,10 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
private fun aRoomSelectState( private fun aRoomSelectState(
mode: RoomSelectMode = RoomSelectMode.Forward, mode: RoomSelectMode = RoomSelectMode.Forward,
resultState: SearchBarResultState<ImmutableList<RoomSummary>> = SearchBarResultState.Initial(), resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
query: String = "", query: String = "",
isSearchActive: Boolean = false, isSearchActive: Boolean = false,
selectedRooms: ImmutableList<RoomSummary> = persistentListOf(), selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
) = RoomSelectState( ) = RoomSelectState(
mode = mode, mode = mode,
resultState = resultState, resultState = resultState,
@ -57,14 +57,16 @@ private fun aRoomSelectState(
) )
private fun aRoomSelectRoomList() = persistentListOf( private fun aRoomSelectRoomList() = persistentListOf(
aRoomSummaryDetails(), aSelectRoomInfo(
aRoomSummaryDetails( roomId = RoomId("!room1:domain"),
name = "Room with name",
),
aSelectRoomInfo(
roomId = RoomId("!room2:domain"), roomId = RoomId("!room2:domain"),
name = "Room with alias", name = "Room with alias",
canonicalAlias = RoomAlias("#alias:example.org"), canonicalAlias = RoomAlias("#alias:example.org"),
), ),
aRoomSummaryDetails( aSelectRoomInfo(
roomId = RoomId("!room3:domain"), roomId = RoomId("!room3:domain"),
name = null,
), ),
) )

View file

@ -47,8 +47,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.components.SelectedRoom import io.element.android.libraries.matrix.ui.components.SelectedRoom
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -65,13 +65,13 @@ fun RoomSelectView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun onRoomRemoved(roomSummary: RoomSummary) { fun onRoomRemoved(roomInfo: SelectRoomInfo) {
// TODO toggle selection when multi-selection is enabled // TODO toggle selection when multi-selection is enabled
state.eventSink(RoomSelectEvents.RemoveSelectedRoom) state.eventSink(RoomSelectEvents.RemoveSelectedRoom)
} }
@Composable @Composable
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<RoomSummary>) { fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<SelectRoomInfo>) {
if (isForwarding) return if (isForwarding) return
SelectedRooms( SelectedRooms(
selectedRooms = selectedRooms, selectedRooms = selectedRooms,
@ -185,8 +185,8 @@ fun RoomSelectView(
@Composable @Composable
private fun SelectedRooms( private fun SelectedRooms(
selectedRooms: ImmutableList<RoomSummary>, selectedRooms: ImmutableList<SelectRoomInfo>,
onRemoveRoom: (RoomSummary) -> Unit, onRemoveRoom: (SelectRoomInfo) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyRow( LazyRow(
@ -194,29 +194,29 @@ private fun SelectedRooms(
contentPadding = PaddingValues(horizontal = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(32.dp) horizontalArrangement = Arrangement.spacedBy(32.dp)
) { ) {
items(selectedRooms, key = { it.roomId.value }) { roomSummary -> items(selectedRooms, key = { it.roomId.value }) { selectRoomInfo ->
SelectedRoom(roomSummary = roomSummary, onRemoveRoom = onRemoveRoom) SelectedRoom(roomInfo = selectRoomInfo, onRemoveRoom = onRemoveRoom)
} }
} }
} }
@Composable @Composable
private fun RoomSummaryView( private fun RoomSummaryView(
summary: RoomSummary, roomInfo: SelectRoomInfo,
isSelected: Boolean, isSelected: Boolean,
onSelection: (RoomSummary) -> Unit, onSelection: (SelectRoomInfo) -> Unit,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.clickable { onSelection(summary) } .clickable { onSelection(roomInfo) }
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, end = 4.dp) .padding(start = 16.dp, end = 4.dp)
.heightIn(56.dp), .heightIn(56.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CompositeAvatar( CompositeAvatar(
avatarData = summary.getAvatarData(size = AvatarSize.RoomSelectRoomListItem), avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
heroes = summary.heroes.map { user -> heroes = roomInfo.heroes.map { user ->
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem) user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
}.toPersistentList() }.toPersistentList()
) )
@ -228,14 +228,14 @@ private fun RoomSummaryView(
// Name // Name
Text( Text(
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
text = summary.name ?: stringResource(id = CommonStrings.common_no_room_name), text = roomInfo.name ?: stringResource(id = CommonStrings.common_no_room_name),
fontStyle = FontStyle.Italic.takeIf { summary.name == null }, fontStyle = FontStyle.Italic.takeIf { roomInfo.name == null },
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
// Alias // Alias
summary.canonicalAlias?.let { alias -> roomInfo.canonicalAlias?.let { alias ->
Text( Text(
text = alias.value, text = alias.value,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
@ -245,7 +245,7 @@ private fun RoomSummaryView(
) )
} }
} }
RadioButton(selected = isSelected, onClick = { onSelection(summary) }) RadioButton(selected = isSelected, onClick = { onSelection(roomInfo) })
} }
} }

View file

@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
import io.element.android.libraries.roomselect.api.RoomSelectMode import io.element.android.libraries.roomselect.api.RoomSelectMode
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
@ -58,8 +59,9 @@ class RoomSelectPresenterTest {
@Test @Test
fun `present - update query`() = runTest { fun `present - update query`() = runTest {
val roomSummary = aRoomSummary()
val roomListService = FakeRoomListService().apply { val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(aRoomSummary())) postAllRooms(listOf(roomSummary))
} }
val presenter = createRoomSelectPresenter( val presenter = createRoomSelectPresenter(
roomListService = roomListService roomListService = roomListService
@ -68,19 +70,10 @@ class RoomSelectPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
val expectedRoomSummary = aRoomSummary() val expectedRoomInfo = roomSummary.toSelectRoomInfo()
// Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result // Do not compare the lambda because they will be different. So copy the lambda from expectedRoomSummary to result
val result = (awaitItem().resultState as SearchBarResultState.Results).results.map { roomSummary -> val result = (awaitItem().resultState as SearchBarResultState.Results).results
roomSummary.copy( assertThat(result).isEqualTo(listOf(expectedRoomInfo))
lastMessage = roomSummary.lastMessage!!.copy(
event = roomSummary.lastMessage!!.event.copy(
debugInfoProvider = expectedRoomSummary.lastMessage!!.event.debugInfoProvider,
messageShieldProvider = expectedRoomSummary.lastMessage!!.event.messageShieldProvider,
)
),
)
}
assertThat(result).isEqualTo(listOf(expectedRoomSummary))
initialState.eventSink(RoomSelectEvents.ToggleSearchActive) initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
skipItems(1) skipItems(1)
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained")) initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
@ -99,8 +92,9 @@ class RoomSelectPresenterTest {
@Test @Test
fun `present - select and remove a room`() = runTest { fun `present - select and remove a room`() = runTest {
val roomSummary = aRoomSummary()
val roomListService = FakeRoomListService().apply { val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(aRoomSummary())) postAllRooms(listOf(roomSummary))
} }
val presenter = createRoomSelectPresenter( val presenter = createRoomSelectPresenter(
roomListService = roomListService, roomListService = roomListService,
@ -109,9 +103,9 @@ class RoomSelectPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
val summary = aRoomSummary() val roomInfo = roomSummary.toSelectRoomInfo()
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary)) initialState.eventSink(RoomSelectEvents.SetSelectedRoom(roomInfo))
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary)) assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(roomInfo))
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom) initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
assertThat(awaitItem().selectedRooms).isEmpty() assertThat(awaitItem().selectedRooms).isEmpty()
cancel() cancel()

View file

@ -8,13 +8,27 @@
package io.element.android.libraries.textcomposer.mentions package io.element.android.libraries.textcomposer.mentions
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
@Immutable @Immutable
sealed interface ResolvedSuggestion { sealed interface ResolvedSuggestion {
data object AtRoom : ResolvedSuggestion data object AtRoom : ResolvedSuggestion
data class Member(val roomMember: RoomMember) : ResolvedSuggestion data class Member(val roomMember: RoomMember) : ResolvedSuggestion
data class Alias(val roomAlias: RoomAlias, val roomSummary: RoomSummary) : ResolvedSuggestion data class Alias(
val roomAlias: RoomAlias,
val roomId: RoomId,
val roomName: String?,
val roomAvatarUrl: String?,
) : ResolvedSuggestion {
fun getAvatarData(size: AvatarSize) = AvatarData(
id = roomId.value,
name = roomName,
url = roomAvatarUrl,
size = size,
)
}
} }

View file

@ -16,10 +16,10 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.IntentionalMention import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ALIAS
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.textcomposer.mentions.MentionSpan import io.element.android.libraries.textcomposer.mentions.MentionSpan
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
@ -34,7 +34,7 @@ class MarkdownTextEditorStateTest {
@Test @Test
fun `insertMention - room alias - getMentions return empty list`() { fun `insertMention - room alias - getMentions return empty list`() {
val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true) val state = MarkdownTextEditorState(initialText = "Hello @", initialFocus = true)
val suggestion = ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary(canonicalAlias = A_ROOM_ALIAS)) val suggestion = aRoomAliasSuggestion()
val permalinkBuilder = FakePermalinkBuilder() val permalinkBuilder = FakePermalinkBuilder()
val mentionSpanProvider = aMentionSpanProvider() val mentionSpanProvider = aMentionSpanProvider()
state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder) state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder)
@ -46,7 +46,7 @@ class MarkdownTextEditorStateTest {
val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply { val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply {
currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "") currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "")
} }
val suggestion = ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary(canonicalAlias = A_ROOM_ALIAS)) val suggestion = aRoomAliasSuggestion()
val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) })
val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.failure(IllegalStateException("Failed")) }) val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.failure(IllegalStateException("Failed")) })
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
@ -58,7 +58,7 @@ class MarkdownTextEditorStateTest {
val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply { val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply {
currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "") currentSuggestion = Suggestion(start = 6, end = 7, type = SuggestionType.Room, text = "")
} }
val suggestion = ResolvedSuggestion.Alias(A_ROOM_ALIAS, aRoomSummary(canonicalAlias = A_ROOM_ALIAS)) val suggestion = aRoomAliasSuggestion()
val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) }) val permalinkParser = FakePermalinkParser(result = { PermalinkData.RoomLink(A_ROOM_ALIAS.toRoomIdOrAlias()) })
val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.success("https://matrix.to/#/${A_ROOM_ALIAS.value}") }) val permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.success("https://matrix.to/#/${A_ROOM_ALIAS.value}") })
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser) val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
@ -202,4 +202,13 @@ class MarkdownTextEditorStateTest {
} }
} }
} }
private fun aRoomAliasSuggestion(): ResolvedSuggestion.Alias {
return ResolvedSuggestion.Alias(
roomAlias = A_ROOM_ALIAS,
roomId = A_ROOM_ID,
roomName = null,
roomAvatarUrl = null
)
}
} }

View file

@ -24,4 +24,5 @@ dependencies {
implementation(libs.autonomousapps.dependencyanalysis.plugin) implementation(libs.autonomousapps.dependencyanalysis.plugin)
implementation(libs.anvil.gradle.plugin) implementation(libs.anvil.gradle.plugin)
implementation(libs.ksp.gradle.plugin) implementation(libs.ksp.gradle.plugin)
implementation(libs.compose.compiler.plugin)
} }

Some files were not shown because too many files have changed in this diff Show more