Merge branch 'release/0.7.0' into main
This commit is contained in:
commit
1ad7a1fd49
197 changed files with 1305 additions and 875 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -65,6 +65,9 @@ captures/
|
|||
.idea/shelf
|
||||
.idea/sonarlint
|
||||
|
||||
# .kotlin folder
|
||||
.kotlin
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
|
|
|
|||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.25" />
|
||||
<option name="version" value="2.0.20" />
|
||||
</component>
|
||||
</project>
|
||||
74
CHANGES.md
74
CHANGES.md
|
|
@ -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)
|
||||
========================================
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
package io.element.android.appconfig
|
||||
|
||||
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 IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.RoomId
|
||||
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.alias.ResolvedRoomAlias
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
@ -120,12 +122,9 @@ class RoomFlowNode @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
|
||||
val roomInfoFlow = client.getRoomInfoFlow(
|
||||
roomId = roomId
|
||||
).map { it.getOrNull() }
|
||||
|
||||
val isSpaceFlow = roomInfoFlow.map { it?.isSpace.orFalse() }.distinctUntilChanged()
|
||||
val currentMembershipFlow = roomInfoFlow.map { it?.currentUserMembership }.distinctUntilChanged()
|
||||
val roomInfoFlow = client.getRoomInfoFlow(roomIdOrAlias = roomId.toRoomIdOrAlias())
|
||||
val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged()
|
||||
val currentMembershipFlow = roomInfoFlow.map { it.getOrNull()?.currentUserMembership }.distinctUntilChanged()
|
||||
combine(currentMembershipFlow, isSpaceFlow) { membership, isSpace ->
|
||||
Timber.d("Room membership: $membership")
|
||||
when (membership) {
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ class LoggedInPresenterTest {
|
|||
distributors = listOf(Distributor("aDistributorValue1", "aDistributorName1")),
|
||||
currentDistributor = { null },
|
||||
),
|
||||
registerWithLambda: suspend (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
registerWithLambda: (MatrixClient, PushProvider, Distributor) -> Result<Unit> = { _, _, _ ->
|
||||
Result.success(Unit)
|
||||
},
|
||||
selectPushProviderLambda: (MatrixClient, PushProvider) -> Unit = { _, _ -> lambdaError() },
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ plugins {
|
|||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) 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.anvil) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
|
|
@ -82,20 +83,15 @@ allprojects {
|
|||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
// Warnings are potential errors, so stop ignoring them
|
||||
// This is disabled by default, but the CI will enforce this.
|
||||
// You can override by passing `-PallWarningsAsErrors=true` in the command line
|
||||
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
||||
kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
|
||||
compilerOptions {
|
||||
// Warnings are potential errors, so stop ignoring them
|
||||
// This is disabled by default, but the CI will enforce this.
|
||||
// You can override by passing `-PallWarningsAsErrors=true` in the command line
|
||||
// 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
|
||||
freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
|
||||
)
|
||||
*/
|
||||
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,19 +188,23 @@ subprojects {
|
|||
|
||||
subprojects {
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
compilerOptions {
|
||||
if (project.findProperty("composeCompilerReports") == "true") {
|
||||
freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
|
||||
freeCompilerArgs.addAll(
|
||||
listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
|
||||
)
|
||||
)
|
||||
}
|
||||
if (project.findProperty("composeCompilerMetrics") == "true") {
|
||||
freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
|
||||
freeCompilerArgs.addAll(
|
||||
listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
fastlane/metadata/android/en-US/changelogs/40007000.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40007000.txt
Normal 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
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -7,13 +9,14 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.cachecleaner.api"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(libs.androidx.startup)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
package io.element.android.features.call.impl.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.WebChromeClient
|
||||
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.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import timber.log.Timber
|
||||
|
||||
typealias RequestPermissionCallback = (Array<String>) -> Unit
|
||||
|
||||
|
|
@ -189,6 +192,25 @@ private fun WebView.setup(
|
|||
override fun onPermissionRequest(request: PermissionRequest) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,12 +109,7 @@ class CallScreenPresenterTest {
|
|||
assertThat(initialState.isInWidgetMode).isTrue()
|
||||
assertThat(widgetProvider.getWidgetCalled).isTrue()
|
||||
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
|
||||
// Called several times because of the recomposition
|
||||
analyticsLambda.assertions().isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(MobileScreen.ScreenName.RoomCall)),
|
||||
listOf(value(MobileScreen.ScreenName.RoomCall))
|
||||
)
|
||||
analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall))
|
||||
sendCallNotificationIfNeededLambda.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -6,13 +8,14 @@
|
|||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.enterprise.impl"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.anvilannotations)
|
||||
api(projects.features.enterprise.api)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -7,7 +9,6 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
|
|
@ -15,6 +16,8 @@ android {
|
|||
namespace = "io.element.android.features.ftue.test"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.ftue.api)
|
||||
implementation(projects.tests.testutils)
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
||||
)
|
||||
}
|
||||
skipItems(1)
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
state.eventSink(
|
||||
|
|
@ -147,7 +147,7 @@ class AcceptDeclineInvitePresenterTest {
|
|||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
||||
)
|
||||
}
|
||||
skipItems(1)
|
||||
skipItems(2)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.core.RoomId
|
||||
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.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomType
|
||||
|
|
@ -70,7 +72,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
|||
override fun present(): JoinRoomState {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
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 knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
val contentState by produceState<ContentState>(
|
||||
|
|
@ -204,7 +206,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
|
|||
name = name,
|
||||
topic = topic,
|
||||
alias = canonicalAlias,
|
||||
numberOfMembers = activeMembersCount,
|
||||
numberOfMembers = activeMembersCount.toLong(),
|
||||
isDm = isDm,
|
||||
roomType = if (isSpace) RoomType.Space else RoomType.Room,
|
||||
roomAvatarUrl = avatarUrl,
|
||||
|
|
|
|||
|
|
@ -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.toRoomIdOrAlias
|
||||
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
|
||||
|
||||
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||
|
|
|
|||
|
|
@ -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.FakeMatrixClient
|
||||
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.aRoomSummary
|
||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
|
|
@ -67,10 +67,10 @@ class JoinRoomPresenterTest {
|
|||
|
||||
@Test
|
||||
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 {
|
||||
getRoomInfoFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomInfo))
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
}
|
||||
}
|
||||
val presenter = createJoinRoomPresenter(
|
||||
|
|
@ -81,22 +81,22 @@ class JoinRoomPresenterTest {
|
|||
awaitItem().also { state ->
|
||||
val contentState = state.contentState as ContentState.Loaded
|
||||
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(contentState.name).isEqualTo(roomInfo.name)
|
||||
assertThat(contentState.topic).isEqualTo(roomInfo.topic)
|
||||
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
|
||||
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount)
|
||||
assertThat(contentState.isDm).isEqualTo(roomInfo.isDirect)
|
||||
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
|
||||
assertThat(contentState.name).isEqualTo(roomSummary.info.name)
|
||||
assertThat(contentState.topic).isEqualTo(roomSummary.info.topic)
|
||||
assertThat(contentState.alias).isEqualTo(roomSummary.info.canonicalAlias)
|
||||
assertThat(contentState.numberOfMembers).isEqualTo(roomSummary.info.activeMembersCount)
|
||||
assertThat(contentState.isDm).isEqualTo(roomSummary.info.isDirect)
|
||||
assertThat(contentState.roomAvatarUrl).isEqualTo(roomSummary.info.avatarUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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 {
|
||||
getRoomInfoFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomInfo))
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
val inviter = aRoomMember(userId = UserId("@bob:example.com"), displayName = "Bob")
|
||||
val expectedInviteSender = inviter.toInviteSender()
|
||||
val roomInfo = aRoomInfo(
|
||||
val roomSummary = aRoomSummary(
|
||||
currentUserMembership = CurrentUserMembership.INVITED,
|
||||
inviter = inviter,
|
||||
)
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
getRoomInfoFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomInfo))
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
}
|
||||
}
|
||||
val presenter = createJoinRoomPresenter(
|
||||
|
|
@ -140,10 +140,10 @@ class JoinRoomPresenterTest {
|
|||
val acceptDeclinePresenter = Presenter {
|
||||
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
|
||||
}
|
||||
val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED)
|
||||
val roomSummary = aRoomSummary(currentUserMembership = CurrentUserMembership.INVITED)
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
getRoomInfoFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomInfo))
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
}
|
||||
}
|
||||
val presenter = createJoinRoomPresenter(
|
||||
|
|
@ -224,10 +224,10 @@ class JoinRoomPresenterTest {
|
|||
|
||||
@Test
|
||||
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 {
|
||||
getRoomInfoFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomInfo))
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
}
|
||||
}
|
||||
val presenter = createJoinRoomPresenter(
|
||||
|
|
@ -243,10 +243,10 @@ class JoinRoomPresenterTest {
|
|||
|
||||
@Test
|
||||
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 {
|
||||
getRoomInfoFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomInfo))
|
||||
getRoomSummaryFlowLambda = { _ ->
|
||||
flowOf(Optional.of(roomSummary))
|
||||
}
|
||||
}
|
||||
val presenter = createJoinRoomPresenter(
|
||||
|
|
@ -338,11 +338,15 @@ class JoinRoomPresenterTest {
|
|||
awaitItem().also { state ->
|
||||
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||
}
|
||||
|
||||
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
|
||||
fakeKnockRoom.lambda = knockRoomFailure
|
||||
state.eventSink(JoinRoomEvents.KnockRoom)
|
||||
}
|
||||
|
||||
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
|
||||
awaitItem().also { state ->
|
||||
assertThat(state.knockAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import extension.setupAnvil
|
|||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.anvil)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class WebViewMessageInterceptor(
|
|||
}
|
||||
|
||||
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
|
||||
onOpenExternalUrl(request.url.toString())
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -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.utils.annotatedTextWithBold
|
||||
import io.element.android.libraries.permissions.api.PermissionsView
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
|
|
@ -51,6 +50,7 @@ fun QrCodeIntroView(
|
|||
onBackClick = onBackClick,
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
|
||||
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) },
|
||||
buttons = { Buttons(state = state) }
|
||||
)
|
||||
|
|
@ -87,7 +87,7 @@ private fun ColumnScope.Buttons(
|
|||
state: QrCodeIntroState,
|
||||
) {
|
||||
Button(
|
||||
text = stringResource(id = CommonStrings.action_continue),
|
||||
text = stringResource(id = R.string.screen_qr_code_login_initial_state_button_title),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
state.eventSink.invoke(QrCodeIntroEvents.Continue)
|
||||
|
|
|
|||
|
|
@ -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_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_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_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>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
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.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
|
|
@ -61,12 +61,12 @@ class QrCodeIntroViewTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on continue button clicked - emits the Continue event`() {
|
||||
fun `on submit button clicked - emits the Continue event`() {
|
||||
val eventRecorder = EventsRecorder<QrCodeIntroEvents>()
|
||||
rule.setQrCodeIntroView(
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
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.encryption.EncryptionService
|
||||
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.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
|
@ -40,6 +41,7 @@ import javax.inject.Inject
|
|||
class IdentityChangeStatePresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
) : Presenter<IdentityChangeState> {
|
||||
@Composable
|
||||
override fun present(): IdentityChangeState {
|
||||
|
|
@ -62,14 +64,18 @@ class IdentityChangeStatePresenter @Inject constructor(
|
|||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun ProduceStateScope<PersistentList<RoomMemberIdentityStateChange>>.observeRoomMemberIdentityStateChange() {
|
||||
room.syncUpdateFlow
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.IdentityPinningViolationNotifications)
|
||||
.filter { it }
|
||||
.flatMapLatest {
|
||||
room.syncUpdateFlow
|
||||
}
|
||||
.filter {
|
||||
// Room cannot become unencrypted, so we can just apply a filter here.
|
||||
room.isEncrypted
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest {
|
||||
combine(room.identityStateChangesFlow, room.membersStateFlow,) { identityStateChanges, membersState ->
|
||||
combine(room.identityStateChangesFlow, room.membersStateFlow) { identityStateChanges, membersState ->
|
||||
identityStateChanges.map { identityStateChange ->
|
||||
val member = membersState.roomMembers()
|
||||
?.firstOrNull { roomMember -> roomMember.userId == identityStateChange.userId }
|
||||
|
|
|
|||
|
|
@ -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.preview.error.sendAttachmentError
|
||||
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.timeline.TimelineController
|
||||
import io.element.android.features.messages.impl.utils.TextPillificationHelper
|
||||
|
|
|
|||
|
|
@ -5,20 +5,22 @@
|
|||
* 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 io.element.android.libraries.di.SessionScope
|
||||
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.RoomSummary
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
data class RoomAliasSuggestion(
|
||||
val roomAlias: RoomAlias,
|
||||
val roomSummary: RoomSummary,
|
||||
val roomId: RoomId,
|
||||
val roomName: String?,
|
||||
val roomAvatarUrl: String?,
|
||||
)
|
||||
|
||||
interface RoomAliasSuggestionsDataSource {
|
||||
|
|
@ -32,14 +34,16 @@ class DefaultRoomAliasSuggestionsDataSource @Inject constructor(
|
|||
override fun getAllRoomAliasSuggestions(): Flow<List<RoomAliasSuggestion>> {
|
||||
return roomListService
|
||||
.allRooms
|
||||
.filteredSummaries
|
||||
.summaries
|
||||
.map { roomSummaries ->
|
||||
roomSummaries
|
||||
.mapNotNull { roomSummary ->
|
||||
roomSummary.canonicalAlias?.let { roomAlias ->
|
||||
roomSummary.info.canonicalAlias?.let { roomAlias ->
|
||||
RoomAliasSuggestion(
|
||||
roomAlias = roomAlias,
|
||||
roomSummary = roomSummary,
|
||||
roomId = roomSummary.roomId,
|
||||
roomName = roomSummary.info.name,
|
||||
roomAvatarUrl = roomSummary.info.avatarUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.room.RoomMember
|
||||
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.textcomposer.mentions.ResolvedSuggestion
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -60,7 +59,7 @@ fun SuggestionsPickerView(
|
|||
when (suggestion) {
|
||||
is ResolvedSuggestion.AtRoom -> "@room"
|
||||
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) {
|
||||
is ResolvedSuggestion.AtRoom -> roomAvatar?.copy(size = avatarSize) ?: AvatarData(roomId, roomName, null, 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) {
|
||||
is ResolvedSuggestion.AtRoom -> stringResource(R.string.screen_room_mentions_at_room_title)
|
||||
is ResolvedSuggestion.Member -> suggestion.roomMember.displayName
|
||||
is ResolvedSuggestion.Alias -> suggestion.roomSummary.name
|
||||
is ResolvedSuggestion.Alias -> suggestion.roomName
|
||||
}
|
||||
val subtitle = when (suggestion) {
|
||||
is ResolvedSuggestion.AtRoom -> "@room"
|
||||
|
|
@ -152,11 +151,6 @@ internal fun SuggestionsPickerViewPreview() {
|
|||
role = RoomMember.Role.USER,
|
||||
)
|
||||
val anAlias = remember { RoomAlias("#room:domain.org") }
|
||||
val roomSummaryDetails = remember {
|
||||
aRoomSummaryDetails(
|
||||
name = "My room",
|
||||
)
|
||||
}
|
||||
SuggestionsPickerView(
|
||||
roomId = RoomId("!room:matrix.org"),
|
||||
roomName = "Room",
|
||||
|
|
@ -166,8 +160,10 @@ internal fun SuggestionsPickerViewPreview() {
|
|||
ResolvedSuggestion.Member(roomMember),
|
||||
ResolvedSuggestion.Member(roomMember.copy(userId = UserId("@bob:server.org"), displayName = "Bob")),
|
||||
ResolvedSuggestion.Alias(
|
||||
anAlias,
|
||||
roomSummaryDetails,
|
||||
roomAlias = anAlias,
|
||||
roomId = RoomId("!room:matrix.org"),
|
||||
roomName = "My room",
|
||||
roomAvatarUrl = null,
|
||||
)
|
||||
),
|
||||
onSelectSuggestion = {}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
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.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||
|
|
@ -55,7 +54,14 @@ class SuggestionsProcessor @Inject constructor() {
|
|||
SuggestionType.Room -> {
|
||||
roomAliasSuggestions
|
||||
.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,
|
||||
is SuggestionType.Custom -> {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ package io.element.android.features.messages.impl.crypto.identity
|
|||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.encryption.EncryptionService
|
||||
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
|
||||
fun `present - when the clear room emits identity change, the presenter does not emits new state`() = runTest {
|
||||
val room = FakeMatrixRoom(isEncrypted = false)
|
||||
|
|
@ -147,10 +187,16 @@ class IdentityChangeStatePresenterTest {
|
|||
private fun createIdentityChangeStatePresenter(
|
||||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(
|
||||
initialState = mapOf(
|
||||
FeatureFlags.IdentityPinningViolationNotifications.key to true,
|
||||
)
|
||||
),
|
||||
): IdentityChangeStatePresenter {
|
||||
return IdentityChangeStatePresenter(
|
||||
room = room,
|
||||
encryptionService = encryptionService,
|
||||
featureFlagService = featureFlagService,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ package io.element.android.features.messages.impl.messagecomposer
|
|||
|
||||
import app.cash.turbine.test
|
||||
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_ID_2
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||
|
|
@ -38,7 +40,9 @@ class DefaultRoomAliasSuggestionsDataSourceTest {
|
|||
listOf(
|
||||
RoomAliasSuggestion(
|
||||
roomAlias = A_ROOM_ALIAS,
|
||||
roomSummary = aRoomSummaryWithAnAlias
|
||||
roomId = aRoomSummaryWithAnAlias.roomId,
|
||||
roomName = aRoomSummaryWithAnAlias.info.name,
|
||||
roomAvatarUrl = aRoomSummaryWithAnAlias.info.avatarUrl
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
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.MutableStateFlow
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package io.element.android.features.messages.impl.messagecomposer.suggestions
|
||||
|
||||
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.room.MatrixRoomMembersState
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
|
|
@ -110,13 +109,25 @@ class SuggestionsProcessorTest {
|
|||
val result = suggestionsProcessor.process(
|
||||
suggestion = aRoomSuggestion("ALI"),
|
||||
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,
|
||||
canSendRoomMention = { true },
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
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(
|
||||
suggestion = aRoomSuggestion("ali"),
|
||||
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,
|
||||
canSendRoomMention = { true },
|
||||
)
|
||||
assertThat(result).isEqualTo(
|
||||
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(
|
||||
suggestion = aRoomSuggestion("tot"),
|
||||
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,
|
||||
canSendRoomMention = { true },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -7,13 +9,14 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.migration.impl"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.migration.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ import dagger.assisted.AssistedInject
|
|||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
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.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
|
|
@ -40,7 +41,6 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
|
|||
private val notificationSettingsService: NotificationSettingsService,
|
||||
@Assisted private val isOneToOne: Boolean,
|
||||
private val roomListService: RoomListService,
|
||||
private val matrixClient: MatrixClient,
|
||||
) : Presenter<EditDefaultNotificationSettingState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -57,8 +57,8 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
|
|||
|
||||
val changeNotificationSettingAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||
|
||||
val roomsWithUserDefinedMode: MutableState<List<RoomSummary>> = remember {
|
||||
mutableStateOf(listOf())
|
||||
val roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>> = remember {
|
||||
mutableStateOf(emptyList())
|
||||
}
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
|
@ -106,31 +106,37 @@ class EditDefaultNotificationSettingPresenter @AssistedInject constructor(
|
|||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<RoomSummary>>) {
|
||||
private fun CoroutineScope.observeRoomSummaries(roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>) {
|
||||
roomListService.allRooms
|
||||
.summaries
|
||||
.onEach {
|
||||
updateRoomsWithUserDefinedMode(it, roomsWithUserDefinedMode)
|
||||
.onEach { roomSummaries ->
|
||||
updateRoomsWithUserDefinedMode(roomSummaries, roomsWithUserDefinedMode)
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.updateRoomsWithUserDefinedMode(
|
||||
private suspend fun updateRoomsWithUserDefinedMode(
|
||||
summaries: List<RoomSummary>,
|
||||
roomsWithUserDefinedMode: MutableState<List<RoomSummary>>
|
||||
) = launch {
|
||||
val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrThrow().toSet()
|
||||
|
||||
val sortedSummaries = summaries
|
||||
.filterIsInstance<RoomSummary>()
|
||||
.filter {
|
||||
val room = matrixClient.getRoom(it.roomId) ?: return@filter false
|
||||
roomWithUserDefinedRules.contains(it.roomId.value) && isOneToOne == room.isOneToOne
|
||||
roomsWithUserDefinedMode: MutableState<List<EditNotificationSettingRoomInfo>>
|
||||
) {
|
||||
val roomWithUserDefinedRules: Set<String> = notificationSettingsService.getRoomsWithUserDefinedRules().getOrDefault(emptyList()).toSet()
|
||||
roomsWithUserDefinedMode.value = summaries
|
||||
.filter { roomSummary ->
|
||||
roomWithUserDefinedRules.contains(roomSummary.roomId.value) && roomSummary.isOneToOne == isOneToOne
|
||||
}
|
||||
.map { roomSummary ->
|
||||
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
|
||||
.sortedWith(compareBy(Collator.getInstance()) { it.name })
|
||||
|
||||
roomsWithUserDefinedMode.value = sortedSummaries
|
||||
.sortedWith(compareBy(Collator.getInstance()) { roomSummary -> roomSummary.name })
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setDefaultNotificationMode(mode: RoomNotificationMode, action: MutableState<AsyncAction<Unit>>) = launch {
|
||||
|
|
|
|||
|
|
@ -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.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class EditDefaultNotificationSettingState(
|
||||
val isOneToOne: Boolean,
|
||||
val mode: RoomNotificationMode?,
|
||||
val roomsWithUserDefinedMode: ImmutableList<RoomSummary>,
|
||||
val roomsWithUserDefinedMode: ImmutableList<EditNotificationSettingRoomInfo>,
|
||||
val changeNotificationSettingAction: AsyncAction<Unit>,
|
||||
val displayMentionsOnlyDisclaimer: Boolean,
|
||||
val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit,
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ package io.element.android.features.preferences.impl.notifications.edit
|
|||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.ui.components.aRoomSummaryDetails
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider<EditDefaultNotificationSettingState> {
|
||||
|
|
@ -33,21 +34,25 @@ private fun anEditDefaultNotificationSettingsState(
|
|||
isOneToOne = isOneToOne,
|
||||
mode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||
roomsWithUserDefinedMode = persistentListOf(
|
||||
aRoomSummary("Room"),
|
||||
aRoomSummary(null),
|
||||
anEditNotificationSettingRoomInfo("Room"),
|
||||
anEditNotificationSettingRoomInfo(null),
|
||||
),
|
||||
changeNotificationSettingAction = changeNotificationSettingAction,
|
||||
displayMentionsOnlyDisclaimer = displayMentionsOnlyDisclaimer,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
private fun aRoomSummary(
|
||||
private fun anEditNotificationSettingRoomInfo(
|
||||
name: String?,
|
||||
) = aRoomSummaryDetails(
|
||||
) = EditNotificationSettingRoomInfo(
|
||||
roomId = RoomId("!roomId:domain"),
|
||||
name = name,
|
||||
avatarUrl = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
avatarData = AvatarData(
|
||||
id = "!roomId:domain",
|
||||
name = name,
|
||||
url = null,
|
||||
size = AvatarSize.CustomRoomNotificationSetting,
|
||||
),
|
||||
heroesAvatar = persistentListOf(),
|
||||
notificationMode = RoomNotificationMode.MENTIONS_AND_KEYWORDS_ONLY,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.text.font.FontStyle
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.features.preferences.impl.R
|
||||
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.list.ListItemContent
|
||||
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.matrix.api.core.RoomId
|
||||
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 kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
PreferenceCategory(title = stringResource(id = R.string.screen_notification_settings_edit_custom_settings_section_title)) {
|
||||
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.MENTIONS_AND_KEYWORDS_ONLY -> {
|
||||
stringResource(id = R.string.screen_notification_settings_edit_mode_mentions_and_keywords)
|
||||
|
|
@ -101,10 +98,8 @@ fun EditDefaultNotificationSettingView(
|
|||
},
|
||||
leadingContent = ListItemContent.Custom {
|
||||
CompositeAvatar(
|
||||
avatarData = summary.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting),
|
||||
heroes = summary.heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.CustomRoomNotificationSetting)
|
||||
}.toPersistentList()
|
||||
avatarData = summary.avatarData,
|
||||
heroes = summary.heroesAvatar,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
)
|
||||
|
|
@ -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.EditDefaultNotificationSettingStateEvents
|
||||
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.FakeMatrixClient
|
||||
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.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
|
|
@ -49,24 +46,20 @@ class EditDefaultNotificationSettingsPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - ensure list of rooms with user defined mode`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
val notificationSettingsService = FakeNotificationSettingsService(
|
||||
initialRoomMode = RoomNotificationMode.ALL_MESSAGES,
|
||||
initialRoomModeIsDefault = false
|
||||
)
|
||||
val matrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService).apply {
|
||||
givenGetRoomResult(A_ROOM_ID, room)
|
||||
}
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService, matrixClient)
|
||||
val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService, roomListService)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
roomListService.postAllRooms(listOf(aRoomSummary(notificationMode = RoomNotificationMode.ALL_MESSAGES)))
|
||||
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = RoomNotificationMode.ALL_MESSAGES)))
|
||||
val loadedState = consumeItemsUntilPredicate { state ->
|
||||
state.roomsWithUserDefinedMode.any { it.userDefinedNotificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||
state.roomsWithUserDefinedMode.any { it.notificationMode == RoomNotificationMode.ALL_MESSAGES }
|
||||
}.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(
|
||||
notificationSettingsService: FakeNotificationSettingsService = FakeNotificationSettingsService(),
|
||||
roomListService: FakeRoomListService = FakeRoomListService(),
|
||||
matrixClient: FakeMatrixClient = FakeMatrixClient(notificationSettingsService = notificationSettingsService)
|
||||
): EditDefaultNotificationSettingPresenter {
|
||||
return EditDefaultNotificationSettingPresenter(
|
||||
notificationSettingsService = notificationSettingsService,
|
||||
isOneToOne = false,
|
||||
roomListService = roomListService,
|
||||
matrixClient = matrixClient
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import extension.setupAnvil
|
|||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.eventformatter.api.RoomLastMessageFormatter
|
||||
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.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||
|
|
@ -24,34 +25,35 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
|||
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
|
||||
private val roomLastMessageFormatter: RoomLastMessageFormatter,
|
||||
) {
|
||||
fun create(details: RoomSummary): RoomListRoomSummary {
|
||||
val avatarData = details.getAvatarData(size = AvatarSize.RoomListItem)
|
||||
fun create(roomSummary: RoomSummary): RoomListRoomSummary {
|
||||
val roomInfo = roomSummary.info
|
||||
val avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomListItem)
|
||||
return RoomListRoomSummary(
|
||||
id = details.roomId.value,
|
||||
roomId = details.roomId,
|
||||
name = details.name,
|
||||
numberOfUnreadMessages = details.numUnreadMessages,
|
||||
numberOfUnreadMentions = details.numUnreadMentions,
|
||||
numberOfUnreadNotifications = details.numUnreadNotifications,
|
||||
isMarkedUnread = details.isMarkedUnread,
|
||||
timestamp = lastMessageTimestampFormatter.format(details.lastMessageTimestamp),
|
||||
lastMessage = details.lastMessage?.let { message ->
|
||||
roomLastMessageFormatter.format(message.event, details.isDm)
|
||||
id = roomSummary.roomId.value,
|
||||
roomId = roomSummary.roomId,
|
||||
name = roomInfo.name,
|
||||
numberOfUnreadMessages = roomInfo.numUnreadMessages,
|
||||
numberOfUnreadMentions = roomInfo.numUnreadMentions,
|
||||
numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
|
||||
isMarkedUnread = roomInfo.isMarkedUnread,
|
||||
timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp),
|
||||
lastMessage = roomSummary.lastMessage?.let { message ->
|
||||
roomLastMessageFormatter.format(message.event, roomInfo.isDm)
|
||||
}.orEmpty(),
|
||||
avatarData = avatarData,
|
||||
userDefinedNotificationMode = details.userDefinedNotificationMode,
|
||||
hasRoomCall = details.hasRoomCall,
|
||||
isDirect = details.isDirect,
|
||||
isFavorite = details.isFavorite,
|
||||
inviteSender = details.inviter?.toInviteSender(),
|
||||
isDm = details.isDm,
|
||||
canonicalAlias = details.canonicalAlias,
|
||||
displayType = if (details.currentUserMembership == CurrentUserMembership.INVITED) {
|
||||
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
|
||||
hasRoomCall = roomInfo.hasRoomCall,
|
||||
isDirect = roomInfo.isDirect,
|
||||
isFavorite = roomInfo.isFavorite,
|
||||
inviteSender = roomInfo.inviter?.toInviteSender(),
|
||||
isDm = roomInfo.isDm,
|
||||
canonicalAlias = roomInfo.canonicalAlias,
|
||||
displayType = if (roomInfo.currentUserMembership == CurrentUserMembership.INVITED) {
|
||||
RoomSummaryDisplayType.INVITE
|
||||
} else {
|
||||
RoomSummaryDisplayType.ROOM
|
||||
},
|
||||
heroes = details.heroes.map { user ->
|
||||
heroes = roomInfo.heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.RoomListItem)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ data class RoomListRoomSummary(
|
|||
val roomId: RoomId,
|
||||
val name: String?,
|
||||
val canonicalAlias: RoomAlias?,
|
||||
val numberOfUnreadMessages: Int,
|
||||
val numberOfUnreadMentions: Int,
|
||||
val numberOfUnreadNotifications: Int,
|
||||
val numberOfUnreadMessages: Long,
|
||||
val numberOfUnreadMentions: Long,
|
||||
val numberOfUnreadNotifications: Long,
|
||||
val isMarkedUnread: Boolean,
|
||||
val timestamp: String?,
|
||||
val lastMessage: CharSequence?,
|
||||
|
|
|
|||
|
|
@ -119,9 +119,9 @@ internal fun anInviteSender(
|
|||
internal fun aRoomListRoomSummary(
|
||||
id: String = "!roomId:domain",
|
||||
name: String? = "Room name",
|
||||
numberOfUnreadMessages: Int = 0,
|
||||
numberOfUnreadMentions: Int = 0,
|
||||
numberOfUnreadNotifications: Int = 0,
|
||||
numberOfUnreadMessages: Long = 0,
|
||||
numberOfUnreadMentions: Long = 0,
|
||||
numberOfUnreadNotifications: Long = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
lastMessage: String? = "Last message",
|
||||
timestamp: String? = lastMessage?.let { "88:88" },
|
||||
|
|
|
|||
|
|
@ -396,7 +396,7 @@ class RoomListPresenterTest {
|
|||
val notificationSettingsService = FakeNotificationSettingsService()
|
||||
val roomListService = FakeRoomListService()
|
||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||
roomListService.postAllRooms(listOf(aRoomSummary(notificationMode = userDefinedMode)))
|
||||
roomListService.postAllRooms(listOf(aRoomSummary(userDefinedNotificationMode = userDefinedMode)))
|
||||
val matrixClient = FakeMatrixClient(
|
||||
roomListService = roomListService,
|
||||
notificationSettingsService = notificationSettingsService
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ class RoomListRoomSummaryTest {
|
|||
}
|
||||
|
||||
internal fun createRoomListRoomSummary(
|
||||
numberOfUnreadMentions: Int = 0,
|
||||
numberOfUnreadMessages: Int = 0,
|
||||
numberOfUnreadNotifications: Int = 0,
|
||||
numberOfUnreadMentions: Long = 0,
|
||||
numberOfUnreadMessages: Long = 0,
|
||||
numberOfUnreadNotifications: Long = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
userDefinedNotificationMode: RoomNotificationMode? = null,
|
||||
isFavorite: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -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.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
|
|
@ -50,7 +49,7 @@ fun ResetIdentityRootView(
|
|||
buttons = {
|
||||
Button(
|
||||
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) },
|
||||
destructive = true,
|
||||
)
|
||||
|
|
@ -98,9 +97,9 @@ private fun Content() {
|
|||
iconComposable = {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = CompoundIcons.Close(),
|
||||
imageVector = CompoundIcons.Info(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
)
|
||||
},
|
||||
),
|
||||
|
|
@ -109,9 +108,9 @@ private fun Content() {
|
|||
iconComposable = {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = CompoundIcons.Close(),
|
||||
imageVector = CompoundIcons.Info(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconCriticalPrimary,
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
)
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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_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_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 that’s 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_footer">"Only reset your identity if you don’t have access to another signed-in device and you’ve lost your recovery key."</string>
|
||||
<string name="screen_encryption_reset_title">"Can\'t confirm? You’ll need to reset your identity."</string>
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class ResetIdentityRootViewTest {
|
|||
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder),
|
||||
)
|
||||
|
||||
rule.clickOn(CommonStrings.action_continue)
|
||||
rule.clickOn(R.string.screen_encryption_reset_action_continue_reset)
|
||||
|
||||
eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ android {
|
|||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.appconfig)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ import com.bumble.appyx.core.plugin.plugins
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
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.di.SessionScope
|
||||
|
||||
|
|
@ -36,6 +38,10 @@ class VerifySelfSessionNode @AssistedInject constructor(
|
|||
showDeviceVerifiedScreen = inputs<VerifySessionEntryPoint.Params>().showDeviceVerifiedScreen,
|
||||
)
|
||||
|
||||
private fun onLearnMoreClick(activity: Activity, dark: Boolean) {
|
||||
activity.openUrlInChromeCustomTab(null, dark, LearnMoreConfig.ENCRYPTION_URL)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
|
@ -44,6 +50,9 @@ class VerifySelfSessionNode @AssistedInject constructor(
|
|||
VerifySelfSessionView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onLearnMoreClick = {
|
||||
onLearnMoreClick(activity, isDark)
|
||||
},
|
||||
onEnterRecoveryKey = callback::onEnterRecoveryKey,
|
||||
onResetKey = callback::onResetKey,
|
||||
onFinish = callback::onDone,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ data class VerifySelfSessionState(
|
|||
@Stable
|
||||
sealed interface VerificationStep {
|
||||
data object Loading : VerificationStep
|
||||
|
||||
// FIXME canEnterRecoveryKey value is never read.
|
||||
data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : VerificationStep
|
||||
data object Canceled : VerificationStep
|
||||
data object AwaitingOtherDeviceResponse : VerificationStep
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ package io.element.android.features.verifysession.impl
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -64,6 +64,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver
|
|||
@Composable
|
||||
fun VerifySelfSessionView(
|
||||
state: VerifySelfSessionState,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onEnterRecoveryKey: () -> Unit,
|
||||
onResetKey: () -> 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
|
||||
private fun Content(flowState: FlowStep) {
|
||||
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) {
|
||||
if (flowState is FlowStep.Verifying) {
|
||||
private fun Content(
|
||||
flowState: FlowStep,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
) {
|
||||
when (flowState) {
|
||||
is VerifySelfSessionState.VerificationStep.Initial -> {
|
||||
ContentInitial(onLearnMoreClick)
|
||||
}
|
||||
is FlowStep.Verifying -> {
|
||||
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
|
||||
private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) {
|
||||
when (verificationFlowStep.data) {
|
||||
is SessionVerificationData.Decimals -> {
|
||||
val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() }
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
is SessionVerificationData.Emojis -> {
|
||||
// We want each row to have up to 4 emojis
|
||||
val rows = verificationFlowStep.data.emojis.chunked(4)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(40.dp),
|
||||
) {
|
||||
rows.forEach { emojis ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
for (emoji in emojis) {
|
||||
EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp))
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (verificationFlowStep.data) {
|
||||
is SessionVerificationData.Decimals -> {
|
||||
val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() }
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text,
|
||||
style = ElementTheme.typography.fontHeadingLgBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
is SessionVerificationData.Emojis -> {
|
||||
// We want each row to have up to 4 emojis
|
||||
val rows = verificationFlowStep.data.emojis.chunked(4)
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(40.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),
|
||||
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
|
||||
)
|
||||
OutlinedButton(
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||
onClick = onEnterRecoveryKey,
|
||||
)
|
||||
}
|
||||
// This option should always be displayed
|
||||
TextButton(
|
||||
OutlinedButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
||||
onClick = onResetKey,
|
||||
|
|
@ -402,6 +436,7 @@ private fun BottomMenu(
|
|||
internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview {
|
||||
VerifySelfSessionView(
|
||||
state = state,
|
||||
onLearnMoreClick = {},
|
||||
onEnterRecoveryKey = {},
|
||||
onResetKey = {},
|
||||
onFinish = {},
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ class VerifySelfSessionPresenterTest {
|
|||
skipItems(1)
|
||||
val initialItem = awaitItem()
|
||||
initialItem.eventSink(VerifySelfSessionViewEvents.SignOut)
|
||||
assertThat(awaitItem().signOutAction.isLoading()).isTrue()
|
||||
val finalItem = awaitItem()
|
||||
assertThat(finalItem.signOutAction.isSuccess()).isTrue()
|
||||
assertThat(finalItem.signOutAction.dataOrNull()).isEqualTo("aUrl")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
fun `clicking on they match emits the expected event`() {
|
||||
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
|
||||
|
|
@ -222,6 +238,7 @@ class VerifySelfSessionViewTest {
|
|||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView(
|
||||
state: VerifySelfSessionState,
|
||||
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||
onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(),
|
||||
onFinished: () -> Unit = EnsureNeverCalled(),
|
||||
onResetKey: () -> Unit = EnsureNeverCalled(),
|
||||
|
|
@ -230,6 +247,7 @@ class VerifySelfSessionViewTest {
|
|||
setContent {
|
||||
VerifySelfSessionView(
|
||||
state = state,
|
||||
onLearnMoreClick = onLearnMoreClick,
|
||||
onEnterRecoveryKey = onEnterRecoveryKey,
|
||||
onFinish = onFinished,
|
||||
onResetKey = onResetKey,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
[versions]
|
||||
# Project
|
||||
android_gradle_plugin = "8.7.0"
|
||||
kotlin = "1.9.25"
|
||||
kotlin = "2.0.20"
|
||||
kotlinpoetKsp = "1.18.1"
|
||||
ksp = "1.9.25-1.0.20"
|
||||
ksp = "2.0.20-1.0.25"
|
||||
firebaseAppDistribution = "5.0.0"
|
||||
|
||||
# AndroidX
|
||||
|
|
@ -25,7 +25,7 @@ media3 = "1.4.1"
|
|||
camera = "1.3.4"
|
||||
|
||||
# Compose
|
||||
compose_bom = "2024.09.02"
|
||||
compose_bom = "2024.09.03"
|
||||
composecompiler = "1.5.15"
|
||||
|
||||
# Coroutines
|
||||
|
|
@ -40,7 +40,7 @@ test_core = "1.6.1"
|
|||
#other
|
||||
coil = "2.7.0"
|
||||
datetime = "0.6.0"
|
||||
dependencyAnalysis = "2.1.3"
|
||||
dependencyAnalysis = "2.1.4"
|
||||
serialization_json = "1.6.3"
|
||||
showkase = "1.0.3"
|
||||
appyx = "1.4.0"
|
||||
|
|
@ -62,6 +62,7 @@ kover = "0.8.3"
|
|||
[libraries]
|
||||
# Project
|
||||
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
|
||||
android_desugar = "com.android.tools:desugar_jdk_libs:2.1.2"
|
||||
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_startup = "androidx.startup:startup-runtime:1.2.0"
|
||||
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_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_junit = "junit:junit:4.13.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_turbine = "app.cash.turbine:turbine:1.1.0"
|
||||
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"
|
||||
|
||||
# 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"
|
||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.25.0"
|
||||
|
||||
# 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"
|
||||
|
||||
# Di
|
||||
|
|
@ -238,3 +239,4 @@ firebaseAppDistribution = { id = "com.google.firebase.appdistribution", version.
|
|||
knit = { id = "org.jetbrains.kotlinx.knit", version = "0.5.0" }
|
||||
sonarqube = "org.sonarqube:5.1.0.4882"
|
||||
licensee = "app.cash.licensee:1.11.0"
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.PreviewGroup
|
||||
|
|
@ -24,6 +25,7 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup
|
|||
fun LinearProgressIndicator(
|
||||
progress: () -> Float,
|
||||
modifier: Modifier = Modifier,
|
||||
gapSize: Dp = 0.dp,
|
||||
color: Color = ProgressIndicatorDefaults.linearColor,
|
||||
trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
|
||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
||||
|
|
@ -31,9 +33,11 @@ fun LinearProgressIndicator(
|
|||
androidx.compose.material3.LinearProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = progress,
|
||||
gapSize = gapSize,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap,
|
||||
drawStopIndicator = {},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +45,7 @@ fun LinearProgressIndicator(
|
|||
fun LinearProgressIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = ProgressIndicatorDefaults.linearColor,
|
||||
gapSize: Dp = 0.dp,
|
||||
trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
|
||||
strokeCap: StrokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
|
||||
) {
|
||||
|
|
@ -49,14 +54,17 @@ fun LinearProgressIndicator(
|
|||
androidx.compose.material3.LinearProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = { 0.75F },
|
||||
gapSize = gapSize,
|
||||
color = color,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap,
|
||||
drawStopIndicator = {},
|
||||
)
|
||||
} else {
|
||||
androidx.compose.material3.LinearProgressIndicator(
|
||||
modifier = modifier,
|
||||
color = color,
|
||||
gapSize = gapSize,
|
||||
trackColor = trackColor,
|
||||
strokeCap = strokeCap,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
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.exponentialDecay
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
|
|
@ -296,13 +295,9 @@ internal object AnchoredDraggableDefaults {
|
|||
/**
|
||||
* The default animation used by [AnchoredDraggableState].
|
||||
*/
|
||||
@get:ExperimentalMaterial3Api
|
||||
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
|
||||
@ExperimentalMaterial3Api
|
||||
val SnapAnimationSpec = SpringSpec<Float>()
|
||||
|
||||
@get:ExperimentalMaterial3Api
|
||||
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
|
||||
@ExperimentalMaterial3Api
|
||||
val DecayAnimationSpec = exponentialDecay<Float>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ package io.element.android.libraries.featureflag.api
|
|||
|
||||
import io.element.android.appconfig.OnBoardingConfig
|
||||
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.
|
||||
|
|
@ -93,13 +92,7 @@ enum class FeatureFlags(
|
|||
key = "feature.qrCodeLogin",
|
||||
title = "Enable login using QR code",
|
||||
description = "Allow the user to login using the QR code flow",
|
||||
defaultValue = { buildMeta ->
|
||||
when (buildMeta.buildType) {
|
||||
// TODO remove once the feature is ready to publish
|
||||
BuildType.RELEASE -> false
|
||||
else -> OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE
|
||||
}
|
||||
},
|
||||
defaultValue = { OnBoardingConfig.CAN_LOGIN_WITH_QR_CODE },
|
||||
isFinished = false,
|
||||
),
|
||||
IncomingShare(
|
||||
|
|
@ -132,4 +125,17 @@ enum class FeatureFlags(
|
|||
defaultValue = { 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,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.Closeable
|
||||
import java.util.Optional
|
||||
|
||||
|
|
@ -94,7 +96,13 @@ interface MatrixClient : Closeable {
|
|||
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
|
||||
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
|
||||
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
|
||||
|
||||
|
|
@ -142,3 +150,14 @@ interface MatrixClient : Closeable {
|
|||
fun canDeactivateAccount(): Boolean
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ data class MatrixRoomInfo(
|
|||
val canonicalAlias: RoomAlias?,
|
||||
val alternativeAliases: ImmutableList<RoomAlias>,
|
||||
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 activeMembersCount: Long,
|
||||
val invitedMembersCount: Long,
|
||||
|
|
@ -43,7 +50,26 @@ data class MatrixRoomInfo(
|
|||
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||
val hasRoomCall: Boolean,
|
||||
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 pinnedEventIds: ImmutableList<EventId>,
|
||||
val creator: UserId?,
|
||||
)
|
||||
) {
|
||||
val aliases: List<RoomAlias>
|
||||
get() = listOfNotNull(canonicalAlias) + alternativeAliases
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,35 +7,14 @@
|
|||
|
||||
package io.element.android.libraries.matrix.api.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.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.MatrixRoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class RoomSummary(
|
||||
val roomId: RoomId,
|
||||
val name: String?,
|
||||
val canonicalAlias: RoomAlias?,
|
||||
val alternativeAliases: List<RoomAlias>,
|
||||
val isDirect: Boolean,
|
||||
val avatarUrl: String?,
|
||||
val info: MatrixRoomInfo,
|
||||
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 aliases: List<RoomAlias>
|
||||
get() = listOfNotNull(canonicalAlias) + alternativeAliases
|
||||
val isOneToOne get() = info.activeMembersCount == 2L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.InvitedRoom
|
||||
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.alias.ResolvedRoomAlias
|
||||
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.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -105,8 +104,8 @@ import org.matrix.rustcomponents.sdk.use
|
|||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.Optional
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.INFINITE
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters
|
||||
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
|
||||
* @throws TimeoutCancellationException if the room is not available after the timeout
|
||||
*/
|
||||
private suspend fun awaitJoinedRoom(roomIdOrAlias: RoomIdOrAlias, timeout: Duration): RoomSummary {
|
||||
val predicate: (List<RoomSummary>) -> Boolean = when (roomIdOrAlias) {
|
||||
is RoomIdOrAlias.Alias -> { roomSummaries: List<RoomSummary> ->
|
||||
val found = roomSummaries.find { it.aliases.contains(roomIdOrAlias.roomAlias) }
|
||||
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
|
||||
}
|
||||
}
|
||||
private suspend fun awaitJoinedRoom(
|
||||
roomIdOrAlias: RoomIdOrAlias,
|
||||
timeout: Duration
|
||||
): RoomSummary {
|
||||
return withTimeout(timeout) {
|
||||
roomListService.allRooms.summaries
|
||||
.filter(predicate)
|
||||
.first()
|
||||
getRoomSummaryFlow(roomIdOrAlias)
|
||||
.mapNotNull { optionalRoomSummary -> optionalRoomSummary.getOrNull() }
|
||||
.filter { roomSummary -> roomSummary.info.currentUserMembership == CurrentUserMembership.JOINED }
|
||||
.first()
|
||||
// Ensure that the room is ready
|
||||
.also { client.awaitRoomRemoteEcho(it.roomId.value) }
|
||||
|
|
@ -568,20 +560,21 @@ class RustMatrixClient(
|
|||
|
||||
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
|
||||
|
||||
override fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>> {
|
||||
return flow {
|
||||
var room = getRoom(roomId)
|
||||
if (room == null) {
|
||||
emit(Optional.empty())
|
||||
awaitJoinedRoom(roomId.toRoomIdOrAlias(), INFINITE)
|
||||
room = getRoom(roomId)
|
||||
override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<RoomSummary>> {
|
||||
val predicate: (RoomSummary) -> Boolean = when (roomIdOrAlias) {
|
||||
is RoomIdOrAlias.Alias -> { roomSummary ->
|
||||
roomSummary.info.aliases.contains(roomIdOrAlias.roomAlias)
|
||||
}
|
||||
room?.use {
|
||||
room.roomInfoFlow
|
||||
.map { roomInfo -> Optional.of(roomInfo) }
|
||||
.collect(this)
|
||||
is RoomIdOrAlias.Id -> { roomSummary ->
|
||||
roomSummary.roomId == roomIdOrAlias.roomId
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
return roomListService.allRooms.summaries
|
||||
.map { roomSummaries ->
|
||||
val roomSummary = roomSummaries.firstOrNull(predicate)
|
||||
Optional.ofNullable(roomSummary)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
override suspend fun setAllSendQueuesEnabled(enabled: Boolean) = withContext(sessionDispatcher) {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ class MatrixRoomInfoMapper {
|
|||
activeRoomCallParticipants = it.activeRoomCallParticipants.map(::UserId).toImmutableList(),
|
||||
heroes = it.elementHeroes().toImmutableList(),
|
||||
pinnedEventIds = it.pinnedEventIds.map(::EventId).toImmutableList(),
|
||||
isMarkedUnread = it.isMarkedUnread,
|
||||
numUnreadMessages = it.numUnreadMessages.toLong(),
|
||||
numUnreadMentions = it.numUnreadMentions.toLong(),
|
||||
numUnreadNotifications = it.numUnreadNotifications.toLong(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ internal class RoomListFactory(
|
|||
private val innerRoomListService: RoomListService,
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
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.isDm
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
|
||||
|
|
@ -17,19 +18,19 @@ val RoomListFilter.predicate
|
|||
is RoomListFilter.Any -> { _: RoomSummary -> true }
|
||||
RoomListFilter.None -> { _: RoomSummary -> false }
|
||||
RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
|
||||
!roomSummary.isDm && !roomSummary.isInvited()
|
||||
!roomSummary.info.isDm && !roomSummary.isInvited()
|
||||
}
|
||||
RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
|
||||
roomSummary.isDm && !roomSummary.isInvited()
|
||||
roomSummary.info.isDm && !roomSummary.isInvited()
|
||||
}
|
||||
RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
|
||||
roomSummary.isFavorite && !roomSummary.isInvited()
|
||||
roomSummary.info.isFavorite && !roomSummary.isInvited()
|
||||
}
|
||||
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 ->
|
||||
roomSummary.name.orEmpty().contains(pattern, ignoreCase = true)
|
||||
roomSummary.info.name.orEmpty().contains(pattern, ignoreCase = true)
|
||||
}
|
||||
RoomListFilter.Invite -> { roomSummary: RoomSummary ->
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,9 +23,8 @@ class RoomSummaryListProcessor(
|
|||
private val roomSummaries: MutableSharedFlow<List<RoomSummary>>,
|
||||
private val roomListService: RoomListServiceInterface,
|
||||
private val coroutineContext: CoroutineContext,
|
||||
private val roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
private val roomSummaryDetailsFactory: RoomSummaryFactory = RoomSummaryFactory(),
|
||||
) {
|
||||
private val roomSummariesByIdentifier = HashMap<String, RoomSummary>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun postUpdate(updates: List<RoomListEntriesUpdate>) {
|
||||
|
|
@ -40,7 +39,7 @@ class RoomSummaryListProcessor(
|
|||
suspend fun rebuildRoomSummaries() {
|
||||
updateRoomSummaries {
|
||||
forEachIndexed { i, summary ->
|
||||
val result = buildAndCacheRoomSummaryForIdentifier(summary.roomId.value)
|
||||
val result = buildRoomSummaryForIdentifier(summary.roomId.value)
|
||||
if (result != null) {
|
||||
this[i] = result
|
||||
}
|
||||
|
|
@ -97,23 +96,17 @@ class RoomSummaryListProcessor(
|
|||
}
|
||||
|
||||
private suspend fun buildSummaryForRoomListEntry(entry: RoomListItem): RoomSummary {
|
||||
return buildAndCacheRoomSummaryForRoomListItem(entry)
|
||||
return buildRoomSummaryForRoomListItem(entry)
|
||||
}
|
||||
|
||||
private suspend fun buildAndCacheRoomSummaryForIdentifier(identifier: String): RoomSummary? {
|
||||
val builtRoomSummary = roomListService.roomOrNull(identifier)?.use { roomListItem ->
|
||||
buildAndCacheRoomSummaryForRoomListItem(roomListItem)
|
||||
private suspend fun buildRoomSummaryForIdentifier(identifier: String): RoomSummary? {
|
||||
return roomListService.roomOrNull(identifier)?.use { roomListItem ->
|
||||
buildRoomSummaryForRoomListItem(roomListItem)
|
||||
}
|
||||
if (builtRoomSummary == null) {
|
||||
roomSummariesByIdentifier.remove(identifier)
|
||||
}
|
||||
return builtRoomSummary
|
||||
}
|
||||
|
||||
private suspend fun buildAndCacheRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary {
|
||||
val builtRoomSummary = roomSummaryDetailsFactory.create(roomListItem = roomListItem)
|
||||
roomSummariesByIdentifier[builtRoomSummary.roomId.value] = builtRoomSummary
|
||||
return builtRoomSummary
|
||||
private suspend fun buildRoomSummaryForRoomListItem(roomListItem: RoomListItem): RoomSummary {
|
||||
return roomSummaryDetailsFactory.create(roomListItem = roomListItem)
|
||||
}
|
||||
|
||||
private suspend fun updateRoomSummaries(block: suspend MutableList<RoomSummary>.() -> Unit) = withContext(coroutineContext) {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ class MatrixRoomInfoMapperTest {
|
|||
).toImmutableList(),
|
||||
pinnedEventIds = listOf(AN_EVENT_ID).toPersistentList(),
|
||||
creator = A_USER_ID,
|
||||
isMarkedUnread = false,
|
||||
numUnreadMessages = 12L,
|
||||
numUnreadNotifications = 13L,
|
||||
numUnreadMentions = 14L,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -174,6 +178,10 @@ class MatrixRoomInfoMapperTest {
|
|||
heroes = emptyList<MatrixUser>().toImmutableList(),
|
||||
pinnedEventIds = emptyList<EventId>().toPersistentList(),
|
||||
creator = null,
|
||||
isMarkedUnread = true,
|
||||
numUnreadMessages = 12L,
|
||||
numUnreadNotifications = 13L,
|
||||
numUnreadMentions = 14L,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.FakeMatrixClient
|
||||
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.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
|
|
@ -29,7 +29,7 @@ import org.junit.Test
|
|||
class DefaultJoinRoomTest {
|
||||
@Test
|
||||
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 joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
|
||||
val roomResult = FakeMatrixRoom()
|
||||
|
|
@ -64,7 +64,7 @@ class DefaultJoinRoomTest {
|
|||
|
||||
@Test
|
||||
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 joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
|
||||
val roomResult = FakeMatrixRoom()
|
||||
|
|
@ -100,7 +100,7 @@ class DefaultJoinRoomTest {
|
|||
|
||||
@Test
|
||||
fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest {
|
||||
val roomSummary = aRoomSummaryFilled()
|
||||
val roomSummary = aRoomSummary()
|
||||
val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) }
|
||||
val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List<String> -> Result.success(roomSummary) }
|
||||
val roomResult = FakeMatrixRoom()
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ import org.junit.Test
|
|||
|
||||
class RoomListFilterTest {
|
||||
private val regularRoom = aRoomSummary(
|
||||
isDm = false
|
||||
isDirect = false,
|
||||
)
|
||||
private val dmRoom = aRoomSummary(
|
||||
isDm = true
|
||||
isDirect = true,
|
||||
activeMembersCount = 2
|
||||
)
|
||||
private val favoriteRoom = aRoomSummary(
|
||||
isFavorite = true
|
||||
|
|
|
|||
|
|
@ -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_3
|
||||
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.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
|
@ -40,7 +39,7 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
fun `PushBack adds a new entry at the end of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
summaries.value = listOf(aRoomSummary())
|
||||
val processor = createProcessor()
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushBack(FakeRustRoomListItem(A_ROOM_ID_2))))
|
||||
|
||||
|
|
@ -50,7 +49,7 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
fun `PushFront inserts a new entry at the start of the list`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
summaries.value = listOf(aRoomSummary())
|
||||
val processor = createProcessor()
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.PushFront(FakeRustRoomListItem(A_ROOM_ID_2))))
|
||||
|
||||
|
|
@ -60,7 +59,7 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
fun `Set replaces an entry at some index`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
summaries.value = listOf(aRoomSummary())
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
|
|
@ -72,7 +71,7 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
fun `Insert inserts a new entry at the provided index`() = runTest {
|
||||
summaries.value = listOf(aRoomSummaryFilled())
|
||||
summaries.value = listOf(aRoomSummary())
|
||||
val processor = createProcessor()
|
||||
val index = 0
|
||||
|
||||
|
|
@ -84,7 +83,10 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
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 index = 0
|
||||
|
||||
|
|
@ -96,7 +98,10 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
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 index = 0
|
||||
|
||||
|
|
@ -108,7 +113,10 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
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 index = 0
|
||||
|
||||
|
|
@ -120,7 +128,10 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
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()
|
||||
|
||||
processor.postUpdate(listOf(RoomListEntriesUpdate.Clear))
|
||||
|
|
@ -130,7 +141,10 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
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 index = 0
|
||||
|
||||
|
|
@ -142,7 +156,10 @@ class RoomSummaryListProcessorTest {
|
|||
|
||||
@Test
|
||||
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 index = 0
|
||||
|
||||
|
|
@ -156,6 +173,6 @@ class RoomSummaryListProcessorTest {
|
|||
summaries,
|
||||
FakeRustRoomListService(),
|
||||
coroutineContext = StandardTestDispatcher(testScheduler),
|
||||
roomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
|
||||
roomSummaryDetailsFactory = RoomSummaryFactory(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.room.InvitedRoom
|
||||
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.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.preview.RoomPreview
|
||||
|
|
@ -118,8 +117,8 @@ class FakeMatrixClient(
|
|||
var knockRoomLambda: (RoomId) -> Result<Unit> = {
|
||||
Result.success(Unit)
|
||||
}
|
||||
var getRoomInfoFlowLambda = { _: RoomId ->
|
||||
flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
|
||||
var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
|
||||
flowOf<Optional<RoomSummary>>(Optional.empty())
|
||||
}
|
||||
var logoutLambda: (Boolean, Boolean) -> String? = { _, _ ->
|
||||
null
|
||||
|
|
@ -316,7 +315,7 @@ class FakeMatrixClient(
|
|||
return Result.success(visitedRoomsId)
|
||||
}
|
||||
|
||||
override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId)
|
||||
override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomSummaryFlowLambda(roomIdOrAlias)
|
||||
|
||||
var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean ->
|
||||
// no-op
|
||||
|
|
|
|||
|
|
@ -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.notificationsettings.NotificationSettingsService
|
||||
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.MatrixRoom
|
||||
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.MessageEventType
|
||||
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.draft.ComposerDraft
|
||||
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.timeline.ReceiptType
|
||||
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.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_NAME
|
||||
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.notificationsettings.FakeNotificationSettingsService
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
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.flow.Flow
|
||||
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(
|
||||
ban = 50,
|
||||
invite = 0,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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.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.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.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 io.element.android.libraries.matrix.test.A_USER_ID
|
||||
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(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String = A_ROOM_NAME,
|
||||
isDirect: Boolean = false,
|
||||
avatarUrl: String? = null,
|
||||
fun aRoomSummary(
|
||||
info: MatrixRoomInfo = aRoomInfo(),
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
numUnreadMentions: Int = 0,
|
||||
numUnreadMessages: Int = 0,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||
) = aRoomSummary(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
isDirect = isDirect,
|
||||
avatarUrl = avatarUrl,
|
||||
) = RoomSummary(
|
||||
info = info,
|
||||
lastMessage = lastMessage,
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
notificationMode = notificationMode,
|
||||
currentUserMembership = currentUserMembership,
|
||||
)
|
||||
|
||||
fun aRoomSummary(
|
||||
roomId: RoomId = A_ROOM_ID,
|
||||
name: String? = A_ROOM_NAME,
|
||||
isDirect: Boolean = false,
|
||||
rawName: String? = A_ROOM_RAW_NAME,
|
||||
topic: String? = A_ROOM_TOPIC,
|
||||
avatarUrl: String? = null,
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
numUnreadMentions: Int = 0,
|
||||
numUnreadMessages: Int = 0,
|
||||
numUnreadNotifications: Int = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
inviter: RoomMember? = null,
|
||||
isDirect: Boolean = false,
|
||||
isPublic: Boolean = true,
|
||||
isSpace: Boolean = false,
|
||||
isTombstoned: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
canonicalAlias: RoomAlias? = null,
|
||||
alternativeAliases: List<RoomAlias> = emptyList(),
|
||||
hasRoomCall: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
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,
|
||||
lastMessage: RoomMessage? = aRoomMessage(),
|
||||
) = RoomSummary(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
isDirect = isDirect,
|
||||
avatarUrl = avatarUrl,
|
||||
info = MatrixRoomInfo(
|
||||
id = roomId,
|
||||
name = name,
|
||||
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,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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.Surface
|
||||
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.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun SelectedRoom(
|
||||
roomSummary: RoomSummary,
|
||||
onRemoveRoom: (RoomSummary) -> Unit,
|
||||
roomInfo: SelectRoomInfo,
|
||||
onRemoveRoom: (SelectRoomInfo) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
|
|
@ -53,14 +53,12 @@ fun SelectedRoom(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CompositeAvatar(
|
||||
avatarData = roomSummary.getAvatarData(size = AvatarSize.SelectedRoom),
|
||||
heroes = roomSummary.heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.SelectedRoom)
|
||||
}.toImmutableList()
|
||||
avatarData = roomInfo.getAvatarData(AvatarSize.SelectedRoom),
|
||||
heroes = roomInfo.heroes.map { it.getAvatarData(AvatarSize.SelectedRoom) }.toImmutableList(),
|
||||
)
|
||||
Text(
|
||||
// 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,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
|
|
@ -69,14 +67,14 @@ fun SelectedRoom(
|
|||
Surface(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(20.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
.clickable(
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onRemoveRoom(roomSummary) }
|
||||
),
|
||||
.clip(CircleShape)
|
||||
.size(20.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
.clickable(
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onRemoveRoom(roomInfo) }
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Close(),
|
||||
|
|
@ -91,10 +89,10 @@ fun SelectedRoom(
|
|||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun SelectedRoomPreview(
|
||||
@PreviewParameter(RoomSummaryDetailsProvider::class) roomSummary: RoomSummary
|
||||
@PreviewParameter(SelectRoomInfoProvider::class) roomInfo: SelectRoomInfo
|
||||
) = ElementPreview {
|
||||
SelectedRoom(
|
||||
roomSummary = roomSummary,
|
||||
roomInfo = roomInfo,
|
||||
onRemoveRoom = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.fetch.Fetcher
|
||||
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
|
||||
|
||||
internal class AvatarDataFetcherFactory(
|
||||
private val context: Context,
|
||||
private val client: MatrixClient
|
||||
) : Fetcher.Factory<AvatarData> {
|
||||
override fun create(
|
||||
|
|
@ -24,7 +22,6 @@ internal class AvatarDataFetcherFactory(
|
|||
imageLoader: ImageLoader
|
||||
): Fetcher {
|
||||
return CoilMediaFetcher(
|
||||
scalingFunction = { context.resources.displayMetrics.density * it },
|
||||
mediaLoader = client.mediaLoader,
|
||||
mediaData = data.toMediaRequestData(),
|
||||
options = options
|
||||
|
|
|
|||
|
|
@ -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.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 {
|
||||
return MediaRequestData(
|
||||
source = url?.let { MediaSource(it) },
|
||||
kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong())
|
||||
kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,8 @@ import okio.Buffer
|
|||
import okio.Path.Companion.toOkioPath
|
||||
import timber.log.Timber
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
internal class CoilMediaFetcher(
|
||||
private val scalingFunction: (Float) -> Float,
|
||||
private val mediaLoader: MatrixMediaLoader,
|
||||
private val mediaData: MediaRequestData,
|
||||
private val options: Options
|
||||
|
|
@ -74,8 +72,8 @@ internal class CoilMediaFetcher(
|
|||
private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? {
|
||||
return mediaLoader.loadMediaThumbnail(
|
||||
source = mediaSource,
|
||||
width = scalingFunction(kind.width.toFloat()).roundToLong(),
|
||||
height = scalingFunction(kind.height.toFloat()).roundToLong(),
|
||||
width = kind.width,
|
||||
height = kind.height,
|
||||
).map { byteArray ->
|
||||
byteArray.asSourceResult(options)
|
||||
}.onFailure {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ class DefaultLoggedInImageLoaderFactory @Inject constructor(
|
|||
}
|
||||
add(AvatarDataKeyer())
|
||||
add(MediaRequestDataKeyer())
|
||||
add(AvatarDataFetcherFactory(context, matrixClient))
|
||||
add(MediaRequestDataFetcherFactory(context, matrixClient))
|
||||
add(AvatarDataFetcherFactory(matrixClient))
|
||||
add(MediaRequestDataFetcherFactory(matrixClient))
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@
|
|||
|
||||
package io.element.android.libraries.matrix.ui.media
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.Options
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
|
||||
internal class MediaRequestDataFetcherFactory(
|
||||
private val context: Context,
|
||||
private val client: MatrixClient
|
||||
) : Fetcher.Factory<MediaRequestData> {
|
||||
override fun create(
|
||||
|
|
@ -23,7 +21,6 @@ internal class MediaRequestDataFetcherFactory(
|
|||
imageLoader: ImageLoader
|
||||
): Fetcher {
|
||||
return CoilMediaFetcher(
|
||||
scalingFunction = { context.resources.displayMetrics.density * it },
|
||||
mediaLoader = client.mediaLoader,
|
||||
mediaData = data,
|
||||
options = options
|
||||
|
|
|
|||
|
|
@ -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.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(
|
||||
id = roomId.value,
|
||||
fun MatrixRoomInfo.getAvatarData(size: AvatarSize) = AvatarData(
|
||||
id = id.value,
|
||||
name = name,
|
||||
url = avatarUrl,
|
||||
size = size,
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -7,9 +9,10 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.mediapickers.api"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import extension.setupAnvil
|
||||
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
|
|
@ -7,9 +9,10 @@
|
|||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
alias(libs.plugins.anvil)
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.mediapickers.test"
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
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.push.api.notifications.NotificationBitmapLoader
|
||||
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? {
|
||||
return try {
|
||||
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())
|
||||
.build()
|
||||
val result = imageLoader.execute(imageRequest)
|
||||
|
|
@ -73,7 +74,7 @@ class DefaultNotificationBitmapLoader @Inject constructor(
|
|||
private suspend fun loadUserIcon(path: String, imageLoader: ImageLoader): IconCompat? {
|
||||
return try {
|
||||
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())
|
||||
.build()
|
||||
val result = imageLoader.execute(imageRequest)
|
||||
|
|
|
|||
|
|
@ -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_TIMESTAMP
|
||||
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.push.impl.notifications.factories.createNotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
|
||||
|
|
@ -84,7 +85,7 @@ class DefaultRoomGroupMessageCreatorTest {
|
|||
expectedCoilRequests = listOf(
|
||||
MediaRequestData(
|
||||
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(
|
||||
MediaRequestData(
|
||||
source = MediaSource(url = A_USER_AVATAR_1),
|
||||
kind = MediaRequestData.Kind.Thumbnail(1024)
|
||||
kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
|
||||
),
|
||||
MediaRequestData(
|
||||
source = MediaSource(url = A_USER_AVATAR_2),
|
||||
kind = MediaRequestData.Kind.Thumbnail(1024)
|
||||
kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
|
||||
),
|
||||
MediaRequestData(
|
||||
source = MediaSource(url = A_ROOM_AVATAR),
|
||||
kind = MediaRequestData.Kind.Thumbnail(1024)
|
||||
kind = MediaRequestData.Kind.Thumbnail(AVATAR_THUMBNAIL_SIZE_IN_PIXEL)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ package io.element.android.libraries.pushproviders.unifiedpush
|
|||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeUnifiedPushNewGatewayHandler(
|
||||
private val handleResult: suspend (String, String, String) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
||||
private val handleResult: (String, String, String) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
||||
) : UnifiedPushNewGatewayHandler {
|
||||
override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> {
|
||||
return handleResult(endpoint, pushGateway, clientSecret)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
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 {
|
||||
data class SetSelectedRoom(val room: RoomSummary) : RoomSelectEvents
|
||||
data class SetSelectedRoom(val room: SelectRoomInfo) : RoomSelectEvents
|
||||
|
||||
// TODO remove to restore multi-selection
|
||||
data object RemoveSelectedRoom : RoomSelectEvents
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.libraries.roomselect.impl
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -20,8 +21,9 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ class RoomSelectPresenter @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun present(): RoomSelectState {
|
||||
var selectedRooms by remember { mutableStateOf(persistentListOf<RoomSummary>()) }
|
||||
var selectedRooms by remember { mutableStateOf(persistentListOf<SelectRoomInfo>()) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -48,9 +50,9 @@ class RoomSelectPresenter @AssistedInject constructor(
|
|||
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 {
|
||||
when {
|
||||
roomSummaryDetailsList.isNotEmpty() -> SearchBarResultState.Results(roomSummaryDetailsList.toImmutableList())
|
||||
|
|
|
|||
|
|
@ -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.RoomListFilter
|
||||
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.ui.model.SelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.toSelectRoomInfo
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -38,11 +39,12 @@ class RoomSelectSearchDataSource @Inject constructor(
|
|||
source = RoomList.Source.All,
|
||||
)
|
||||
|
||||
val roomSummaries: Flow<PersistentList<RoomSummary>> = roomList.filteredSummaries
|
||||
val roomInfoList: Flow<PersistentList<SelectRoomInfo>> = roomList.filteredSummaries
|
||||
.map { 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
|
||||
.map { roomSummary -> roomSummary.toSelectRoomInfo() }
|
||||
.toPersistentList()
|
||||
}
|
||||
.flowOn(coroutineDispatchers.computation)
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@
|
|||
package io.element.android.libraries.roomselect.impl
|
||||
|
||||
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 kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class RoomSelectState(
|
||||
val mode: RoomSelectMode,
|
||||
val resultState: SearchBarResultState<ImmutableList<RoomSummary>>,
|
||||
val resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>>,
|
||||
val query: String,
|
||||
val isSearchActive: Boolean,
|
||||
val selectedRooms: ImmutableList<RoomSummary>,
|
||||
val selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||
val eventSink: (RoomSelectEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.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.ui.components.aRoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.ui.components.aSelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.SelectRoomInfo
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -32,7 +32,7 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
|||
resultState = SearchBarResultState.Results(aRoomSelectRoomList()),
|
||||
query = "Test",
|
||||
isSearchActive = true,
|
||||
selectedRooms = persistentListOf(aRoomSummaryDetails(roomId = RoomId("!room2:domain")))
|
||||
selectedRooms = aRoomSelectRoomList().subList(0, 1),
|
||||
),
|
||||
aRoomSelectState(
|
||||
mode = RoomSelectMode.Share,
|
||||
|
|
@ -43,10 +43,10 @@ open class RoomSelectStateProvider : PreviewParameterProvider<RoomSelectState> {
|
|||
|
||||
private fun aRoomSelectState(
|
||||
mode: RoomSelectMode = RoomSelectMode.Forward,
|
||||
resultState: SearchBarResultState<ImmutableList<RoomSummary>> = SearchBarResultState.Initial(),
|
||||
resultState: SearchBarResultState<ImmutableList<SelectRoomInfo>> = SearchBarResultState.Initial(),
|
||||
query: String = "",
|
||||
isSearchActive: Boolean = false,
|
||||
selectedRooms: ImmutableList<RoomSummary> = persistentListOf(),
|
||||
selectedRooms: ImmutableList<SelectRoomInfo> = persistentListOf(),
|
||||
) = RoomSelectState(
|
||||
mode = mode,
|
||||
resultState = resultState,
|
||||
|
|
@ -57,14 +57,16 @@ private fun aRoomSelectState(
|
|||
)
|
||||
|
||||
private fun aRoomSelectRoomList() = persistentListOf(
|
||||
aRoomSummaryDetails(),
|
||||
aRoomSummaryDetails(
|
||||
aSelectRoomInfo(
|
||||
roomId = RoomId("!room1:domain"),
|
||||
name = "Room with name",
|
||||
),
|
||||
aSelectRoomInfo(
|
||||
roomId = RoomId("!room2:domain"),
|
||||
name = "Room with alias",
|
||||
canonicalAlias = RoomAlias("#alias:example.org"),
|
||||
),
|
||||
aRoomSummaryDetails(
|
||||
aSelectRoomInfo(
|
||||
roomId = RoomId("!room3:domain"),
|
||||
name = null,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.TopAppBar
|
||||
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.model.SelectRoomInfo
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.roomselect.api.RoomSelectMode
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
|
@ -65,13 +65,13 @@ fun RoomSelectView(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onRoomRemoved(roomSummary: RoomSummary) {
|
||||
fun onRoomRemoved(roomInfo: SelectRoomInfo) {
|
||||
// TODO toggle selection when multi-selection is enabled
|
||||
state.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<RoomSummary>) {
|
||||
fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList<SelectRoomInfo>) {
|
||||
if (isForwarding) return
|
||||
SelectedRooms(
|
||||
selectedRooms = selectedRooms,
|
||||
|
|
@ -185,8 +185,8 @@ fun RoomSelectView(
|
|||
|
||||
@Composable
|
||||
private fun SelectedRooms(
|
||||
selectedRooms: ImmutableList<RoomSummary>,
|
||||
onRemoveRoom: (RoomSummary) -> Unit,
|
||||
selectedRooms: ImmutableList<SelectRoomInfo>,
|
||||
onRemoveRoom: (SelectRoomInfo) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyRow(
|
||||
|
|
@ -194,29 +194,29 @@ private fun SelectedRooms(
|
|||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(32.dp)
|
||||
) {
|
||||
items(selectedRooms, key = { it.roomId.value }) { roomSummary ->
|
||||
SelectedRoom(roomSummary = roomSummary, onRemoveRoom = onRemoveRoom)
|
||||
items(selectedRooms, key = { it.roomId.value }) { selectRoomInfo ->
|
||||
SelectedRoom(roomInfo = selectRoomInfo, onRemoveRoom = onRemoveRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoomSummaryView(
|
||||
summary: RoomSummary,
|
||||
roomInfo: SelectRoomInfo,
|
||||
isSelected: Boolean,
|
||||
onSelection: (RoomSummary) -> Unit,
|
||||
onSelection: (SelectRoomInfo) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { onSelection(summary) }
|
||||
.clickable { onSelection(roomInfo) }
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.heightIn(56.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CompositeAvatar(
|
||||
avatarData = summary.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
|
||||
heroes = summary.heroes.map { user ->
|
||||
avatarData = roomInfo.getAvatarData(size = AvatarSize.RoomSelectRoomListItem),
|
||||
heroes = roomInfo.heroes.map { user ->
|
||||
user.getAvatarData(size = AvatarSize.RoomSelectRoomListItem)
|
||||
}.toPersistentList()
|
||||
)
|
||||
|
|
@ -228,14 +228,14 @@ private fun RoomSummaryView(
|
|||
// Name
|
||||
Text(
|
||||
style = ElementTheme.typography.fontBodyLgRegular,
|
||||
text = summary.name ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic.takeIf { summary.name == null },
|
||||
text = roomInfo.name ?: stringResource(id = CommonStrings.common_no_room_name),
|
||||
fontStyle = FontStyle.Italic.takeIf { roomInfo.name == null },
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Alias
|
||||
summary.canonicalAlias?.let { alias ->
|
||||
roomInfo.canonicalAlias?.let { alias ->
|
||||
Text(
|
||||
text = alias.value,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
|
|
@ -245,7 +245,7 @@ private fun RoomSummaryView(
|
|||
)
|
||||
}
|
||||
}
|
||||
RadioButton(selected = isSelected, onClick = { onSelection(summary) })
|
||||
RadioButton(selected = isSelected, onClick = { onSelection(roomInfo) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.test.room.aRoomSummary
|
||||
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.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
|
|
@ -58,8 +59,9 @@ class RoomSelectPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - update query`() = runTest {
|
||||
val roomSummary = aRoomSummary()
|
||||
val roomListService = FakeRoomListService().apply {
|
||||
postAllRooms(listOf(aRoomSummary()))
|
||||
postAllRooms(listOf(roomSummary))
|
||||
}
|
||||
val presenter = createRoomSelectPresenter(
|
||||
roomListService = roomListService
|
||||
|
|
@ -68,19 +70,10 @@ class RoomSelectPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
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
|
||||
val result = (awaitItem().resultState as SearchBarResultState.Results).results.map { roomSummary ->
|
||||
roomSummary.copy(
|
||||
lastMessage = roomSummary.lastMessage!!.copy(
|
||||
event = roomSummary.lastMessage!!.event.copy(
|
||||
debugInfoProvider = expectedRoomSummary.lastMessage!!.event.debugInfoProvider,
|
||||
messageShieldProvider = expectedRoomSummary.lastMessage!!.event.messageShieldProvider,
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
assertThat(result).isEqualTo(listOf(expectedRoomSummary))
|
||||
val result = (awaitItem().resultState as SearchBarResultState.Results).results
|
||||
assertThat(result).isEqualTo(listOf(expectedRoomInfo))
|
||||
initialState.eventSink(RoomSelectEvents.ToggleSearchActive)
|
||||
skipItems(1)
|
||||
initialState.eventSink(RoomSelectEvents.UpdateQuery("string not contained"))
|
||||
|
|
@ -99,8 +92,9 @@ class RoomSelectPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - select and remove a room`() = runTest {
|
||||
val roomSummary = aRoomSummary()
|
||||
val roomListService = FakeRoomListService().apply {
|
||||
postAllRooms(listOf(aRoomSummary()))
|
||||
postAllRooms(listOf(roomSummary))
|
||||
}
|
||||
val presenter = createRoomSelectPresenter(
|
||||
roomListService = roomListService,
|
||||
|
|
@ -109,9 +103,9 @@ class RoomSelectPresenterTest {
|
|||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
val summary = aRoomSummary()
|
||||
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(summary))
|
||||
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary))
|
||||
val roomInfo = roomSummary.toSelectRoomInfo()
|
||||
initialState.eventSink(RoomSelectEvents.SetSelectedRoom(roomInfo))
|
||||
assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(roomInfo))
|
||||
initialState.eventSink(RoomSelectEvents.RemoveSelectedRoom)
|
||||
assertThat(awaitItem().selectedRooms).isEmpty()
|
||||
cancel()
|
||||
|
|
|
|||
|
|
@ -8,13 +8,27 @@
|
|||
package io.element.android.libraries.textcomposer.mentions
|
||||
|
||||
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.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
|
||||
@Immutable
|
||||
sealed interface ResolvedSuggestion {
|
||||
data object AtRoom : 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.room.IntentionalMention
|
||||
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.FakePermalinkParser
|
||||
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.MentionSpanProvider
|
||||
import io.element.android.libraries.textcomposer.mentions.ResolvedSuggestion
|
||||
|
|
@ -34,7 +34,7 @@ class MarkdownTextEditorStateTest {
|
|||
@Test
|
||||
fun `insertMention - room alias - getMentions return empty list`() {
|
||||
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 mentionSpanProvider = aMentionSpanProvider()
|
||||
state.insertSuggestion(suggestion, mentionSpanProvider, permalinkBuilder)
|
||||
|
|
@ -46,7 +46,7 @@ class MarkdownTextEditorStateTest {
|
|||
val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply {
|
||||
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 permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.failure(IllegalStateException("Failed")) })
|
||||
val mentionSpanProvider = aMentionSpanProvider(permalinkParser = permalinkParser)
|
||||
|
|
@ -58,7 +58,7 @@ class MarkdownTextEditorStateTest {
|
|||
val state = MarkdownTextEditorState(initialText = "Hello #", initialFocus = true).apply {
|
||||
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 permalinkBuilder = FakePermalinkBuilder(permalinkForRoomAliasLambda = { Result.success("https://matrix.to/#/${A_ROOM_ALIAS.value}") })
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,5 @@ dependencies {
|
|||
implementation(libs.autonomousapps.dependencyanalysis.plugin)
|
||||
implementation(libs.anvil.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
Loading…
Add table
Add a link
Reference in a new issue