diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 8358eae14b..223a273b68 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -5,7 +5,7 @@ on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
- name: Danger
+ name: Danger main check
steps:
- uses: actions/checkout@v3
- run: |
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index 86e823f335..c945559c8d 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -12,6 +12,14 @@ env:
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon --warn
jobs:
+ checkScript:
+ name: Search for forbidden patterns
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Run code quality check suite
+ run: ./tools/check/check_code_quality.sh
+
check:
name: Project Check Suite
runs-on: ubuntu-latest
diff --git a/.github/workflows/triage-labelled.yml b/.github/workflows/triage-labelled.yml
index d7951a012b..ec69eae6bb 100644
--- a/.github/workflows/triage-labelled.yml
+++ b/.github/workflows/triage-labelled.yml
@@ -43,9 +43,21 @@ jobs:
name: Add labelled issues to QA project
runs-on: ubuntu-latest
if: >
- contains(github.event.issue.labels.*.name, 'Team: QA')
+ contains(github.event.issue.labels.*.name, 'Team: QA') ||
+ contains(github.event.issue.labels.*.name, 'X-Needs-Signoff')
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/vector-im/projects/69
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+
+ signoff:
+ name: Add labelled issues to signoff project
+ runs-on: ubuntu-latest
+ if: >
+ contains(github.event.issue.labels.*.name, 'X-Needs-Signoff')
+ steps:
+ - uses: actions/add-to-project@main
+ with:
+ project-url: https://github.com/orgs/vector-im/projects/89
+ github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 345da6fb97..cde20b0085 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,7 +48,6 @@ captures/
.idea/navEditor.xml
.idea/tasks.xml
.idea/workspace.xml
-.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000000..cdef735570
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000000..79ee123c2b
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml
new file mode 100644
index 0000000000..216a8cbd20
--- /dev/null
+++ b/.idea/dictionaries/shared.xml
@@ -0,0 +1,8 @@
+
+
+
+ backstack
+ textfields
+
+
+
diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts
index 6712bddee0..e592d98b2e 100644
--- a/anvilcodegen/build.gradle.kts
+++ b/anvilcodegen/build.gradle.kts
@@ -23,7 +23,7 @@ dependencies {
implementation(projects.anvilannotations)
api(libs.anvil.compiler.api)
implementation(libs.anvil.compiler.utils)
- implementation("com.squareup:kotlinpoet:1.13.2")
+ implementation("com.squareup:kotlinpoet:1.14.2")
implementation(libs.dagger)
compileOnly("com.google.auto.service:auto-service-annotations:1.1.0")
kapt("com.google.auto.service:auto-service:1.1.0")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 22a63b60c9..f5eacd03a4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -33,7 +33,7 @@
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true"
- android:launchMode="singleInstance"
+ android:launchMode="singleTop"
android:theme="@style/Theme.ElementX.Splash"
android:windowSoftInputMode="adjustResize">
@@ -49,6 +49,14 @@
android:host="open"
android:scheme="elementx" />
+
+
+
+
+
+
+
+
{
- roomDetailsEntryPoint.createNode(this, buildContext, emptyList())
+ val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomDetails)
+ roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
+ }
+ is NavTarget.RoomMemberDetails -> {
+ val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId))
+ roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
}
}
}
@@ -134,6 +144,9 @@ class RoomFlowNode @AssistedInject constructor(
@Parcelize
object RoomDetails : NavTarget
+
+ @Parcelize
+ data class RoomMemberDetails(val userId: UserId) : NavTarget
}
private val timeline = inputs.room.timeline()
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index 32f45bd77d..5447327152 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -38,14 +38,17 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.di.MatrixClientsHolder
+import io.element.android.appnav.intent.IntentResolver
+import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.api.oidc.OidcActionFlow
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.deeplink.DeeplinkData
-import io.element.android.libraries.deeplink.DeeplinkParser
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -65,7 +68,8 @@ class RootFlowNode @AssistedInject constructor(
private val matrixClientsHolder: MatrixClientsHolder,
private val presenter: RootPresenter,
private val bugReportEntryPoint: BugReportEntryPoint,
- private val deeplinkParser: DeeplinkParser,
+ private val intentResolver: IntentResolver,
+ private val oidcActionFlow: OidcActionFlow,
) :
BackstackNode(
backstack = BackStack(
@@ -204,8 +208,11 @@ class RootFlowNode @AssistedInject constructor(
}
suspend fun handleIntent(intent: Intent) {
- deeplinkParser.getFromIntent(intent)
- ?.let { navigateTo(it) }
+ val resolvedIntent = intentResolver.resolve(intent) ?: return
+ when (resolvedIntent) {
+ is ResolvedIntent.Navigation -> navigateTo(resolvedIntent.deeplinkData)
+ is ResolvedIntent.Oidc -> onOidcAction(resolvedIntent.oidcAction)
+ }
}
private suspend fun navigateTo(deeplinkData: DeeplinkData) {
@@ -223,6 +230,10 @@ class RootFlowNode @AssistedInject constructor(
}
}
+ private fun onOidcAction(oidcAction: OidcAction) {
+ oidcActionFlow.post(oidcAction)
+ }
+
private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode {
return attachChild {
backstack.newRoot(NavTarget.LoggedInFlow(sessionId))
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
new file mode 100644
index 0000000000..b567395c1e
--- /dev/null
+++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.appnav.intent
+
+import android.content.Intent
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.api.oidc.OidcIntentResolver
+import io.element.android.libraries.deeplink.DeeplinkData
+import io.element.android.libraries.deeplink.DeeplinkParser
+import timber.log.Timber
+import javax.inject.Inject
+
+sealed interface ResolvedIntent {
+ data class Navigation(val deeplinkData: DeeplinkData) : ResolvedIntent
+ data class Oidc(val oidcAction: OidcAction) : ResolvedIntent
+}
+
+class IntentResolver @Inject constructor(
+ private val deeplinkParser: DeeplinkParser,
+ private val oidcIntentResolver: OidcIntentResolver
+) {
+ fun resolve(intent: Intent): ResolvedIntent? {
+ val deepLinkData = deeplinkParser.getFromIntent(intent)
+ if (deepLinkData != null) return ResolvedIntent.Navigation(deepLinkData)
+
+ val oidcAction = oidcIntentResolver.resolve(intent)
+ if (oidcAction != null) return ResolvedIntent.Oidc(oidcAction)
+
+ // Unknown intent
+ Timber.w("Unknown intent")
+ return null
+ }
+}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt
index a151be665c..daff06af16 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt
@@ -60,7 +60,12 @@ class RoomFlowNodeTest {
var nodeId: String? = null
- override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node {
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: RoomDetailsEntryPoint.Inputs,
+ plugins: List
+ ): Node {
return node(buildContext) {}.also {
nodeId = it.id
}
diff --git a/changelog.d/242.feature b/changelog.d/242.feature
new file mode 100644
index 0000000000..fd82c425d3
--- /dev/null
+++ b/changelog.d/242.feature
@@ -0,0 +1 @@
+[Create and join rooms] Update room properties from room details
diff --git a/changelog.d/424.feature b/changelog.d/424.feature
new file mode 100644
index 0000000000..1fb267318e
--- /dev/null
+++ b/changelog.d/424.feature
@@ -0,0 +1 @@
+[Create and join rooms] Show a notice for MXIDs that don't resolve when searching for users to invite
diff --git a/changelog.d/480.feature b/changelog.d/480.feature
new file mode 100644
index 0000000000..e64dcc1f33
--- /dev/null
+++ b/changelog.d/480.feature
@@ -0,0 +1 @@
+Open room member details when tapping on a user in the timeline
diff --git a/docs/oidc.md b/docs/oidc.md
new file mode 100644
index 0000000000..5f9e70268d
--- /dev/null
+++ b/docs/oidc.md
@@ -0,0 +1,47 @@
+This file contains some rough notes about Oidc implementation, with some examples of actual data.
+
+[ios implementation](https://github.com/vector-im/element-x-ios/compare/develop...doug/oidc-temp)
+
+Rust sdk branch: https://github.com/matrix-org/matrix-rust-sdk/tree/oidc-ffi
+
+Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0
+
+Server list: https://github.com/vector-im/oidc-playground
+
+Metadata iOS: (from https://github.com/vector-im/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)
+
+clientName: InfoPlistReader.main.bundleDisplayName,
+redirectUri: "io.element:/callback",
+clientUri: "https://element.io",
+tosUri: "https://element.io/user-terms-of-service",
+policyUri: "https://element.io/privacy"
+
+
+Android:
+clientName = "Element",
+redirectUri = "io.element:/callback",
+clientUri = "https://element.io",
+tosUri = "https://element.io/user-terms-of-service",
+policyUri = "https://element.io/privacy"
+
+
+Example of OidcData (from presentUrl callback):
+url: https://auth-oidc.lab.element.dev/authorize?response_type=code&client_id=01GYCAGG3PA70CJ97ZVP0WFJY3&redirect_uri=io.element%3A%2Fcallback&scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG&state=ex6mNJVFZ5jn9wL8&nonce=NZ93DOyIGQd9exPQ&code_challenge_method=S256&code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U&prompt=consent
+
+Formatted url:
+https://auth-oidc.lab.element.dev/authorize?
+ response_type=code&
+ client_id=01GYCAGG3PA70CJ97ZVP0WFJY3&
+ redirect_uri=io.element%3A%2Fcallback&
+ scope=openid+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Aapi%3A*+urn%3Amatrix%3Aorg.matrix.msc2967.client%3Adevice%3AYAgcPW4mcG&
+ state=ex6mNJVFZ5jn9wL8&
+ nonce=NZ93DOyIGQd9exPQ&
+ code_challenge_method=S256&
+ code_challenge=FFRcPALNSPCh-ZgpyTRFu_h8NZJVncfvihbfT9CyX8U&
+ prompt=consent
+
+state: ex6mNJVFZ5jn9wL8
+
+
+Oidc client example: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/examples/oidc_cli/src/main.rs
+Oidc sdk doc: https://github.com/matrix-org/matrix-rust-sdk/blob/39ad8a46801fb4317a777ebf895822b3675b709c/crates/matrix-sdk/src/oidc.rs
diff --git a/docs/pull_request.md b/docs/pull_request.md
index 323734bcaa..6144dd0d92 100644
--- a/docs/pull_request.md
+++ b/docs/pull_request.md
@@ -79,7 +79,7 @@ Exceptions can occur:
**Important notice 1:** Releases are created from the `develop` branch. So `develop` branch should always contain a "releasable" source code. So when a feature is being implemented with several PRs, it has to be disabled by default (using a feature flag for instance), until the feature is fully implemented. A last PR to enable the feature can then be created.
-**Important notice 2:** Database migration: some developers and some people from the community are using the nightly build from `develop`. Multiple database migrations should be properly handled for them. This is OK to have multiple migrations between 2 releases, this is not OK to add steps to the pending database migration on `develop`. So for instance `develop` users will migrate from version 11 to version 12, then 13, then 14, and `main` users will do all those steps after they get the app upgrade.
+**Important notice 2:** Database migration: some developers and some people from the community are using the nightly build from `develop`. Multiple database migrations should be properly handled for them. It is OK to have multiple migrations between 2 releases, It is not OK to add steps to existing database migrations on `develop`. So for instance `develop` users will migrate from version 11 to version 12, then 13, then 14, and `main` users will do all those steps after they get the app upgrade.
##### PR Review Assignment
diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts
index 65c43e8fc3..f203501315 100644
--- a/features/createroom/impl/build.gradle.kts
+++ b/features/createroom/impl/build.gradle.kts
@@ -48,8 +48,8 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.mediapickers.api)
implementation(projects.libraries.mediaupload.api)
- implementation(libs.coil.compose)
implementation(projects.libraries.usersearch.impl)
+ implementation(libs.coil.compose)
api(projects.features.createroom.api)
testImplementation(libs.test.junit)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
index cf13768a82..7dd0ce3e3c 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt
@@ -22,40 +22,49 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
-import io.element.android.libraries.designsystem.preview.ElementPreviewDark
-import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow
+import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow
import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.usersearch.api.UserSearchResult
@Composable
fun SearchMultipleUsersResultItem(
- matrixUser: MatrixUser,
+ searchResult: UserSearchResult,
isUserSelected: Boolean,
modifier: Modifier = Modifier,
onCheckedChange: (Boolean) -> Unit = {},
) {
- CheckableMatrixUserRow(
- checked = isUserSelected,
- modifier = modifier,
- matrixUser = matrixUser,
- avatarSize = AvatarSize.Custom(36.dp),
- onCheckedChange = onCheckedChange,
- )
+ if (searchResult.isUnresolved) {
+ CheckableUnresolvedUserRow(
+ checked = isUserSelected,
+ modifier = modifier,
+ avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
+ id = searchResult.matrixUser.userId.value,
+ onCheckedChange = onCheckedChange,
+ )
+ } else {
+ CheckableMatrixUserRow(
+ checked = isUserSelected,
+ modifier = modifier,
+ matrixUser = searchResult.matrixUser,
+ avatarSize = AvatarSize.Custom(36.dp),
+ onCheckedChange = onCheckedChange,
+ )
+ }
}
@Preview
@Composable
-internal fun SearchMultipleUsersResultItemLightPreview() = ElementPreviewLight { ContentToPreview() }
-
-@Preview
-@Composable
-internal fun SearchMultipleUsersResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
+internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Column {
- SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = true)
- SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = false)
+ SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false)
+ SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true)
+ SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false)
+ SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true)
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
index 8dcaeb2bdf..f1279cfdec 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt
@@ -17,39 +17,48 @@
package io.element.android.features.createroom.impl.components
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
-import io.element.android.libraries.designsystem.preview.ElementPreviewDark
-import io.element.android.libraries.designsystem.preview.ElementPreviewLight
-import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
+import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow
import io.element.android.libraries.matrix.ui.components.aMatrixUser
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.usersearch.api.UserSearchResult
@Composable
fun SearchSingleUserResultItem(
- matrixUser: MatrixUser,
+ searchResult: UserSearchResult,
modifier: Modifier = Modifier,
onClick: () -> Unit = {},
) {
- MatrixUserRow(
- modifier = modifier.clickable(onClick = onClick),
- matrixUser = matrixUser,
- avatarSize = AvatarSize.Custom(36.dp),
- )
+ if (searchResult.isUnresolved) {
+ UnresolvedUserRow(
+ modifier = modifier.clickable(onClick = onClick),
+ avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)),
+ id = searchResult.matrixUser.userId.value,
+ )
+ } else {
+ MatrixUserRow(
+ modifier = modifier.clickable(onClick = onClick),
+ matrixUser = searchResult.matrixUser,
+ avatarSize = AvatarSize.Custom(36.dp),
+ )
+ }
}
@Preview
@Composable
-internal fun SearchSingleUserResultItemLightPreview() = ElementPreviewLight { ContentToPreview() }
-
-@Preview
-@Composable
-internal fun SearchSingleUserResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() }
+internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview{ ContentToPreview() }
@Composable
private fun ContentToPreview() {
- SearchSingleUserResultItem(matrixUser = aMatrixUser())
+ Column {
+ SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false))
+ SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true))
+ }
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
index be87e1cf51..bbbcd715e5 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt
@@ -29,12 +29,13 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
import io.element.android.libraries.ui.strings.R
+import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.ImmutableList
@Composable
fun SearchUserBar(
query: String,
- state: SearchBarResultState>,
+ state: SearchBarResultState>,
selectedUsers: ImmutableList,
active: Boolean,
isMultiSelectionEnabled: Boolean,
@@ -68,26 +69,26 @@ fun SearchUserBar(
resultHandler = { users ->
LazyColumn {
if (isMultiSelectionEnabled) {
- items(users) { matrixUser ->
+ items(users) { searchResult ->
SearchMultipleUsersResultItem(
modifier = Modifier.fillMaxWidth(),
- matrixUser = matrixUser,
- isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null,
+ searchResult = searchResult,
+ isUserSelected = selectedUsers.find { it.userId == searchResult.matrixUser.userId } != null,
onCheckedChange = { checked ->
if (checked) {
- onUserSelected(matrixUser)
+ onUserSelected(searchResult.matrixUser)
} else {
- onUserDeselected(matrixUser)
+ onUserDeselected(searchResult.matrixUser)
}
}
)
}
} else {
- items(users) { matrixUser ->
+ items(users) { searchResult ->
SearchSingleUserResultItem(
modifier = Modifier.fillMaxWidth(),
- matrixUser = matrixUser,
- onClick = { onUserSelected(matrixUser) }
+ searchResult = searchResult,
+ onClick = { onUserSelected(searchResult.matrixUser) }
)
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
index 9082849954..a020b387cb 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
@@ -17,8 +17,8 @@
package io.element.android.features.createroom.impl.configureroom
import io.element.android.features.createroom.impl.CreateRoomConfig
-import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface ConfigureRoomEvents {
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
index df0013972d..ca714b1e59 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
@@ -27,7 +27,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.execute
@@ -37,6 +36,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
+import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import kotlinx.collections.immutable.toImmutableList
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
index b99d70bb13..2e34f3bda2 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
@@ -16,8 +16,8 @@
package io.element.android.features.createroom.impl.configureroom
+import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.features.createroom.impl.CreateRoomConfig
-import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
index 53d46b03c9..4aae1936a9 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
@@ -17,6 +17,7 @@
package io.element.android.features.createroom.impl.configureroom
import android.net.Uri
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -24,9 +25,11 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
@@ -46,11 +49,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.createroom.impl.R
-import io.element.android.features.createroom.impl.components.Avatar
-import io.element.android.features.createroom.impl.components.LabelledTextField
import io.element.android.features.createroom.impl.components.RoomPrivacyOption
-import io.element.android.features.createroom.impl.configureroom.avatar.AvatarActionListView
import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.designsystem.components.LabelledTextField
import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
@@ -61,7 +62,9 @@ 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.TextButton
import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
import io.element.android.libraries.matrix.ui.components.SelectedUsersList
+import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
import kotlinx.coroutines.launch
import io.element.android.libraries.ui.strings.R as StringR
@@ -105,54 +108,48 @@ fun ConfigureRoomView(
)
}
) { padding ->
- LazyColumn(
+ Column(
modifier = Modifier
.padding(padding)
+ .imePadding()
+ .verticalScroll(rememberScrollState())
.consumeWindowInsets(padding),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
- item {
- RoomNameWithAvatar(
- modifier = Modifier.padding(horizontal = 16.dp),
- avatarUri = state.config.avatarUri,
- roomName = state.config.roomName.orEmpty(),
- onAvatarClick = ::onAvatarClicked,
- onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
- )
- }
- item {
- RoomTopic(
- modifier = Modifier.padding(horizontal = 16.dp),
- topic = state.config.topic.orEmpty(),
- onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
- )
- }
+ RoomNameWithAvatar(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ avatarUri = state.config.avatarUri,
+ roomName = state.config.roomName.orEmpty(),
+ onAvatarClick = ::onAvatarClicked,
+ onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
+ )
+ RoomTopic(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ topic = state.config.topic.orEmpty(),
+ onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
+ )
if (state.config.invites.isNotEmpty()) {
- item {
- SelectedUsersList(
- contentPadding = PaddingValues(horizontal = 24.dp),
- selectedUsers = state.config.invites,
- onUserRemoved = {
- focusManager.clearFocus()
- state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it))
- },
- )
- }
- }
- item {
- RoomPrivacyOptions(
- modifier = Modifier.padding(bottom = 40.dp),
- selected = state.config.privacy,
- onOptionSelected = {
+ SelectedUsersList(
+ contentPadding = PaddingValues(horizontal = 24.dp),
+ selectedUsers = state.config.invites,
+ onUserRemoved = {
focusManager.clearFocus()
- state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy))
+ state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it))
},
)
}
+ RoomPrivacyOptions(
+ modifier = Modifier.padding(bottom = 40.dp),
+ selected = state.config.privacy,
+ onOptionSelected = {
+ focusManager.clearFocus()
+ state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy))
+ },
+ )
}
}
- AvatarActionListView(
+ AvatarActionBottomSheet(
actions = state.avatarActions,
modalBottomSheetState = itemActionsBottomSheetState,
onActionSelected = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
@@ -221,16 +218,17 @@ fun RoomNameWithAvatar(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- Avatar(
+ UnsavedAvatar(
avatarUri = avatarUri,
- onClick = onAvatarClick,
+ modifier = Modifier.clickable(onClick = onAvatarClick),
)
LabelledTextField(
label = stringResource(R.string.screen_create_room_room_name_label),
value = roomName,
placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
- onValueChange = onRoomNameChanged
+ singleLine = true,
+ onValueChange = onRoomNameChanged,
)
}
}
@@ -269,6 +267,13 @@ fun RoomPrivacyOptions(
}
}
+private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
+ pointerInput(Unit) {
+ detectTapGestures(onTap = {
+ focusManager.clearFocus()
+ })
+ }
+
@Preview
@Composable
fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
@@ -286,10 +291,3 @@ private fun ContentToPreview(state: ConfigureRoomState) {
)
}
-private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
- pointerInput(Unit) {
- detectTapGestures(onTap = {
- focusManager.clearFocus()
- })
- }
-
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt
index 2efbdb7b9a..867fdc9a60 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenter.kt
@@ -30,8 +30,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.usersearch.api.UserRepository
+import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -56,7 +56,7 @@ class DefaultUserListPresenter @AssistedInject constructor(
var isSearchActive by rememberSaveable { mutableStateOf(false) }
val selectedUsers by userListDataStore.selectedUsers().collectAsState(emptyList())
var searchQuery by rememberSaveable { mutableStateOf("") }
- var searchResults: SearchBarResultState> by remember {
+ var searchResults: SearchBarResultState> by remember {
mutableStateOf(SearchBarResultState.NotSearching())
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt
index d8ff391163..60a5bea506 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListState.kt
@@ -18,11 +18,12 @@ package io.element.android.features.createroom.impl.userlist
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.ImmutableList
data class UserListState(
val searchQuery: String,
- val searchResults: SearchBarResultState>,
+ val searchResults: SearchBarResultState>,
val selectedUsers: ImmutableList,
val isSearchActive: Boolean,
val selectionMode: SelectionMode,
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt
index cb8bf284b0..31d1f6953a 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/userlist/UserListStateProvider.kt
@@ -19,6 +19,7 @@ package io.element.android.features.createroom.impl.userlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -38,14 +39,14 @@ open class UserListStateProvider : PreviewParameterProvider {
isSearchActive = true,
searchQuery = "@someone:matrix.org",
selectedUsers = aListOfSelectedUsers(),
- searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
+ searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
),
aUserListState().copy(
isSearchActive = true,
searchQuery = "@someone:matrix.org",
selectionMode = SelectionMode.Multiple,
selectedUsers = aListOfSelectedUsers(),
- searchResults = SearchBarResultState.Results(aMatrixUserList().toImmutableList()),
+ searchResults = SearchBarResultState.Results(aMatrixUserList().map { UserSearchResult(it) }.toImmutableList()),
),
aUserListState().copy(
isSearchActive = true,
diff --git a/features/createroom/impl/src/main/res/values-cs/translations.xml b/features/createroom/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..646519f91e
--- /dev/null
+++ b/features/createroom/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,17 @@
+
+
+ "Nová místnost"
+ "Pozvat lidi"
+ "Přidat lidi"
+ "Při vytváření místnosti došlo k chybě"
+ "Zprávy v této místnosti jsou šifrované. Šifrování nelze později vypnout."
+ "Soukromá místnost (jen pro pozvané)"
+ "Zprávy nejsou šifrované a může si je přečíst kdokoli. Šifrování můžete povolit později."
+ "Veřejná místnost (kdokoli)"
+ "Název místnosti"
+ "např. Produktový sprint"
+ "Téma (nepovinné)"
+ "O čem je tato místnost?"
+ "Při pokusu o zahájení chatu došlo k chybě"
+ "Vytvořit místnost"
+
\ No newline at end of file
diff --git a/features/createroom/impl/src/main/res/values-de/translations.xml b/features/createroom/impl/src/main/res/values-de/translations.xml
index 6c3a7c8130..1d9f69ff63 100644
--- a/features/createroom/impl/src/main/res/values-de/translations.xml
+++ b/features/createroom/impl/src/main/res/values-de/translations.xml
@@ -3,7 +3,15 @@
"Neuer Raum"
"Personen einladen"
"Personen hinzufügen"
+ "Beim Erstellen des Raums ist ein Fehler aufgetreten"
+ "Die Nachrichten in diesem Raum sind verschlüsselt. Die Verschlüsselung kann nicht nachträglich deaktiviert werden."
"Privater Raum (nur auf Einladung)"
+ "Nachrichten sind nicht verschlüsselt und jeder kann sie lesen. Du kannst die Verschlüsselung zu einem späteren Zeitpunkt aktivieren."
+ "Öffentlicher Raum (jeder)"
"Raumname"
+ "z.B. Produkt-Sprint"
"Thema (optional)"
+ "Worum geht es in diesem Raum?"
+ "Beim Versuch, einen Chat zu starten, ist ein Fehler aufgetreten"
+ "Raum erstellen"
\ No newline at end of file
diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml
index 8c8fc9b48f..9a5d672fd4 100644
--- a/features/createroom/impl/src/main/res/values-es/translations.xml
+++ b/features/createroom/impl/src/main/res/values-es/translations.xml
@@ -4,6 +4,5 @@
"Invitar gente"
"Añadir personas"
"Se ha producido un error al intentar iniciar un chat"
- "No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación."
"Crear una sala"
-
\ No newline at end of file
+
diff --git a/features/createroom/impl/src/main/res/values-fr/translations.xml b/features/createroom/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..d0aab95e8d
--- /dev/null
+++ b/features/createroom/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,17 @@
+
+
+ "Nouvelle salle"
+ "Inviter des personnes"
+ "Ajouter des personnes"
+ "Une erreur s\'est produite lors de la création de la salle"
+ "Les messages dans cette salle sont chiffrés. Le chiffrement ne peut pas être désactivé par la suite."
+ "Salle privée (sur invitation uniquement)"
+ "Les messages ne sont pas chiffrés et n\'importe qui peut les lire. Vous pouvez activer le chiffrement ultérieurement."
+ "Salle publique (n’importe qui)"
+ "Nom de la salle"
+ "Ex: Sprint Produit"
+ "Sujet (optionnel)"
+ "De quoi parle cette salle ?"
+ "Une erreur s\'est produite lors de la tentative de démarrage d\'une discussion"
+ "Créer une salle"
+
\ No newline at end of file
diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml
index 3562a5f017..ceddb71154 100644
--- a/features/createroom/impl/src/main/res/values-it/translations.xml
+++ b/features/createroom/impl/src/main/res/values-it/translations.xml
@@ -4,6 +4,5 @@
"Invita persone"
"Aggiungi persone"
"Si è verificato un errore durante il tentativo di avviare una chat"
- "Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto."
"Crea una stanza"
-
\ No newline at end of file
+
diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml
index 8fd60b5a9e..e546e55587 100644
--- a/features/createroom/impl/src/main/res/values-ro/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ro/translations.xml
@@ -13,6 +13,5 @@
"Subiect (opțional)"
"Despre ce este această cameră?"
"A apărut o eroare la încercarea începerii conversației"
- "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."
"Creați o cameră"
-
\ No newline at end of file
+
diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml
index 578c8334da..b3c0a6e618 100644
--- a/features/createroom/impl/src/main/res/values/localazy.xml
+++ b/features/createroom/impl/src/main/res/values/localazy.xml
@@ -1,8 +1,8 @@
"New room"
- "Invite people"
- "Add people"
+ "Invite friends to Element"
+ "Invite people"
"An error occurred when creating the room"
"Messages in this room are encrypted. Encryption can’t be disabled afterwards."
"Private room (invite only)"
@@ -13,6 +13,5 @@
"Topic (optional)"
"What is this room about?"
"An error occurred when trying to start a chat"
- "We can’t validate this user’s Matrix ID. The invite might not be received."
"Create a room"
\ No newline at end of file
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt
index ed44cc1983..7f36b9e2b9 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt
@@ -21,9 +21,9 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
-import io.element.android.features.createroom.impl.configureroom.avatar.AvatarAction
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.core.RoomId
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt
index 40b7e6549b..745bdf74f9 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/userlist/DefaultUserListPresenterTests.kt
@@ -23,6 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import io.element.android.libraries.usersearch.api.UserSearchResult
import io.element.android.libraries.usersearch.test.FakeUserRepository
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.runTest
@@ -130,18 +131,18 @@ class DefaultUserListPresenterTests {
skipItems(2)
// When the user repository emits a result, it's copied to the state
- userRepository.emitResult(listOf(aMatrixUser()))
+ userRepository.emitResult(listOf(UserSearchResult(aMatrixUser())))
assertThat(awaitItem().searchResults).isEqualTo(
SearchBarResultState.Results(
- persistentListOf(aMatrixUser())
+ persistentListOf(UserSearchResult(aMatrixUser()))
)
)
// When the user repository emits another result, it replaces the previous value
- userRepository.emitResult(aMatrixUserList())
+ userRepository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
assertThat(awaitItem().searchResults).isEqualTo(
SearchBarResultState.Results(
- aMatrixUserList()
+ aMatrixUserList().map { UserSearchResult(it) }
)
)
}
diff --git a/features/invitelist/impl/src/main/res/values-cs/translations.xml b/features/invitelist/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..021830fbf1
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "Opravdu chcete odmítnout připojení k %1$s?"
+ "Odmítnout pozvání"
+ "Opravdu chcete odmítnout chat s %1$s?"
+ "Odmítnout chat"
+ "Žádné pozvánky"
+
\ No newline at end of file
diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml
index 95e63cf5f2..cc86ca00a7 100644
--- a/features/invitelist/impl/src/main/res/values-de/translations.xml
+++ b/features/invitelist/impl/src/main/res/values-de/translations.xml
@@ -1,5 +1,8 @@
+ "Möchten Sie den Beitritt zu %1$s wirklich ablehnen?"
+ "Einladung ablehnen"
+ "Möchten Sie den Chat mit %1$s wirklich ablehnen?"
"Chat ablehnen"
"Keine Einladungen"
"%1$s hat dich eingeladen"
diff --git a/features/invitelist/impl/src/main/res/values-fr/translations.xml b/features/invitelist/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..19935998f0
--- /dev/null
+++ b/features/invitelist/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Aucune invitation"
+ "%1$s vous a invité."
+
\ No newline at end of file
diff --git a/features/invitelist/impl/src/main/res/values/localazy.xml b/features/invitelist/impl/src/main/res/values/localazy.xml
index 44f6d91267..56163ec3a8 100644
--- a/features/invitelist/impl/src/main/res/values/localazy.xml
+++ b/features/invitelist/impl/src/main/res/values/localazy.xml
@@ -6,4 +6,4 @@
"Decline chat"
"No Invites"
"%1$s invited you"
-
\ No newline at end of file
+
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt
new file mode 100644
index 0000000000..6e90a390c4
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.api.oidc
+
+sealed interface OidcAction {
+ object GoBack : OidcAction
+ data class Success(val url: String) : OidcAction
+}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt
new file mode 100644
index 0000000000..004e7c8a51
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.api.oidc
+
+interface OidcActionFlow {
+ fun post(oidcAction: OidcAction)
+}
diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt
new file mode 100644
index 0000000000..a6ecf26fca
--- /dev/null
+++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.api.oidc
+
+import android.content.Intent
+
+interface OidcIntentResolver {
+ fun resolve(intent: Intent): OidcAction?
+}
diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts
index 5666fc1179..c43a5c2cd3 100644
--- a/features/login/impl/build.gradle.kts
+++ b/features/login/impl/build.gradle.kts
@@ -45,6 +45,7 @@ dependencies {
implementation(projects.libraries.elementresources)
implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings)
+ implementation(libs.androidx.browser)
api(projects.features.login.api)
ksp(libs.showkase.processor)
diff --git a/features/login/impl/src/main/AndroidManifest.xml b/features/login/impl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..172e8645c1
--- /dev/null
+++ b/features/login/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
index dcdaf1a347..36153a33ba 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt
@@ -16,9 +16,13 @@
package io.element.android.features.login.impl
+import android.app.Activity
import android.os.Parcelable
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@@ -29,17 +33,24 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.impl.changeserver.ChangeServerNode
+import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
+import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler
+import io.element.android.features.login.impl.oidc.webview.OidcNode
import io.element.android.features.login.impl.root.LoginRootNode
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode
+import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.auth.OidcDetails
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
class LoginFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List,
+ private val customTabAvailabilityChecker: CustomTabAvailabilityChecker,
+ private val customTabHandler: CustomTabHandler,
) : BackstackNode(
backstack = BackStack(
initialElement = NavTarget.Root,
@@ -48,6 +59,8 @@ class LoginFlowNode @AssistedInject constructor(
buildContext = buildContext,
plugins = plugins,
) {
+ private var activity: Activity? = null
+ private var darkTheme: Boolean = false
sealed interface NavTarget : Parcelable {
@Parcelize
@@ -55,6 +68,9 @@ class LoginFlowNode @AssistedInject constructor(
@Parcelize
object ChangeServer : NavTarget
+
+ @Parcelize
+ data class OidcView(val oidcDetails: OidcDetails) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -64,15 +80,37 @@ class LoginFlowNode @AssistedInject constructor(
override fun onChangeHomeServer() {
backstack.push(NavTarget.ChangeServer)
}
+
+ override fun onOidcDetails(oidcDetails: OidcDetails) {
+ if (customTabAvailabilityChecker.supportCustomTab()) {
+ // In this case open a Chrome Custom tab
+ activity?.let { customTabHandler.open(it, darkTheme, oidcDetails.url) }
+ } else {
+ // Fallback to WebView mode
+ backstack.push(NavTarget.OidcView(oidcDetails))
+ }
+ }
}
createNode(buildContext, plugins = listOf(callback))
}
+
NavTarget.ChangeServer -> createNode(buildContext)
+ is NavTarget.OidcView -> {
+ val input = OidcNode.Inputs(navTarget.oidcDetails)
+ createNode(buildContext, plugins = listOf(input))
+ }
}
}
@Composable
override fun View(modifier: Modifier) {
+ activity = LocalContext.current as? Activity
+ darkTheme = !ElementTheme.colors.isLight
+ DisposableEffect(Unit) {
+ onDispose {
+ activity = null
+ }
+ }
Children(
navModel = backstack,
modifier = modifier,
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt
new file mode 100644
index 0000000000..424e9f13bc
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc
+
+import android.content.Context
+import androidx.browser.customtabs.CustomTabsClient
+import io.element.android.libraries.di.ApplicationContext
+import javax.inject.Inject
+
+class CustomTabAvailabilityChecker @Inject constructor(
+ @ApplicationContext private val context: Context,
+) {
+ /**
+ * Return true if the device supports Custom tab, i.e. there is an third party app with
+ * CustomTab support (ex: Chrome, Firefox, etc.).
+ */
+ fun supportCustomTab(): Boolean {
+ val packageName = CustomTabsClient.getPackageName(context, null)
+ return packageName != null
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt
new file mode 100644
index 0000000000..8b6844e0f3
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc
+
+import android.content.Intent
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.api.oidc.OidcIntentResolver
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultOidcIntentResolver @Inject constructor(
+ private val oidcUrlParser: OidcUrlParser,
+) : OidcIntentResolver {
+ override fun resolve(intent: Intent): OidcAction? {
+ return oidcUrlParser.parse(intent.dataString.orEmpty())
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
new file mode 100644
index 0000000000..487df70253
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc
+
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.libraries.matrix.api.auth.OidcConfig
+import javax.inject.Inject
+
+/**
+ * Simple parser for oidc url interception.
+ * TODO Find documentation about the format.
+ */
+class OidcUrlParser @Inject constructor() {
+
+ // When user press button "Cancel", we get the url:
+ // `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO`
+ // On success, we get:
+ // `io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB`
+ /**
+ * Return a OidcAction, or null if the url is not a OidcUrl.
+ */
+ fun parse(url: String): OidcAction? {
+ if (url.startsWith(OidcConfig.redirectUri).not()) return null
+ if (url.contains("error=access_denied")) return OidcAction.GoBack
+ if (url.contains("code=")) return OidcAction.Success(url)
+
+ // Other case not supported, let's crash the app for now
+ error("Not supported: $url")
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt
new file mode 100644
index 0000000000..407459c5bf
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.customtab
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsClient
+import androidx.browser.customtabs.CustomTabsServiceConnection
+import androidx.browser.customtabs.CustomTabsSession
+import io.element.android.libraries.di.ApplicationContext
+import javax.inject.Inject
+
+class CustomTabHandler @Inject constructor(
+ @ApplicationContext private val context: Context,
+) {
+ private var customTabsSession: CustomTabsSession? = null
+ private var customTabsClient: CustomTabsClient? = null
+ private var customTabsServiceConnection: CustomTabsServiceConnection? = null
+
+ fun prepareCustomTab(url: String) {
+ val packageName = CustomTabsClient.getPackageName(context, null)
+
+ // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
+ if (packageName != null) {
+ customTabsServiceConnection = object : CustomTabsServiceConnection() {
+ override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
+ customTabsClient = client
+ .also { it.warmup(0L) }
+ prefetchUrl(url)
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ }
+ }
+ .also {
+ CustomTabsClient.bindCustomTabsService(
+ context,
+ // Despite the API, packageName cannot be null
+ packageName,
+ it
+ )
+ }
+ }
+ }
+
+ private fun prefetchUrl(url: String) {
+ if (customTabsSession == null) {
+ customTabsSession = customTabsClient?.newSession(null)
+ }
+
+ customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
+ }
+
+ fun disposeCustomTab() {
+ customTabsServiceConnection?.let { context.unbindService(it) }
+ customTabsServiceConnection = null
+ }
+
+ fun open(activity: Activity, darkTheme: Boolean, url: String) {
+ activity.openUrlInChromeCustomTab(customTabsSession, darkTheme, url)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt
new file mode 100644
index 0000000000..17dfa8418f
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.customtab
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.api.oidc.OidcActionFlow
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.SingleIn
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.MutableStateFlow
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+@SingleIn(AppScope::class)
+class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow {
+ private val mutableStateFlow = MutableStateFlow(null)
+
+ override fun post(oidcAction: OidcAction) {
+ mutableStateFlow.value = oidcAction
+ }
+
+ suspend fun collect(collector: FlowCollector) {
+ mutableStateFlow.collect(collector)
+ }
+
+ fun reset() {
+ mutableStateFlow.value = null
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt
new file mode 100644
index 0000000000..be98566e7c
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.customtab
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.browser.customtabs.CustomTabsSession
+
+/**
+ * Open url in custom tab or, if not available, in the default browser.
+ * If several compatible browsers are installed, the user will be proposed to choose one.
+ * Ref: https://developer.chrome.com/multidevice/android/customtabs.
+ */
+fun Activity.openUrlInChromeCustomTab(
+ session: CustomTabsSession?,
+ darkTheme: Boolean,
+ url: String
+) {
+ try {
+ CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(
+ CustomTabColorSchemeParams.Builder()
+ // TODO .setToolbarColor(ThemeUtils.getColor(context, android.R.attr.colorBackground))
+ // TODO .setNavigationBarColor(ThemeUtils.getColor(context, android.R.attr.colorBackground))
+ .build()
+ )
+ .setColorScheme(
+ when (darkTheme) {
+ false -> CustomTabsIntent.COLOR_SCHEME_LIGHT
+ true -> CustomTabsIntent.COLOR_SCHEME_DARK
+ }
+ )
+ // Note: setting close button icon does not work
+ // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
+ // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
+ // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
+ .apply { session?.let { setSession(it) } }
+ .build()
+ .launchUrl(this, Uri.parse(url))
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ // TODO context.toast(R.string.error_no_external_application_found)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt
new file mode 100644
index 0000000000..6265cfc85a
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+import io.element.android.features.login.api.oidc.OidcAction
+
+sealed interface OidcEvents {
+ object Cancel : OidcEvents
+ data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents
+ object ClearError : OidcEvents
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt
new file mode 100644
index 0000000000..dd16b5e57b
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.inputs
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+
+@ContributesNode(AppScope::class)
+class OidcNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ presenterFactory: OidcPresenter.Factory,
+) : Node(buildContext, plugins = plugins) {
+
+ data class Inputs(
+ val oidcDetails: OidcDetails,
+ ) : NodeInputs
+
+ private val inputs: Inputs = inputs()
+ private val presenter = presenterFactory.create(inputs.oidcDetails)
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ OidcView(
+ state = state,
+ modifier = modifier,
+ onNavigateBack = ::navigateUp,
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt
new file mode 100644
index 0000000000..66926b3734
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+import kotlinx.coroutines.launch
+
+class OidcPresenter @AssistedInject constructor(
+ @Assisted private val oidcDetails: OidcDetails,
+ private val authenticationService: MatrixAuthenticationService,
+) : Presenter {
+
+ @AssistedFactory
+ interface Factory {
+ fun create(oidcDetails: OidcDetails): OidcPresenter
+ }
+
+ @Composable
+ override fun present(): OidcState {
+ var requestState: Async by remember {
+ mutableStateOf(Async.Uninitialized)
+ }
+ val localCoroutineScope = rememberCoroutineScope()
+
+ fun handleCancel() {
+ requestState = Async.Loading()
+ localCoroutineScope.launch {
+ authenticationService.cancelOidcLogin()
+ .fold(
+ onSuccess = {
+ // Then go back
+ requestState = Async.Success(Unit)
+ },
+ onFailure = {
+ requestState = Async.Failure(it)
+ }
+ )
+ }
+ }
+
+ fun handleSuccess(url: String) {
+ requestState = Async.Loading()
+ localCoroutineScope.launch {
+ authenticationService.loginWithOidc(url)
+ .onFailure {
+ requestState = Async.Failure(it)
+ }
+ // On success, the node tree will be updated, there is nothing to do
+ }
+ }
+
+ fun handleAction(action: OidcAction) {
+ when (action) {
+ OidcAction.GoBack -> handleCancel()
+ is OidcAction.Success -> handleSuccess(action.url)
+ }
+ }
+
+ fun handleEvents(event: OidcEvents) {
+ when (event) {
+ OidcEvents.Cancel -> handleCancel()
+ is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction)
+ OidcEvents.ClearError -> requestState = Async.Uninitialized
+ }
+ }
+
+ return OidcState(
+ oidcDetails = oidcDetails,
+ requestState = requestState,
+ eventSink = ::handleEvents
+ )
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt
new file mode 100644
index 0000000000..fc9507a89d
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+
+data class OidcState(
+ val oidcDetails: OidcDetails,
+ val requestState: Async,
+ val eventSink: (OidcEvents) -> Unit
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt
new file mode 100644
index 0000000000..80878cf8f8
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.auth.OidcDetails
+
+open class OidcStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aOidcState(),
+ aOidcState().copy(requestState = Async.Loading()),
+ )
+}
+
+fun aOidcState() = OidcState(
+ oidcDetails = aOidcDetails(),
+ requestState = Async.Uninitialized,
+ eventSink = {}
+)
+
+fun aOidcDetails() = OidcDetails(
+ url = "aUrl",
+)
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
new file mode 100644
index 0000000000..c1235b76c5
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+import android.webkit.WebView
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.viewinterop.AndroidView
+import io.element.android.features.login.impl.oidc.OidcUrlParser
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.core.bool.orFalse
+import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+
+@Composable
+fun OidcView(
+ state: OidcState,
+ onNavigateBack: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val oidcUrlParser = remember { OidcUrlParser() }
+ var webView by remember { mutableStateOf(null) }
+ fun shouldOverrideUrl(url: String): Boolean {
+ val action = oidcUrlParser.parse(url)
+ if (action != null) {
+ state.eventSink.invoke(OidcEvents.OidcActionEvent(action))
+ return true
+ }
+ return false
+ }
+
+ val oidcWebViewClient = remember {
+ OidcWebViewClient(::shouldOverrideUrl)
+ }
+
+ BackHandler {
+ if (webView?.canGoBack().orFalse()) {
+ webView?.goBack()
+ } else {
+ // To properly cancel Oidc login
+ state.eventSink.invoke(OidcEvents.Cancel)
+ }
+ }
+
+ Box(modifier = modifier.statusBarsPadding()) {
+ AndroidView(
+ factory = { context ->
+ WebView(context).apply {
+ webViewClient = oidcWebViewClient
+ loadUrl(state.oidcDetails.url)
+ }.also {
+ webView = it
+ }
+ }
+ )
+
+ when (state.requestState) {
+ Async.Uninitialized -> Unit
+ is Async.Failure -> {
+ ErrorDialog(
+ content = state.requestState.error.toString(),
+ onDismiss = { state.eventSink(OidcEvents.ClearError) }
+ )
+ }
+ is Async.Loading -> {
+ CircularProgressIndicator(
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ is Async.Success -> onNavigateBack()
+ }
+ }
+}
+
+@Preview
+@Composable
+fun OidcViewLightPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
+ ElementPreviewLight { ContentToPreview(state) }
+
+@Preview
+@Composable
+fun OidcViewDarkPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) =
+ ElementPreviewDark { ContentToPreview(state) }
+
+@Composable
+private fun ContentToPreview(state: OidcState) {
+ OidcView(
+ state = state,
+ onNavigateBack = { },
+ )
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt
new file mode 100644
index 0000000000..7d8e789715
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.os.Build
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+
+class OidcWebViewClient(
+ private val eventListener: WebViewEventListener,
+) : WebViewClient() {
+ // We will revert to API 23, in the mean time ignore the warning here.
+ @SuppressLint("ObsoleteSdkInt")
+ @TargetApi(Build.VERSION_CODES.N)
+ override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
+ return shouldOverrideUrl(request.url.toString())
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
+ return shouldOverrideUrl(url)
+ }
+
+ private fun shouldOverrideUrl(url: String): Boolean {
+ // Timber.d("shouldOverrideUrl: $url")
+ return eventListener.shouldOverrideUrlLoading(url)
+ }
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt
new file mode 100644
index 0000000000..446754aced
--- /dev/null
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc.webview
+
+fun interface WebViewEventListener {
+ /**
+ * Triggered when a Webview loads an url.
+ *
+ * @param url The url about to be rendered.
+ * @return true if the method needs to manage some custom handling
+ */
+ fun shouldOverrideUrlLoading(url: String): Boolean
+}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt
index d3f8738dc7..5aa5071876 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt
@@ -17,6 +17,7 @@
package io.element.android.features.login.impl.root
sealed interface LoginRootEvents {
+ object RetryFetchServerInfo : LoginRootEvents
data class SetLogin(val login: String) : LoginRootEvents
data class SetPassword(val password: String) : LoginRootEvents
object Submit : LoginRootEvents
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt
index fce214f2e4..787f5d0b48 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt
@@ -26,6 +26,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.matrix.api.auth.OidcDetails
@ContributesNode(AppScope::class)
class LoginRootNode @AssistedInject constructor(
@@ -36,20 +37,26 @@ class LoginRootNode @AssistedInject constructor(
interface Callback : Plugin {
fun onChangeHomeServer()
+ fun onOidcDetails(oidcDetails: OidcDetails)
}
private fun onChangeHomeServer() {
plugins().forEach { it.onChangeHomeServer() }
}
+ private fun onOidcDetails(oidcDetails: OidcDetails) {
+ plugins().forEach { it.onOidcDetails(oidcDetails) }
+ }
+
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
LoginRootView(
state = state,
modifier = modifier,
- onChangeServer = this::onChangeHomeServer,
- onBackPressed = this::navigateUp
+ onChangeServer = ::onChangeHomeServer,
+ onOidcDetails = ::onOidcDetails,
+ onBackPressed = ::navigateUp
)
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
index c9a75365f1..f55c2030e7 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt
@@ -17,28 +17,49 @@
package io.element.android.features.login.impl.root
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.features.login.impl.util.LoginConstants
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.execute
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
-class LoginRootPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter {
-
- private val defaultHomeserver = MatrixHomeServerDetails(LoginConstants.DEFAULT_HOMESERVER_URL, true, null)
+class LoginRootPresenter @Inject constructor(
+ private val authenticationService: MatrixAuthenticationService,
+ private val defaultOidcActionFlow: DefaultOidcActionFlow,
+) : Presenter {
@Composable
override fun present(): LoginRootState {
val localCoroutineScope = rememberCoroutineScope()
- val homeserver = authenticationService.getHomeserverDetails().collectAsState().value ?: defaultHomeserver
+ val currentHomeServerDetails = authenticationService.getHomeserverDetails().collectAsState().value
+ val homeserver = currentHomeServerDetails?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL
+ val getHomeServerDetailsAction: MutableState> = remember {
+ if (currentHomeServerDetails != null) {
+ mutableStateOf(Async.Success(currentHomeServerDetails))
+ } else {
+ mutableStateOf(Async.Uninitialized)
+ }
+ }
+
+ LaunchedEffect(Unit) {
+ if (currentHomeServerDetails == null) {
+ getHomeServerDetails(homeserver, getHomeServerDetailsAction)
+ }
+ }
+
val loggedInState: MutableState = remember {
mutableStateOf(LoggedInState.NotLoggedIn)
}
@@ -46,31 +67,69 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
mutableStateOf(LoginFormState.Default)
}
+ LaunchedEffect(Unit) {
+ launch {
+ defaultOidcActionFlow.collect {
+ onOidcAction(it, loggedInState)
+ }
+ }
+ }
+
fun handleEvents(event: LoginRootEvents) {
when (event) {
+ LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction)
is LoginRootEvents.SetLogin -> updateFormState(formState) {
copy(login = event.login)
}
is LoginRootEvents.SetPassword -> updateFormState(formState) {
copy(password = event.password)
}
- LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState)
+ LoginRootEvents.Submit -> {
+ val homeServerDetails = getHomeServerDetailsAction.value.dataOrNull() ?: return
+ when {
+ homeServerDetails.supportsOidcLogin -> localCoroutineScope.submitOidc(loggedInState)
+ homeServerDetails.supportsPasswordLogin -> localCoroutineScope.submit(formState.value, loggedInState)
+ }
+ }
LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn
}
}
return LoginRootState(
- homeserverDetails = homeserver,
+ homeserverUrl = homeserver,
+ homeserverDetails = getHomeServerDetailsAction.value,
loggedInState = loggedInState.value,
formState = formState.value,
eventSink = ::handleEvents
)
}
- private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState) = launch {
+ private fun CoroutineScope.getHomeServerDetails(
+ homeserver: String,
+ state: MutableState>,
+ ) = launch {
+ suspend {
+ authenticationService.setHomeserver(homeserver)
+ .map {
+ authenticationService.getHomeserverDetails().value!!
+ }
+ .getOrThrow()
+ }.execute(state)
+ }
+
+ private fun CoroutineScope.submitOidc(loggedInState: MutableState) = launch {
+ loggedInState.value = LoggedInState.LoggingIn
+ authenticationService.getOidcUrl()
+ .onSuccess {
+ loggedInState.value = LoggedInState.OidcStarted(it)
+ }
+ .onFailure { failure ->
+ loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
+ }
+ }
+
+ private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState) = launch {
loggedInState.value = LoggedInState.LoggingIn
- //TODO rework the setHomeserver flow
- authenticationService.setHomeserver(homeserver)
authenticationService.login(formState.login.trim(), formState.password)
.onSuccess { sessionId ->
loggedInState.value = LoggedInState.LoggedIn(sessionId)
@@ -83,4 +142,30 @@ class LoginRootPresenter @Inject constructor(private val authenticationService:
private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) {
formState.value = updateLambda(formState.value)
}
+
+ private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState) {
+ oidcAction ?: return
+ loggedInState.value = LoggedInState.LoggingIn
+ when (oidcAction) {
+ OidcAction.GoBack -> {
+ authenticationService.cancelOidcLogin()
+ .onSuccess {
+ loggedInState.value = LoggedInState.NotLoggedIn
+ }
+ .onFailure { failure ->
+ loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
+ }
+ }
+ is OidcAction.Success -> {
+ authenticationService.loginWithOidc(oidcAction.url)
+ .onSuccess { sessionId ->
+ loggedInState.value = LoggedInState.LoggedIn(sessionId)
+ }
+ .onFailure { failure ->
+ loggedInState.value = LoggedInState.ErrorLoggingIn(failure)
+ }
+ }
+ }
+ defaultOidcActionFlow.reset()
+ }
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt
index 34e903dd0e..45eafa744c 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt
@@ -17,23 +17,31 @@
package io.element.android.features.login.impl.root
import android.os.Parcelable
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
+import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.parcelize.Parcelize
data class LoginRootState(
- val homeserverDetails: MatrixHomeServerDetails,
+ val homeserverUrl: String,
+ val homeserverDetails: Async,
val loggedInState: LoggedInState,
val formState: LoginFormState,
val eventSink: (LoginRootEvents) -> Unit
) {
- val submitEnabled: Boolean get() =
- formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState !is LoggedInState.ErrorLoggingIn
+ val supportPasswordLogin = (homeserverDetails as? Async.Success)?.state?.supportsPasswordLogin.orFalse()
+ val supportOidcLogin = (homeserverDetails as? Async.Success)?.state?.supportsOidcLogin.orFalse()
+ val submitEnabled: Boolean
+ get() = loggedInState !is LoggedInState.ErrorLoggingIn &&
+ ((formState.login.isNotEmpty() && formState.password.isNotEmpty()) || supportOidcLogin)
}
sealed interface LoggedInState {
object NotLoggedIn : LoggedInState
object LoggingIn : LoggedInState
+ data class OidcStarted(val oidcDetail: OidcDetails) : LoggedInState
data class ErrorLoggingIn(val failure: Throwable) : LoggedInState
data class LoggedIn(val sessionId: SessionId) : LoggedInState
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt
index 9864bf2380..5f6d7c1f3a 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt
@@ -17,6 +17,7 @@
package io.element.android.features.login.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.api.core.SessionId
@@ -24,16 +25,51 @@ open class LoginRootStateProvider : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aLoginRootState(),
- aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", true, null)),
+ aLoginRootState().copy(
+ homeserverDetails = Async.Success(
+ MatrixHomeServerDetails(
+ "some-custom-server.com",
+ supportsPasswordLogin = true,
+ supportsOidcLogin = false
+ )
+ )
+ ),
aLoginRootState().copy(formState = LoginFormState("user", "pass")),
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggingIn),
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.ErrorLoggingIn(Throwable())),
aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggedIn(SessionId("@user:domain"))),
+ // Oidc
+ aLoginRootState().copy(
+ homeserverUrl = "server-with-oidc.org",
+ homeserverDetails = Async.Success(
+ MatrixHomeServerDetails(
+ "server-with-oidc.org",
+ supportsPasswordLogin = false,
+ supportsOidcLogin = true
+ )
+ )
+ ),
+ // No password, no oidc support
+ aLoginRootState().copy(
+ homeserverUrl = "wrong.org",
+ homeserverDetails = Async.Success(
+ MatrixHomeServerDetails(
+ "wrong.org",
+ supportsPasswordLogin = false,
+ supportsOidcLogin = false
+ )
+ )
+ ),
+ // Loading
+ aLoginRootState().copy(homeserverDetails = Async.Loading()),
+ //Error
+ aLoginRootState().copy(homeserverDetails = Async.Failure(Exception("An error occurred"))),
)
}
fun aLoginRootState() = LoginRootState(
- homeserverDetails = MatrixHomeServerDetails("matrix.org", true, null),
+ homeserverUrl = "matrix.org",
+ homeserverDetails = Async.Success(MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidcLogin = false)),
loggedInState = LoggedInState.NotLoggedIn,
formState = LoginFormState.Default,
eventSink = {}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt
index b72122c914..2444418725 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt
@@ -68,7 +68,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.loginError
+import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.ElementTextStyles
+import io.element.android.libraries.designsystem.components.async.AsyncFailure
+import io.element.android.libraries.designsystem.components.async.AsyncLoading
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@@ -83,7 +86,7 @@ import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.components.autofill
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.R as StringR
@@ -94,7 +97,7 @@ fun LoginRootView(
state: LoginRootState,
modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {},
- onLoginWithSuccess: (SessionId) -> Unit = {},
+ onOidcDetails: (OidcDetails) -> Unit = {},
onBackPressed: () -> Unit,
) {
val isLoading by remember(state.loggedInState) {
@@ -102,6 +105,15 @@ fun LoginRootView(
state.loggedInState == LoggedInState.LoggingIn
}
}
+ val focusManager = LocalFocusManager.current
+
+ fun submit() {
+ // Clear focus to prevent keyboard issues with textfields
+ focusManager.clearFocus(force = true)
+
+ state.eventSink(LoginRootEvents.Submit)
+ }
+
Scaffold(
topBar = {
TopAppBar(
@@ -137,19 +149,26 @@ fun LoginRootView(
ChangeServerSection(
interactionEnabled = !isLoading,
- homeserver = state.homeserverDetails.url,
+ homeserver = state.homeserverUrl,
onChangeServer = onChangeServer
)
Spacer(Modifier.height(32.dp))
- LoginForm(state = state, isLoading = isLoading)
-
- Spacer(modifier = Modifier.height(32.dp))
-
+ when (state.homeserverDetails) {
+ Async.Uninitialized,
+ is Async.Loading -> AsyncLoading()
+ is Async.Failure -> AsyncFailure(
+ throwable = state.homeserverDetails.error,
+ onRetry = {
+ state.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo)
+ }
+ )
+ is Async.Success -> ServerDetailForm(state, isLoading, ::submit)
+ }
}
when (val loggedInState = state.loggedInState) {
- is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
+ is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail)
else -> Unit
}
}
@@ -162,6 +181,43 @@ fun LoginRootView(
}
}
+@Composable
+fun ServerDetailForm(
+ state: LoginRootState,
+ isLoading: Boolean,
+ submit: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ when {
+ state.supportOidcLogin -> {
+ // Oidc, in this case, just display a Spacer and the submit button
+ Spacer(modifier.height(28.dp))
+ }
+ state.supportPasswordLogin -> {
+ LoginForm(state = state, isLoading = isLoading, onSubmit = submit, modifier = modifier)
+ }
+ else -> {
+ Text(modifier = modifier, text = "No supported login flow")
+ }
+ }
+
+ Spacer(Modifier.height(28.dp))
+
+ if (state.supportOidcLogin || state.supportPasswordLogin) {
+ // Submit
+ ButtonWithProgress(
+ text = stringResource(R.string.screen_login_submit),
+ showProgress = isLoading,
+ onClick = submit,
+ enabled = state.submitEnabled,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(TestTags.loginContinue)
+ )
+ Spacer(modifier = Modifier.height(32.dp))
+ }
+}
+
@Composable
internal fun ChangeServerSection(
interactionEnabled: Boolean,
@@ -217,6 +273,7 @@ internal fun ChangeServerSection(
internal fun LoginForm(
state: LoginRootState,
isLoading: Boolean,
+ onSubmit: () -> Unit,
modifier: Modifier = Modifier
) {
var loginFieldState by textFieldState(stateValue = state.formState.login)
@@ -225,13 +282,6 @@ internal fun LoginForm(
val focusManager = LocalFocusManager.current
val eventSink = state.eventSink
- fun submit() {
- // Clear focus to prevent keyboard issues with textfields
- focusManager.clearFocus(force = true)
-
- eventSink(LoginRootEvents.Submit)
- }
-
Column(modifier) {
Text(
text = stringResource(R.string.screen_login_form_header),
@@ -318,23 +368,11 @@ internal fun LoginForm(
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
- onDone = { submit() }
+ onDone = { onSubmit() }
),
singleLine = true,
maxLines = 1,
)
- Spacer(Modifier.height(28.dp))
-
- // Submit
- ButtonWithProgress(
- text = stringResource(R.string.screen_login_submit),
- showProgress = isLoading,
- onClick = ::submit,
- enabled = state.submitEnabled,
- modifier = Modifier
- .fillMaxWidth()
- .testTag(TestTags.loginContinue)
- )
}
}
diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
index 8e2c37904f..cb01f8095a 100644
--- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
+++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt
@@ -18,7 +18,6 @@ package io.element.android.features.login.impl.util
object LoginConstants {
- const val DEFAULT_HOMESERVER_URL = "matrix.org"
+ const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev"
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
-
}
diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..37923cddd1
--- /dev/null
+++ b/features/login/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,18 @@
+
+
+ "Nepodařilo se nám připojit k tomuto domovskému serveru. Zkontrolujte prosím, zda jste správně zadali adresu URL domovského serveru. Pokud je adresa URL správná, obraťte se na správce domovského serveru, který vám poskytne další pomoc."
+ "Adresa URL domovského serveru"
+ "Jaká je adresa vašeho serveru?"
+ "Tento účet byl deaktivován."
+ "Nesprávné uživatelské jméno nebo heslo"
+ "Toto není platný identifikátor uživatele. Očekávaný formát: \'@user:homeserver.org\'"
+ "Vybraný domovský server nepodporuje přihlášení pomocí hesla nebo OIDC. Kontaktujte prosím svého správce nebo vyberte jiný domovský server."
+ "Zadejte své údaje"
+ "Kde budou vaše konverzace probíhat"
+ "Vítejte zpět!"
+ "Pokračovat"
+ "Vyberte svůj server"
+ "Heslo"
+ "Pokračovat"
+ "Uživatelské jméno"
+
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml
index 4c9b232147..cb380abfad 100644
--- a/features/login/impl/src/main/res/values-de/translations.xml
+++ b/features/login/impl/src/main/res/values-de/translations.xml
@@ -1,11 +1,20 @@
"Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie, ob Sie die Homeserver-URL korrekt eingegeben haben. Wenn die URL korrekt ist, wenden Sie sich an Ihren Homeserver-Administrator, um weitere Hilfe zu erhalten."
- "Dieser Server unterstützt derzeit kein Sliding Sync."
+ "Dieser Server unterstützt derzeit keine Sliding Sync."
"Homeserver-URL"
- "Sie können nur eine Verbindung zu einem vorhandenen Server herstellen, der Sliding Sync unterstützt. Ihr Homeserver-Administrator muss dies konfigurieren. %1$s"
+ "Du kannst dich nur mit einem existierenden Server verbinden, der Sliding Sync unterstützt. Dein Homeserver-Administrator muss es konfigurieren. %1$s"
"Wie lautet die Adresse deines Servers?"
+ "Dieses Konto wurde deaktiviert."
+ "Falscher Benutzername und/oder Passwort"
+ "Dies ist kein gültiger Benutzeridentifikator. Erwartetes Format: \'@user:homeserver.org\'"
+ "Der ausgewählte Homeserver unterstützt kein Passwort- oder OIDC-Login. Bitte kontaktiere deinen Admin oder wähle einen anderen Homeserver."
+ "Gib deine Daten ein"
+ "Wo deine Gespräche leben"
"Willkommen zurück!"
+ "Weiter"
+ "Wählen deinen Server"
"Passwort"
+ "Weiter"
"Benutzername"
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml
index 1b64d9f5fd..9d8f50e979 100644
--- a/features/login/impl/src/main/res/values-fr/translations.xml
+++ b/features/login/impl/src/main/res/values-fr/translations.xml
@@ -1,5 +1,20 @@
+ "Nous n\'avons pas pu atteindre ce serveur domestique. Vérifiez que vous avez correctement saisi l\'URL du serveur d\'accueil. Si l\'URL est correcte, contactez l\'administrateur de votre serveur domestique pour obtenir de l\'aide."
+ "Ce serveur ne prend actuellement pas en charge la synchronisation glissante."
+ "URL du serveur d\'accueil"
+ "Vous ne pouvez vous connecter qu\'à un serveur existant qui prend en charge la synchronisation glissante. L\'administrateur de votre serveur domestique devra la configurer. %1$s"
+ "Quelle est l\'adresse de votre serveur ?"
+ "Ce compte a été désactivé."
+ "Nom d\'utilisateur et/ou mot de passe incorrect"
+ "Il ne s\'agit pas d\'un identifiant utilisateur valide. Format attendu : « @user:homeserver.org »"
+ "Le serveur domestique sélectionné ne prend pas en charge le mot de passe ou la connexion OIDC. Contactez votre administrateur ou choisissez un autre serveur domestique."
+ "Saisir vos informations personnelles"
+ "Où se déroulent vos conversations"
+ "Heureux de vous revoir!"
"Continuer"
+ "Sélectionnez votre serveur"
+ "Mot de passe"
"Continuer"
+ "Nom d\'utilisateur"
\ No newline at end of file
diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml
index 6b0ecee43d..cf59844e89 100644
--- a/features/login/impl/src/main/res/values/localazy.xml
+++ b/features/login/impl/src/main/res/values/localazy.xml
@@ -12,6 +12,7 @@
"Enter your details"
"Where your conversations live"
"Welcome back!"
+ "Sign in to %1$s"
"Continue"
"Select your server"
"Password"
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt
index cc216cc9dd..a30bd9449c 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt
@@ -20,9 +20,9 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.impl.util.LoginConstants
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER
-import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
@@ -39,7 +39,7 @@ class ChangeServerPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER_URL)
+ assertThat(initialState.homeserver).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
assertThat(initialState.submitEnabled).isTrue()
}
}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
new file mode 100644
index 0000000000..a0275f8f47
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.login.impl.oidc
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.libraries.matrix.api.auth.OidcConfig
+import org.junit.Assert
+import org.junit.Test
+
+class OidcUrlParserTest {
+ @Test
+ fun `test empty url`() {
+ val sut = OidcUrlParser()
+ assertThat(sut.parse("")).isNull()
+ }
+
+ @Test
+ fun `test regular url`() {
+ val sut = OidcUrlParser()
+ assertThat(sut.parse("https://matrix.org")).isNull()
+ }
+
+ @Test
+ fun `test cancel url`() {
+ val sut = OidcUrlParser()
+ val aCancelUrl = OidcConfig.redirectUri + "?error=access_denied&state=IFF1UETGye2ZA8pO"
+ assertThat(sut.parse(aCancelUrl)).isEqualTo(OidcAction.GoBack)
+ }
+
+ @Test
+ fun `test success url`() {
+ val sut = OidcUrlParser()
+ val aSuccessUrl = OidcConfig.redirectUri + "?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
+ assertThat(sut.parse(aSuccessUrl)).isEqualTo(OidcAction.Success(aSuccessUrl))
+ }
+
+ @Test
+ fun `test unknown url`() {
+ val sut = OidcUrlParser()
+ val anUnknownUrl = OidcConfig.redirectUri + "?state=IFF1UETGye2ZA8pO&goat=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
+ Assert.assertThrows(IllegalStateException::class.java) {
+ assertThat(sut.parse(anUnknownUrl))
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt
new file mode 100644
index 0000000000..5756cd13d2
--- /dev/null
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package io.element.android.features.login.impl.oidc.webview
+
+import app.cash.molecule.RecompositionClock
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.test.A_THROWABLE
+import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
+import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class OidcPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val presenter = OidcPresenter(
+ A_OIDC_DATA,
+ FakeAuthenticationService(),
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA)
+ assertThat(initialState.requestState).isEqualTo(Async.Uninitialized)
+ }
+ }
+
+ @Test
+ fun `present - go back`() = runTest {
+ val presenter = OidcPresenter(
+ A_OIDC_DATA,
+ FakeAuthenticationService(),
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(OidcEvents.Cancel)
+ val loadingState = awaitItem()
+ assertThat(loadingState.requestState).isEqualTo(Async.Loading())
+ val finalState = awaitItem()
+ assertThat(finalState.requestState).isEqualTo(Async.Success(Unit))
+ }
+ }
+
+ @Test
+ fun `present - go back with failure`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val presenter = OidcPresenter(
+ A_OIDC_DATA,
+ authenticationService,
+ )
+ authenticationService.givenOidcCancelError(A_THROWABLE)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(OidcEvents.Cancel)
+ val loadingState = awaitItem()
+ assertThat(loadingState.requestState).isEqualTo(Async.Loading())
+ val finalState = awaitItem()
+ assertThat(finalState.requestState).isEqualTo(Async.Failure(A_THROWABLE))
+ // Note: in real life I do not think this can happen, and the app should not block the user.
+ }
+ }
+
+ @Test
+ fun `present - user cancels from webview`() = runTest {
+ val presenter = OidcPresenter(
+ A_OIDC_DATA,
+ FakeAuthenticationService(),
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack))
+ val loadingState = awaitItem()
+ assertThat(loadingState.requestState).isEqualTo(Async.Loading())
+ val finalState = awaitItem()
+ assertThat(finalState.requestState).isEqualTo(Async.Success(Unit))
+ }
+ }
+
+ @Test
+ fun `present - login success`() = runTest {
+ val presenter = OidcPresenter(
+ A_OIDC_DATA,
+ FakeAuthenticationService(),
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
+ val loadingState = awaitItem()
+ assertThat(loadingState.requestState).isEqualTo(Async.Loading())
+ // In this case, no success, the session is created and the node get destroyed.
+ }
+ }
+
+ @Test
+ fun `present - login error`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val presenter = OidcPresenter(
+ A_OIDC_DATA,
+ authenticationService,
+ )
+ authenticationService.givenLoginError(A_THROWABLE)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL")))
+ val loadingState = awaitItem()
+ assertThat(loadingState.requestState).isEqualTo(Async.Loading())
+ val errorState = awaitItem()
+ assertThat(errorState.requestState).isEqualTo(Async.Failure(A_THROWABLE))
+ errorState.eventSink.invoke(OidcEvents.ClearError)
+ val finalState = awaitItem()
+ assertThat(finalState.requestState).isEqualTo(Async.Uninitialized)
+ }
+ }
+}
diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt
index be940feacf..0dee8d47c0 100644
--- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt
+++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt
@@ -20,11 +20,18 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.login.api.oidc.OidcAction
+import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
+import io.element.android.features.login.impl.util.LoginConstants
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
import io.element.android.libraries.matrix.test.A_HOMESERVER
+import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC
import io.element.android.libraries.matrix.test.A_PASSWORD
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_NAME
+import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -34,23 +41,91 @@ class LoginRootPresenterTest {
fun `present - initial state`() = runTest {
val presenter = LoginRootPresenter(
FakeAuthenticationService(),
+ DefaultOidcActionFlow(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
- assertThat(initialState.homeserverDetails).isEqualTo(A_HOMESERVER)
+ assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
+ assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
assertThat(initialState.submitEnabled).isFalse()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - initial state server load`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
+ val presenter = LoginRootPresenter(
+ authenticationService,
+ oidcActionFlow,
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
+ assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
+ assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
+ assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
+ assertThat(initialState.submitEnabled).isFalse()
+ val loadingState = awaitItem()
+ assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading())
+ authenticationService.givenHomeserver(A_HOMESERVER)
+ skipItems(1)
+ val loadedState = awaitItem()
+ assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER))
+ }
+ }
+
+ @Test
+ fun `present - initial state server load error and retry`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
+ val presenter = LoginRootPresenter(
+ authenticationService,
+ oidcActionFlow,
+ )
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL)
+ assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized)
+ assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
+ assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
+ assertThat(initialState.submitEnabled).isFalse()
+ val loadingState = awaitItem()
+ assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading())
+ val aThrowable = Throwable("Error")
+ authenticationService.givenChangeServerError(aThrowable)
+ val errorState = awaitItem()
+ assertThat(errorState.homeserverDetails).isEqualTo(Async.Failure(aThrowable))
+ // Retry
+ errorState.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo)
+ val loadingState2 = awaitItem()
+ assertThat(loadingState2.homeserverDetails).isEqualTo(Async.Loading())
+ authenticationService.givenChangeServerError(null)
+ authenticationService.givenHomeserver(A_HOMESERVER)
+ skipItems(1)
+ val loadedState = awaitItem()
+ assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER))
}
}
@Test
fun `present - enter login and password`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
val presenter = LoginRootPresenter(
- FakeAuthenticationService(),
+ authenticationService,
+ oidcActionFlow,
)
+ authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -67,10 +142,99 @@ class LoginRootPresenterTest {
}
@Test
- fun `present - submit`() = runTest {
+ fun `present - oidc login`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
val presenter = LoginRootPresenter(
- FakeAuthenticationService(),
+ authenticationService,
+ oidcActionFlow,
)
+ authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.submitEnabled).isTrue()
+ initialState.eventSink.invoke(LoginRootEvents.Submit)
+ val oidcState = awaitItem()
+ assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA))
+ }
+ }
+
+ @Test
+ fun `present - oidc login error`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
+ val presenter = LoginRootPresenter(
+ authenticationService,
+ oidcActionFlow,
+ )
+ authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
+ authenticationService.givenOidcError(A_THROWABLE)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.submitEnabled).isTrue()
+ initialState.eventSink.invoke(LoginRootEvents.Submit)
+ val oidcState = awaitItem()
+ assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
+ }
+ }
+
+ @Test
+ fun `present - oidc custom tab login`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
+ val presenter = LoginRootPresenter(
+ authenticationService,
+ oidcActionFlow,
+ )
+ authenticationService.givenHomeserver(A_HOMESERVER_OIDC)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.submitEnabled).isTrue()
+ initialState.eventSink.invoke(LoginRootEvents.Submit)
+ val oidcState = awaitItem()
+ assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA))
+ // Oidc cancel, sdk error
+ authenticationService.givenOidcCancelError(A_THROWABLE)
+ oidcActionFlow.post(OidcAction.GoBack)
+ val stateCancelSdkError = awaitItem()
+ assertThat(stateCancelSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
+ // Oidc cancel, sdk OK
+ authenticationService.givenOidcCancelError(null)
+ oidcActionFlow.post(OidcAction.GoBack)
+ val stateCancel = awaitItem()
+ assertThat(stateCancel.loggedInState).isEqualTo(LoggedInState.NotLoggedIn)
+ // Oidc success, sdk error
+ authenticationService.givenLoginError(A_THROWABLE)
+ oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url))
+ val stateSuccessSdkErrorLoading = awaitItem()
+ assertThat(stateSuccessSdkErrorLoading.loggedInState).isEqualTo(LoggedInState.LoggingIn)
+ val stateSuccessSdkError = awaitItem()
+ assertThat(stateSuccessSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE))
+ // Oidc success
+ authenticationService.givenLoginError(null)
+ oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url))
+ val stateSuccess = awaitItem()
+ assertThat(stateSuccess.loggedInState).isEqualTo(LoggedInState.LoggingIn)
+ val stateSuccessLoggedIn = awaitItem()
+ assertThat(stateSuccessLoggedIn.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID))
+ }
+ }
+
+ @Test
+ fun `present - submit`() = runTest {
+ val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
+ val presenter = LoginRootPresenter(
+ authenticationService,
+ oidcActionFlow,
+ )
+ authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -90,9 +254,12 @@ class LoginRootPresenterTest {
@Test
fun `present - submit with error`() = runTest {
val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
val presenter = LoginRootPresenter(
authenticationService,
+ oidcActionFlow,
)
+ authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
@@ -113,14 +280,16 @@ class LoginRootPresenterTest {
@Test
fun `present - clear error`() = runTest {
val authenticationService = FakeAuthenticationService()
+ val oidcActionFlow = DefaultOidcActionFlow()
val presenter = LoginRootPresenter(
authenticationService,
+ oidcActionFlow,
)
+ authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
-
// Submit will return an error
authenticationService.givenLoginError(A_THROWABLE)
initialState.eventSink(LoginRootEvents.Submit)
diff --git a/features/logout/api/src/main/res/values-cs/translations.xml b/features/logout/api/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..31761ee2d9
--- /dev/null
+++ b/features/logout/api/src/main/res/values-cs/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "Opravdu se chcete odhlásit?"
+ "Odhlásit se"
+ "Odhlašování…"
+ "Odhlásit se"
+ "Odhlásit se"
+
\ No newline at end of file
diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/api/src/main/res/values-de/translations.xml
index 429a6017da..5b9fac1031 100644
--- a/features/logout/api/src/main/res/values-de/translations.xml
+++ b/features/logout/api/src/main/res/values-de/translations.xml
@@ -1,5 +1,6 @@
+ "Möchtest du Dich wirklich abmelden?"
"Abmelden"
"Abmeldung läuft…"
"Abmelden"
diff --git a/features/logout/api/src/main/res/values-fr/translations.xml b/features/logout/api/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..9d9ad724df
--- /dev/null
+++ b/features/logout/api/src/main/res/values-fr/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "Êtes-vous sûr de vouloir vous déconnecter?"
+ "Se déconnecter"
+ "Déconnexion en cours…"
+ "Se déconnecter"
+ "Se déconnecter"
+
\ No newline at end of file
diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt
index 9f15a77f4c..f1ed5c18dd 100644
--- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt
+++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt
@@ -20,6 +20,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.matrix.api.core.UserId
interface MessagesEntryPoint : FeatureEntryPoint {
fun createNode(
@@ -30,5 +31,6 @@ interface MessagesEntryPoint : FeatureEntryPoint {
interface Callback : Plugin {
fun onRoomDetailsClicked()
+ fun onUserDataClicked(userId: UserId)
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
index c07bbeff12..3dc77bc00a 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
@@ -40,6 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.Parcelize
@@ -90,6 +91,10 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onPreviewAttachments(attachments: ImmutableList) {
backstack.push(NavTarget.AttachmentPreview(attachments.first()))
}
+
+ override fun onUserDataClicked(userId: UserId) {
+ callback?.onUserDataClicked(userId)
+ }
}
createNode(buildContext, listOf(callback))
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
index 626b3bd682..96f76d3a34 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt
@@ -28,6 +28,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.di.RoomScope
+import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
@ContributesNode(RoomScope::class)
@@ -43,6 +44,7 @@ class MessagesNode @AssistedInject constructor(
fun onRoomDetailsClicked()
fun onEventClicked(event: TimelineItem.Event)
fun onPreviewAttachments(attachments: ImmutableList)
+ fun onUserDataClicked(userId: UserId)
}
private fun onRoomDetailsClicked() {
@@ -57,6 +59,10 @@ class MessagesNode @AssistedInject constructor(
callback?.onPreviewAttachments(attachments)
}
+ private fun onUserDataClicked(userId: UserId) {
+ callback?.onUserDataClicked(userId)
+ }
+
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@@ -66,6 +72,7 @@ class MessagesNode @AssistedInject constructor(
onRoomDetailsClicked = this::onRoomDetailsClicked,
onEventClicked = this::onEventClicked,
onPreviewAttachments = this::onPreviewAttachments,
+ onUserDataClicked = this::onUserDataClicked,
modifier = modifier,
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
index e3c6160b20..3aca34ee2b 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
@@ -85,6 +85,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.designsystem.utils.LogCompositions
+import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -97,6 +98,7 @@ fun MessagesView(
onBackPressed: () -> Unit,
onRoomDetailsClicked: () -> Unit,
onEventClicked: (event: TimelineItem.Event) -> Unit,
+ onUserDataClicked: (UserId) -> Unit,
onPreviewAttachments: (ImmutableList) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -153,11 +155,6 @@ fun MessagesView(
}
}
- fun onExpandGroupClick(event: TimelineItem.GroupedEvents) {
- Timber.v("onExpandGroupClick= ${event.id}")
- state.timelineState.eventSink(TimelineEvents.ToggleExpandGroup(event))
- }
-
fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) {
state.eventSink(MessagesEvents.HandleAction(action, event))
}
@@ -208,7 +205,7 @@ fun MessagesView(
.consumeWindowInsets(padding),
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked,
- onExpandGroupClick = ::onExpandGroupClick,
+ onUserDataClicked = onUserDataClicked,
)
},
snackbarHost = {
@@ -246,8 +243,8 @@ fun MessagesViewContent(
state: MessagesState,
modifier: Modifier = Modifier,
onMessageClicked: (TimelineItem.Event) -> Unit = {},
+ onUserDataClicked: (UserId) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
- onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
) {
Column(
modifier = modifier
@@ -262,7 +259,7 @@ fun MessagesViewContent(
modifier = Modifier.weight(1f),
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
- onExpandGroupClick = onExpandGroupClick,
+ onUserDataClicked = onUserDataClicked,
)
}
MessageComposerView(
@@ -362,6 +359,7 @@ private fun ContentToPreview(state: MessagesState) {
onBackPressed = {},
onRoomDetailsClicked = {},
onEventClicked = {},
- onPreviewAttachments = {}
+ onPreviewAttachments = {},
+ onUserDataClicked = {},
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
index 11f1a1a483..ff64441198 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
@@ -16,11 +16,9 @@
package io.element.android.features.messages.impl.timeline
-import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.core.EventId
sealed interface TimelineEvents {
object LoadMore : TimelineEvents
data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents
- data class ToggleExpandGroup(val event: TimelineItem.GroupedEvents) : TimelineEvents
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
index 1693a623f3..26c0bedf01 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
@@ -20,19 +20,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
-import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.libraries.architecture.Presenter
-import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
-import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -45,7 +40,6 @@ private const val backPaginationPageSize = 50
class TimelinePresenter @Inject constructor(
private val timelineItemsFactory: TimelineItemsFactory,
- private val timelineItemGrouper: TimelineItemGrouper,
room: MatrixRoom,
) : Presenter {
@@ -57,7 +51,6 @@ class TimelinePresenter @Inject constructor(
val highlightedEventId: MutableState = rememberSaveable {
mutableStateOf(null)
}
- val expandedGroups = remember { mutableStateMapOf() }
val timelineItems = timelineItemsFactory
.flow()
@@ -71,9 +64,6 @@ class TimelinePresenter @Inject constructor(
when (event) {
TimelineEvents.LoadMore -> localCoroutineScope.loadMore(paginationState.value)
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
- is TimelineEvents.ToggleExpandGroup -> {
- expandedGroups[event.event.identifier()] = expandedGroups[event.event.identifier()].orFalse().not()
- }
}
}
@@ -92,7 +82,7 @@ class TimelinePresenter @Inject constructor(
return TimelineState(
highlightedEventId = highlightedEventId.value,
paginationState = paginationState.value,
- timelineItems = timelineItemGrouper.group(timelineItems.value, expandedGroups).toImmutableList(),
+ timelineItems = timelineItems.value,
eventSink = ::handleEvents
)
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
index a8e17ea462..2cee290871 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
@@ -94,7 +94,7 @@ internal fun aTimelineItemEvent(
eventId: EventId = EventId("\$" + Random.nextInt().toString()),
isMine: Boolean = false,
content: TimelineItemEventContent = aTimelineItemTextContent(),
- groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.First,
+ groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None,
sendState: EventSendState = EventSendState.Sent(eventId),
): TimelineItem.Event {
return TimelineItem.Event(
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
index 0f31652ec8..f5ebdcc796 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
@@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@@ -47,8 +48,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -85,6 +88,7 @@ import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -93,9 +97,9 @@ import kotlinx.coroutines.launch
fun TimelineView(
state: TimelineState,
modifier: Modifier = Modifier,
+ onUserDataClicked: (UserId) -> Unit = {},
onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
- onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit = {},
) {
fun onReachedLoadMore() {
@@ -119,7 +123,7 @@ fun TimelineView(
highlightedItem = state.highlightedEventId?.value,
onClick = onMessageClicked,
onLongClick = onMessageLongClicked,
- onExpandGroupClick = onExpandGroupClick,
+ onUserDataClick = onUserDataClicked,
)
if (index == state.timelineItems.lastIndex) {
onReachedLoadMore()
@@ -139,9 +143,9 @@ fun TimelineView(
fun TimelineItemRow(
timelineItem: TimelineItem,
highlightedItem: String?,
+ onUserDataClick: (UserId) -> Unit,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
- onExpandGroupClick: (TimelineItem.GroupedEvents) -> Unit,
modifier: Modifier = Modifier
) {
when (timelineItem) {
@@ -174,13 +178,16 @@ fun TimelineItemRow(
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = ::onClick,
onLongClick = ::onLongClick,
+ onUserDataClick = onUserDataClick,
modifier = modifier,
)
}
}
is TimelineItem.GroupedEvents -> {
+ val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) }
+
fun onExpandGroupClick() {
- onExpandGroupClick(timelineItem)
+ isExpanded.value = !isExpanded.value
}
Column(modifier = modifier.animateContentSize()) {
@@ -190,11 +197,11 @@ fun TimelineItemRow(
count = timelineItem.events.size,
timelineItem.events.size
),
- isExpanded = timelineItem.expanded,
- isHighlighted = !timelineItem.expanded && timelineItem.events.any { it.identifier() == highlightedItem },
+ isExpanded = isExpanded.value,
+ isHighlighted = !isExpanded.value && timelineItem.events.any { it.identifier() == highlightedItem },
onClick = ::onExpandGroupClick,
)
- if (timelineItem.expanded) {
+ if (isExpanded.value) {
Column {
timelineItem.events.forEach { subGroupEvent ->
TimelineItemRow(
@@ -202,7 +209,7 @@ fun TimelineItemRow(
highlightedItem = highlightedItem,
onClick = onClick,
onLongClick = onLongClick,
- onExpandGroupClick = {}
+ onUserDataClick = onUserDataClick,
)
}
}
@@ -230,15 +237,21 @@ fun TimelineItemEventRow(
isHighlighted: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
+ onUserDataClick: (UserId) -> Unit,
modifier: Modifier = Modifier
) {
val interactionSource = remember { MutableInteractionSource() }
+ fun onUserDataClicked() {
+ onUserDataClick(event.senderId)
+ }
+
val (parentAlignment, contentAlignment) = if (event.isMine) {
Pair(Alignment.CenterEnd, Alignment.End)
} else {
Pair(Alignment.CenterStart, Alignment.Start)
}
+
Box(
modifier = modifier
.fillMaxWidth()
@@ -247,14 +260,17 @@ fun TimelineItemEventRow(
) {
Row {
if (!event.isMine) {
- Spacer(modifier = Modifier.width(16.dp))
+ Spacer(modifier = Modifier.width(4.dp))
}
Column(horizontalAlignment = contentAlignment) {
if (event.showSenderInformation) {
MessageSenderInformation(
event.safeSenderName,
event.senderAvatar,
- Modifier.zIndex(1f)
+ Modifier
+ .zIndex(1f)
+ .offset(y = 12.dp)
+ .clickable(onClick = ::onUserDataClicked)
)
}
val bubbleState = BubbleState(
@@ -282,7 +298,7 @@ fun TimelineItemEventRow(
reactionsState = event.reactionsState,
modifier = Modifier
.zIndex(1f)
- .offset(x = if (event.isMine) 0.dp else 20.dp, y = -(16.dp))
+ .offset(x = if (event.isMine) 0.dp else 20.dp, y = -(4.dp))
)
}
if (event.isMine) {
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
index 606d185568..220d653e8f 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt
@@ -20,7 +20,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -35,13 +34,16 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState
import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider
+import io.element.android.libraries.core.extensions.to01
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface
+import io.element.android.libraries.designsystem.theme.components.Text
private val BUBBLE_RADIUS = 16.dp
@@ -84,10 +86,9 @@ fun MessageEventBubble(
fun Modifier.offsetForItem(): Modifier {
return if (state.isMine) {
- // FIXME setting y offset to -12.dp can overlap a state event displayed above.
- offset(y = -(12.dp))
+ this
} else {
- offset(x = 20.dp, y = -(12.dp))
+ offset(x = 20.dp)
}
}
@@ -130,7 +131,7 @@ internal fun MessageEventBubbleDarkPreview(@PreviewParameter(BubbleStateProvider
@Composable
private fun ContentToPreview(state: BubbleState) {
- // Due to y offset, surround with a Box
+ // Due to position offset, surround with a Box
Box(
modifier = Modifier
.size(width = 240.dp, height = 64.dp)
@@ -141,7 +142,18 @@ private fun ContentToPreview(state: BubbleState) {
state = state,
interactionSource = MutableInteractionSource(),
) {
- Spacer(modifier = Modifier.size(width = 120.dp, height = 32.dp))
+ // Render the state as a text to better understand the previews
+ Box(
+ modifier = Modifier
+ .size(width = 120.dp, height = 32.dp)
+ .padding(horizontal = 12.dp, vertical = 6.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ fontSize = 10.sp,
+ text = "${state.groupPosition.javaClass.simpleName} m:${state.isMine.to01()} h:${state.isHighlighted.to01()}"
+ )
+ }
}
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
index 8358d6cc1e..ed4febf982 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
@@ -25,13 +25,18 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.ElementTextStyles
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
@@ -48,7 +53,10 @@ fun TimelineEventTimestampView(
val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed
val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse()
val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null
- Row(modifier = modifier.clickable(onClick = onClick)) {
+ Row(
+ modifier = modifier.clickable(onClick = onClick),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
if (isMessageEdited) {
Text(
stringResource(R.string.common_edited_suffix),
@@ -68,3 +76,21 @@ fun TimelineEventTimestampView(
}
}
}
+
+@Preview
+@Composable
+internal fun TimelineEventTimestampViewLightPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
+ ElementPreviewLight { ContentToPreview(event) }
+
+@Preview
+@Composable
+internal fun TimelineEventTimestampViewDarkPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) =
+ ElementPreviewDark { ContentToPreview(event) }
+
+@Composable
+private fun ContentToPreview(event: TimelineItem.Event) {
+ TimelineEventTimestampView(
+ event = event,
+ onClick = {}
+ )
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt
new file mode 100644
index 0000000000..24daed065a
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.messages.impl.timeline.components
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
+import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
+import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState
+
+class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aTimelineItemEvent(),
+ // Sending failed
+ aTimelineItemEvent().copy(sendState = EventSendState.SendingFailed("AN_ERROR")),
+ // Edited
+ aTimelineItemEvent().copy(content = aTimelineItemTextContent().copy(isEdited = true)),
+ // Sending failed + Edited (not sure this is possible IRL, but should be covered by test)
+ aTimelineItemEvent().copy(
+ sendState = EventSendState.SendingFailed("AN_ERROR"),
+ content = aTimelineItemTextContent().copy(isEdited = true),
+ ),
+ )
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt
index 44b49cbae0..f4e771a32d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt
@@ -21,9 +21,13 @@ import io.element.android.features.messages.impl.timeline.diff.CacheInvalidator
import io.element.android.features.messages.impl.timeline.diff.MatrixTimelineItemsDiffCallback
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
+import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -38,9 +42,10 @@ class TimelineItemsFactory @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val eventItemFactory: TimelineItemEventFactory,
private val virtualItemFactory: TimelineItemVirtualFactory,
+ private val timelineItemGrouper: TimelineItemGrouper,
) {
- private val timelineItems = MutableStateFlow>(emptyList())
+ private val timelineItems = MutableStateFlow(emptyList().toImmutableList())
private val timelineItemsCache = arrayListOf()
// Items from rust sdk, used for diffing
@@ -49,7 +54,7 @@ class TimelineItemsFactory @Inject constructor(
private val lock = Mutex()
private val cacheInvalidator = CacheInvalidator(timelineItemsCache)
- fun flow(): StateFlow> = timelineItems.asStateFlow()
+ fun flow(): StateFlow> = timelineItems.asStateFlow()
suspend fun replaceWith(
timelineItems: List,
@@ -72,7 +77,8 @@ class TimelineItemsFactory @Inject constructor(
newTimelineItemStates.add(cacheItem)
}
}
- this.timelineItems.emit(newTimelineItemStates)
+ val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
+ this.timelineItems.emit(result)
}
private fun calculateAndApplyDiff(newTimelineItems: List) {
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
index af3d7e6f24..87792edf1d 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt
@@ -16,10 +16,12 @@
package io.element.android.features.messages.impl.timeline.factories.event
+import io.element.android.features.messages.impl.timeline.groups.canBeDisplayedInBubbleBlock
import io.element.android.features.messages.impl.timeline.model.AggregatedReaction
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
+import io.element.android.libraries.core.bool.orTrue
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.timeline.MatrixTimelineItem
@@ -102,10 +104,39 @@ class TimelineItemEventFactory @Inject constructor(
val previousSender = prevTimelineItem?.event?.sender
val nextSender = nextTimelineItem?.event?.sender
+ val previousIsGroupable = prevTimelineItem?.canBeDisplayedInBubbleBlock().orTrue()
+ val nextIsGroupable = nextTimelineItem?.canBeDisplayedInBubbleBlock().orTrue()
+
return when {
- previousSender != currentSender && nextSender == currentSender -> TimelineItemGroupPosition.First
- previousSender == currentSender && nextSender == currentSender -> TimelineItemGroupPosition.Middle
- previousSender == currentSender && nextSender != currentSender -> TimelineItemGroupPosition.Last
+ previousSender != currentSender && nextSender == currentSender -> {
+ if (nextIsGroupable) {
+ TimelineItemGroupPosition.First
+ } else {
+ TimelineItemGroupPosition.None
+ }
+ }
+ previousSender == currentSender && nextSender == currentSender -> {
+ if (previousIsGroupable) {
+ if (nextIsGroupable) {
+ TimelineItemGroupPosition.Middle
+ } else {
+ TimelineItemGroupPosition.Last
+ }
+ } else {
+ if (nextIsGroupable) {
+ TimelineItemGroupPosition.First
+ } else {
+ TimelineItemGroupPosition.None
+ }
+ }
+ }
+ previousSender == currentSender /* && nextSender != currentSender (== true) */ -> {
+ if (previousIsGroupable) {
+ TimelineItemGroupPosition.Last
+ } else {
+ TimelineItemGroupPosition.None
+ }
+ }
else -> TimelineItemGroupPosition.None
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt
new file mode 100644
index 0000000000..879f33e328
--- /dev/null
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.messages.impl.timeline.groups
+
+import io.element.android.features.messages.impl.timeline.model.TimelineItem
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
+import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
+import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
+import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
+import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
+import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
+import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
+import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
+import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
+import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
+import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
+
+/**
+ * Return true if the Event can be grouped in a collapse/expand block
+ * When [canBeGrouped] returns a value, [canBeDisplayedInBubbleBlock] MUST return the opposite value.
+ * Since the receiving type are not the same, the two functions exist.
+ */
+internal fun TimelineItem.Event.canBeGrouped(): Boolean {
+ return when (content) {
+ is TimelineItemTextBasedContent,
+ is TimelineItemEncryptedContent,
+ is TimelineItemImageContent,
+ is TimelineItemFileContent,
+ is TimelineItemVideoContent,
+ TimelineItemRedactedContent,
+ TimelineItemUnknownContent -> false
+ is TimelineItemProfileChangeContent,
+ is TimelineItemRoomMembershipContent,
+ is TimelineItemStateEventContent -> true
+ }
+}
+
+/**
+ * Return true if the Event can be grouped in a block of message bubbles.
+ * When [canBeDisplayedInBubbleBlock] returns a value, [canBeGrouped] MUST return the opposite value.
+ * Since the receiving type are not the same, the two functions exist.
+ */
+internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
+ return when (event.content) {
+ is FailedToParseMessageLikeContent,
+ is MessageContent,
+ RedactedContent,
+ is StickerContent,
+ is UnableToDecryptContent -> true
+ is FailedToParseStateContent,
+ is ProfileChangeContent,
+ is RoomMembershipContent,
+ UnknownContent,
+ is StateContent -> false
+ }
+}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt
index 24f74f4b5c..b04509ba22 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt
@@ -17,28 +17,14 @@
package io.element.android.features.messages.impl.timeline.groups
import io.element.android.features.messages.impl.timeline.model.TimelineItem
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRoomMembershipContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
-import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
-import io.element.android.libraries.core.bool.orFalse
import kotlinx.collections.immutable.toImmutableList
-
import javax.inject.Inject
class TimelineItemGrouper @Inject constructor() {
/**
* Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents].
*/
- fun group(from: List, expandedGroups: Map): List {
+ fun group(from: List): List {
val result = mutableListOf()
val currentGroup = mutableListOf()
from.forEach { timelineItem ->
@@ -48,42 +34,24 @@ class TimelineItemGrouper @Inject constructor() {
// timelineItem cannot be grouped
if (currentGroup.isNotEmpty()) {
// There is a pending group, create a TimelineItem.GroupedEvents if there is more than 1 Event in the pending group.
- result.addGroup(currentGroup, expandedGroups)
+ result.addGroup(currentGroup)
currentGroup.clear()
}
result.add(timelineItem)
}
}
if (currentGroup.isNotEmpty()) {
- result.addGroup(currentGroup, expandedGroups)
+ result.addGroup(currentGroup)
}
return result
}
-
- private fun TimelineItem.Event.canBeGrouped(): Boolean {
- return when (content) {
- is TimelineItemEncryptedContent,
- is TimelineItemImageContent,
- TimelineItemRedactedContent,
- is TimelineItemEmoteContent,
- is TimelineItemNoticeContent,
- is TimelineItemTextContent,
- is TimelineItemFileContent,
- is TimelineItemVideoContent,
- TimelineItemUnknownContent -> false
- is TimelineItemProfileChangeContent,
- is TimelineItemRoomMembershipContent,
- is TimelineItemStateEventContent -> true
- }
- }
}
/**
* Will add a group if there is more than 1 item, else add the item to the list.
*/
private fun MutableList.addGroup(
- group: MutableList,
- expandedGroups: Map,
+ group: MutableList
) {
if (group.size == 1) {
// Do not create a group with just 1 item, just add the item to the result
@@ -91,7 +59,6 @@ private fun MutableList.addGroup(
} else {
add(
TimelineItem.GroupedEvents(
- expanded = expandedGroups[group.first().id + "_group"].orFalse(),
events = group.toImmutableList()
)
)
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt
index 5daa75a8c4..c004901232 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt
@@ -68,10 +68,9 @@ sealed interface TimelineItem {
@Immutable
data class GroupedEvents(
- val expanded: Boolean,
val events: ImmutableList,
) : TimelineItem {
- // use first id with a suffix
- val id = events.first().id + "_group"
+ // use last id with a suffix. Last will not change in cas of new event from backpagination.
+ val id = events.last().id + "_group"
}
}
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt
index ec3a94519b..5a93e87e73 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemGroupPosition.kt
@@ -18,13 +18,48 @@ package io.element.android.features.messages.impl.timeline.model
import androidx.compose.runtime.Immutable
+/**
+ * Attribute for a TimelineItem, used to render successive events from the same sender differently.
+ *
+ * Possible sequences in the timeline will be:
+ *
+ * Only one Event:
+ * - [None]
+ *
+ * Two Events
+ * - [First]
+ * - [Last]
+ *
+ * Many Events:
+ * - [First]
+ * - [Middle] (repeated if necessary)
+ * - [Last]
+ */
@Immutable
sealed interface TimelineItemGroupPosition {
+ /**
+ * The event is part of a group of events from the same sender and is the first sent Event.
+ */
object First : TimelineItemGroupPosition
+
+ /**
+ * The event is part of a group of events from the same sender and is neither the first nor the last sent Event.
+ */
object Middle : TimelineItemGroupPosition
+
+ /**
+ * The event is part of a group of events from the same sender and is the last sent Event.
+ */
object Last : TimelineItemGroupPosition
+
+ /**
+ * The event is not part of a group of events. Sender of previous event is different, and sender of next event is different.
+ */
object None : TimelineItemGroupPosition
+ /**
+ * Return true if the previous sender of the event is a different sender.
+ */
fun isNew(): Boolean = when (this) {
First, None -> true
else -> false
diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt
index 91804278ae..fbcbcc5454 100644
--- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt
+++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt
@@ -25,6 +25,7 @@ open class BubbleStateProvider : PreviewParameterProvider {
TimelineItemGroupPosition.First,
TimelineItemGroupPosition.Middle,
TimelineItemGroupPosition.Last,
+ TimelineItemGroupPosition.None,
).map { groupPosition ->
sequenceOf(false, true).map { isMine ->
sequenceOf(false, true).map { isHighlighted ->
diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..6072ac8545
--- /dev/null
+++ b/features/messages/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,8 @@
+
+
+ "Fotoaparát"
+ "Vyfotit"
+ "Natočit video"
+ "Příloha"
+ "Knihovna fotografií a videí"
+
\ No newline at end of file
diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml
index 64f863c55f..78088386ad 100644
--- a/features/messages/impl/src/main/res/values-de/translations.xml
+++ b/features/messages/impl/src/main/res/values-de/translations.xml
@@ -1,5 +1,9 @@
+
+ - "%1$d Raumänderung"
+ - "%1$d Raumänderungen"
+
"Kamera"
"Foto aufnehmen"
"Video aufnehmen"
diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..2eb6016f2f
--- /dev/null
+++ b/features/messages/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,8 @@
+
+
+
+ - "%1$d changement dans la conversation"
+ - "%1$d changements dans la conversation"
+
+ "Prendre une photo"
+
\ No newline at end of file
diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml
index 68d83cacfe..0a2aa20456 100644
--- a/features/messages/impl/src/main/res/values-ro/translations.xml
+++ b/features/messages/impl/src/main/res/values-ro/translations.xml
@@ -5,4 +5,10 @@
- "%1$d schimbări ale camerei"
- "%1$d schimbări ale camerei"
+ "Cameră foto"
+ "Faceți o fotografie"
+ "Înregistrați un videoclip"
+ "Atașament"
+ "Bibliotecă foto și video"
+ "Procesarea datelor media a eșuat, vă rugăm să încercați din nou."
\ No newline at end of file
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
index 1d8ee54505..05345a4e3c 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
@@ -16,6 +16,7 @@
package io.element.android.features.messages
+import android.net.Uri
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
@@ -28,7 +29,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter
-import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.features.messages.media.FakeLocalMediaFactory
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
@@ -41,11 +41,15 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.textcomposer.MessageComposerMode
+import io.mockk.mockk
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
class MessagesPresenterTest {
+
+ private val mockMediaUrl: Uri = mockk("localMediaUri")
+
@Test
fun `present - initial state`() = runTest {
val presenter = createMessagePresenter()
@@ -136,13 +140,12 @@ class MessagesPresenterTest {
room = matrixRoom,
mediaPickerProvider = FakePickerProvider(),
featureFlagService = FakeFeatureFlagService(),
- localMediaFactory = FakeLocalMediaFactory(),
+ localMediaFactory = FakeLocalMediaFactory(mockMediaUrl),
mediaSender = MediaSender(FakeMediaPreProcessor(),matrixRoom),
snackbarDispatcher = SnackbarDispatcher(),
)
val timelinePresenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
- timelineItemGrouper = TimelineItemGrouper(),
room = matrixRoom,
)
val actionListPresenter = ActionListPresenter()
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt
index 0b16a254a1..7d7316b290 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package io.element.android.features.messages.attachments
+import android.net.Uri
import androidx.media3.common.MimeTypes
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
@@ -33,12 +36,15 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
class AttachmentsPreviewPresenterTest {
private val mediaPreProcessor = FakeMediaPreProcessor()
+ private val mockMediaUrl: Uri = mockk("localMediaUri")
@Test
fun `present - send media success scenario`() = runTest {
@@ -84,7 +90,10 @@ class AttachmentsPreviewPresenterTest {
}
private fun anAttachmentsPreviewPresenter(
- localMedia: LocalMedia = aLocalMedia(mimeType = MimeTypes.IMAGE_JPEG),
+ localMedia: LocalMedia = aLocalMedia(
+ uri = mockMediaUrl,
+ mimeType = MimeTypes.IMAGE_JPEG
+ ),
room: MatrixRoom = FakeMatrixRoom()
): AttachmentsPreviewPresenter {
return AttachmentsPreviewPresenter(
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt
index ce5847f559..60a01a76d4 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt
@@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.media.local.LocalMedia
import io.mockk.mockk
fun aLocalMedia(
- uri: Uri = mockk("localMediaUri"),
+ uri: Uri,
mimeType: String = MimeTypes.IMAGE_JPEG,
name: String = "a media",
size: Long = 1000,
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
index 5d355be8d0..a695ae6401 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt
@@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
+import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
@@ -56,7 +57,8 @@ internal fun aTimelineItemsFactory(): TimelineItemsFactory {
daySeparatorFactory = TimelineItemDaySeparatorFactory(
FakeDaySeparatorFormatter()
),
- )
+ ),
+ timelineItemGrouper = TimelineItemGrouper(),
)
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt
index 0382941d87..89f4e96173 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt
@@ -23,12 +23,12 @@ import io.element.android.features.messages.impl.media.local.LocalMediaFactory
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.MediaFile
-class FakeLocalMediaFactory : LocalMediaFactory {
+class FakeLocalMediaFactory(private val localMediaUri: Uri) : LocalMediaFactory {
var fallbackMimeType: String = MimeTypes.OctetStream
override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia {
- return aLocalMedia(mimeType = mimeType ?: fallbackMimeType)
+ return aLocalMedia(uri = localMediaUri, mimeType = mimeType ?: fallbackMimeType)
}
override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia {
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt
index 16f2894303..1ef8097d4a 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package io.element.android.features.messages.media.viewer
+import android.net.Uri
import androidx.media3.common.MimeTypes
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
@@ -29,6 +32,8 @@ import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.media.aMediaSource
+import io.mockk.mockk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -37,7 +42,8 @@ private const val TESTED_MEDIA_NAME = "MediaName"
class MediaViewerPresenterTest {
- private val localMediaFactory = FakeLocalMediaFactory()
+ private val mockMediaUrl: Uri = mockk("localMediaUri")
+ private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
private val mediaLoader = FakeMediaLoader()
@Test
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
index 01c6e0eb49..bfef3b4011 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt
@@ -18,6 +18,7 @@
package io.element.android.features.messages.textcomposer
+import android.net.Uri
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
@@ -69,7 +70,8 @@ class MessageComposerPresenterTest {
)
private val mediaPreProcessor = FakeMediaPreProcessor()
private val snackbarDispatcher = SnackbarDispatcher()
- private val localMediaFactory = FakeLocalMediaFactory()
+ private val mockMediaUrl: Uri = mockk("localMediaUri")
+ private val localMediaFactory = FakeLocalMediaFactory(mockMediaUrl)
@Test
fun `present - initial state`() = runTest {
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
index 7b13a93a5a..a88b2251a4 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt
@@ -23,13 +23,8 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelinePresenter
-import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper
-import io.element.android.features.messages.impl.timeline.model.TimelineItem
-import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
-import io.element.android.libraries.matrix.test.room.anEventTimelineItem
-import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -38,7 +33,6 @@ class TimelinePresenterTest {
fun `present - initial state`() = runTest {
val presenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
- timelineItemGrouper = TimelineItemGrouper(),
room = FakeMatrixRoom(),
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -55,7 +49,6 @@ class TimelinePresenterTest {
fun `present - load more`() = runTest {
val presenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
- timelineItemGrouper = TimelineItemGrouper(),
room = FakeMatrixRoom(),
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -78,7 +71,6 @@ class TimelinePresenterTest {
fun `present - set highlighted event`() = runTest {
val presenter = TimelinePresenter(
timelineItemsFactory = aTimelineItemsFactory(),
- timelineItemGrouper = TimelineItemGrouper(),
room = FakeMatrixRoom(),
)
moleculeFlow(RecompositionClock.Immediate) {
@@ -95,37 +87,4 @@ class TimelinePresenterTest {
assertThat(withoutHighlightedState.highlightedEventId).isNull()
}
}
-
- @Test
- fun `present - expand and collapse grouped events`() = runTest {
- val fakeTimeline = FakeMatrixTimeline(
- initialTimelineItems = listOf(
- MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */),
- MatrixTimelineItem.Event(anEventTimelineItem() /* This is a groupable event */),
- )
- )
- val fakeRoom = FakeMatrixRoom(matrixTimeline = fakeTimeline)
- val presenter = TimelinePresenter(
- timelineItemsFactory = aTimelineItemsFactory(),
- timelineItemGrouper = TimelineItemGrouper(),
- room = fakeRoom,
- )
- moleculeFlow(RecompositionClock.Immediate) {
- presenter.present()
- }.test {
- skipItems(1)
- fakeTimeline.updateTimelineItems { it }
- val loadedState = awaitItem()
- val group1 = loadedState.timelineItems.first() as TimelineItem.GroupedEvents
- assertThat(group1.expanded).isFalse()
- loadedState.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group1))
- val withExpandedGroup = awaitItem()
- val group2 = withExpandedGroup.timelineItems.first() as TimelineItem.GroupedEvents
- assertThat(group2.expanded).isTrue()
- withExpandedGroup.eventSink.invoke(TimelineEvents.ToggleExpandGroup(group2))
- val withCollapsedGroup = awaitItem()
- val group3 = withCollapsedGroup.timelineItems.first() as TimelineItem.GroupedEvents
- assertThat(group3.expanded).isFalse()
- }
- }
}
diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt
index 2644a12431..3edde16841 100644
--- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt
+++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt
@@ -49,7 +49,7 @@ class TimelineItemGrouperTest {
@Test
fun `test empty`() {
- val result = sut.group(emptyList(), emptyMap())
+ val result = sut.group(emptyList())
assertThat(result).isEmpty()
}
@@ -60,7 +60,6 @@ class TimelineItemGrouperTest {
aNonGroupableItem,
aNonGroupableItem,
),
- emptyMap()
)
assertThat(result).isEqualTo(
listOf(
@@ -77,12 +76,10 @@ class TimelineItemGrouperTest {
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
aGroupableItem,
),
- emptyMap()
)
assertThat(result).isEqualTo(
listOf(
TimelineItem.GroupedEvents(
- expanded = false,
events = listOf(
aGroupableItem,
aGroupableItem.copy(id = AN_EVENT_ID_2.value),
@@ -92,28 +89,6 @@ class TimelineItemGrouperTest {
)
}
- @Test
- fun `test groupables expanded`() {
- val result = sut.group(
- listOf(
- aGroupableItem,
- aGroupableItem.copy(id = AN_EVENT_ID_2.value),
- ),
- mapOf("${AN_EVENT_ID_2.value}_group" to true)
- )
- assertThat(result).isEqualTo(
- listOf(
- TimelineItem.GroupedEvents(
- expanded = true,
- events = listOf(
- aGroupableItem.copy(id = AN_EVENT_ID_2.value),
- aGroupableItem,
- ).toImmutableList()
- ),
- )
- )
- }
-
@Test
fun `test 1 groupable, not group must be created`() {
val listsToTest = listOf(
@@ -130,7 +105,7 @@ class TimelineItemGrouperTest {
listOf(aNonGroupableItemNoEvent),
)
listsToTest.forEach { listToTest ->
- val result = sut.group(listToTest, emptyMap())
+ val result = sut.group(listToTest)
assertThat(result).isEqualTo(listToTest)
}
}
@@ -146,12 +121,10 @@ class TimelineItemGrouperTest {
aGroupableItem,
aGroupableItem,
),
- emptyMap()
)
assertThat(result).isEqualTo(
listOf(
TimelineItem.GroupedEvents(
- expanded = false,
events = listOf(
aGroupableItem,
aGroupableItem,
@@ -159,7 +132,6 @@ class TimelineItemGrouperTest {
),
aNonGroupableItem,
TimelineItem.GroupedEvents(
- expanded = false,
events = listOf(
aGroupableItem,
aGroupableItem,
diff --git a/features/onboarding/impl/src/main/res/values-cs/translations.xml b/features/onboarding/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..f9eb134f01
--- /dev/null
+++ b/features/onboarding/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Vítejte v %1$s Beta. Vylepšený, pro rychlost a jednoduchost."
+ "Buďte ve svém živlu"
+
\ No newline at end of file
diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml
index 82e01aa522..60f4334e45 100644
--- a/features/onboarding/impl/src/main/res/values-de/translations.xml
+++ b/features/onboarding/impl/src/main/res/values-de/translations.xml
@@ -1,4 +1,5 @@
+ "Willkommen zur %1$s Beta. Verbessert, für Geschwindigkeit und Einfachheit."
"Sei in deinem Element"
\ No newline at end of file
diff --git a/features/onboarding/impl/src/main/res/values-fr/translations.xml b/features/onboarding/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..502b464fa7
--- /dev/null
+++ b/features/onboarding/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "Bienvenue dans la version %1$s Beta. Affiné pour plus de rapidité et de simplicité."
+ "Soyez dans votre Element"
+
\ No newline at end of file
diff --git a/features/onboarding/impl/src/main/res/values/localazy.xml b/features/onboarding/impl/src/main/res/values/localazy.xml
index 3baebbaa6b..54d86ba247 100644
--- a/features/onboarding/impl/src/main/res/values/localazy.xml
+++ b/features/onboarding/impl/src/main/res/values/localazy.xml
@@ -1,5 +1,9 @@
+ "Sign in manually"
+ "Sign in with QR code"
+ "Create account"
+ "Communicate and collaborate securely"
"Welcome to the %1$s Beta. Supercharged, for speed and simplicity."
"Be in your Element"
\ No newline at end of file
diff --git a/features/rageshake/api/src/main/res/values-cs/translations.xml b/features/rageshake/api/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..ce53ad9285
--- /dev/null
+++ b/features/rageshake/api/src/main/res/values-cs/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"
+
\ No newline at end of file
diff --git a/features/rageshake/api/src/main/res/values-de/translations.xml b/features/rageshake/api/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..1633cd340e
--- /dev/null
+++ b/features/rageshake/api/src/main/res/values-de/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?"
+ "Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?"
+
\ No newline at end of file
diff --git a/features/rageshake/api/src/main/res/values-fr/translations.xml b/features/rageshake/api/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..32bdaf4685
--- /dev/null
+++ b/features/rageshake/api/src/main/res/values-fr/translations.xml
@@ -0,0 +1,5 @@
+
+
+ "%1$s a planté la dernière fois qu\'il a été utilisé. Souhaitez-vous partager un rapport de crash avec nous ?"
+ "Vous semblez secouer le téléphone de frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?"
+
\ No newline at end of file
diff --git a/features/rageshake/impl/src/main/res/values-cs/translations.xml b/features/rageshake/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..b02e635326
--- /dev/null
+++ b/features/rageshake/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?"
+
\ No newline at end of file
diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml
index 8ca17d23ee..8712bba1a0 100644
--- a/features/rageshake/impl/src/main/res/values-de/translations.xml
+++ b/features/rageshake/impl/src/main/res/values-de/translations.xml
@@ -1,7 +1,14 @@
"Bildschirmfoto anhängen"
+ "Sie können mich kontaktieren, wenn Sie weitere Fragen haben"
+ "Bildschirmfoto bearbeiten"
+ "Beschreibe bitte den Fehler. Was hast du gemacht? Was hätte passieren sollen? Was ist passiert? Bitte beschreibe alles mit so vielen Details wie möglich."
"Beschreibe den Fehler…"
+ "Wenn möglich, verfassen Sie die Beschreibung bitte auf Englisch."
"Absturzprotokolle senden"
+ "Senden Sie Protokolle, um zu helfen"
"Bildschirmfoto senden"
+ "Um zu überprüfen, ob alles wie vorgesehen funktioniert, werden Protokolle mit deiner Nachricht gesendet. Diese werden privat sein. Um nur Ihre Nachricht zu senden, schalte diese Einstellung aus."
+ "%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?"
\ No newline at end of file
diff --git a/features/rageshake/impl/src/main/res/values-fr/translations.xml b/features/rageshake/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..e5e88975a6
--- /dev/null
+++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,14 @@
+
+
+ "Joindre une capture d\'écran"
+ "Vous pouvez me contacter si vous avez des questions complémentaires"
+ "Modifier la capture d\'écran"
+ "S\'il vous plait, veuillez décrire le bogue. Qu\'avez-vous fait ? À quoi vous attendiez-vous ? Que s\'est-il réellement passé. Veuillez ajouter le plus de détails possible."
+ "Décrire le bogue"
+ "Si possible, veuillez rédiger la description en anglais."
+ "Envoyer des journaux d’incident"
+ "Envoyer le journal pour nous aider"
+ "Envoyer une capture d’écran"
+ "Pour vérifier que les choses fonctionnent comme prévu, les journaux seront envoyés avec votre message. Ceux-ci seront privées. Pour simplement envoyer votre message, désactivez ce paramètre."
+ "%1$s a planté la dernière fois qu\'il a été utilisé. Souhaitez-vous partager un rapport de crash avec nous ?"
+
\ No newline at end of file
diff --git a/features/roomdetails/api/build.gradle.kts b/features/roomdetails/api/build.gradle.kts
index c93ec69f89..ddc062cb3b 100644
--- a/features/roomdetails/api/build.gradle.kts
+++ b/features/roomdetails/api/build.gradle.kts
@@ -16,6 +16,7 @@
plugins {
id("io.element.android-library")
+ id("kotlin-parcelize")
}
android {
diff --git a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt
index 560e9d5dd7..e73d63f38c 100644
--- a/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt
+++ b/features/roomdetails/api/src/main/kotlin/io/element/android/features/roomdetails/api/RoomDetailsEntryPoint.kt
@@ -16,11 +16,26 @@
package io.element.android.features.roomdetails.api
+import android.os.Parcelable
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.matrix.api.core.UserId
+import kotlinx.parcelize.Parcelize
interface RoomDetailsEntryPoint : FeatureEntryPoint {
- fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node
+
+ sealed interface InitialTarget : Parcelable {
+ @Parcelize
+ object RoomDetails : InitialTarget
+
+ @Parcelize
+ data class RoomMemberDetails(val roomMemberId: UserId) : InitialTarget
+ }
+
+ data class Inputs(val initialElement: InitialTarget) : NodeInputs
+
+ fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs, plugins: List): Node
}
diff --git a/features/roomdetails/impl/build.gradle.kts b/features/roomdetails/impl/build.gradle.kts
index bcd618eccd..2ad93a95f4 100644
--- a/features/roomdetails/impl/build.gradle.kts
+++ b/features/roomdetails/impl/build.gradle.kts
@@ -41,6 +41,8 @@ dependencies {
implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.androidutils)
+ implementation(projects.libraries.mediapickers.api)
+ implementation(projects.libraries.mediaupload.api)
api(projects.features.roomdetails.api)
api(projects.libraries.usersearch.api)
api(projects.services.apperror.api)
@@ -52,7 +54,10 @@ dependencies {
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
+ testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.mediaupload.test)
+ testImplementation(projects.libraries.mediapickers.test)
testImplementation(projects.libraries.usersearch.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.features.leaveroom.fake)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt
index eebdbea062..be6b915212 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/DefaultRoomDetailsEntryPoint.kt
@@ -21,13 +21,25 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
+import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint.InitialTarget
+import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode.NavTarget
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomDetailsEntryPoint @Inject constructor() : RoomDetailsEntryPoint {
- override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node {
- return parentNode.createNode(buildContext, plugins)
+ override fun createNode(
+ parentNode: Node,
+ buildContext: BuildContext,
+ inputs: RoomDetailsEntryPoint.Inputs,
+ plugins: List
+ ): Node {
+ return parentNode.createNode(buildContext, plugins + inputs)
}
}
+
+internal fun InitialTarget.toNavTarget() = when (this) {
+ is InitialTarget.RoomDetails -> NavTarget.RoomDetails
+ is InitialTarget.RoomMemberDetails -> NavTarget.RoomMemberDetails(roomMemberId)
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsAction.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsAction.kt
new file mode 100644
index 0000000000..61b3da21f9
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsAction.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl
+
+sealed interface RoomDetailsAction {
+ object Edit : RoomDetailsAction
+
+ object AddTopic : RoomDetailsAction
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
index 97fb3311cb..7298e0eda6 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
@@ -28,6 +28,8 @@ import com.bumble.appyx.navmodel.backstack.operation.push
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
+import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
+import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
@@ -44,7 +46,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Assisted plugins: List,
) : BackstackNode(
backstack = BackStack(
- initialElement = NavTarget.RoomDetails,
+ initialElement = plugins.filterIsInstance().first().initialElement.toNavTarget(),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
@@ -58,6 +60,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
object RoomMemberList : NavTarget
+ @Parcelize
+ object RoomDetailsEdit : NavTarget
+
@Parcelize
object InviteMembers : NavTarget
@@ -73,12 +78,17 @@ class RoomDetailsFlowNode @AssistedInject constructor(
backstack.push(NavTarget.RoomMemberList)
}
+ override fun editRoomDetails() {
+ backstack.push(NavTarget.RoomDetailsEdit)
+ }
+
override fun openInviteMembers() {
backstack.push(NavTarget.InviteMembers)
}
}
createNode(buildContext, listOf(roomDetailsCallback))
}
+
NavTarget.RoomMemberList -> {
val roomMemberListCallback = object : RoomMemberListNode.Callback {
override fun openRoomMemberDetails(roomMemberId: UserId) {
@@ -91,11 +101,18 @@ class RoomDetailsFlowNode @AssistedInject constructor(
}
createNode(buildContext, listOf(roomMemberListCallback))
}
+
+ NavTarget.RoomDetailsEdit -> {
+ createNode(buildContext)
+ }
+
NavTarget.InviteMembers -> {
createNode(buildContext)
}
+
is NavTarget.RoomMemberDetails -> {
- createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMemberId)))
+ val plugins = listOf(RoomMemberDetailsNode.RoomMemberDetailsInput(navTarget.roomMemberId))
+ createNode(buildContext, plugins)
}
}
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
index 8fe4f774d7..77153ad62d 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt
@@ -46,6 +46,7 @@ class RoomDetailsNode @AssistedInject constructor(
interface Callback : Plugin {
fun openRoomMemberList()
fun openInviteMembers()
+ fun editRoomDetails()
}
private val callbacks = plugins()
@@ -90,6 +91,10 @@ class RoomDetailsNode @AssistedInject constructor(
}
}
+ private fun onEditRoomDetails() {
+ callbacks.forEach { it.editRoomDetails() }
+ }
+
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
@@ -103,10 +108,18 @@ class RoomDetailsNode @AssistedInject constructor(
this.onShareMember(context, roomMember)
}
+ fun onActionClicked(action: RoomDetailsAction) {
+ when (action) {
+ RoomDetailsAction.Edit -> onEditRoomDetails()
+ RoomDetailsAction.AddTopic -> onEditRoomDetails()
+ }
+ }
+
RoomDetailsView(
state = state,
modifier = modifier,
goBack = this::navigateUp,
+ onActionClicked = ::onActionClicked,
onShareRoom = ::onShareRoom,
onShareMember = ::onShareMember,
openRoomMemberList = ::openRoomMemberList,
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
index 4e7d88b18e..f016b38cb3 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt
@@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
import javax.inject.Inject
@@ -52,10 +53,23 @@ class RoomDetailsPresenter @Inject constructor(
val membersState by room.membersStateFlow.collectAsState()
val memberCount by getMemberCount(membersState)
val canInvite by getCanInvite(membersState)
+ val canEditName by getCanSendStateEvent(membersState, StateEventType.ROOM_NAME)
+ val canEditAvatar by getCanSendStateEvent(membersState, StateEventType.ROOM_AVATAR)
+ val canEditTopic by getCanSendStateEvent(membersState, StateEventType.ROOM_TOPIC)
val dmMember by room.getDirectRoomMember(membersState)
val roomMemberDetailsPresenter = roomMemberDetailsPresenter(dmMember)
val roomType = getRoomType(dmMember)
+ val topicState = remember(canEditTopic, room.topic) {
+ val topic = room.topic
+
+ when {
+ !topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic)
+ canEditTopic -> RoomTopicState.CanAddTopic
+ else -> RoomTopicState.Hidden
+ }
+ }
+
fun handleEvents(event: RoomDetailsEvent) {
when (event) {
is RoomDetailsEvent.LeaveRoom ->
@@ -70,10 +84,11 @@ class RoomDetailsPresenter @Inject constructor(
roomName = room.name ?: room.displayName,
roomAlias = room.alias,
roomAvatarUrl = room.avatarUrl,
- roomTopic = room.topic,
+ roomTopic = topicState,
memberCount = memberCount,
isEncrypted = room.isEncrypted,
canInvite = canInvite,
+ canEdit = canEditAvatar || canEditName || canEditTopic,
roomType = roomType.value,
roomMemberDetailsState = roomMemberDetailsState,
leaveRoomState = leaveRoomState,
@@ -108,6 +123,15 @@ class RoomDetailsPresenter @Inject constructor(
return canInvite
}
+ @Composable
+ private fun getCanSendStateEvent(membersState: MatrixRoomMembersState, type: StateEventType): State {
+ val canSendEvent = remember(membersState) { mutableStateOf(false) }
+ LaunchedEffect(membersState) {
+ canSendEvent.value = room.canSendStateEvent(type).getOrElse { false }
+ }
+ return canSendEvent
+ }
+
@Composable
private fun getMemberCount(membersState: MatrixRoomMembersState): State> {
return remember(membersState) {
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
index a046548b19..4554e7aec9 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsState.kt
@@ -26,11 +26,12 @@ data class RoomDetailsState(
val roomName: String,
val roomAlias: String?,
val roomAvatarUrl: String?,
- val roomTopic: String?,
+ val roomTopic: RoomTopicState,
val memberCount: Async,
val isEncrypted: Boolean,
val roomType: RoomDetailsType,
val roomMemberDetailsState: RoomMemberDetailsState?,
+ val canEdit: Boolean,
val canInvite: Boolean,
val leaveRoomState: LeaveRoomState,
val eventSink: (RoomDetailsEvent) -> Unit
@@ -40,3 +41,9 @@ sealed interface RoomDetailsType {
object Room : RoomDetailsType
data class Dm(val roomMember: RoomMember) : RoomDetailsType
}
+
+sealed interface RoomTopicState {
+ object Hidden : RoomTopicState
+ object CanAddTopic : RoomTopicState
+ data class ExistingTopic(val topic: String) : RoomTopicState
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
index 9cb5b925fa..82fd0ae0aa 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt
@@ -28,13 +28,15 @@ open class RoomDetailsStateProvider : PreviewParameterProvider
override val values: Sequence
get() = sequenceOf(
aRoomDetailsState(),
- aRoomDetailsState().copy(roomTopic = null),
+ aRoomDetailsState().copy(roomTopic = RoomTopicState.Hidden),
+ aRoomDetailsState().copy(roomTopic = RoomTopicState.CanAddTopic),
aRoomDetailsState().copy(isEncrypted = false),
aRoomDetailsState().copy(roomAlias = null),
aRoomDetailsState().copy(memberCount = Async.Failure(Throwable())),
aDmRoomDetailsState().copy(roomName = "Daniel"),
aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"),
aRoomDetailsState().copy(canInvite = true),
+ aRoomDetailsState().copy(canEdit = true),
// Add other state here
)
}
@@ -64,14 +66,17 @@ fun aRoomDetailsState() = RoomDetailsState(
roomName = "Marketing",
roomAlias = "#marketing:domain.com",
roomAvatarUrl = null,
- roomTopic = "Welcome to #marketing, home of the Marketing team " +
- "|| WIKI PAGE: https://domain.org/wiki/Marketing " +
- "|| MAIL iki/Marketing " +
- "|| MAI iki/Marketing " +
- "|| MAI iki/Marketing...",
+ roomTopic = RoomTopicState.ExistingTopic(
+ "Welcome to #marketing, home of the Marketing team " +
+ "|| WIKI PAGE: https://domain.org/wiki/Marketing " +
+ "|| MAIL iki/Marketing " +
+ "|| MAI iki/Marketing " +
+ "|| MAI iki/Marketing..."
+ ),
memberCount = Async.Success(32),
isEncrypted = true,
canInvite = false,
+ canEdit = false,
roomType = RoomDetailsType.Room,
roomMemberDetailsState = null,
leaveRoomState = LeaveRoomState(),
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
index 7d90342f65..491c1eb22b 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
@@ -28,16 +28,25 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.PersonAddAlt
import androidx.compose.material.icons.outlined.Share
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -45,9 +54,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
+import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
-import io.element.android.features.leaveroom.api.LeaveRoomView
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
import io.element.android.libraries.architecture.isLoading
@@ -63,23 +72,26 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.preview.LargeHeightPreview
import io.element.android.libraries.designsystem.theme.LocalColors
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconButton
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.matrix.api.room.RoomMember
+import io.element.android.libraries.ui.strings.R as StringR
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
+@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RoomDetailsView(
state: RoomDetailsState,
goBack: () -> Unit,
+ onActionClicked: (RoomDetailsAction) -> Unit,
onShareRoom: () -> Unit,
onShareMember: (RoomMember) -> Unit,
openRoomMemberList: () -> Unit,
invitePeople: () -> Unit,
modifier: Modifier = Modifier,
) {
-
fun onShareMember() {
onShareMember((state.roomType as RoomDetailsType.Dm).roomMember)
}
@@ -87,13 +99,18 @@ fun RoomDetailsView(
Scaffold(
modifier = modifier,
topBar = {
- TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) })
+ RoomDetailsTopBar(
+ goBack = goBack,
+ showEdit = state.canEdit,
+ onActionClicked = onActionClicked
+ )
},
) { padding ->
- Column(modifier = Modifier
- .padding(padding)
- .consumeWindowInsets(padding)
- .verticalScroll(rememberScrollState())
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .verticalScroll(rememberScrollState())
+ .consumeWindowInsets(padding)
) {
LeaveRoomView(state = state.leaveRoomState)
@@ -107,6 +124,7 @@ fun RoomDetailsView(
)
MainActionsSection(onShareRoom = onShareRoom)
}
+
is RoomDetailsType.Dm -> {
val member = state.roomType.roomMember
RoomMemberHeaderSection(
@@ -119,8 +137,11 @@ fun RoomDetailsView(
}
Spacer(Modifier.height(26.dp))
- if (state.roomTopic != null) {
- TopicSection(roomTopic = state.roomTopic)
+ if (state.roomTopic !is RoomTopicState.Hidden) {
+ TopicSection(
+ roomTopic = state.roomTopic,
+ onActionClicked = onActionClicked,
+ )
}
if (state.roomType is RoomDetailsType.Room) {
@@ -128,10 +149,14 @@ fun RoomDetailsView(
MembersSection(
memberCount = memberCount,
isLoading = state.memberCount.isLoading(),
- showInvite = state.canInvite,
openRoomMemberList = openRoomMemberList,
- invitePeople = invitePeople,
)
+
+ if (state.canInvite) {
+ InviteSection(
+ invitePeople = invitePeople
+ )
+ }
}
if (state.isEncrypted) {
@@ -151,6 +176,45 @@ fun RoomDetailsView(
}
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun RoomDetailsTopBar(
+ goBack: () -> Unit,
+ onActionClicked: (RoomDetailsAction) -> Unit,
+ showEdit: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ var showMenu by remember { mutableStateOf(false) }
+
+ TopAppBar(
+ modifier = modifier,
+ title = { },
+ navigationIcon = { BackButton(onClick = goBack) },
+ actions = {
+ if (showEdit) {
+ IconButton(onClick = { showMenu = !showMenu }) {
+ Icon(Icons.Default.MoreVert, "")
+ }
+ DropdownMenu(
+ modifier = Modifier.widthIn(200.dp),
+ expanded = showMenu,
+ onDismissRequest = { showMenu = false },
+ ) {
+ DropdownMenuItem(
+ text = { Text(stringResource(id = StringR.string.action_edit)) },
+ onClick = {
+ // Explicitly close the menu before handling the action, as otherwise it stays open during the
+ // transition and renders really badly.
+ showMenu = false
+ onActionClicked(RoomDetailsAction.Edit)
+ },
+ )
+ }
+ }
+ },
+ )
+}
+
@Composable
internal fun MainActionsSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) {
Row(modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
@@ -184,14 +248,26 @@ internal fun RoomHeaderSection(
}
@Composable
-internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
- PreferenceCategory(title = stringResource(R.string.screen_room_details_topic_title), modifier = modifier) {
- Text(
- roomTopic,
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.tertiary
- )
+internal fun TopicSection(
+ roomTopic: RoomTopicState,
+ onActionClicked: (RoomDetailsAction) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ PreferenceCategory(title = stringResource(StringR.string.common_topic), modifier = modifier) {
+ if (roomTopic is RoomTopicState.CanAddTopic) {
+ PreferenceText(
+ title = stringResource(R.string.screen_room_details_add_topic_title),
+ icon = Icons.Outlined.Add,
+ onClick = { onActionClicked(RoomDetailsAction.AddTopic) },
+ )
+ } else if (roomTopic is RoomTopicState.ExistingTopic) {
+ Text(
+ roomTopic.topic,
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.tertiary
+ )
+ }
}
}
@@ -199,8 +275,6 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
internal fun MembersSection(
memberCount: Int?,
isLoading: Boolean,
- showInvite: Boolean,
- invitePeople: () -> Unit,
openRoomMemberList: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -212,13 +286,20 @@ internal fun MembersSection(
onClick = openRoomMemberList,
loadingCurrentValue = isLoading,
)
- if (showInvite) {
- PreferenceText(
- title = stringResource(R.string.screen_room_details_invite_people_title),
- icon = Icons.Outlined.PersonAddAlt,
- onClick = invitePeople,
- )
- }
+ }
+}
+
+@Composable
+internal fun InviteSection(
+ invitePeople: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PreferenceCategory(modifier = modifier) {
+ PreferenceText(
+ title = stringResource(R.string.screen_room_details_invite_people_title),
+ icon = Icons.Outlined.PersonAddAlt,
+ onClick = invitePeople,
+ )
}
}
@@ -260,6 +341,7 @@ private fun ContentToPreview(state: RoomDetailsState) {
RoomDetailsView(
state = state,
goBack = {},
+ onActionClicked = {},
onShareRoom = {},
onShareMember = {},
openRoomMemberList = {},
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditEvents.kt
new file mode 100644
index 0000000000..b4bc348b8a
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditEvents.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.edit
+
+import io.element.android.libraries.matrix.ui.media.AvatarAction
+
+sealed interface RoomDetailsEditEvents {
+ data class HandleAvatarAction(val action: AvatarAction) : RoomDetailsEditEvents
+ data class UpdateRoomName(val name: String) : RoomDetailsEditEvents
+ data class UpdateRoomTopic(val topic: String) : RoomDetailsEditEvents
+ object Save : RoomDetailsEditEvents
+ object CancelSaveChanges : RoomDetailsEditEvents
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditNode.kt
new file mode 100644
index 0000000000..af05b547b2
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditNode.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.edit
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import io.element.android.anvilannotations.ContributesNode
+import io.element.android.libraries.di.RoomScope
+
+@ContributesNode(RoomScope::class)
+class RoomDetailsEditNode @AssistedInject constructor(
+ @Assisted buildContext: BuildContext,
+ @Assisted plugins: List,
+ private val presenter: RoomDetailsEditPresenter,
+) : Node(buildContext, plugins = plugins) {
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = presenter.present()
+ RoomDetailsEditView(
+ state = state,
+ onBackPressed = ::navigateUp,
+ onRoomEdited = ::navigateUp,
+ modifier = modifier,
+ )
+ }
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt
new file mode 100644
index 0000000000..a6726f9105
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditPresenter.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.edit
+
+import android.net.Uri
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.core.net.toUri
+import io.element.android.libraries.matrix.ui.media.AvatarAction
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.architecture.execute
+import io.element.android.libraries.core.mimetype.MimeTypes
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.room.StateEventType
+import io.element.android.libraries.mediapickers.api.PickerProvider
+import io.element.android.libraries.mediaupload.api.MediaPreProcessor
+import io.element.android.libraries.mediaupload.api.MediaUploadInfo
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+class RoomDetailsEditPresenter @Inject constructor(
+ private val room: MatrixRoom,
+ private val mediaPickerProvider: PickerProvider,
+ private val mediaPreProcessor: MediaPreProcessor,
+) : Presenter {
+
+ @Composable
+ override fun present(): RoomDetailsEditState {
+ val roomSyncUpdateFlow = room.syncUpdateFlow().collectAsState(0L)
+
+ // Since there is no way to obtain the new avatar uri after uploading a new avatar,
+ // just erase the local value when the room field has changed
+ var roomAvatarUri by rememberSaveable(room.avatarUrl) { mutableStateOf(room.avatarUrl?.toUri()) }
+
+ var roomName by rememberSaveable { mutableStateOf((room.name ?: room.displayName).trim()) }
+ var roomTopic by rememberSaveable { mutableStateOf(room.topic?.trim()) }
+
+ val saveButtonEnabled by remember(
+ roomSyncUpdateFlow.value,
+ roomName,
+ roomTopic,
+ roomAvatarUri,
+ ) {
+ derivedStateOf {
+ roomAvatarUri?.toString()?.trim() != room.avatarUrl?.toUri()?.toString()?.trim()
+ || roomName.trim() != (room.name ?: room.displayName).trim()
+ || roomTopic.orEmpty().trim() != room.topic.orEmpty().trim()
+ }
+ }
+
+ var canChangeName by remember { mutableStateOf(false) }
+ var canChangeTopic by remember { mutableStateOf(false) }
+ var canChangeAvatar by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ canChangeName = room.canSendStateEvent(StateEventType.ROOM_NAME).getOrElse { false }
+ canChangeTopic = room.canSendStateEvent(StateEventType.ROOM_TOPIC).getOrElse { false }
+ canChangeAvatar = room.canSendStateEvent(StateEventType.ROOM_AVATAR).getOrElse { false }
+ }
+
+ val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
+ onResult = { uri -> if (uri != null) roomAvatarUri = uri }
+ )
+ val galleryImagePicker = mediaPickerProvider.registerGalleryImagePicker(
+ onResult = { uri -> if (uri != null) roomAvatarUri = uri }
+ )
+
+ val avatarActions by remember(roomAvatarUri) {
+ derivedStateOf {
+ listOfNotNull(
+ AvatarAction.TakePhoto,
+ AvatarAction.ChoosePhoto,
+ AvatarAction.Remove.takeIf { roomAvatarUri != null },
+ ).toImmutableList()
+ }
+ }
+
+ val saveAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) }
+ val localCoroutineScope = rememberCoroutineScope()
+ fun handleEvents(event: RoomDetailsEditEvents) {
+ when (event) {
+ is RoomDetailsEditEvents.Save -> localCoroutineScope.saveChanges(roomName, roomTopic, roomAvatarUri, saveAction)
+ is RoomDetailsEditEvents.HandleAvatarAction -> {
+ when (event.action) {
+ AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
+ AvatarAction.TakePhoto -> cameraPhotoPicker.launch()
+ AvatarAction.Remove -> roomAvatarUri = null
+ }
+ }
+
+ is RoomDetailsEditEvents.UpdateRoomName -> roomName = event.name
+ is RoomDetailsEditEvents.UpdateRoomTopic -> roomTopic = event.topic.takeUnless { it.isEmpty() }
+ RoomDetailsEditEvents.CancelSaveChanges -> saveAction.value = Async.Uninitialized
+ }
+ }
+
+ return RoomDetailsEditState(
+ roomId = room.roomId.value,
+ roomName = roomName,
+ canChangeName = canChangeName,
+ roomTopic = roomTopic.orEmpty(),
+ canChangeTopic = canChangeTopic,
+ roomAvatarUrl = roomAvatarUri,
+ canChangeAvatar = canChangeAvatar,
+ avatarActions = avatarActions,
+ saveButtonEnabled = saveButtonEnabled,
+ saveAction = saveAction.value,
+ eventSink = ::handleEvents,
+ )
+ }
+
+ private fun CoroutineScope.saveChanges(name: String, topic: String?, avatarUri: Uri?, action: MutableState>) = launch {
+ val results = mutableListOf>()
+ suspend {
+ if (topic.orEmpty().trim() != room.topic.orEmpty().trim()) {
+ results.add(room.setTopic(topic.orEmpty()))
+ }
+ if (name.isNotEmpty() && name.trim() != room.name.orEmpty().trim()) {
+ results.add(room.setName(name))
+ }
+ if (avatarUri?.toString()?.trim() != room.avatarUrl?.trim()) {
+ results.add(updateAvatar(avatarUri))
+ }
+ if (results.all { it.isSuccess }) Unit else results.first { it.isFailure }.getOrThrow()
+ }.execute(action)
+ }
+
+ private suspend fun updateAvatar(avatarUri: Uri?): Result {
+ return runCatching {
+ val result = if (avatarUri != null) {
+ val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow() as? MediaUploadInfo.Image
+ val byteArray = preprocessed?.file?.readBytes()
+ byteArray?.let { room.updateAvatar(MimeTypes.Jpeg, it) } ?: error("Could not process the given uri ($avatarUri)")
+ } else {
+ room.removeAvatar()
+ }
+ result.getOrThrow()
+ }
+ }
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditState.kt
new file mode 100644
index 0000000000..ceb87b6f27
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.edit
+
+import android.net.Uri
+import io.element.android.libraries.matrix.ui.media.AvatarAction
+import io.element.android.libraries.architecture.Async
+import kotlinx.collections.immutable.ImmutableList
+
+data class RoomDetailsEditState(
+ val roomId: String,
+ val roomName: String,
+ val canChangeName: Boolean,
+ val roomTopic: String,
+ val canChangeTopic: Boolean,
+ val roomAvatarUrl: Uri?,
+ val canChangeAvatar: Boolean,
+ val avatarActions: ImmutableList,
+ val saveButtonEnabled: Boolean,
+ val saveAction: Async,
+ val eventSink: (RoomDetailsEditEvents) -> Unit
+)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt
new file mode 100644
index 0000000000..62a363aeac
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditStateProvider.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.impl.edit
+
+import android.net.Uri
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.architecture.Async
+import kotlinx.collections.immutable.persistentListOf
+
+open class RoomDetailsEditStateProvider : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ aRoomDetailsEditState(),
+ aRoomDetailsEditState().copy(roomTopic = ""),
+ aRoomDetailsEditState().copy(roomAvatarUrl = Uri.EMPTY),
+ aRoomDetailsEditState().copy(canChangeName = true, canChangeTopic = false, canChangeAvatar = true, saveButtonEnabled = false),
+ aRoomDetailsEditState().copy(canChangeName = false, canChangeTopic = true, canChangeAvatar = false, saveButtonEnabled = false),
+ aRoomDetailsEditState().copy(saveAction = Async.Loading()),
+ aRoomDetailsEditState().copy(saveAction = Async.Failure(Throwable("Whelp")))
+ )
+}
+
+fun aRoomDetailsEditState() = RoomDetailsEditState(
+ roomId = "a room id",
+ roomName = "Marketing",
+ canChangeName = true,
+ roomTopic = "a room topic that is quite long so should wrap onto multiple lines",
+ canChangeTopic = true,
+ roomAvatarUrl = null,
+ canChangeAvatar = true,
+ avatarActions = persistentListOf(),
+ saveButtonEnabled = true,
+ saveAction = Async.Uninitialized,
+ eventSink = {}
+)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt
new file mode 100644
index 0000000000..414eaf9831
--- /dev/null
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/edit/RoomDetailsEditView.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
+
+package io.element.android.features.roomdetails.impl.edit
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.AddAPhoto
+import androidx.compose.material.rememberModalBottomSheetState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.element.android.features.roomdetails.impl.R
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.designsystem.components.LabelledTextField
+import io.element.android.libraries.designsystem.components.ProgressDialog
+import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.LocalColors
+import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
+import io.element.android.libraries.designsystem.theme.components.Icon
+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.TextButton
+import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet
+import io.element.android.libraries.matrix.ui.components.UnsavedAvatar
+import kotlinx.coroutines.launch
+import io.element.android.libraries.ui.strings.R as StringR
+
+@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
+@Composable
+fun RoomDetailsEditView(
+ state: RoomDetailsEditState,
+ onBackPressed: () -> Unit,
+ onRoomEdited: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val focusManager = LocalFocusManager.current
+ val itemActionsBottomSheetState = rememberModalBottomSheetState(
+ initialValue = ModalBottomSheetValue.Hidden,
+ )
+
+ fun onAvatarClicked() {
+ focusManager.clearFocus()
+ coroutineScope.launch {
+ itemActionsBottomSheetState.show()
+ }
+ }
+
+ Scaffold(
+ modifier = modifier.clearFocusOnTap(focusManager),
+ topBar = {
+ CenterAlignedTopAppBar(
+ title = {
+ Text(
+ text = stringResource(id = R.string.screen_room_details_edit_room_title),
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold,
+ )
+ },
+ navigationIcon = { BackButton(onClick = onBackPressed) },
+ actions = {
+ TextButton(
+ enabled = state.saveButtonEnabled,
+ onClick = {
+ focusManager.clearFocus()
+ state.eventSink(RoomDetailsEditEvents.Save)
+ },
+ ) {
+ Text(
+ text = stringResource(StringR.string.action_save),
+ fontSize = 16.sp,
+ )
+ }
+ }
+ )
+ },
+ ) { padding ->
+ Column(
+ modifier = Modifier
+ .padding(padding)
+ .padding(horizontal = 16.dp)
+ .navigationBarsPadding()
+ .imePadding()
+ .verticalScroll(rememberScrollState())
+ ) {
+ Spacer(modifier = Modifier.height(24.dp))
+ EditableAvatarView(state, ::onAvatarClicked)
+ Spacer(modifier = Modifier.height(60.dp))
+
+ if (state.canChangeName) {
+ LabelledTextField(
+ label = stringResource(id = R.string.screen_room_details_room_name_label),
+ value = state.roomName,
+ placeholder = stringResource(id = R.string.screen_room_details_room_name_placeholder),
+ singleLine = true,
+ onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomName(it)) },
+ )
+ } else {
+ LabelledReadOnlyField(
+ title = stringResource(R.string.screen_room_details_room_name_label),
+ value = state.roomName
+ )
+ }
+
+ Spacer(modifier = Modifier.height(28.dp))
+
+ if (state.canChangeTopic) {
+ LabelledTextField(
+ label = stringResource(id = StringR.string.common_topic),
+ value = state.roomTopic,
+ placeholder = stringResource(id = R.string.screen_room_details_topic_placeholder),
+ maxLines = 10,
+ onValueChange = { state.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(it)) },
+ )
+ } else {
+ LabelledReadOnlyField(
+ title = stringResource(R.string.screen_room_details_topic_title),
+ value = state.roomTopic
+ )
+ }
+ }
+ }
+
+ AvatarActionBottomSheet(
+ actions = state.avatarActions,
+ modalBottomSheetState = itemActionsBottomSheetState,
+ onActionSelected = { state.eventSink(RoomDetailsEditEvents.HandleAvatarAction(it)) }
+ )
+
+ when (state.saveAction) {
+ is Async.Loading -> {
+ ProgressDialog(text = stringResource(R.string.screen_room_details_updating_room))
+ }
+
+ is Async.Failure -> {
+ ErrorDialog(
+ content = stringResource(R.string.screen_room_details_edition_error),
+ onDismiss = { state.eventSink(RoomDetailsEditEvents.CancelSaveChanges) },
+ )
+ }
+
+ is Async.Success -> {
+ LaunchedEffect(state.saveAction) {
+ onRoomEdited()
+ }
+ }
+
+ else -> Unit
+ }
+}
+
+@Composable
+private fun EditableAvatarView(
+ state: RoomDetailsEditState,
+ onAvatarClicked: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
+ Box(
+ modifier = Modifier
+ .size(70.dp)
+ .clickable(onClick = onAvatarClicked, enabled = state.canChangeAvatar)
+ ) {
+ // TODO this might be able to be simplified into a single component once send/receive media is done
+ when (state.roomAvatarUrl?.scheme) {
+ null, "mxc" -> {
+ Avatar(
+ avatarData = AvatarData(state.roomId, state.roomName, state.roomAvatarUrl?.toString(), size = AvatarSize.HUGE),
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+
+ else -> {
+ UnsavedAvatar(
+ avatarUri = state.roomAvatarUrl,
+ modifier = Modifier.fillMaxSize(),
+ )
+ }
+ }
+
+ if (state.canChangeAvatar) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .clip(CircleShape)
+ .background(LocalColors.current.gray1400)
+ .size(24.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ modifier = Modifier.size(16.dp),
+ imageVector = Icons.Outlined.AddAPhoto,
+ contentDescription = "",
+ tint = MaterialTheme.colorScheme.onPrimary,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun LabelledReadOnlyField(
+ title: String,
+ value: String,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
+ text = title,
+ )
+
+ Text(
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.primary,
+ text = value,
+ )
+ }
+}
+
+private fun Modifier.clearFocusOnTap(focusManager: FocusManager): Modifier =
+ pointerInput(Unit) {
+ detectTapGestures(onTap = {
+ focusManager.clearFocus()
+ })
+ }
+
+@Preview
+@Composable
+fun RoomDetailsEditViewLightPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) =
+ ElementPreviewLight { ContentToPreview(state) }
+
+@Preview
+@Composable
+fun RoomDetailsEditViewDarkPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) =
+ ElementPreviewDark { ContentToPreview(state) }
+
+@Composable
+private fun ContentToPreview(state: RoomDetailsEditState) {
+ RoomDetailsEditView(
+ state = state,
+ onBackPressed = {},
+ onRoomEdited = {},
+ )
+}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt
index 07b3bb1708..ef882c2f40 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenter.kt
@@ -126,15 +126,16 @@ class RoomInviteMembersPresenter @Inject constructor(
userRepository.search(searchQuery).collect {
searchResults.value = when {
it.isEmpty() -> SearchBarResultState.NoResults()
- else -> SearchBarResultState.Results(it.map { user ->
- val existingMembership = joinedMembers.firstOrNull { j -> j.userId == user.userId }?.membership
+ else -> SearchBarResultState.Results(it.map { result ->
+ val existingMembership = joinedMembers.firstOrNull { j -> j.userId == result.matrixUser.userId }?.membership
val isJoined = existingMembership == RoomMembershipState.JOIN
val isInvited = existingMembership == RoomMembershipState.INVITE
InvitableUser(
- matrixUser = user,
- isSelected = selectedUsers.value.contains(user) || isJoined || isInvited,
+ matrixUser = result.matrixUser,
+ isSelected = selectedUsers.value.contains(result.matrixUser) || isJoined || isInvited,
isAlreadyJoined = isJoined,
isAlreadyInvited = isInvited,
+ isUnresolved = result.isUnresolved,
)
}.toImmutableList())
}
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt
index b96cc30935..9a2ceb7c4b 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersState.kt
@@ -35,4 +35,5 @@ data class InvitableUser(
val isSelected: Boolean = false,
val isAlreadyJoined: Boolean = false,
val isAlreadyInvited: Boolean = false,
+ val isUnresolved: Boolean = false,
)
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt
index 137d47b660..00e9496c2a 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersStateProvider.kt
@@ -48,5 +48,19 @@ internal class RoomInviteMembersStateProvider : PreviewParameterProvider
- CheckableUserRow(
- checked = invitableUser.isSelected,
- enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined,
- avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
- name = invitableUser.matrixUser.getBestName(),
- subtext = when {
- // If they're already invited or joined we show that information
- invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member)
- invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited)
- // Otherwise show the ID, unless that's already used for their name
- invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value
- else -> null
- },
- onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
- modifier = Modifier.fillMaxWidth()
- )
+ if (invitableUser.isUnresolved && !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined) {
+ CheckableUnresolvedUserRow(
+ checked = invitableUser.isSelected,
+ avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
+ id = invitableUser.matrixUser.userId.value,
+ onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ } else {
+ CheckableUserRow(
+ checked = invitableUser.isSelected,
+ enabled = !invitableUser.isAlreadyInvited && !invitableUser.isAlreadyJoined,
+ avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.MEDIUM),
+ name = invitableUser.matrixUser.getBestName(),
+ subtext = when {
+ // If they're already invited or joined we show that information
+ invitableUser.isAlreadyJoined -> stringResource(R.string.screen_room_details_already_a_member)
+ invitableUser.isAlreadyInvited -> stringResource(R.string.screen_room_details_already_invited)
+ // Otherwise show the ID, unless that's already used for their name
+ invitableUser.matrixUser.displayName.isNullOrEmpty().not() -> invitableUser.matrixUser.userId.value
+ else -> null
+ },
+ onCheckedChange = { onUserToggled(invitableUser.matrixUser) },
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
}
}
},
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
index 7fd4dd3876..bd4258ac88 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt
@@ -42,11 +42,11 @@ class RoomMemberDetailsNode @AssistedInject constructor(
presenterFactory: RoomMemberDetailsPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
- data class Inputs(
- val roomMemberId: UserId,
+ data class RoomMemberDetailsInput(
+ val roomMemberId: UserId
) : NodeInputs
- private val inputs = inputs()
+ private val inputs = inputs()
private val presenter = presenterFactory.create(inputs.roomMemberId)
@Composable
diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
index 594152e241..7ef6cd4aa4 100644
--- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
+++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt
@@ -33,7 +33,7 @@ import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
-import io.element.android.libraries.matrix.ui.room.getRoomMember
+import io.element.android.libraries.matrix.ui.room.getRoomMemberAsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -51,7 +51,7 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
override fun present(): RoomMemberDetailsState {
val coroutineScope = rememberCoroutineScope()
var confirmationDialog by remember { mutableStateOf(null) }
- val roomMember by room.getRoomMember(roomMemberId)
+ val roomMember by room.getRoomMemberAsState(roomMemberId)
// the room member is not really live...
val isBlocked = remember {
mutableStateOf(roomMember?.isIgnored.orFalse())
diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..97dcbdfa4b
--- /dev/null
+++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,23 @@
+
+
+
+ - "1 osoba"
+ - "%1$d osoby"
+ - "%1$d osob"
+
+ "Zprávy jsou zabezpečeny zámky. Pouze vy a příjemci máte jedinečné klíče k jejich odemčení."
+ "Šifrování zpráv povoleno"
+ "Sdílet místnost"
+ "Nevyřízeno"
+ "Zablokovat"
+ "Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny zprávy od nich budou skryty. Tuto akci můžete kdykoli vrátit zpět."
+ "Zablokovat uživatele"
+ "Odblokovat"
+ "Po odblokování uživatele budete moci opět zobrazit všechny jeho zprávy."
+ "Odblokovat uživatele"
+ "Pozvat lidi"
+ "Opustit místnost"
+ "Lidé"
+ "Zabezpečení"
+ "Téma"
+
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml
index 4e5b3e9a74..f5eb451171 100644
--- a/features/roomdetails/impl/src/main/res/values-de/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml
@@ -5,13 +5,18 @@
- "%1$d Personen"
"Bereits eingeladen"
+ "Nachrichten sind mit Schlössern gesichert. Nur du und der Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren."
+ "Nachrichtenverschlüsselung aktiviert"
"Raum teilen"
"Blockieren"
+ "Blockierte Benutzer können dir keine Nachrichten senden und alle Nachrichten von ihnen werden ausgeblendet. Du kannst diese Aktion jederzeit rückgängig machen."
"Nutzer blockieren"
"Blockierung aufheben"
+ "Wenn du den Benutzer entsperrst, kannst du wieder alle Nachrichten von ihm sehen."
"Nutzer entblockieren"
"Personen einladen"
"Raum verlassen"
+ "Personen"
"Sicherheit"
"Thema"
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..56e76a2053
--- /dev/null
+++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,21 @@
+
+
+
+ - "1 membre"
+ - "%1$d membres"
+
+ "Les messages sont sécurisés par des verrous. Seuls vous et les destinataires possédez les clés uniques pour les déverrouiller."
+ "Chiffrement des messages activé"
+ "Partager la salle"
+ "Bloquer"
+ "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez annuler cette action à tout moment."
+ "Bloquer l\'utilisateur"
+ "Débloquer"
+ "Lorsque vous débloquez l\'utilisateur, vous pourrez à nouveau voir tous leur messages."
+ "Débloquer l\'utilisateur"
+ "Inviter des personnes"
+ "Quitter la salle"
+ "Personnes"
+ "Sécurité"
+ "Sujet"
+
\ No newline at end of file
diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml
index dc154df2e0..8f9bd6f30a 100644
--- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml
+++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml
@@ -4,9 +4,16 @@
- "o persoană"
- "%1$d persoane"
+ "Adăugare subiect"
+ "Deja membru"
+ "Deja invitat"
+ "A apărut o eroare la actualizarea detaliilor camerei"
"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."
"Criptarea mesajelor este activată"
"Partajați camera"
+ "Se actualizează camera…"
+ "În așteptare"
+ "Membrii camerei"
"Blocați"
"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."
"Blocați utilizatorul"
diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml
index 393052591f..4f45bb6b49 100644
--- a/features/roomdetails/impl/src/main/res/values/localazy.xml
+++ b/features/roomdetails/impl/src/main/res/values/localazy.xml
@@ -4,12 +4,18 @@
- "1 person"
- "%1$d people"
+ "Add topic"
"Already a member"
"Already invited"
- "An error occurred when updating the room details"
+ "Edit Room"
+ "We were unable to update all the information for this room."
+ "Unable to update room"
"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."
"Message encryption enabled"
+ "Room name"
+ "e.g. Product Sprint"
"Share room"
+ "What is this room about?"
"Updating room…"
"Pending"
"Room members"
@@ -19,7 +25,7 @@
"Unblock"
"On unblocking the user, you will be able to see all messages by them again."
"Unblock user"
- "Invite people"
+ "Invite friends to Element"
"Leave room"
"People"
"Security"
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
index 0c950071ca..bcaa824104 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt
@@ -19,10 +19,11 @@ package io.element.android.features.roomdetails
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import io.element.android.features.leaveroom.fake.LeaveRoomPresenterFake
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
import io.element.android.features.roomdetails.impl.RoomDetailsType
+import io.element.android.features.roomdetails.impl.RoomTopicState
import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.libraries.architecture.Async
@@ -31,8 +32,8 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
-import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.api.room.StateEventType
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
@@ -40,7 +41,6 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
-import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -48,9 +48,6 @@ import org.junit.Test
@ExperimentalCoroutinesApi
class RoomDetailsPresenterTests {
- private val roomMembershipObserver = RoomMembershipObserver()
- private val testCoroutineDispatchers = testCoroutineDispatchers()
-
private fun aRoomDetailsPresenter(room: MatrixRoom): RoomDetailsPresenter {
val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory {
override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter {
@@ -68,12 +65,12 @@ class RoomDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitItem()
- Truth.assertThat(initialState.roomId).isEqualTo(room.roomId.value)
- Truth.assertThat(initialState.roomName).isEqualTo(room.name)
- Truth.assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
- Truth.assertThat(initialState.roomTopic).isEqualTo(room.topic)
- Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
- Truth.assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
+ assertThat(initialState.roomId).isEqualTo(room.roomId.value)
+ assertThat(initialState.roomName).isEqualTo(room.name)
+ assertThat(initialState.roomAvatarUrl).isEqualTo(room.avatarUrl)
+ assertThat(initialState.roomTopic).isEqualTo(RoomTopicState.ExistingTopic(room.topic!!))
+ assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
+ assertThat(initialState.isEncrypted).isEqualTo(room.isEncrypted)
cancelAndIgnoreRemainingEvents()
}
@@ -93,22 +90,22 @@ class RoomDetailsPresenterTests {
}.test {
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
val initialState = awaitItem()
- Truth.assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
+ assertThat(initialState.memberCount).isEqualTo(Async.Uninitialized)
skipItems(1)
room.givenRoomMembersState(MatrixRoomMembersState.Pending(null))
val loadingState = awaitItem()
- Truth.assertThat(loadingState.memberCount).isEqualTo(Async.Loading(null))
+ assertThat(loadingState.memberCount).isEqualTo(Async.Loading(null))
room.givenRoomMembersState(MatrixRoomMembersState.Error(error))
skipItems(1)
val failureState = awaitItem()
- Truth.assertThat(failureState.memberCount).isEqualTo(Async.Failure(error, null))
+ assertThat(failureState.memberCount).isEqualTo(Async.Failure(error, null))
room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
skipItems(1)
val successState = awaitItem()
- Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(1))
+ assertThat(successState.memberCount).isEqualTo(Async.Success(1))
cancelAndIgnoreRemainingEvents()
}
@@ -122,7 +119,7 @@ class RoomDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitItem()
- Truth.assertThat(initialState.roomName).isEqualTo(room.displayName)
+ assertThat(initialState.roomName).isEqualTo(room.displayName)
cancelAndIgnoreRemainingEvents()
}
@@ -144,7 +141,7 @@ class RoomDetailsPresenterTests {
presenter.present()
}.test {
val initialState = awaitItem()
- Truth.assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
+ assertThat(initialState.roomType).isEqualTo(RoomDetailsType.Dm(otherRoomMember))
cancelAndIgnoreRemainingEvents()
}
@@ -160,9 +157,9 @@ class RoomDetailsPresenterTests {
presenter.present()
}.test {
// Initially false
- Truth.assertThat(awaitItem().canInvite).isFalse()
+ assertThat(awaitItem().canInvite).isFalse()
// Then the asynchronous check completes and it becomes true
- Truth.assertThat(awaitItem().canInvite).isTrue()
+ assertThat(awaitItem().canInvite).isTrue()
cancelAndIgnoreRemainingEvents()
}
@@ -177,7 +174,7 @@ class RoomDetailsPresenterTests {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
- Truth.assertThat(awaitItem().canInvite).isFalse()
+ assertThat(awaitItem().canInvite).isFalse()
}
}
@@ -190,7 +187,103 @@ class RoomDetailsPresenterTests {
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
- Truth.assertThat(awaitItem().canInvite).isFalse()
+ assertThat(awaitItem().canInvite).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - initial state when user can edit one attribute`() = runTest {
+ val room = aMatrixRoom().apply {
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
+ givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
+ givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Whelp")))
+ givenCanInviteResult(Result.success(false))
+ }
+ val presenter = aRoomDetailsPresenter(room)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // Initially false
+ assertThat(awaitItem().canEdit).isFalse()
+ // Then the asynchronous check completes and it becomes true
+ assertThat(awaitItem().canEdit).isTrue()
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - initial state when user can edit all attributes`() = runTest {
+ val room = aMatrixRoom().apply {
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
+ givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(true))
+ givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
+ givenCanInviteResult(Result.success(false))
+ }
+ val presenter = aRoomDetailsPresenter(room)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // Initially false
+ assertThat(awaitItem().canEdit).isFalse()
+ // Then the asynchronous check completes and it becomes true
+ assertThat(awaitItem().canEdit).isTrue()
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - initial state when user can edit no attributes`() = runTest {
+ val room = aMatrixRoom().apply {
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(false))
+ givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
+ givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false))
+ givenCanInviteResult(Result.success(false))
+ }
+ val presenter = aRoomDetailsPresenter(room)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // Initially false, and no further events
+ assertThat(awaitItem().canEdit).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - topic state is hidden when no topic and user has no permission`() = runTest {
+ val room = aMatrixRoom(topic = null).apply {
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(false))
+ givenCanInviteResult(Result.success(false))
+ }
+
+ val presenter = aRoomDetailsPresenter(room)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // The initial state is "hidden" and no further state changes happen
+ assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.Hidden)
+ }
+ }
+
+ @Test
+ fun `present - topic state is 'can add topic' when no topic and user has permission`() = runTest {
+ val room = aMatrixRoom(topic = null).apply {
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
+ givenCanInviteResult(Result.success(false))
+ }
+
+ val presenter = aRoomDetailsPresenter(room)
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // Ignore the initial state
+ skipItems(1)
+
+ // When the async permission check finishes, the topic state will be updated
+ assertThat(awaitItem().roomTopic).isEqualTo(RoomTopicState.CanAddTopic)
+
+ cancelAndIgnoreRemainingEvents()
}
}
}
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt
new file mode 100644
index 0000000000..adbec76ae7
--- /dev/null
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt
@@ -0,0 +1,618 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.features.roomdetails.edit
+
+import android.net.Uri
+import app.cash.molecule.RecompositionClock
+import app.cash.molecule.moleculeFlow
+import app.cash.turbine.test
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.ui.media.AvatarAction
+import io.element.android.features.roomdetails.aMatrixRoom
+import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditEvents
+import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditPresenter
+import io.element.android.libraries.architecture.Async
+import io.element.android.libraries.matrix.api.room.MatrixRoom
+import io.element.android.libraries.matrix.api.room.StateEventType
+import io.element.android.libraries.matrix.test.AN_AVATAR_URL
+import io.element.android.libraries.mediapickers.test.FakePickerProvider
+import io.element.android.libraries.mediaupload.api.MediaUploadInfo
+import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
+import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.io.File
+
+@ExperimentalCoroutinesApi
+class RoomDetailsEditPresenterTest {
+
+ private lateinit var fakePickerProvider: FakePickerProvider
+ private lateinit var fakeMediaPreProcessor: FakeMediaPreProcessor
+
+ private val roomAvatarUri: Uri = mockk()
+ private val anotherAvatarUri: Uri = mockk()
+
+ private val fakeFileContents = ByteArray(2)
+
+ @Before
+ fun setup() {
+ fakePickerProvider = FakePickerProvider()
+ fakeMediaPreProcessor = FakeMediaPreProcessor()
+ mockkStatic(Uri::class)
+
+ every { Uri.parse(AN_AVATAR_URL) } returns roomAvatarUri
+ every { Uri.parse(ANOTHER_AVATAR_URL) } returns anotherAvatarUri
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ private fun aRoomDetailsEditPresenter(room: MatrixRoom): RoomDetailsEditPresenter {
+ return RoomDetailsEditPresenter(
+ room = room,
+ mediaPickerProvider = fakePickerProvider,
+ mediaPreProcessor = fakeMediaPreProcessor,
+ )
+ }
+
+ @Test
+ fun `present - initial state is created from room info`() = runTest {
+ val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL)
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.roomId).isEqualTo(room.roomId.value)
+ assertThat(initialState.roomName).isEqualTo(room.name)
+ assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri)
+ assertThat(initialState.roomTopic).isEqualTo(room.topic.orEmpty())
+ assertThat(initialState.avatarActions).containsExactly(
+ AvatarAction.ChoosePhoto,
+ AvatarAction.TakePhoto,
+ AvatarAction.Remove
+ )
+ assertThat(initialState.saveButtonEnabled).isEqualTo(false)
+ assertThat(initialState.saveAction).isInstanceOf(Async.Uninitialized::class.java)
+ }
+ }
+
+ @Test
+ fun `present - sets canChangeName if user has permission`() = runTest {
+ val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL).apply {
+ givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(true))
+ givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false))
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops"))) }
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // Initially false
+ val initialState = awaitItem()
+ assertThat(initialState.canChangeName).isFalse()
+ assertThat(initialState.canChangeAvatar).isFalse()
+ assertThat(initialState.canChangeTopic).isFalse()
+
+ // When the asynchronous check completes, the single field we can edit is true
+ val settledState = awaitItem()
+ assertThat(settledState.canChangeName).isTrue()
+ assertThat(settledState.canChangeAvatar).isFalse()
+ assertThat(settledState.canChangeTopic).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - sets canChangeAvatar if user has permission`() = runTest {
+ val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL).apply {
+ givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
+ givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(true))
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops")))
+ }
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // Initially false
+ val initialState = awaitItem()
+ assertThat(initialState.canChangeName).isFalse()
+ assertThat(initialState.canChangeAvatar).isFalse()
+ assertThat(initialState.canChangeTopic).isFalse()
+
+ // When the asynchronous check completes, the single field we can edit is true
+ val settledState = awaitItem()
+ assertThat(settledState.canChangeName).isFalse()
+ assertThat(settledState.canChangeAvatar).isTrue()
+ assertThat(settledState.canChangeTopic).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - sets canChangeTopic if user has permission`() = runTest {
+ val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL).apply {
+ givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(false))
+ givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.failure(Throwable("Oops")))
+ givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.success(true))
+ }
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ // Initially false
+ val initialState = awaitItem()
+ assertThat(initialState.canChangeName).isFalse()
+ assertThat(initialState.canChangeAvatar).isFalse()
+ assertThat(initialState.canChangeTopic).isFalse()
+
+ // When the asynchronous check completes, the single field we can edit is true
+ val settledState = awaitItem()
+ assertThat(settledState.canChangeName).isFalse()
+ assertThat(settledState.canChangeAvatar).isFalse()
+ assertThat(settledState.canChangeTopic).isTrue()
+ }
+ }
+
+ @Test
+ fun `present - updates state in response to changes`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.roomTopic).isEqualTo("My topic")
+ assertThat(initialState.roomName).isEqualTo("Name")
+ assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri)
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II"))
+ awaitItem().apply {
+ assertThat(roomTopic).isEqualTo("My topic")
+ assertThat(roomName).isEqualTo("Name II")
+ assertThat(roomAvatarUrl).isEqualTo(roomAvatarUri)
+ }
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name III"))
+ awaitItem().apply {
+ assertThat(roomTopic).isEqualTo("My topic")
+ assertThat(roomName).isEqualTo("Name III")
+ assertThat(roomAvatarUrl).isEqualTo(roomAvatarUri)
+ }
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic"))
+ awaitItem().apply {
+ assertThat(roomTopic).isEqualTo("Another topic")
+ assertThat(roomName).isEqualTo("Name III")
+ assertThat(roomAvatarUrl).isEqualTo(roomAvatarUri)
+ }
+
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
+ awaitItem().apply {
+ assertThat(roomTopic).isEqualTo("Another topic")
+ assertThat(roomName).isEqualTo("Name III")
+ assertThat(roomAvatarUrl).isNull()
+ }
+ }
+ }
+
+ @Test
+ fun `present - obtains avatar uris from gallery`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ fakePickerProvider.givenResult(anotherAvatarUri)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri)
+
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ awaitItem().apply {
+ assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri)
+ }
+ }
+ }
+
+ @Test
+ fun `present - obtains avatar uris from camera`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ fakePickerProvider.givenResult(anotherAvatarUri)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.roomAvatarUrl).isEqualTo(roomAvatarUri)
+
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.TakePhoto))
+ awaitItem().apply {
+ assertThat(roomAvatarUrl).isEqualTo(anotherAvatarUri)
+ }
+ }
+ }
+
+ @Test
+ fun `present - updates save button state`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ fakePickerProvider.givenResult(roomAvatarUri)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.saveButtonEnabled).isEqualTo(false)
+
+ // Once a change is made, the save button is enabled
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II"))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(true)
+ }
+
+ // If it's reverted then the save disables again
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name"))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(false)
+ }
+
+ // Make a change...
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic"))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(true)
+ }
+
+ // Revert it...
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("My topic"))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(false)
+ }
+
+ // Make a change...
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(true)
+ }
+
+ // Revert it...
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(false)
+ }
+ }
+ }
+
+ @Test
+ fun `present - updates save button state when initial values are null`() = runTest {
+ val room = aMatrixRoom(topic = null, name = null, displayName = "fallback", avatarUrl = null)
+
+ fakePickerProvider.givenResult(roomAvatarUri)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ assertThat(initialState.saveButtonEnabled).isEqualTo(false)
+
+ // Once a change is made, the save button is enabled
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("Name II"))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(true)
+ }
+
+ // If it's reverted then the save disables again
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("fallback"))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(false)
+ }
+
+ // Make a change...
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("Another topic"))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(true)
+ }
+
+ // Revert it...
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(""))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(false)
+ }
+
+ // Make a change...
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(true)
+ }
+
+ // Revert it...
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
+ awaitItem().apply {
+ assertThat(saveButtonEnabled).isEqualTo(false)
+ }
+ }
+ }
+
+ @Test
+ fun `present - save changes room details if different`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName("New name"))
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("New topic"))
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+
+ assertThat(room.newName).isEqualTo("New name")
+ assertThat(room.newTopic).isEqualTo("New topic")
+ assertThat(room.newAvatarData).isNull()
+ assertThat(room.removedAvatar).isTrue()
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - save doesn't change room details if they're the same trimmed`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName(" Name "))
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(" My topic "))
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+
+ assertThat(room.newName).isNull()
+ assertThat(room.newTopic).isNull()
+ assertThat(room.newAvatarData).isNull()
+ assertThat(room.removedAvatar).isFalse()
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - save doesn't change topic if it was unset and is now blank`() = runTest {
+ val room = aMatrixRoom(topic = null, name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic(""))
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+
+ assertThat(room.newName).isNull()
+ assertThat(room.newTopic).isNull()
+ assertThat(room.newAvatarData).isNull()
+ assertThat(room.removedAvatar).isFalse()
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - save doesn't change name if it's now empty`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomName(""))
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+
+ assertThat(room.newName).isNull()
+ assertThat(room.newTopic).isNull()
+ assertThat(room.newAvatarData).isNull()
+ assertThat(room.removedAvatar).isFalse()
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - save processes and sets avatar when processor returns successfully`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ givenPickerReturnsFile()
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+ skipItems(2)
+
+ assertThat(room.newName).isNull()
+ assertThat(room.newTopic).isNull()
+ assertThat(room.newAvatarData).isSameInstanceAs(fakeFileContents)
+ assertThat(room.removedAvatar).isFalse()
+ }
+ }
+
+ @Test
+ fun `present - save does not set avatar data if processor fails`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL)
+
+ fakePickerProvider.givenResult(anotherAvatarUri)
+ fakeMediaPreProcessor.givenResult(Result.failure(Throwable("Oh no")))
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+ skipItems(1)
+
+ assertThat(room.newName).isNull()
+ assertThat(room.newTopic).isNull()
+ assertThat(room.newAvatarData).isNull()
+ assertThat(room.removedAvatar).isFalse()
+
+ assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
+ }
+ }
+
+ @Test
+ fun `present - sets save action to failure if name update fails`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL).apply {
+ givenSetNameResult(Result.failure(Throwable("!")))
+ }
+
+ saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomName("New name"))
+ }
+
+ @Test
+ fun `present - sets save action to failure if topic update fails`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL).apply {
+ givenSetTopicResult(Result.failure(Throwable("!")))
+ }
+
+ saveAndAssertFailure(room, RoomDetailsEditEvents.UpdateRoomTopic("New topic"))
+ }
+
+ @Test
+ fun `present - sets save action to failure if removing avatar fails`() = runTest {
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL).apply {
+ givenRemoveAvatarResult(Result.failure(Throwable("!")))
+ }
+
+ saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove))
+ }
+
+ @Test
+ fun `present - sets save action to failure if setting avatar fails`() = runTest {
+ givenPickerReturnsFile()
+
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL).apply {
+ givenUpdateAvatarResult(Result.failure(Throwable("!")))
+ }
+
+ saveAndAssertFailure(room, RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ }
+
+ @Test
+ fun `present - CancelSaveChanges resets save action state`() = runTest {
+ givenPickerReturnsFile()
+
+ val room = aMatrixRoom(topic = "My topic", name = "Name", avatarUrl = AN_AVATAR_URL).apply {
+ givenSetTopicResult(Result.failure(Throwable("!")))
+ }
+
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("foo"))
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+ skipItems(1)
+
+ assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
+
+ initialState.eventSink(RoomDetailsEditEvents.CancelSaveChanges)
+ assertThat(awaitItem().saveAction).isInstanceOf(Async.Uninitialized::class.java)
+ }
+ }
+
+ private suspend fun saveAndAssertFailure(room: MatrixRoom, event: RoomDetailsEditEvents) {
+ val presenter = aRoomDetailsEditPresenter(room)
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+
+ initialState.eventSink(event)
+ initialState.eventSink(RoomDetailsEditEvents.Save)
+ skipItems(1)
+
+ assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java)
+ }
+ }
+
+ private fun givenPickerReturnsFile() {
+ mockkStatic(File::readBytes)
+ val processedFile: File = mockk {
+ every { readBytes() } returns fakeFileContents
+ }
+
+ fakePickerProvider.givenResult(anotherAvatarUri)
+ fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(
+ file = processedFile,
+ info = mockk(),
+ thumbnailInfo = ThumbnailProcessingInfo(
+ file = processedFile,
+ info = mockk(),
+ blurhash = "",
+ )
+ )))
+ }
+
+ companion object {
+ private const val ANOTHER_AVATAR_URL = "example://camera/foo.jpg"
+ }
+
+}
diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
index 49ecff7d3b..3baea96990 100644
--- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
+++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt
@@ -29,10 +29,12 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.RoomMembershipState
+import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import io.element.android.libraries.usersearch.api.UserSearchResult
import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.ImmutableList
@@ -130,7 +132,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitResult(aMatrixUserList())
+ repository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
skipItems(1)
val resultState = awaitItem()
@@ -175,7 +177,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitResult(aMatrixUserList())
+ repository.emitResult(aMatrixUserList().map { UserSearchResult(it) })
skipItems(1)
val resultState = awaitItem()
@@ -202,6 +204,53 @@ internal class RoomInviteMembersPresenterTest {
}
}
+ @Test
+ fun `present - performs search and handles unresolved results`() = runTest {
+ val userList = aMatrixUserList()
+ val joinedUser = userList[0]
+ val invitedUser = userList[1]
+
+ val repository = FakeUserRepository()
+ val presenter = RoomInviteMembersPresenter(
+ userRepository = repository,
+ roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply {
+ givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(
+ aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN),
+ aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE),
+ )))
+ }),
+ coroutineDispatchers = testCoroutineDispatchers()
+ )
+
+ moleculeFlow(RecompositionClock.Immediate) {
+ presenter.present()
+ }.test {
+ val initialState = awaitItem()
+ skipItems(1)
+
+ initialState.eventSink(RoomInviteMembersEvents.UpdateSearchQuery("some query"))
+ skipItems(1)
+
+ assertThat(repository.providedQuery).isEqualTo("some query")
+
+ val unresolvedUser = UserSearchResult(aMatrixUser(id = A_USER_ID.value), isUnresolved = true)
+ repository.emitResult(listOf(unresolvedUser) + aMatrixUserList().map { UserSearchResult(it) })
+ skipItems(1)
+
+ val resultState = awaitItem()
+ assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
+
+ val users = resultState.searchResults.users()
+
+ val userWhoShouldBeUnresolved = users.first()
+ assertThat(userWhoShouldBeUnresolved.isUnresolved).isTrue()
+
+ // All other users are neither joined nor invited
+ val otherUsers = users.minus(userWhoShouldBeUnresolved)
+ assertThat(otherUsers.none { it.isUnresolved }).isTrue()
+ }
+ }
+
@Test
fun `present - toggle users updates selected user state`() = runTest {
val repository = FakeUserRepository()
@@ -254,7 +303,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitResult(aMatrixUserList() + selectedUser)
+ repository.emitResult((aMatrixUserList() + selectedUser).map { UserSearchResult(it) })
skipItems(2)
val resultState = awaitItem()
@@ -296,7 +345,7 @@ internal class RoomInviteMembersPresenterTest {
skipItems(1)
assertThat(repository.providedQuery).isEqualTo("some query")
- repository.emitResult(aMatrixUserList() + selectedUser)
+ repository.emitResult((aMatrixUserList() + selectedUser).map { UserSearchResult(it) })
skipItems(2)
// And then a user is toggled
diff --git a/features/roomlist/impl/src/main/res/values-cs/translations.xml b/features/roomlist/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..a222367f09
--- /dev/null
+++ b/features/roomlist/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "Vytvořte novou konverzaci nebo místnost"
+ "Všechny chaty"
+ "Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám."
+ "Přístup k historii zpráv"
+
\ No newline at end of file
diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml
index be0109cbe2..ab61ed4c4e 100644
--- a/features/roomlist/impl/src/main/res/values-de/translations.xml
+++ b/features/roomlist/impl/src/main/res/values-de/translations.xml
@@ -1,4 +1,7 @@
+ "Ein neues Gespräch oder einen neuen Raum erstellen"
"Alle Chats"
+ "Es sieht so aus, als ob du ein neues Gerät verwendest. Verifiziere, dass du es bist, um auf deine verschlüsselten Nachrichten zuzugreifen."
+ "Greife auf deine Nachrichten-Historie zu"
\ No newline at end of file
diff --git a/features/roomlist/impl/src/main/res/values-fr/translations.xml b/features/roomlist/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..6a049a4e72
--- /dev/null
+++ b/features/roomlist/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,7 @@
+
+
+ "Créer une nouvelle conversation ou une nouvelle salle"
+ "Toutes les conversations"
+ "Il semblerait que vous utilisiez un nouvel appareil. Vérifiez que vous êtes bien autorisé à accéder à vos messages cryptés."
+ "Accédez à l\'historique de vos messages"
+
\ No newline at end of file
diff --git a/features/verifysession/impl/src/main/res/values-cs/translations.xml b/features/verifysession/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..41f95d41fe
--- /dev/null
+++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,19 @@
+
+
+ "Něco není v pořádku. Buď vypršel časový limit požadavku, nebo byl požadavek zamítnut."
+ "Zkontrolujte, zda se níže uvedené emotikony shodují s emotikony zobrazenými na jiné relaci."
+ "Porovnání emotikonů"
+ "Vaše nová relace je nyní ověřena. Má přístup k vašim zašifrovaným zprávám a ostatní uživatelé ji uvidí jako důvěryhodnou."
+ "Pro přístup k historii zašifrovaných zpráv prokažte, že jste to vy."
+ "Otevřete existující relaci"
+ "Opakovat ověření"
+ "Jsem připraven"
+ "Čekání na shodu"
+ "Porovnejte jedinečné emotikony a ujistěte se, že jsou zobrazeny ve stejném pořadí."
+ "Neshodují se"
+ "Shodují se"
+ "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci."
+ "Čekání na přijetí žádosti"
+ "Ověření zrušeno"
+ "Začít"
+
\ No newline at end of file
diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml
index e3817c2507..53479844b4 100644
--- a/features/verifysession/impl/src/main/res/values-de/translations.xml
+++ b/features/verifysession/impl/src/main/res/values-de/translations.xml
@@ -1,10 +1,19 @@
+ "Etwas scheint nicht zu stimmen. Entweder ist die Antwortzeit für die Anfrage abgelaufen oder die Anfrage wurde abgelehnt."
+ "Bestätige, dass die folgenden Emojis mit denen deiner anderen Sitzung übereinstimmen."
"Emojis vergleichen"
+ "Deine neue Sitzung ist jetzt verifiziert. Sie hat Zugriff auf deine verschlüsselten Nachrichten und andere Benutzer werden sie als vertrauenswürdig sehen."
"Beweise, dass du es bist, um auf deinen verschlüsselten Nachrichtenverlauf zuzugreifen."
+ "Eine bestehende Sitzung öffnen"
+ "Verifizierung erneut versuchen"
"Ich bin bereit"
"Warten auf Übereinstimmung"
+ "Vergleiche die einzigartigen Emojis und achte darauf, dass sie in derselben Reihenfolge erscheinen."
"Sie stimmen nicht überein"
"Sie stimmen überein"
+ "Akzeptiere die Aufforderung zum Starten des Verifizierungsprozesses in deiner anderen Sitzung, um fortzufahren."
+ "Warten auf die Annahme der Anfrage"
"Verifizierung abgebrochen"
+ "Starten"
\ No newline at end of file
diff --git a/features/verifysession/impl/src/main/res/values-fr/translations.xml b/features/verifysession/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..cd06c5db8b
--- /dev/null
+++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,19 @@
+
+
+ "Quelque chose ne semble pas normal. Soit la demande a dépassé le temps imparti, soit elle a été refusée."
+ "Confirmez que les emojis ci-dessous correspondent à ceux affichés sur votre autre session."
+ "Comparez les émojis"
+ "Votre nouvelle session est désormais vérifiée. Elle a accès à vos messages cryptés et les autres utilisateurs la verront identifiée comme fiable."
+ "Prouvez qu\'il s\'agit bien de vous pour accéder à l\'historique de vos messages cryptés."
+ "Ouvrir une session existante"
+ "Réessayer la vérification"
+ "Je suis prêt.e"
+ "En attente de correspondance"
+ "Comparez les emoji uniques en veillant à ce qu\'ils apparaissent dans le même ordre."
+ "Ils ne correspondent pas"
+ "Ils correspondent"
+ "Pour continuer, acceptez la demande de lancement de la procédure de vérification dans votre autre session."
+ "En attente d\'acceptation de la demande"
+ "Vérification annulée"
+ "Démarrer"
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 49823363bc..2f84422fce 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@
[versions]
# Project
-android_gradle_plugin = "8.0.1"
+android_gradle_plugin = "8.0.2"
kotlin = "1.8.21"
ksp = "1.8.21-1.0.11"
molecule = "0.9.0"
@@ -18,6 +18,7 @@ lifecycle = "2.6.1"
activity = "1.7.2"
startup = "1.1.1"
media3 = "1.0.2"
+browser = "1.5.0"
# Compose
compose_bom = "2023.05.01"
@@ -49,7 +50,7 @@ dagger = "2.46.1"
anvil = "2.4.6"
# quality
-detekt = "1.22.0"
+detekt = "1.23.0"
dependencygraph = "0.10"
[libraries]
@@ -59,7 +60,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref
android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3"
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# https://firebase.google.com/docs/android/setup#available-libraries
-google_firebase_bom = "com.google.firebase:firebase-bom:32.0.0"
+google_firebase_bom = "com.google.firebase:firebase-bom:32.1.0"
# AndroidX
androidx_material = { module = "com.google.android.material:material", version.ref = "material" }
@@ -70,6 +71,7 @@ androidx_datastore_datastore = { module = "androidx.datastore:datastore", versio
androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.6"
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+androidx_browser = { module = "androidx.browser:browser", version.ref = "browser" }
androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx_splash = "androidx.core:core-splashscreen:1.0.1"
@@ -118,8 +120,8 @@ test_mockk = "io.mockk:mockk:1.13.5"
test_barista = "com.adevinta.android:barista:4.3.0"
test_hamcrest = "org.hamcrest:hamcrest:2.2"
test_orchestrator = "androidx.test:orchestrator:1.4.2"
-test_turbine = "app.cash.turbine:turbine:0.12.3"
-test_truth = "com.google.truth:truth:1.1.3"
+test_turbine = "app.cash.turbine:turbine:0.13.0"
+test_truth = "com.google.truth:truth:1.1.4"
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.12"
test_robolectric = "org.robolectric:robolectric:4.10.3"
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
@@ -127,6 +129,7 @@ test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "
# Others
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
+coil_gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" }
datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
serialization_json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_json" }
showkase = { module = "com.airbnb.android:showkase", version.ref = "showkase" }
@@ -178,6 +181,6 @@ ktlint = "org.jlleitschuh.gradle.ktlint:11.3.2"
dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" }
dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" }
paparazzi = "app.cash.paparazzi:1.2.0"
-sonarqube = "org.sonarqube:4.0.0.2929"
+sonarqube = "org.sonarqube:4.2.0.3129"
kover = "org.jetbrains.kotlinx.kover:0.6.1"
sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" }
diff --git a/libraries/androidutils/src/main/res/values-cs/translations.xml b/libraries/androidutils/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..ab592fee1d
--- /dev/null
+++ b/libraries/androidutils/src/main/res/values-cs/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Nebyla nalezena žádná kompatibilní aplikace, která by tuto akci zpracovala."
+
\ No newline at end of file
diff --git a/libraries/androidutils/src/main/res/values-de/translations.xml b/libraries/androidutils/src/main/res/values-de/translations.xml
new file mode 100644
index 0000000000..a34a5b393b
--- /dev/null
+++ b/libraries/androidutils/src/main/res/values-de/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Keine kompatible App für diese Aktion gefunden."
+
\ No newline at end of file
diff --git a/libraries/androidutils/src/main/res/values-fr/translations.xml b/libraries/androidutils/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..d564c18817
--- /dev/null
+++ b/libraries/androidutils/src/main/res/values-fr/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Aucune application compatible n\'a été trouvée pour gérer cette action."
+
\ No newline at end of file
diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
index b845a5efeb..cc97b5fda0 100644
--- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
+++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt
@@ -17,6 +17,7 @@
package io.element.android.libraries.core.extensions
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
+fun Boolean.to01() = if (this) "1" else "0"
inline fun T.ooi(block: (T) -> Unit): T = also(block)
diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts
index d850074d66..08ec84c227 100644
--- a/libraries/deeplink/build.gradle.kts
+++ b/libraries/deeplink/build.gradle.kts
@@ -31,6 +31,7 @@ dependencies {
implementation(libs.dagger)
implementation(libs.androidx.corektx)
implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.architecture)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt
index 50585f9dba..e61b2e66a3 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt
@@ -79,3 +79,6 @@ val Compound_Gray_300_Light = Color(0xFFF0F2F5)
val Compound_Gray_300_Dark = Color(0xFF1D1F24)
val Compound_Gray_400_Light = Color(0xFFE1E6EC)
val Compound_Gray_400_Dark = Color(0xFF26282D)
+
+val Gray_1400_Light = Color(0xFF1B1D22)
+val Gray_1400_Dark = Color(0xFFEBEEF2)
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
similarity index 77%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt
rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
index 2a28c56253..0f0ada5497 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt
@@ -14,18 +14,17 @@
* limitations under the License.
*/
-package io.element.android.features.createroom.impl.components
+package io.element.android.libraries.designsystem.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import io.element.android.features.createroom.impl.R
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Text
@@ -36,8 +35,9 @@ fun LabelledTextField(
label: String,
value: String,
modifier: Modifier = Modifier,
- placeholder: String = "",
- maxLines: Int = 1,
+ placeholder: String? = null,
+ maxLines: Int = Int.MAX_VALUE,
+ singleLine: Boolean = false,
onValueChange: (String) -> Unit = {},
) {
Column(
@@ -46,15 +46,17 @@ fun LabelledTextField(
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
text = label
)
TextField(
modifier = Modifier.fillMaxWidth(),
value = value,
- placeholder = { Text(placeholder) },
+ placeholder = placeholder?.let { { Text(placeholder) } },
onValueChange = onValueChange,
- singleLine = maxLines == 1,
+ singleLine = singleLine,
maxLines = maxLines,
)
}
@@ -72,14 +74,14 @@ fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() }
private fun ContentToPreview() {
Column {
LabelledTextField(
- label = stringResource(R.string.screen_create_room_room_name_label),
+ label = "Room name",
value = "",
- placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
+ placeholder = "e.g. Product Sprint",
)
LabelledTextField(
- label = stringResource(R.string.screen_create_room_room_name_label),
+ label = "Room name",
value = "a room name",
- placeholder = stringResource(R.string.screen_create_room_room_name_placeholder),
+ placeholder = "e.g. Product Sprint",
)
}
}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
new file mode 100644
index 0000000000..5863f4c80c
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.async
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.Button
+import io.element.android.libraries.designsystem.theme.components.Text
+
+@Composable
+fun AsyncFailure(
+ throwable: Throwable,
+ onRetry: (() -> Unit)?,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(vertical = 32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(text = throwable.message ?: "An error occurred")
+ if (onRetry != null) {
+ Spacer(modifier = Modifier.height(24.dp))
+ Button(onClick = onRetry) {
+ Text(text = "Retry")
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+internal fun AsyncFailurePreviewLight() = ElementPreviewLight { ContentToPreview() }
+
+@Preview
+@Composable
+internal fun AsyncFailurePreviewDark() = ElementPreviewDark { ContentToPreview() }
+
+@Composable
+private fun ContentToPreview() {
+ AsyncFailure(
+ throwable = IllegalStateException("An error occurred"),
+ onRetry = {}
+ )
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncLoading.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncLoading.kt
new file mode 100644
index 0000000000..f63d34fd9b
--- /dev/null
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncLoading.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.designsystem.components.async
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.element.android.libraries.designsystem.preview.ElementPreviewDark
+import io.element.android.libraries.designsystem.preview.ElementPreviewLight
+import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
+
+@Composable
+fun AsyncLoading(modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(120.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+}
+
+@Preview
+@Composable
+internal fun AsyncLoadingPreviewLight() = ElementPreviewLight { ContentToPreview() }
+
+@Preview
+@Composable
+internal fun AsyncLoadingPreviewDark() = ElementPreviewDark { ContentToPreview() }
+
+@Composable
+private fun ContentToPreview() {
+ AsyncLoading()
+}
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt
index 166a2f3f79..56a287a9e9 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt
@@ -25,6 +25,7 @@ import io.element.android.libraries.designsystem.Black_800
import io.element.android.libraries.designsystem.Black_950
import io.element.android.libraries.designsystem.Compound_Gray_300_Dark
import io.element.android.libraries.designsystem.DarkGrey
+import io.element.android.libraries.designsystem.Gray_1400_Dark
import io.element.android.libraries.designsystem.Gray_300
import io.element.android.libraries.designsystem.Gray_400
import io.element.android.libraries.designsystem.Compound_Gray_400_Dark
@@ -42,6 +43,7 @@ fun elementColorsDark() = ElementColors(
quinary = Gray_450,
gray300 = Compound_Gray_300_Dark,
gray400 = Compound_Gray_400_Dark,
+ gray1400 = Gray_1400_Dark,
textActionCritical = TextColorCriticalDark,
isLight = false,
)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt
index 085dd534cd..696baca384 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt
@@ -22,12 +22,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.Azure
import io.element.android.libraries.designsystem.Black_900
+import io.element.android.libraries.designsystem.Compound_Gray_300_Light
+import io.element.android.libraries.designsystem.Compound_Gray_400_Light
import io.element.android.libraries.designsystem.Gray_100
+import io.element.android.libraries.designsystem.Gray_1400_Light
import io.element.android.libraries.designsystem.Gray_150
import io.element.android.libraries.designsystem.Gray_200
import io.element.android.libraries.designsystem.Gray_25
-import io.element.android.libraries.designsystem.Compound_Gray_300_Light
-import io.element.android.libraries.designsystem.Compound_Gray_400_Light
import io.element.android.libraries.designsystem.Gray_50
import io.element.android.libraries.designsystem.SystemGrey5Light
import io.element.android.libraries.designsystem.SystemGrey6Light
@@ -42,6 +43,7 @@ fun elementColorsLight() = ElementColors(
quinary = Gray_50,
gray300 = Compound_Gray_300_Light,
gray400 = Compound_Gray_400_Light,
+ gray1400 = Gray_1400_Light,
textActionCritical = TextColorCriticalLight,
isLight = true,
)
diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt
index 2643e678d3..9f7139b446 100644
--- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt
+++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt
@@ -31,6 +31,7 @@ class ElementColors(
quinary: Color,
gray300: Color,
gray400: Color,
+ gray1400: Color,
textActionCritical: Color,
isLight: Boolean
) {
@@ -53,6 +54,9 @@ class ElementColors(
var gray400 by mutableStateOf(gray400)
private set
+ var gray1400 by mutableStateOf(gray1400)
+ private set
+
var textActionCritical by mutableStateOf(textActionCritical)
private set
@@ -67,6 +71,7 @@ class ElementColors(
quinary: Color = this.quinary,
gray300: Color = this.gray300,
gray400: Color = this.gray400,
+ gray1400: Color = this.gray1400,
textActionCritical: Color = this.textActionCritical,
isLight: Boolean = this.isLight,
) = ElementColors(
@@ -77,6 +82,7 @@ class ElementColors(
quinary = quinary,
gray300 = gray300,
gray400 = gray400,
+ gray1400 = gray1400,
textActionCritical = textActionCritical,
isLight = isLight,
)
@@ -89,6 +95,7 @@ class ElementColors(
quinary = other.quinary
gray300 = other.gray300
gray400 = other.gray400
+ gray1400 = other.gray1400
textActionCritical = other.textActionCritical
isLight = other.isLight
}
diff --git a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml
index 298bd3d40b..62854b7d46 100644
--- a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml
+++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml
@@ -1,17 +1,20 @@
- "(Avatar wurde ebenfalls geändert)"
- "%1$s hat seinen Avatar geändert"
+ "(Profilbild wurde auch geändert)"
+ "%1$s hat sein Profilbild geändert"
"Du hast deinen Avatar geändert"
- "%1$s hat den Anzeigenamen von %2$s in %3$s geändert"
+ "%1$s hat seinen Anzeigenamen von %2$s in %3$s geändert"
"Du hast deinen Anzeigenamen von %1$s in %2$s geändert"
- "%1$s hat den Anzeigenamen entfernt (war %2$s)"
- "Du hast deinen Anzeigenamen entfernt (war %1$s)"
- "%1$s hat den Anzeigenamen auf %2$s gesetzt"
- "Du hast deinen Anzeigenamen auf %1$s gesetzt"
+ "%1$s hat seinen Anzeigenamen entfernt (es war %2$s)"
+ "Du hast deinen Anzeigenamen entfernt (es war %1$s)"
+ "%1$s hat seinen Anzeigenamen zu %2$s geändert"
+ "Du hast deinen Anzeigenamen auf %1$s geändert"
"%1$s hat den Raum-Avatar geändert"
"Du hast den Raum-Avatar geändert"
- "%1$s hat den Raum-Avatar entfernt"
+ "%1$s hat das Raumbild entfernt"
+ "Du hast das Raumbild entfernt"
+ "%1$s hat %2$s gebannt"
+ "Du hast %1$s gebannt"
"%1$s hat den Raum erstellt"
"Du hast den Raum erstellt"
"%1$s hat %2$s eingeladen"
@@ -21,19 +24,34 @@
"%1$s hat dich eingeladen"
"%1$s ist dem Raum beigetreten"
"Du bist dem Raum beigetreten"
+ "%1$s hat um Beitritt gebeten"
+ "%1$s hat %2$s erlaubt, beizutreten"
+ "%1$s hat dir erlaubt beizutreten"
+ "Du hast um Beitritt gebeten"
+ "%1$s hat die Beitrittsanfrage von %2$s abgelehnt"
+ "Du hast die Beitrittsanfrage von %1$s abgelehnt"
"%1$s hat deine Beitrittsanfrage abgelehnt"
+ "%1$s ist nicht mehr daran interessiert, beizutreten"
+ "Du hast deine Beitrittsanfrage zurückgezogen"
"%1$s hat den Raum verlassen"
"Du hast den Raum verlassen"
"%1$s hat den Raumnamen geändert in: %2$s"
- "Sie haben den Raumnamen geändert in: %1$s"
+ "Du hast den Raumnamen geändert in: %1$s"
"%1$s hat den Raumnamen entfernt"
"Du hast den Raumnamen entfernt"
"%1$s hat die Einladung abgelehnt"
"Du hast die Einladung abgelehnt"
"%1$s hat %2$s entfernt"
"Du hast %1$s entfernt"
+ "%1$s hat eine Einladung an %2$s gesendet, um dem Raum beizutreten"
+ "Du hast eine Einladung an %1$s gesendet, um dem Raum beizutreten"
+ "%1$s hat die Einladung für %2$s widerrufen, dem Raum beizutreten"
+ "Du hast die Einladung für %1$s widerrufen, dem Raum beizutreten"
"%1$s hat das Thema geändert zu: %2$s"
- "Sie haben das Thema geändert zu: %1$s"
+ "Du hast das Thema geändert zu: %1$s"
"%1$s hat das Raumthema entfernt"
"Du hast das Raumthema entfernt"
+ "%1$s hat %2$s entbannt"
+ "Du hast %1$s entbannt"
+ "%1$s hat eine unbekannte Änderung an seiner Mitgliedschaft vorgenommen"
\ No newline at end of file
diff --git a/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..bdf693e976
--- /dev/null
+++ b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,57 @@
+
+
+ "(l\'avatar a aussi été modifié)"
+ "%1$s a changé son avatar"
+ "Vous avez changé d\'avatar"
+ "%1$s a changé son nom d\'affichage de %2$s à %3$s"
+ "Vous avez changé votre nom d\'affichage de %1$s à %2$s"
+ "%1$s a supprimé son nom d\'affichage (il s\'agissait de %2$s)"
+ "Vous avez supprimé votre nom d\'affichage (il s\'agissait de %1$s)"
+ "%1$s a défini son nom d\'affichage en tant que %2$s"
+ "Vous avez défini votre nom d\'affichage en tant que %1$s"
+ "%1$s a changé l\'avatar de la salle"
+ "Vous avez changé l\'avatar de la salle"
+ "%1$s a supprimé l\'avatar de la salle"
+ "Vous avez supprimé l\'avatar de la salle"
+ "%1$s a banni %2$s"
+ "Vous avez banni %1$s"
+ "%1$s a créé la salle"
+ "Vous avez créé la salle"
+ "%1$s a invité %2$s"
+ "%1$s a accepté l\'invitation"
+ "Vous avez accepté l\'invitation"
+ "Vous avez invité %1$s"
+ "%1$s vous a invité."
+ "%1$s a rejoint la salle"
+ "Vous avez rejoint la salle"
+ "%1$s a demandé à rejoindre"
+ "%1$s a autorisé %2$s à rejoindre"
+ "%1$s vous a autorisé à rejoindre"
+ "Vous avez demandé à rejoindre"
+ "%1$s a rejeté la demande d\'adhésion de %2$s"
+ "Vous avez rejeté la demande d\'adhésion de %1$s"
+ "%1$s a rejeté votre demande d\'adhésion"
+ "%1$s n’est plus intéressé à rejoindre"
+ "Vous avez annulé votre demande d\'adhésion"
+ "%1$s a quitté la salle"
+ "Vous avez quitté la salle"
+ "%1$s a changé le nom de la salle en : %2$s"
+ "Vous avez changé le nom de la salle en : %1$s"
+ "%1$s a supprimé le nom de la salle"
+ "Vous avez supprimé le nom de la salle"
+ "%1$s a rejeté l\'invitation"
+ "Vous avez refusé l\'invitation"
+ "%1$s a supprimé %2$s"
+ "Vous avez supprimé %1$s"
+ "%1$s a envoyé une invitation à %2$s à rejoindre la salle"
+ "Vous avez envoyé une invitation à %1$s pour rejoindre la salle"
+ "%1$s a révoqué l\'invitation de %2$s à rejoindre la salle"
+ "Vous avez révoqué l\'invitation de %1$s à rejoindre la salle"
+ "%1$s a changé le sujet en : %2$s"
+ "Vous avez changé le sujet en : %1$s"
+ "%1$s a supprimé le sujet de la salle"
+ "Vous avez supprimé le sujet de la salle"
+ "%1$s a débanni %2$s"
+ "Vous avez débanni %1$s"
+ "%1$s a apporté une modification inconnue à son adhésion"
+
\ No newline at end of file
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
index 4c943db0d6..e670e02f11 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/AuthenticationException.kt
@@ -22,4 +22,5 @@ sealed class AuthenticationException(message: String) : Exception(message) {
class SlidingSyncNotAvailable(message: String) : AuthenticationException(message)
class SessionMissing(message: String) : AuthenticationException(message)
class Generic(message: String) : AuthenticationException(message)
+ class OidcError(type: String, message: String) : AuthenticationException(message)
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt
index 3414c4fb6e..c15153876c 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixAuthenticationService.kt
@@ -28,4 +28,23 @@ interface MatrixAuthenticationService {
fun getHomeserverDetails(): StateFlow
suspend fun setHomeserver(homeserver: String): Result
suspend fun login(username: String, password: String): Result
+
+ /*
+ * OIDC part.
+ */
+
+ /**
+ * Get the Oidc url to display to the user.
+ */
+ suspend fun getOidcUrl(): Result
+
+ /**
+ * Cancel Oidc login sequence.
+ */
+ suspend fun cancelOidcLogin(): Result
+
+ /**
+ * Attempt to login using the [callbackUrl] provided by the Oidc page.
+ */
+ suspend fun loginWithOidc(callbackUrl: String): Result
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt
index 7f2b074c67..f5fc38eb16 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/MatrixHomeServerDetails.kt
@@ -23,5 +23,5 @@ import kotlinx.parcelize.Parcelize
data class MatrixHomeServerDetails(
val url: String,
val supportsPasswordLogin: Boolean,
- val authenticationIssuer: String?
+ val supportsOidcLogin: Boolean,
): Parcelable
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt
new file mode 100644
index 0000000000..ae473885da
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.api.auth
+
+object OidcConfig {
+ const val redirectUri = "io.element:/callback"
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt
new file mode 100644
index 0000000000..34c926c8e6
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.api.auth
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class OidcDetails(
+ val url: String,
+) : Parcelable
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
index a30baadb55..4e6d468c54 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt
@@ -34,7 +34,7 @@ object MatrixPatterns {
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find room ids in a string.
- private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
+ private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9-]+$DOMAIN_REGEX"
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
// regex pattern to find room aliases in a string.
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
index 991f8dd117..eb6e9998ac 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt
@@ -20,15 +20,23 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
-//TODO add content
data class NotificationData(
val senderId: UserId,
val eventId: EventId,
val roomId: RoomId,
- val senderAvatarUrl: String? = null,
- val senderDisplayName: String? = null,
- val roomAvatarUrl: String? = null,
+ val senderAvatarUrl: String?,
+ val senderDisplayName: String?,
+ val roomAvatarUrl: String?,
+ val roomDisplayName: String?,
val isDirect: Boolean,
val isEncrypted: Boolean,
val isNoisy: Boolean,
+ val event: NotificationEvent,
+)
+
+data class NotificationEvent(
+ val timestamp: Long,
+ val content: String,
+ // For images for instance
+ val contentUrl: String?
)
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
index 5cf9375e0a..edf85cf471 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
@@ -89,4 +89,14 @@ interface MatrixRoom : Closeable {
suspend fun inviteUserById(id: UserId): Result
suspend fun canInvite(): Result
+
+ suspend fun canSendStateEvent(type: StateEventType): Result
+
+ suspend fun updateAvatar(mimeType: String, data: ByteArray): Result
+
+ suspend fun removeAvatar(): Result
+
+ suspend fun setName(name: String): Result
+
+ suspend fun setTopic(topic: String): Result
}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt
new file mode 100644
index 0000000000..50cde59b37
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/StateEventType.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.api.room
+
+enum class StateEventType {
+ POLICY_RULE_ROOM,
+ POLICY_RULE_SERVER,
+ POLICY_RULE_USER,
+ ROOM_ALIASES,
+ ROOM_AVATAR,
+ ROOM_CANONICAL_ALIAS,
+ ROOM_CREATE,
+ ROOM_ENCRYPTION,
+ ROOM_GUEST_ACCESS,
+ ROOM_HISTORY_VISIBILITY,
+ ROOM_JOIN_RULES,
+ ROOM_MEMBER_EVENT,
+ ROOM_NAME,
+ ROOM_PINNED_EVENTS,
+ ROOM_POWER_LEVELS,
+ ROOM_SERVER_ACL,
+ ROOM_THIRD_PARTY_INVITE,
+ ROOM_TOMBSTONE,
+ ROOM_TOPIC,
+ SPACE_CHILD,
+ SPACE_PARENT;
+}
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt
new file mode 100644
index 0000000000..8141528f34
--- /dev/null
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.api.timeline.item.event
+
+/**
+ * Constants defining known event types from Matrix specifications.
+ */
+object EventType {
+ const val PRESENCE = "m.presence"
+ const val MESSAGE = "m.room.message"
+ const val STICKER = "m.sticker"
+ const val ENCRYPTED = "m.room.encrypted"
+ const val FEEDBACK = "m.room.message.feedback"
+ const val TYPING = "m.typing"
+ const val REDACTION = "m.room.redaction"
+ const val RECEIPT = "m.receipt"
+ const val ROOM_KEY = "m.room_key"
+ const val PLUMBING = "m.room.plumbing"
+ const val BOT_OPTIONS = "m.room.bot.options"
+ const val PREVIEW_URLS = "org.matrix.room.preview_urls"
+
+ // State Events
+
+ const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets"
+ const val STATE_ROOM_WIDGET = "m.widget"
+ const val STATE_ROOM_NAME = "m.room.name"
+ const val STATE_ROOM_TOPIC = "m.room.topic"
+ const val STATE_ROOM_AVATAR = "m.room.avatar"
+ const val STATE_ROOM_MEMBER = "m.room.member"
+ const val STATE_ROOM_THIRD_PARTY_INVITE = "m.room.third_party_invite"
+ const val STATE_ROOM_CREATE = "m.room.create"
+ const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
+ const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
+ const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
+
+ const val STATE_SPACE_CHILD = "m.space.child"
+ const val STATE_SPACE_PARENT = "m.space.parent"
+
+ /**
+ * Note that this Event has been deprecated, see
+ * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
+ * - https://github.com/matrix-org/matrix-doc/pull/2432
+ */
+ const val STATE_ROOM_ALIASES = "m.room.aliases"
+ const val STATE_ROOM_TOMBSTONE = "m.room.tombstone"
+ const val STATE_ROOM_CANONICAL_ALIAS = "m.room.canonical_alias"
+ const val STATE_ROOM_HISTORY_VISIBILITY = "m.room.history_visibility"
+ const val STATE_ROOM_RELATED_GROUPS = "m.room.related_groups"
+ const val STATE_ROOM_PINNED_EVENT = "m.room.pinned_events"
+ const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
+ const val STATE_ROOM_SERVER_ACL = "m.room.server_acl"
+
+ // Call Events
+ const val CALL_INVITE = "m.call.invite"
+ const val CALL_CANDIDATES = "m.call.candidates"
+ const val CALL_ANSWER = "m.call.answer"
+ const val CALL_SELECT_ANSWER = "m.call.select_answer"
+ const val CALL_NEGOTIATE = "m.call.negotiate"
+ const val CALL_REJECT = "m.call.reject"
+ const val CALL_HANGUP = "m.call.hangup"
+
+ // This type is not processed by the client, just sent to the server
+ const val CALL_REPLACES = "m.call.replaces"
+
+ // Key share events
+ const val ROOM_KEY_REQUEST = "m.room_key_request"
+ const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
+
+ const val REQUEST_SECRET = "m.secret.request"
+ const val SEND_SECRET = "m.secret.send"
+
+ // Relation Events
+ const val REACTION = "m.reaction"
+
+ fun isCallEvent(type: String): Boolean {
+ return type == CALL_INVITE ||
+ type == CALL_CANDIDATES ||
+ type == CALL_ANSWER ||
+ type == CALL_HANGUP ||
+ type == CALL_SELECT_ANSWER ||
+ type == CALL_NEGOTIATE ||
+ type == CALL_REJECT ||
+ type == CALL_REPLACES
+ }
+}
diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts
index 6b76a17985..1ac5b4b507 100644
--- a/libraries/matrix/impl/build.gradle.kts
+++ b/libraries/matrix/impl/build.gradle.kts
@@ -32,6 +32,7 @@ dependencies {
// api(projects.libraries.rustsdk)
implementation(libs.matrix.sdk)
implementation(projects.libraries.di)
+ implementation(projects.services.toolbox.api)
api(projects.libraries.matrix.api)
implementation(libs.dagger)
implementation(projects.libraries.core)
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
index bac98dd852..f29482edfc 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
@@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -44,6 +45,7 @@ import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService
import io.element.android.libraries.sessionstorage.api.SessionStore
+import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -77,6 +79,7 @@ class RustMatrixClient constructor(
private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val baseDirectory: File,
+ private val clock: SystemClock,
) : MatrixClient {
override val sessionId: UserId = UserId(client.userId())
@@ -114,9 +117,9 @@ class RustMatrixClient constructor(
.timelineLimit(limit = 1u)
.requiredState(
requiredState = listOf(
- RequiredState(key = "m.room.avatar", value = ""),
- RequiredState(key = "m.room.encryption", value = ""),
- RequiredState(key = "m.room.join_rules", value = ""),
+ RequiredState(key = EventType.STATE_ROOM_AVATAR, value = ""),
+ RequiredState(key = EventType.STATE_ROOM_ENCRYPTION, value = ""),
+ RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
)
)
.filters(visibleRoomsSlidingSyncFilters)
@@ -136,9 +139,9 @@ class RustMatrixClient constructor(
.timelineLimit(limit = 1u)
.requiredState(
requiredState = listOf(
- RequiredState(key = "m.room.avatar", value = ""),
- RequiredState(key = "m.room.encryption", value = ""),
- RequiredState(key = "m.room.canonical_alias", value = ""),
+ RequiredState(key = EventType.STATE_ROOM_AVATAR, value = ""),
+ RequiredState(key = EventType.STATE_ROOM_ENCRYPTION, value = ""),
+ RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
)
)
.filters(invitesSlidingSyncFilters)
@@ -153,7 +156,7 @@ class RustMatrixClient constructor(
private val slidingSync = client
.slidingSync()
- .homeserver("https://slidingsync.lab.matrix.org")
+ // .homeserver("https://slidingsync.lab.matrix.org")
.withCommonExtensions()
.storageKey("ElementX")
.addList(visibleRoomsSlidingSyncListBuilder)
@@ -215,6 +218,7 @@ class RustMatrixClient constructor(
innerRoom = fullRoom,
coroutineScope = coroutineScope,
coroutineDispatchers = dispatchers,
+ clock = clock,
)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
index 96e3a367cb..c264a95f67 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt
@@ -26,6 +26,15 @@ fun Throwable.mapAuthenticationException(): Throwable {
is RustAuthenticationException.InvalidServerName -> AuthenticationException.InvalidServerName(this.message!!)
is RustAuthenticationException.SessionMissing -> AuthenticationException.SessionMissing(this.message!!)
is RustAuthenticationException.SlidingSyncNotAvailable -> AuthenticationException.SlidingSyncNotAvailable(this.message!!)
+
+ /* TODO Oidc
+ is RustAuthenticationException.OidcException -> AuthenticationException.OidcError("OidcException", message!!)
+ is RustAuthenticationException.OidcMetadataInvalid -> AuthenticationException.OidcError("OidcMetadataInvalid", message!!)
+ is RustAuthenticationException.OidcMetadataMissing -> AuthenticationException.OidcError("OidcMetadataMissing", message!!)
+ is RustAuthenticationException.OidcNotStarted -> AuthenticationException.OidcError("OidcNotStarted", message!!)
+ is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!)
+ */
+
else -> this
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt
index e2f3cfe676..a3d277c6da 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetails.kt
@@ -23,6 +23,6 @@ fun HomeserverLoginDetails.map(): MatrixHomeServerDetails = use {
MatrixHomeServerDetails(
url = url(),
supportsPasswordLogin = supportsPasswordLogin(),
- authenticationIssuer = authenticationIssuer()
+ supportsOidcLogin = false // TODO Oidc supportsOidcLogin(),
)
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt
new file mode 100644
index 0000000000..1ba5063df9
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.impl.auth
+
+import io.element.android.libraries.matrix.api.auth.OidcConfig
+// TODO Oidc
+// import org.matrix.rustcomponents.sdk.OidcClientMetadata
+
+/*
+val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata(
+ clientName = "Element",
+ redirectUri = OidcConfig.redirectUri,
+ clientUri = "https://element.io",
+ tosUri = "https://element.io/user-terms-of-service",
+ policyUri = "https://element.io/privacy"
+)
+ */
+
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
index aff83c3b5a..df3c7f3d6f 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt
@@ -24,11 +24,12 @@ import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
+import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.core.SessionId
-import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.impl.RustMatrixClient
import io.element.android.libraries.sessionstorage.api.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore
+import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,6 +37,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientBuilder
+// TODO Oidc
+// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.use
import java.io.File
@@ -49,9 +52,16 @@ class RustMatrixAuthenticationService @Inject constructor(
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,
+ private val clock: SystemClock,
) : MatrixAuthenticationService {
- private val authService: RustAuthenticationService = RustAuthenticationService(baseDirectory.absolutePath, null, null)
+ private val authService: RustAuthenticationService = RustAuthenticationService(
+ basePath = baseDirectory.absolutePath,
+ passphrase = null,
+ // TODO Oidc
+ // oidcClientMetadata = oidcClientMetadata,
+ customSlidingSyncProxy = null
+ )
private var currentHomeserver = MutableStateFlow(null)
override fun isLoggedIn(): Flow {
@@ -91,9 +101,9 @@ class RustMatrixAuthenticationService @Inject constructor(
if (homeServerDetails != null) {
currentHomeserver.value = homeServerDetails.copy(url = homeserver)
}
+ }.mapFailure { failure ->
+ failure.mapAuthenticationException()
}
- }.mapFailure { failure ->
- failure.mapAuthenticationException()
}
override suspend fun login(username: String, password: String): Result =
@@ -103,11 +113,65 @@ class RustMatrixAuthenticationService @Inject constructor(
val sessionData = client.use { it.session().toSessionData() }
sessionStore.storeData(sessionData)
SessionId(sessionData.userId)
+ }.mapFailure { failure ->
+ failure.mapAuthenticationException()
}
- }.mapFailure { failure ->
- failure.mapAuthenticationException()
}
+ // TODO Oidc
+ // private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null
+
+ override suspend fun getOidcUrl(): Result {
+ TODO("Oidc")
+ /*
+ return withContext(coroutineDispatchers.io) {
+ runCatching {
+ val urlForOidcLogin = authService.urlForOidcLogin()
+ val url = urlForOidcLogin.loginUrl()
+ pendingUrlForOidcLogin = urlForOidcLogin
+ OidcDetails(url)
+ }.mapFailure { failure ->
+ failure.mapAuthenticationException()
+ }
+ }
+ */
+ }
+
+ override suspend fun cancelOidcLogin(): Result {
+ TODO("Oidc")
+ /*
+ return withContext(coroutineDispatchers.io) {
+ runCatching {
+ pendingUrlForOidcLogin?.close()
+ pendingUrlForOidcLogin = null
+ }.mapFailure { failure ->
+ failure.mapAuthenticationException()
+ }
+ }
+ */
+ }
+
+ /**
+ * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters).
+ */
+ override suspend fun loginWithOidc(callbackUrl: String): Result {
+ TODO("Oidc")
+ /*
+ return withContext(coroutineDispatchers.io) {
+ runCatching {
+ val urlForOidcLogin = pendingUrlForOidcLogin ?: error("You need to call `getOidcUrl()` first")
+ val client = authService.loginWithOidcCallback(urlForOidcLogin, callbackUrl)
+ val sessionData = client.use { it.session().toSessionData() }
+ pendingUrlForOidcLogin = null
+ sessionStore.storeData(sessionData)
+ SessionId(sessionData.userId)
+ }.mapFailure { failure ->
+ failure.mapAuthenticationException()
+ }
+ }
+ */
+ }
+
private fun createMatrixClient(client: Client): MatrixClient {
return RustMatrixClient(
client = client,
@@ -115,6 +179,7 @@ class RustMatrixAuthenticationService @Inject constructor(
coroutineScope = coroutineScope,
dispatchers = coroutineDispatchers,
baseDirectory = baseDirectory,
+ clock = clock,
)
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
index 4b121db9bf..e6125cf69b 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt
@@ -23,9 +23,9 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationData
import org.matrix.rustcomponents.sdk.NotificationItem
import org.matrix.rustcomponents.sdk.use
-import javax.inject.Inject
-class NotificationMapper @Inject constructor() {
+class NotificationMapper {
+ private val timelineEventMapper = TimelineEventMapper()
fun map(notificationItem: NotificationItem): NotificationData {
return notificationItem.use {
@@ -36,9 +36,11 @@ class NotificationMapper @Inject constructor() {
senderAvatarUrl = it.senderAvatarUrl,
senderDisplayName = it.senderDisplayName,
roomAvatarUrl = it.roomAvatarUrl,
+ roomDisplayName = it.roomDisplayName,
isDirect = it.isDirect,
isEncrypted = it.isEncrypted.orFalse(),
- isNoisy = it.isNoisy
+ isNoisy = it.isNoisy,
+ event = it.event.use { event -> timelineEventMapper.map(event) }
)
}
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
index bd94de21fc..8b630cd64a 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt
@@ -16,18 +16,13 @@
package io.element.android.libraries.matrix.impl.notification
-import io.element.android.libraries.core.coroutine.CoroutineDispatchers
-import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
-import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationData
import io.element.android.libraries.matrix.api.notification.NotificationService
-import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.use
-import java.io.File
class RustNotificationService(
private val client: Client,
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt
new file mode 100644
index 0000000000..adb9dcce72
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.impl.notification
+
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.notification.NotificationEvent
+import org.matrix.rustcomponents.sdk.MessageLikeEventContent
+import org.matrix.rustcomponents.sdk.MessageType
+import org.matrix.rustcomponents.sdk.StateEventContent
+import org.matrix.rustcomponents.sdk.TimelineEvent
+import org.matrix.rustcomponents.sdk.TimelineEventType
+import org.matrix.rustcomponents.sdk.use
+import javax.inject.Inject
+
+class TimelineEventMapper @Inject constructor() {
+
+ fun map(timelineEvent: TimelineEvent): NotificationEvent {
+ return timelineEvent.use {
+ NotificationEvent(
+ timestamp = it.timestamp().toLong(),
+ content = it.eventType().toContent(),
+ contentUrl = null // TODO it.eventType().toContentUrl(),
+ )
+ }
+ }
+}
+
+private fun TimelineEventType.toContent(): String {
+ return when (this) {
+ is TimelineEventType.MessageLike -> content.toContent()
+ is TimelineEventType.State -> content.toContent()
+ }
+}
+
+private fun StateEventContent.toContent(): String {
+ return when (this) {
+ StateEventContent.PolicyRuleRoom -> "PolicyRuleRoom"
+ StateEventContent.PolicyRuleServer -> "PolicyRuleServer"
+ StateEventContent.PolicyRuleUser -> "PolicyRuleUser"
+ StateEventContent.RoomAliases -> "RoomAliases"
+ StateEventContent.RoomAvatar -> "RoomAvatar"
+ StateEventContent.RoomCanonicalAlias -> "RoomCanonicalAlias"
+ StateEventContent.RoomCreate -> "RoomCreate"
+ StateEventContent.RoomEncryption -> "RoomEncryption"
+ StateEventContent.RoomGuestAccess -> "RoomGuestAccess"
+ StateEventContent.RoomHistoryVisibility -> "RoomHistoryVisibility"
+ StateEventContent.RoomJoinRules -> "RoomJoinRules"
+ is StateEventContent.RoomMemberContent -> "$userId is now $membershipState"
+ StateEventContent.RoomName -> "RoomName"
+ StateEventContent.RoomPinnedEvents -> "RoomPinnedEvents"
+ StateEventContent.RoomPowerLevels -> "RoomPowerLevels"
+ StateEventContent.RoomServerAcl -> "RoomServerAcl"
+ StateEventContent.RoomThirdPartyInvite -> "RoomThirdPartyInvite"
+ StateEventContent.RoomTombstone -> "RoomTombstone"
+ StateEventContent.RoomTopic -> "RoomTopic"
+ StateEventContent.SpaceChild -> "SpaceChild"
+ StateEventContent.SpaceParent -> "SpaceParent"
+ }
+}
+
+private fun MessageLikeEventContent.toContent(): String {
+ return use {
+ when (it) {
+ MessageLikeEventContent.CallAnswer -> "CallAnswer"
+ MessageLikeEventContent.CallCandidates -> "CallCandidates"
+ MessageLikeEventContent.CallHangup -> "CallHangup"
+ MessageLikeEventContent.CallInvite -> "CallInvite"
+ MessageLikeEventContent.KeyVerificationAccept -> "KeyVerificationAccept"
+ MessageLikeEventContent.KeyVerificationCancel -> "KeyVerificationCancel"
+ MessageLikeEventContent.KeyVerificationDone -> "KeyVerificationDone"
+ MessageLikeEventContent.KeyVerificationKey -> "KeyVerificationKey"
+ MessageLikeEventContent.KeyVerificationMac -> "KeyVerificationMac"
+ MessageLikeEventContent.KeyVerificationReady -> "KeyVerificationReady"
+ MessageLikeEventContent.KeyVerificationStart -> "KeyVerificationStart"
+ is MessageLikeEventContent.ReactionContent -> "Reacted to ${it.relatedEventId.take(8)}…"
+ MessageLikeEventContent.RoomEncrypted -> "RoomEncrypted"
+ is MessageLikeEventContent.RoomMessage -> it.messageType.toContent()
+ MessageLikeEventContent.RoomRedaction -> "RoomRedaction"
+ MessageLikeEventContent.Sticker -> "Sticker"
+ }
+ }
+}
+
+private fun MessageType.toContent(): String {
+ return when (this) {
+ is MessageType.Audio -> content.use { it.body }
+ is MessageType.Emote -> content.body
+ is MessageType.File -> content.use { it.body }
+ is MessageType.Image -> content.use { it.body }
+ is MessageType.Notice -> content.body
+ is MessageType.Text -> content.body
+ is MessageType.Video -> content.use { it.body }
+ }
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 55576c7b96..383710873a 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
@@ -27,11 +27,14 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
+import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
+import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -54,6 +57,7 @@ class RustMatrixRoom(
private val innerRoom: Room,
private val coroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
+ private val clock: SystemClock,
) : MatrixRoom {
override val membersStateFlow: StateFlow
@@ -77,9 +81,9 @@ class RustMatrixRoom(
it.rooms.contains(roomId.value)
}
.map {
- System.currentTimeMillis()
+ clock.epochMillis()
}
- .onStart { emit(System.currentTimeMillis()) }
+ .onStart { emit(clock.epochMillis()) }
}
override fun timeline(): MatrixTimeline {
@@ -222,6 +226,12 @@ class RustMatrixRoom(
}
}
+ override suspend fun canSendStateEvent(type: StateEventType): Result = withContext(coroutineDispatchers.io) {
+ runCatching {
+ innerRoom.member(sessionId.value).use { it.canSendState(type.map()) }
+ }
+ }
+
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = withContext(coroutineDispatchers.io) {
runCatching {
innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map())
@@ -245,4 +255,33 @@ class RustMatrixRoom(
innerRoom.sendFile(file.path, fileInfo.map())
}
}
+
+ @OptIn(ExperimentalUnsignedTypes::class)
+ override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList())
+ }
+ }
+
+ override suspend fun removeAvatar(): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ innerRoom.removeAvatar()
+ }
+ }
+
+ override suspend fun setName(name: String): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ innerRoom.setName(name)
+ }
+ }
+
+ override suspend fun setTopic(topic: String): Result =
+ withContext(Dispatchers.IO) {
+ runCatching {
+ innerRoom.setTopic(topic)
+ }
+ }
}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt
new file mode 100644
index 0000000000..2cd09e213c
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/StateEventType.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.impl.room
+
+import io.element.android.libraries.matrix.api.room.StateEventType
+import org.matrix.rustcomponents.sdk.StateEventType as RustStateEventType
+
+fun StateEventType.map(): RustStateEventType = when (this) {
+ StateEventType.POLICY_RULE_ROOM -> RustStateEventType.POLICY_RULE_ROOM
+ StateEventType.POLICY_RULE_SERVER -> RustStateEventType.POLICY_RULE_SERVER
+ StateEventType.POLICY_RULE_USER -> RustStateEventType.POLICY_RULE_USER
+ StateEventType.ROOM_ALIASES -> RustStateEventType.ROOM_ALIASES
+ StateEventType.ROOM_AVATAR -> RustStateEventType.ROOM_AVATAR
+ StateEventType.ROOM_CANONICAL_ALIAS -> RustStateEventType.ROOM_CANONICAL_ALIAS
+ StateEventType.ROOM_CREATE -> RustStateEventType.ROOM_CREATE
+ StateEventType.ROOM_ENCRYPTION -> RustStateEventType.ROOM_ENCRYPTION
+ StateEventType.ROOM_GUEST_ACCESS -> RustStateEventType.ROOM_GUEST_ACCESS
+ StateEventType.ROOM_HISTORY_VISIBILITY -> RustStateEventType.ROOM_HISTORY_VISIBILITY
+ StateEventType.ROOM_JOIN_RULES -> RustStateEventType.ROOM_JOIN_RULES
+ StateEventType.ROOM_MEMBER_EVENT -> RustStateEventType.ROOM_MEMBER_EVENT
+ StateEventType.ROOM_NAME -> RustStateEventType.ROOM_NAME
+ StateEventType.ROOM_PINNED_EVENTS -> RustStateEventType.ROOM_PINNED_EVENTS
+ StateEventType.ROOM_POWER_LEVELS -> RustStateEventType.ROOM_POWER_LEVELS
+ StateEventType.ROOM_SERVER_ACL -> RustStateEventType.ROOM_SERVER_ACL
+ StateEventType.ROOM_THIRD_PARTY_INVITE -> RustStateEventType.ROOM_THIRD_PARTY_INVITE
+ StateEventType.ROOM_TOMBSTONE -> RustStateEventType.ROOM_TOMBSTONE
+ StateEventType.ROOM_TOPIC -> RustStateEventType.ROOM_TOPIC
+ StateEventType.SPACE_CHILD -> RustStateEventType.SPACE_CHILD
+ StateEventType.SPACE_PARENT -> RustStateEventType.SPACE_PARENT
+}
+
+fun RustStateEventType.map(): StateEventType = when (this) {
+ RustStateEventType.POLICY_RULE_ROOM -> StateEventType.POLICY_RULE_ROOM
+ RustStateEventType.POLICY_RULE_SERVER -> StateEventType.POLICY_RULE_SERVER
+ RustStateEventType.POLICY_RULE_USER -> StateEventType.POLICY_RULE_USER
+ RustStateEventType.ROOM_ALIASES -> StateEventType.ROOM_ALIASES
+ RustStateEventType.ROOM_AVATAR -> StateEventType.ROOM_AVATAR
+ RustStateEventType.ROOM_CANONICAL_ALIAS -> StateEventType.ROOM_CANONICAL_ALIAS
+ RustStateEventType.ROOM_CREATE -> StateEventType.ROOM_CREATE
+ RustStateEventType.ROOM_ENCRYPTION -> StateEventType.ROOM_ENCRYPTION
+ RustStateEventType.ROOM_GUEST_ACCESS -> StateEventType.ROOM_GUEST_ACCESS
+ RustStateEventType.ROOM_HISTORY_VISIBILITY -> StateEventType.ROOM_HISTORY_VISIBILITY
+ RustStateEventType.ROOM_JOIN_RULES -> StateEventType.ROOM_JOIN_RULES
+ RustStateEventType.ROOM_MEMBER_EVENT -> StateEventType.ROOM_MEMBER_EVENT
+ RustStateEventType.ROOM_NAME -> StateEventType.ROOM_NAME
+ RustStateEventType.ROOM_PINNED_EVENTS -> StateEventType.ROOM_PINNED_EVENTS
+ RustStateEventType.ROOM_POWER_LEVELS -> StateEventType.ROOM_POWER_LEVELS
+ RustStateEventType.ROOM_SERVER_ACL -> StateEventType.ROOM_SERVER_ACL
+ RustStateEventType.ROOM_THIRD_PARTY_INVITE -> StateEventType.ROOM_THIRD_PARTY_INVITE
+ RustStateEventType.ROOM_TOMBSTONE -> StateEventType.ROOM_TOMBSTONE
+ RustStateEventType.ROOM_TOPIC -> StateEventType.ROOM_TOPIC
+ RustStateEventType.SPACE_CHILD -> StateEventType.SPACE_CHILD
+ RustStateEventType.SPACE_PARENT -> StateEventType.SPACE_PARENT
+}
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
index 4884dc2d84..923815a714 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
@@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
@@ -149,10 +150,10 @@ class RustMatrixTimeline(
runCatching {
val settings = RoomSubscription(
requiredState = listOf(
- RequiredState(key = "m.room.canonical_alias", value = ""),
- RequiredState(key = "m.room.topic", value = ""),
- RequiredState(key = "m.room.join_rules", value = ""),
- RequiredState(key = "m.room.power_levels", value = ""),
+ RequiredState(key = EventType.STATE_ROOM_CANONICAL_ALIAS, value = ""),
+ RequiredState(key = EventType.STATE_ROOM_TOPIC, value = ""),
+ RequiredState(key = EventType.STATE_ROOM_JOIN_RULES, value = ""),
+ RequiredState(key = EventType.STATE_ROOM_POWER_LEVELS, value = ""),
),
timelineLimit = null
)
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt
index d9322ed2ae..7e54d8e851 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt
@@ -47,7 +47,8 @@ const val ANOTHER_MESSAGE = "Hello universe!"
const val A_HOMESERVER_URL = "matrix.org"
const val A_HOMESERVER_URL_2 = "matrix-client.org"
-val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, true, null)
+val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = true, supportsOidcLogin = false)
+val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidcLogin = true)
const val AN_AVATAR_URL = "mxc://data"
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt
index 886eac95ef..2b34a158a4 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt
@@ -19,16 +19,22 @@ package io.element.android.libraries.matrix.test.auth
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails
+import io.element.android.libraries.matrix.api.auth.OidcDetails
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_USER_ID
+import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
+val A_OIDC_DATA = OidcDetails(url = "a-url")
+
class FakeAuthenticationService : MatrixAuthenticationService {
private var homeserver = MutableStateFlow(null)
+ private var oidcError: Throwable? = null
+ private var oidcCancelError: Throwable? = null
private var loginError: Throwable? = null
private var changeServerError: Throwable? = null
@@ -53,15 +59,36 @@ class FakeAuthenticationService : MatrixAuthenticationService {
}
override suspend fun setHomeserver(homeserver: String): Result {
- delay(100)
+ delay(FAKE_DELAY_IN_MS)
return changeServerError?.let { Result.failure(it) } ?: Result.success(Unit)
}
override suspend fun login(username: String, password: String): Result {
- delay(100)
+ delay(FAKE_DELAY_IN_MS)
return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID)
}
+ override suspend fun getOidcUrl(): Result {
+ return oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA)
+ }
+
+ override suspend fun cancelOidcLogin(): Result {
+ return oidcCancelError?.let { Result.failure(it) } ?: Result.success(Unit)
+ }
+
+ override suspend fun loginWithOidc(callbackUrl: String): Result {
+ delay(FAKE_DELAY_IN_MS)
+ return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID)
+ }
+
+ fun givenOidcError(throwable: Throwable?) {
+ oidcError = throwable
+ }
+
+ fun givenOidcCancelError(throwable: Throwable?) {
+ oidcCancelError = throwable
+ }
+
fun givenLoginError(throwable: Throwable?) {
loginError = throwable
}
diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
index ff91db14b3..8769d9ddfb 100644
--- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
+++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
@@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
+import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
@@ -62,7 +63,13 @@ class FakeMatrixRoom(
private var rejectInviteResult = Result.success(Unit)
private var inviteUserResult = Result.success(Unit)
private var canInviteResult = Result.success(true)
+ private val canSendStateResults = mutableMapOf>()
private var sendMediaResult = Result.success(Unit)
+ private var setNameResult = Result.success(Unit)
+ private var setTopicResult = Result.success(Unit)
+ private var updateAvatarResult = Result.success(Unit)
+ private var removeAvatarResult = Result.success(Unit)
+
var sendMediaCount = 0
private set
@@ -75,6 +82,18 @@ class FakeMatrixRoom(
var invitedUserId: UserId? = null
private set
+ var newTopic: String? = null
+ private set
+
+ var newName: String? = null
+ private set
+
+ var newAvatarData: ByteArray? = null
+ private set
+
+ var removedAvatar: Boolean = false
+ private set
+
private var leaveRoomError: Throwable? = null
override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown)
@@ -151,6 +170,10 @@ class FakeMatrixRoom(
return canInviteResult
}
+ override suspend fun canSendStateEvent(type: StateEventType): Result {
+ return canSendStateResults[type] ?: Result.failure(IllegalStateException("No fake answer"))
+ }
+
override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = fakeSendMedia()
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = fakeSendMedia()
@@ -166,6 +189,26 @@ class FakeMatrixRoom(
}
}
+ override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result {
+ newAvatarData = data
+ return updateAvatarResult
+ }
+
+ override suspend fun removeAvatar(): Result {
+ removedAvatar = true
+ return removeAvatarResult
+ }
+
+ override suspend fun setName(name: String): Result {
+ newName = name
+ return setNameResult
+ }
+
+ override suspend fun setTopic(topic: String): Result {
+ newTopic = topic
+ return setTopicResult
+ }
+
override fun close() = Unit
fun givenLeaveRoomError(throwable: Throwable?) {
@@ -204,6 +247,10 @@ class FakeMatrixRoom(
canInviteResult = result
}
+ fun givenCanSendStateResult(type: StateEventType, result: Result) {
+ canSendStateResults[type] = result
+ }
+
fun givenIgnoreResult(result: Result) {
ignoreResult = result
}
@@ -215,4 +262,20 @@ class FakeMatrixRoom(
fun givenSendMediaResult(result: Result) {
sendMediaResult = result
}
+
+ fun givenUpdateAvatarResult(result: Result) {
+ updateAvatarResult = result
+ }
+
+ fun givenRemoveAvatarResult(result: Result) {
+ removeAvatarResult = result
+ }
+
+ fun givenSetNameResult(result: Result) {
+ setNameResult = result
+ }
+
+ fun givenSetTopicResult(result: Result) {
+ setTopicResult = result
+ }
}
diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts
index 380f19f792..7302eeddfb 100644
--- a/libraries/matrixui/build.gradle.kts
+++ b/libraries/matrixui/build.gradle.kts
@@ -39,6 +39,7 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.uiStrings)
implementation(libs.coil.compose)
+ implementation(libs.coil.gif)
ksp(libs.showkase.processor)
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt
similarity index 92%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt
rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt
index 422a187290..59178d3a45 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarActionListView.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AvatarActionBottomSheet.kt
@@ -16,7 +16,7 @@
@file:OptIn(ExperimentalMaterialApi::class)
-package io.element.android.features.createroom.impl.configureroom.avatar
+package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
@@ -39,12 +39,13 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout
+import io.element.android.libraries.matrix.ui.media.AvatarAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
@Composable
-fun AvatarActionListView(
+fun AvatarActionBottomSheet(
actions: ImmutableList,
modalBottomSheetState: ModalBottomSheetState,
modifier: Modifier = Modifier,
@@ -62,7 +63,7 @@ fun AvatarActionListView(
modifier = modifier,
sheetState = modalBottomSheetState,
sheetContent = {
- SheetContent(
+ AvatarActionBottomSheetContent(
actions = actions,
onActionClicked = ::onItemActionClicked,
modifier = Modifier
@@ -74,7 +75,7 @@ fun AvatarActionListView(
}
@Composable
-private fun SheetContent(
+private fun AvatarActionBottomSheetContent(
actions: ImmutableList,
modifier: Modifier = Modifier,
onActionClicked: (AvatarAction) -> Unit = { },
@@ -107,17 +108,17 @@ private fun SheetContent(
@Preview
@Composable
-fun SheetContentLightPreview() =
+fun AvatarActionBottomSheetLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun SheetContentDarkPreview() =
+fun AvatarActionBottomSheetDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
- AvatarActionListView(
+ AvatarActionBottomSheet(
actions = persistentListOf(AvatarAction.TakePhoto, AvatarAction.ChoosePhoto, AvatarAction.Remove),
modalBottomSheetState = ModalBottomSheetState(
initialValue = ModalBottomSheetValue.Expanded
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt
new file mode 100644
index 0000000000..c195d42685
--- /dev/null
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Error
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.preview.ElementThemedPreview
+import io.element.android.libraries.designsystem.theme.components.Checkbox
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.ui.strings.R
+
+@Composable
+fun UnresolvedUserRow(
+ avatarData: AvatarData,
+ id: String,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
+ .height(IntrinsicSize.Min),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Avatar(avatarData)
+ Column(
+ modifier = Modifier
+ .padding(start = 12.dp),
+ ) {
+ // ID
+ Text(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.SemiBold,
+ text = id,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.primary,
+ )
+
+ // Warning
+ Row(modifier = Modifier.fillMaxWidth()) {
+ Icon(
+ imageVector = Icons.Filled.Error,
+ contentDescription = "",
+ modifier = Modifier
+ .size(18.dp)
+ .align(Alignment.Top)
+ .padding(2.dp),
+ tint = MaterialTheme.colorScheme.error,
+ )
+
+ Text(
+ text = stringResource(R.string.common_invite_unknown_profile),
+ color = MaterialTheme.colorScheme.secondary,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun CheckableUnresolvedUserRow(
+ checked: Boolean,
+ avatarData: AvatarData,
+ id: String,
+ modifier: Modifier = Modifier,
+ onCheckedChange: (Boolean) -> Unit = {},
+ enabled: Boolean = true,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(role = Role.Checkbox, enabled = enabled) {
+ onCheckedChange(!checked)
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ UnresolvedUserRow(
+ modifier = Modifier.weight(1f),
+ avatarData = avatarData,
+ id = id,
+ )
+
+ Checkbox(
+ checked = checked,
+ onCheckedChange = onCheckedChange,
+ enabled = enabled,
+ )
+ }
+}
+
+@Preview
+@Composable
+internal fun UnresolvedUserRowPreview() =
+ ElementThemedPreview {
+ val matrixUser = aMatrixUser()
+ UnresolvedUserRow(matrixUser.getAvatarData(), matrixUser.userId.value)
+ }
+
+@Preview
+@Composable
+internal fun CheckableUnresolvedUserRowPreview() =
+ ElementThemedPreview {
+ val matrixUser = aMatrixUser()
+ Column {
+ CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(), matrixUser.userId.value)
+ CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(), matrixUser.userId.value)
+ CheckableUnresolvedUserRow(false, matrixUser.getAvatarData(), matrixUser.userId.value, enabled = false)
+ CheckableUnresolvedUserRow(true, matrixUser.getAvatarData(), matrixUser.userId.value, enabled = false)
+ }
+ }
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt
similarity index 86%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt
rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt
index bbaf5c46e5..e5816ac014 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package io.element.android.features.createroom.impl.components
+package io.element.android.libraries.matrix.ui.components
import android.net.Uri
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
@@ -43,16 +42,19 @@ import io.element.android.libraries.designsystem.preview.debugPlaceholderBackgro
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Icon
+/**
+ * An avatar that the user has selected, but which has not yet been uploaded to Matrix.
+ *
+ * The image is loaded from a local resource instead of from a MXC URI.
+ */
@Composable
-fun Avatar(
+fun UnsavedAvatar(
avatarUri: Uri?,
modifier: Modifier = Modifier,
- onClick: () -> Unit = {},
) {
val commonModifier = modifier
.size(70.dp)
.clip(CircleShape)
- .clickable(onClick = onClick)
if (avatarUri != null) {
val context = LocalContext.current
@@ -82,16 +84,16 @@ fun Avatar(
@Preview
@Composable
-fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() }
+fun UnsavedAvatarLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
-fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() }
+fun UnsavedAvatarDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Row {
- Avatar(null)
- Avatar(Uri.EMPTY)
+ UnsavedAvatar(null)
+ UnsavedAvatar(Uri.EMPTY)
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarAction.kt
similarity index 95%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt
rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarAction.kt
index 25ecc2b3db..624eb7c607 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/avatar/AvatarAction.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarAction.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package io.element.android.features.createroom.impl.configureroom.avatar
+package io.element.android.libraries.matrix.ui.media
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
index d1fab05544..9038e03611 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt
@@ -17,8 +17,11 @@
package io.element.android.libraries.matrix.ui.media
import android.content.Context
+import android.os.Build
import coil.ImageLoader
import coil.ImageLoaderFactory
+import coil.decode.GifDecoder
+import coil.decode.ImageDecoderDecoder
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import okhttp3.OkHttpClient
@@ -34,6 +37,12 @@ class LoggedInImageLoaderFactory @Inject constructor(
.Builder(context)
.okHttpClient(okHttpClient)
.components {
+ // Add gif support
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ add(ImageDecoderDecoder.Factory())
+ } else {
+ add(GifDecoder.Factory())
+ }
add(AvatarDataKeyer())
add(MediaRequestDataKeyer())
add(CoilMediaFetcher.AvatarFactory(matrixClient))
diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
index 061ce365eb..5980bb138f 100644
--- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
+++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomMembers.kt
@@ -29,13 +29,13 @@ import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.roomMembers
@Composable
-fun MatrixRoom.getRoomMember(userId: UserId): State {
+fun MatrixRoom.getRoomMemberAsState(userId: UserId): State {
val roomMembersState by membersStateFlow.collectAsState()
- return getRoomMember(roomMembersState = roomMembersState, userId = userId)
+ return getRoomMemberAsState(roomMembersState = roomMembersState, userId = userId)
}
@Composable
-fun getRoomMember(roomMembersState: MatrixRoomMembersState, userId: UserId): State {
+fun getRoomMemberAsState(roomMembersState: MatrixRoomMembersState, userId: UserId): State {
val roomMembers = roomMembersState.roomMembers()
return remember(roomMembers) {
derivedStateOf {
diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts
index 2951ca0e25..725961a248 100644
--- a/libraries/push/impl/build.gradle.kts
+++ b/libraries/push/impl/build.gradle.kts
@@ -35,6 +35,7 @@ dependencies {
implementation(libs.androidx.security.crypto)
implementation(libs.network.retrofit)
implementation(libs.serialization.json)
+ implementation(libs.coil)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
@@ -42,6 +43,7 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.network)
implementation(projects.libraries.matrix.api)
+ implementation(projects.libraries.matrixui)
api(projects.libraries.pushproviders.api)
api(projects.libraries.pushstore.api)
api(projects.libraries.push.api)
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt
index 04d2875328..8bb1ddf20c 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt
@@ -115,8 +115,7 @@ class PushersManager @Inject constructor(
appDisplayName = appName,
deviceDisplayName = currentSession.sessionParams.deviceId ?: "MOBILE"
)
-
- */
+ */
}
fun getPusherForCurrentSession() {}/*: Pusher? {
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
index 1b1fe2723e..07c3a99708 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt
@@ -16,6 +16,7 @@
package io.element.android.libraries.push.impl.notifications
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@@ -49,7 +50,7 @@ class NotifiableEventProcessor @Inject constructor(
else -> ProcessedEvent.Type.KEEP
}
is SimpleNotifiableEvent -> when (it.type) {
- /*EventType.REDACTION*/ "m.room.redaction" -> ProcessedEvent.Type.REMOVE
+ EventType.REDACTION -> ProcessedEvent.Type.REMOVE
else -> ProcessedEvent.Type.KEEP
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
index 82e3e43d50..fb3fcfc61f 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt
@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.NotificationData
+import io.element.android.libraries.matrix.api.notification.NotificationEvent
import io.element.android.libraries.push.impl.log.pushLoggerTag
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@@ -44,9 +45,9 @@ class NotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider,
// private val noticeEventFormatter: NoticeEventFormatter,
// private val displayableEventFormatter: DisplayableEventFormatter,
- private val clock: SystemClock,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val buildMeta: BuildMeta,
+ private val clock: SystemClock,
) {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
@@ -71,44 +72,53 @@ class NotifiableEventResolver @Inject constructor(
return notificationData.asNotifiableEvent(sessionId)
}
-}
-private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent {
- return NotifiableMessageEvent(
- sessionId = userId,
- roomId = roomId,
- eventId = eventId,
- editedEventId = null,
- canBeReplaced = true,
- noisy = isNoisy,
- timestamp = System.currentTimeMillis(),
- senderName = senderDisplayName,
- senderId = senderId.value,
- body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
- imageUriString = null,
- threadId = null,
- roomName = null,
- roomIsDirect = false,
- roomAvatarPath = roomAvatarUrl,
- senderAvatarPath = senderAvatarUrl,
- soundName = null,
- outGoingMessage = false,
- outGoingMessageFailed = false,
- isRedacted = false,
- isUpdated = false
- )
-}
+ private fun NotificationData.asNotifiableEvent(userId: SessionId): NotifiableEvent {
+ return NotifiableMessageEvent(
+ sessionId = userId,
+ roomId = roomId,
+ eventId = eventId,
+ editedEventId = null,
+ canBeReplaced = true,
+ noisy = isNoisy,
+ timestamp = event.timestamp,
+ senderName = senderDisplayName,
+ senderId = senderId.value,
+ body = event.content,
+ imageUriString = event.contentUrl,
+ threadId = null,
+ roomName = roomDisplayName,
+ roomIsDirect = isDirect,
+ roomAvatarPath = roomAvatarUrl,
+ senderAvatarPath = senderAvatarUrl,
+ soundName = null,
+ outGoingMessage = false,
+ outGoingMessageFailed = false,
+ isRedacted = false,
+ isUpdated = false
+ )
+ }
-/**
- * TODO This is a temporary method for EAx.
- */
-private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
- return this ?: NotificationData(
- eventId = eventId,
- senderId = UserId("@user:domain"),
- roomId = roomId,
- isNoisy = false,
- isEncrypted = false,
- isDirect = false
- )
+ /**
+ * TODO This is a temporary method for EAx.
+ */
+ private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData {
+ return this ?: NotificationData(
+ eventId = eventId,
+ senderId = UserId("@user:domain"),
+ roomId = roomId,
+ senderAvatarUrl = null,
+ senderDisplayName = null,
+ roomAvatarUrl = null,
+ roomDisplayName = null,
+ isNoisy = false,
+ isEncrypted = false,
+ isDirect = false,
+ event = NotificationEvent(
+ timestamp = clock.epochMillis(),
+ content = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…",
+ contentUrl = null
+ )
+ )
+ }
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
index 7bd76f9f42..c2cdfc5677 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt
@@ -19,9 +19,14 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
-import androidx.annotation.WorkerThread
import androidx.core.graphics.drawable.IconCompat
+import androidx.core.graphics.drawable.toBitmap
+import coil.imageLoader
+import coil.request.ImageRequest
+import coil.transform.CircleCropTransformation
import io.element.android.libraries.di.ApplicationContext
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.ui.media.MediaRequestData
import timber.log.Timber
import javax.inject.Inject
@@ -31,30 +36,24 @@ class NotificationBitmapLoader @Inject constructor(
/**
* Get icon of a room.
+ * @param path mxc url
*/
- @WorkerThread
- fun getRoomBitmap(path: String?): Bitmap? {
+ suspend fun getRoomBitmap(path: String?): Bitmap? {
if (path == null) {
return null
}
return loadRoomBitmap(path)
}
- @WorkerThread
- private fun loadRoomBitmap(path: String): Bitmap? {
+ private suspend fun loadRoomBitmap(path: String): Bitmap? {
return try {
- null
- /* TODO Notification
- Glide.with(context)
- .asBitmap()
- .load(path)
- .format(DecodeFormat.PREFER_ARGB_8888)
- .signature(ObjectKey("room-icon-notification"))
- .submit()
- .get()
- */
- } catch (e: Exception) {
- Timber.e(e, "decodeFile failed")
+ val imageRequest = ImageRequest.Builder(context)
+ .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
+ .build()
+ val result = context.imageLoader.execute(imageRequest)
+ result.drawable?.toBitmap()
+ } catch (e: Throwable) {
+ Timber.e(e, "Unable to load room bitmap")
null
}
}
@@ -62,9 +61,9 @@ class NotificationBitmapLoader @Inject constructor(
/**
* Get icon of a user.
* Before Android P, this does nothing because the icon won't be used
+ * @param path mxc url
*/
- @WorkerThread
- fun getUserIcon(path: String?): IconCompat? {
+ suspend fun getUserIcon(path: String?): IconCompat? {
if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return null
}
@@ -72,23 +71,17 @@ class NotificationBitmapLoader @Inject constructor(
return loadUserIcon(path)
}
- @WorkerThread
- private fun loadUserIcon(path: String): IconCompat? {
+ private suspend fun loadUserIcon(path: String): IconCompat? {
return try {
- null
- /* TODO Notification
- val bitmap = Glide.with(context)
- .asBitmap()
- .load(path)
- .transform(CircleCrop())
- .format(DecodeFormat.PREFER_ARGB_8888)
- .signature(ObjectKey("user-icon-notification"))
- .submit()
- .get()
- IconCompat.createWithBitmap(bitmap)
- */
- } catch (e: Exception) {
- Timber.e(e, "decodeFile failed")
+ val imageRequest = ImageRequest.Builder(context)
+ .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024)))
+ .transformations(CircleCropTransformation())
+ .build()
+ val result = context.imageLoader.execute(imageRequest)
+ val bitmap = result.drawable?.toBitmap()
+ return bitmap?.let { IconCompat.createWithBitmap(it) }
+ } catch (e: Throwable) {
+ Timber.e(e, "Unable to load user bitmap")
null
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt
index cf0307fbd9..87d37e7e33 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt
@@ -16,28 +16,28 @@
package io.element.android.libraries.push.impl.notifications
-import android.content.Context
-import android.os.Handler
-import android.os.HandlerThread
-import androidx.annotation.WorkerThread
import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.core.cache.CircularCache
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
-import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
+import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.api.store.PushDataStore
-import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@@ -48,7 +48,6 @@ import javax.inject.Inject
*/
@SingleIn(AppScope::class)
class NotificationDrawerManager @Inject constructor(
- @ApplicationContext context: Context,
private val pushDataStore: PushDataStore,
private val notifiableEventProcessor: NotifiableEventProcessor,
private val notificationRenderer: NotificationRenderer,
@@ -56,17 +55,14 @@ class NotificationDrawerManager @Inject constructor(
private val filteredEventDetector: FilteredEventDetector,
private val appNavigationStateService: AppNavigationStateService,
private val coroutineScope: CoroutineScope,
+ private val dispatchers: CoroutineDispatchers,
private val buildMeta: BuildMeta,
+ private val matrixAuthenticationService: MatrixAuthenticationService,
) {
-
- private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY)
- private var backgroundHandler: Handler
-
/**
* Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events.
*/
private val notificationState by lazy { createInitialNotificationState() }
- private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
private var currentAppNavigationState: AppNavigationState? = null
private val firstThrottler = FirstThrottler(200)
@@ -74,8 +70,6 @@ class NotificationDrawerManager @Inject constructor(
private var useCompleteNotificationFormat = true
init {
- handlerThread.start()
- backgroundHandler = Handler(handlerThread.looper)
// Observe application state
coroutineScope.launch {
appNavigationStateService.appNavigationStateFlow
@@ -193,30 +187,25 @@ class NotificationDrawerManager @Inject constructor(
notificationState.updateQueuedEvents(this) { queuedEvents, _ ->
action(queuedEvents)
}
- refreshNotificationDrawer()
+ coroutineScope.refreshNotificationDrawer()
}
- private fun refreshNotificationDrawer() {
+ private fun CoroutineScope.refreshNotificationDrawer() = launch {
// Implement last throttler
val canHandle = firstThrottler.canHandle()
Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms")
- backgroundHandler.removeCallbacksAndMessages(null)
-
- backgroundHandler.postDelayed(
- {
- try {
- refreshNotificationDrawerBg()
- } catch (throwable: Throwable) {
- // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
- Timber.w(throwable, "refreshNotificationDrawerBg failure")
- }
- },
- canHandle.waitMillis()
- )
+ withContext(dispatchers.io) {
+ delay(canHandle.waitMillis())
+ try {
+ refreshNotificationDrawerBg()
+ } catch (throwable: Throwable) {
+ // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
+ Timber.w(throwable, "refreshNotificationDrawerBg failure")
+ }
+ }
}
- @WorkerThread
- private fun refreshNotificationDrawerBg() {
+ private suspend fun refreshNotificationDrawerBg() {
Timber.v("refreshNotificationDrawerBg()")
val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents ->
notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also {
@@ -239,24 +228,34 @@ class NotificationDrawerManager @Inject constructor(
}
}
- private fun renderEvents(eventsToRender: List>) {
+ private suspend fun renderEvents(eventsToRender: List>) {
// Group by sessionId
val eventsForSessions = eventsToRender.groupBy {
it.event.sessionId
}
eventsForSessions.forEach { (sessionId, notifiableEvents) ->
- // TODO EAx val user = session.getUserOrDefault(session.myUserId)
- // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
- val myUserDisplayName = "Todo display name" // user.toMatrixItem().getBestName()
- // TODO EAx avatar URL
- val myUserAvatarUrl = null // session.contentUrlResolver().resolveThumbnail(
- // contentUrl = user.avatarUrl,
- // width = avatarSize,
- // height = avatarSize,
- // method = ContentUrlResolver.ThumbnailMethod.SCALE
- //)
- notificationRenderer.render(sessionId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, notifiableEvents)
+ val currentUser = tryOrNull(
+ onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") },
+ operation = {
+ val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull()
+
+ // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
+ val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value
+ val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull()
+ MatrixUser(
+ userId = sessionId,
+ displayName = myUserDisplayName,
+ avatarUrl = userAvatarUrl
+ )
+ }
+ ) ?: MatrixUser(
+ userId = sessionId,
+ displayName = sessionId.value,
+ avatarUrl = null
+ )
+
+ notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents)
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
index 4bb49e168f..79173611dc 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt
@@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@@ -34,10 +34,8 @@ class NotificationFactory @Inject constructor(
private val summaryGroupMessageCreator: SummaryGroupMessageCreator
) {
- fun Map.toNotifications(
- sessionId: SessionId,
- myUserDisplayName: String,
- myUserAvatarUrl: String?
+ suspend fun Map.toNotifications(
+ currentUser: MatrixUser,
): List {
return map { (roomId, events) ->
when {
@@ -45,11 +43,9 @@ class NotificationFactory @Inject constructor(
else -> {
val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted }
roomGroupMessageCreator.createRoomMessage(
- sessionId = sessionId,
+ currentUser = currentUser,
events = messageEvents,
roomId = roomId,
- userDisplayName = myUserDisplayName,
- userAvatarUrl = myUserAvatarUrl
)
}
}
@@ -99,7 +95,7 @@ class NotificationFactory @Inject constructor(
}
fun createSummaryNotification(
- sessionId: SessionId,
+ currentUser: MatrixUser,
roomNotifications: List,
invitationNotifications: List,
simpleNotifications: List,
@@ -112,7 +108,7 @@ class NotificationFactory @Inject constructor(
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
else -> SummaryNotification.Update(
summaryGroupMessageCreator.createSummaryNotification(
- sessionId = sessionId,
+ currentUser = currentUser,
roomNotifications = roomMeta,
invitationNotifications = invitationMeta,
simpleNotifications = simpleMeta,
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
index 277dc3b822..428420211b 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt
@@ -16,9 +16,8 @@
package io.element.android.libraries.push.impl.notifications
-import androidx.annotation.WorkerThread
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
@@ -32,21 +31,18 @@ class NotificationRenderer @Inject constructor(
private val notificationFactory: NotificationFactory,
) {
- @WorkerThread
- fun render(
- sessionId: SessionId,
- myUserDisplayName: String,
- myUserAvatarUrl: String?,
+ suspend fun render(
+ currentUser: MatrixUser,
useCompleteNotificationFormat: Boolean,
eventsToProcess: List>
) {
val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType()
with(notificationFactory) {
- val roomNotifications = roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl)
+ val roomNotifications = roomEvents.toNotifications(currentUser)
val invitationNotifications = invitationEvents.toNotifications()
val simpleNotifications = simpleEvents.toNotifications()
val summaryNotification = createSummaryNotification(
- sessionId = sessionId,
+ currentUser = currentUser,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
simpleNotifications = simpleNotifications,
@@ -56,21 +52,27 @@ class NotificationRenderer @Inject constructor(
// Remove summary first to avoid briefly displaying it after dismissing the last notification
if (summaryNotification == SummaryNotification.Removed) {
Timber.d("Removing summary notification")
- notificationDisplayer.cancelNotificationMessage(null, notificationIdProvider.getSummaryNotificationId(sessionId))
+ notificationDisplayer.cancelNotificationMessage(
+ tag = null,
+ id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
+ )
}
roomNotifications.forEach { wrapper ->
when (wrapper) {
is RoomNotification.Removed -> {
Timber.d("Removing room messages notification ${wrapper.roomId}")
- notificationDisplayer.cancelNotificationMessage(wrapper.roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
+ notificationDisplayer.cancelNotificationMessage(
+ tag = wrapper.roomId.value,
+ id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId)
+ )
}
is RoomNotification.Message -> if (useCompleteNotificationFormat) {
Timber.d("Updating room messages notification ${wrapper.meta.roomId}")
notificationDisplayer.showNotificationMessage(
- wrapper.meta.roomId.value,
- notificationIdProvider.getRoomMessagesNotificationId(sessionId),
- wrapper.notification
+ tag = wrapper.meta.roomId.value,
+ id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
+ notification = wrapper.notification
)
}
}
@@ -80,14 +82,17 @@ class NotificationRenderer @Inject constructor(
when (wrapper) {
is OneShotNotification.Removed -> {
Timber.d("Removing invitation notification ${wrapper.key}")
- notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomInvitationNotificationId(sessionId))
+ notificationDisplayer.cancelNotificationMessage(
+ tag = wrapper.key,
+ id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId)
+ )
}
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
Timber.d("Updating invitation notification ${wrapper.meta.key}")
notificationDisplayer.showNotificationMessage(
- wrapper.meta.key,
- notificationIdProvider.getRoomInvitationNotificationId(sessionId),
- wrapper.notification
+ tag = wrapper.meta.key,
+ id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
+ notification = wrapper.notification
)
}
}
@@ -97,14 +102,17 @@ class NotificationRenderer @Inject constructor(
when (wrapper) {
is OneShotNotification.Removed -> {
Timber.d("Removing simple notification ${wrapper.key}")
- notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomEventNotificationId(sessionId))
+ notificationDisplayer.cancelNotificationMessage(
+ tag = wrapper.key,
+ id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId)
+ )
}
is OneShotNotification.Append -> if (useCompleteNotificationFormat) {
Timber.d("Updating simple notification ${wrapper.meta.key}")
notificationDisplayer.showNotificationMessage(
- wrapper.meta.key,
- notificationIdProvider.getRoomEventNotificationId(sessionId),
- wrapper.notification
+ tag = wrapper.meta.key,
+ id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
+ notification = wrapper.notification
)
}
}
@@ -114,9 +122,9 @@ class NotificationRenderer @Inject constructor(
if (summaryNotification is SummaryNotification.Update) {
Timber.d("Updating summary notification")
notificationDisplayer.showNotificationMessage(
- null,
- notificationIdProvider.getSummaryNotificationId(sessionId),
- summaryNotification.notification
+ tag = null,
+ id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),
+ notification = summaryNotification.notification
)
}
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
index 00222728bf..5656b81dd9 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt
@@ -20,8 +20,9 @@ import android.graphics.Bitmap
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R
+import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.services.toolbox.api.strings.StringProvider
@@ -36,24 +37,22 @@ class RoomGroupMessageCreator @Inject constructor(
private val notificationFactory: NotificationFactory
) {
- fun createRoomMessage(
- sessionId: SessionId,
+ suspend fun createRoomMessage(
+ currentUser: MatrixUser,
events: List,
roomId: RoomId,
- userDisplayName: String,
- userAvatarUrl: String?
): RoomNotification.Message {
val lastKnownRoomEvent = events.last()
val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)"
val roomIsGroup = !lastKnownRoomEvent.roomIsDirect
val style = NotificationCompat.MessagingStyle(
Person.Builder()
- .setName(userDisplayName)
- .setIcon(bitmapLoader.getUserIcon(userAvatarUrl))
+ .setName(currentUser.displayName?.annotateForDebug(50))
+ .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl))
.setKey(lastKnownRoomEvent.sessionId.value)
.build()
).also {
- it.conversationTitle = roomName.takeIf { roomIsGroup }
+ it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51)
it.isGroupConversation = roomIsGroup
it.addMessagesFromEvents(events)
}
@@ -80,7 +79,7 @@ class RoomGroupMessageCreator @Inject constructor(
notificationFactory.createMessagesListNotification(
style,
RoomEventGroupInfo(
- sessionId = sessionId,
+ sessionId = currentUser.userId,
roomId = roomId,
roomDisplayName = roomName,
isDirect = !roomIsGroup,
@@ -99,13 +98,13 @@ class RoomGroupMessageCreator @Inject constructor(
)
}
- private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) {
+ private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) {
events.forEach { event ->
val senderPerson = if (event.outGoingMessage) {
null
} else {
Person.Builder()
- .setName(event.senderName)
+ .setName(event.senderName?.annotateForDebug(70))
.setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath))
.setKey(event.senderId)
.build()
@@ -117,7 +116,11 @@ class RoomGroupMessageCreator @Inject constructor(
senderPerson
)
else -> {
- val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message ->
+ val message = NotificationCompat.MessagingStyle.Message(
+ event.body?.annotateForDebug(71),
+ event.timestamp,
+ senderPerson
+ ).also { message ->
event.imageUri?.let {
message.setData("image/", it)
}
@@ -168,7 +171,7 @@ class RoomGroupMessageCreator @Inject constructor(
}
}
- private fun getRoomBitmap(events: List): Bitmap? {
+ private suspend fun getRoomBitmap(events: List): Bitmap? {
// Use the last event (most recent?)
return events.lastOrNull()
?.roomAvatarPath
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt
index a400c2b7a3..5a7f3d36e8 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt
@@ -18,8 +18,9 @@ package io.element.android.libraries.push.impl.notifications
import android.app.Notification
import androidx.core.app.NotificationCompat
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R
+import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory
import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject
@@ -40,20 +41,20 @@ import javax.inject.Inject
*/
class SummaryGroupMessageCreator @Inject constructor(
private val stringProvider: StringProvider,
- private val notificationFactory: NotificationFactory
+ private val notificationFactory: NotificationFactory,
) {
fun createSummaryNotification(
- sessionId: SessionId,
+ currentUser: MatrixUser,
roomNotifications: List,
invitationNotifications: List,
simpleNotifications: List,
useCompleteNotificationFormat: Boolean
): Notification {
val summaryInboxStyle = NotificationCompat.InboxStyle().also { style ->
- roomNotifications.forEach { style.addLine(it.summaryLine) }
- invitationNotifications.forEach { style.addLine(it.summaryLine) }
- simpleNotifications.forEach { style.addLine(it.summaryLine) }
+ roomNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(40)) }
+ invitationNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(41)) }
+ simpleNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(42)) }
}
val summaryIsNoisy = roomNotifications.any { it.shouldBing } ||
@@ -69,12 +70,13 @@ class SummaryGroupMessageCreator @Inject constructor(
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms
val nbEvents = roomNotifications.size + simpleNotifications.size
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
- summaryInboxStyle.setBigContentTitle(sumTitle)
- // TODO get latest event?
- .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
+ summaryInboxStyle.setBigContentTitle(sumTitle.annotateForDebug(43))
+ //.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents).annotateForDebug(44))
+ // Use account name now, for multi-session
+ .setSummaryText(currentUser.userId.value.annotateForDebug(44))
return if (useCompleteNotificationFormat) {
notificationFactory.createSummaryListNotification(
- sessionId,
+ currentUser,
summaryInboxStyle,
sumTitle,
noisy = summaryIsNoisy,
@@ -82,7 +84,7 @@ class SummaryGroupMessageCreator @Inject constructor(
)
} else {
processSimpleGroupSummary(
- sessionId,
+ currentUser,
summaryIsNoisy,
messageCount,
simpleNotifications.size,
@@ -94,7 +96,7 @@ class SummaryGroupMessageCreator @Inject constructor(
}
private fun processSimpleGroupSummary(
- sessionId: SessionId,
+ currentUser: MatrixUser,
summaryIsNoisy: Boolean,
messageEventsCount: Int,
simpleEventsCount: Int,
@@ -167,7 +169,7 @@ class SummaryGroupMessageCreator @Inject constructor(
}
}
return notificationFactory.createSummaryListNotification(
- sessionId = sessionId,
+ currentUser = currentUser,
style = null,
compatSummary = privacyTitle,
noisy = summaryIsNoisy,
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt
new file mode 100644
index 0000000000..37f33e1188
--- /dev/null
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.push.impl.notifications.debug
+
+fun CharSequence.annotateForDebug(@Suppress("UNUSED_PARAMETER") prefix: Int): CharSequence {
+ return this // "$prefix-$this"
+}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt
index 5795ea5f5f..9da47a6569 100755
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt
@@ -26,11 +26,12 @@ import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.ApplicationContext
-import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
+import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory
import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory
@@ -84,16 +85,16 @@ class NotificationFactory @Inject constructor(
// ID of the corresponding shortcut, for conversation features under API 30+
.setShortcutId(roomInfo.roomId.value)
// Title for API < 16 devices.
- .setContentTitle(roomInfo.roomDisplayName)
+ .setContentTitle(roomInfo.roomDisplayName.annotateForDebug(1))
// Content for API < 16 devices.
- .setContentText(stringProvider.getString(R.string.notification_new_messages))
+ .setContentText(stringProvider.getString(R.string.notification_new_messages).annotateForDebug(2))
// Number of new notifications for API <24 (M and below) devices.
.setSubText(
stringProvider.getQuantityString(
R.plurals.notification_new_messages_for_room,
messageStyle.messages.size,
messageStyle.messages.size
- )
+ ).annotateForDebug(3)
)
// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID
@@ -135,7 +136,7 @@ class NotificationFactory @Inject constructor(
}
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
}
- .setTicker(tickerText)
+ .setTicker(tickerText.annotateForDebug(4))
.build()
}
@@ -147,8 +148,8 @@ class NotificationFactory @Inject constructor(
val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true)
- .setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName)
- .setContentText(inviteNotifiableEvent.description)
+ .setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5))
+ .setContentText(inviteNotifiableEvent.description.annotateForDebug(6))
.setGroup(inviteNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
@@ -196,8 +197,8 @@ class NotificationFactory @Inject constructor(
val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy)
return NotificationCompat.Builder(context, channelId)
.setOnlyAlertOnce(true)
- .setContentTitle(buildMeta.applicationName)
- .setContentText(simpleNotifiableEvent.description)
+ .setContentTitle(buildMeta.applicationName.annotateForDebug(7))
+ .setContentText(simpleNotifiableEvent.description.annotateForDebug(8))
.setGroup(simpleNotifiableEvent.sessionId.value)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL)
.setSmallIcon(smallIcon)
@@ -226,7 +227,7 @@ class NotificationFactory @Inject constructor(
* Create the summary notification.
*/
fun createSummaryListNotification(
- sessionId: SessionId,
+ currentUser: MatrixUser,
style: NotificationCompat.InboxStyle?,
compatSummary: String,
noisy: Boolean,
@@ -240,12 +241,12 @@ class NotificationFactory @Inject constructor(
// used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp)
.setStyle(style)
- .setContentTitle(sessionId.value)
+ .setContentTitle(currentUser.userId.value.annotateForDebug(9))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon)
// set content text to support devices running API level < 24
- .setContentText(compatSummary)
- .setGroup(sessionId.value)
+ .setContentText(compatSummary.annotateForDebug(10))
+ .setGroup(currentUser.userId.value)
// set this notification as the summary for the group
.setGroupSummary(true)
.setColor(accentColor)
@@ -264,8 +265,8 @@ class NotificationFactory @Inject constructor(
priority = NotificationCompat.PRIORITY_LOW
}
}
- .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId))
- .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId))
+ .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId))
+ .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId))
.build()
}
diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
index add172e44f..1216e0fe12 100644
--- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
+++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.services.appnavstate.api.AppNavigationState
import io.element.android.services.appnavstate.api.currentRoomId
import io.element.android.services.appnavstate.api.currentSessionId
@@ -52,10 +53,13 @@ data class NotifiableMessageEvent(
override val isUpdated: Boolean = false
) : NotifiableEvent {
- val type: String = /* EventType.MESSAGE */ "m.room.message"
+ val type: String = EventType.MESSAGE
val description: String = body ?: ""
val title: String = senderName ?: ""
+ // TODO EAx The image has to be downloaded and expose using the file provider.
+ // Example of value from Element Android:
+ // content://im.vector.app.debug.mx-sdk.fileprovider/downloads/downloads/816abf76d806c768760568952b1862c8/F/72c33edd23dee3b95f4d5a18aa25fa54/image.png
val imageUri: Uri?
get() = imageUriString?.let { Uri.parse(it) }
}
diff --git a/libraries/push/impl/src/main/res/values-cs/translations.xml b/libraries/push/impl/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..90806b854c
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-cs/translations.xml
@@ -0,0 +1,28 @@
+
+
+ "Hovor"
+ "Naslouchání událostem"
+ "Hlasitá oznámení"
+ "Tichá oznámení"
+ "Vstoupit"
+ "Odmítnout"
+ "Nové zprávy"
+ "Označit jako přečtené"
+ "Já"
+ "Prohlížíte si oznámení! Klikněte na mě!"
+ "%1$s: %2$s"
+ "%1$s: %2$s %3$s"
+ "%1$s a %2$s"
+ "%1$s in %2$s"
+ "%1$s v %2$s a %3$s"
+
+ - "%1$s: %2$d zpráva"
+ - "%1$s: %2$d zprávy"
+ - "%1$s: %2$d zpráv"
+
+ "Vyberte, jak chcete přijímat oznámení"
+ "Synchronizace na pozadí"
+ "Služby Google"
+ "Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně."
+ "Rychlá odpověď"
+
\ No newline at end of file
diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml
index d1ce970c3c..be957bf68a 100644
--- a/libraries/push/impl/src/main/res/values-de/translations.xml
+++ b/libraries/push/impl/src/main/res/values-de/translations.xml
@@ -1,10 +1,16 @@
+ "Anruf"
+ "Warte auf Ereignisse"
"Laute Benachrichtigungen"
+ "Stumme Benachrichtigungen"
+ "** Senden fehlgeschlagen - bitte Raum öffnen"
"Beitreten"
"Ablehnen"
"Neue Nachrichten"
"Als gelesen markieren"
+ "Ich"
+ "Du siehst die Benachrichtigung an! Klick mich an!"
"%1$s: %2$s"
"%1$s: %2$s %3$s"
"%1$s und %2$s"
@@ -26,10 +32,16 @@
- "%d neue Nachricht"
- "%d neue Nachrichten"
+
+ - "%d ungelesene benachrichtigte Nachricht"
+ - "%d ungelesene benachrichtigte Nachrichten"
+
- "%d Raum"
- "%d Räume"
+ "Auswählen, wie Benachrichtigungen empfangen werden sollen"
+ "Hintergrundsynchronisation"
"Google-Dienste"
"Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig."
"Schnellantwort"
diff --git a/libraries/push/impl/src/main/res/values-fr/translations.xml b/libraries/push/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..3d40a01065
--- /dev/null
+++ b/libraries/push/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,48 @@
+
+
+ "Appel"
+ "À l\'écoute d\'événements"
+ "Notifications bruyantes"
+ "Notifications silencieuses"
+ "** Échec de l\'envoi - veuillez ouvrir la salle"
+ "Rejoindre"
+ "Refuser"
+ "Nouveaux messages"
+ "Marquer comme lu"
+ "Moi"
+ "Vous êtes en train de consulter la notification ! Cliquez-moi !"
+ "%1$s: %2$s"
+ "%1$s: %2$s %3$s"
+ "%1$s et %2$s"
+ "%1$s dans %2$s"
+ "%1$s dans %2$s et %3$s"
+
+ - "%1$s: %2$d message"
+ - "%1$s: %2$d messages"
+
+
+ - "%d notification"
+ - "%d notifications"
+
+
+ - "%d invitation"
+ - "%d invitations"
+
+
+ - "%d nouveau message"
+ - "%d nouveaux messages"
+
+
+ - "%d message notifié non lu"
+ - "%d messages notifiés non lus"
+
+
+ - "%d conversation"
+ - "%d conversations"
+
+ "Choisissez comment recevoir les notifications"
+ "Synchronisation en arrière-plan"
+ "Services Google"
+ "Aucun service Google Play valide n\'a été trouvé. Les notifications peuvent ne pas fonctionner correctement."
+ "Réponse rapide"
+
\ No newline at end of file
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt
index 80246abb14..38f2edd476 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt
@@ -17,6 +17,7 @@
package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -60,7 +61,7 @@ class NotifiableEventProcessorTest {
@Test
fun `given redacted simple event when processing then remove redaction event`() {
- val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = "m.room.redaction"))
+ val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = EventType.REDACTION))
val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList())
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt
index 9fbd723071..18d8870ac3 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt
@@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
+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_SESSION_ID
@@ -27,6 +28,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou
import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent
import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent
import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent
+import kotlinx.coroutines.test.runTest
import org.junit.Test
private val MY_AVATAR_URL: String? = null
@@ -124,11 +126,13 @@ class NotificationFactoryTest {
fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) {
val events = listOf(A_MESSAGE_EVENT)
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
- A_SESSION_ID, events, A_ROOM_ID, A_SESSION_ID.value, MY_AVATAR_URL
+ MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), events, A_ROOM_ID
)
val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT)))
- val result = roomWithMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ val result = roomWithMessage.toNotifications(
+ MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ )
assertThat(result).isEqualTo(listOf(expectedNotification))
}
@@ -138,7 +142,9 @@ class NotificationFactoryTest {
val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT))
val emptyRoom = mapOf(A_ROOM_ID to events)
- val result = emptyRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ val result = emptyRoom.toNotifications(
+ MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ )
assertThat(result).isEqualTo(
listOf(
@@ -153,7 +159,9 @@ class NotificationFactoryTest {
fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) {
val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true))))
- val result = redactedRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ val result = redactedRoom.toNotifications(
+ MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ )
assertThat(result).isEqualTo(
listOf(
@@ -176,19 +184,21 @@ class NotificationFactoryTest {
)
val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted")))
val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor(
- A_SESSION_ID,
+ MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL),
withRedactedRemoved,
A_ROOM_ID,
- A_SESSION_ID.value,
- MY_AVATAR_URL
)
- val result = roomWithRedactedMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ val result = roomWithRedactedMessage.toNotifications(
+ MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL)
+ )
assertThat(result).isEqualTo(listOf(expectedNotification))
}
}
-fun testWith(receiver: T, block: T.() -> Unit) {
- receiver.block()
+fun testWith(receiver: T, block: suspend T.() -> Unit) {
+ runTest {
+ receiver.block()
+ }
}
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
index 79c6dfdb02..c109edb40a 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt
@@ -17,6 +17,7 @@
package io.element.android.libraries.push.impl.notifications
import android.app.Notification
+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_SESSION_ID
@@ -24,6 +25,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio
import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
import org.junit.Test
private const val MY_USER_DISPLAY_NAME = "display-name"
@@ -53,7 +55,7 @@ class NotificationRendererTest {
)
@Test
- fun `given no notifications when rendering then cancels summary notification`() {
+ fun `given no notifications when rendering then cancels summary notification`() = runTest {
givenNoNotifications()
renderEventsAsNotifications()
@@ -63,7 +65,7 @@ class NotificationRendererTest {
}
@Test
- fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() {
+ fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() = runTest {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
@@ -75,7 +77,7 @@ class NotificationRendererTest {
}
@Test
- fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() {
+ fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() = runTest {
givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)))
renderEventsAsNotifications()
@@ -87,7 +89,7 @@ class NotificationRendererTest {
}
@Test
- fun `given a room message group notification is added when rendering then show the message notification and update summary`() {
+ fun `given a room message group notification is added when rendering then show the message notification and update summary`() = runTest {
givenNotifications(
roomNotifications = listOf(
RoomNotification.Message(
@@ -106,7 +108,7 @@ class NotificationRendererTest {
}
@Test
- fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() {
+ fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() = runTest {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
@@ -118,7 +120,7 @@ class NotificationRendererTest {
}
@Test
- fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() {
+ fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() = runTest {
givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)))
renderEventsAsNotifications()
@@ -130,7 +132,7 @@ class NotificationRendererTest {
}
@Test
- fun `given a simple notification is added when rendering then show the simple notification and update summary`() {
+ fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest {
givenNotifications(
simpleNotifications = listOf(
OneShotNotification.Append(
@@ -149,7 +151,7 @@ class NotificationRendererTest {
}
@Test
- fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() {
+ fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() = runTest {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION)
renderEventsAsNotifications()
@@ -161,7 +163,7 @@ class NotificationRendererTest {
}
@Test
- fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() {
+ fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() = runTest {
givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)))
renderEventsAsNotifications()
@@ -173,7 +175,7 @@ class NotificationRendererTest {
}
@Test
- fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() {
+ fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest {
givenNotifications(
simpleNotifications = listOf(
OneShotNotification.Append(
@@ -191,11 +193,9 @@ class NotificationRendererTest {
}
}
- private fun renderEventsAsNotifications() {
+ private suspend fun renderEventsAsNotifications() {
notificationRenderer.render(
- sessionId = A_SESSION_ID,
- myUserDisplayName = MY_USER_DISPLAY_NAME,
- myUserAvatarUrl = MY_USER_AVATAR_URL,
+ MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT,
eventsToProcess = AN_EVENT_LIST
)
@@ -214,9 +214,7 @@ class NotificationRendererTest {
) {
notificationFactory.givenNotificationsFor(
groupedEvents = A_PROCESSED_EVENTS,
- sessionId = A_SESSION_ID,
- myUserDisplayName = MY_USER_DISPLAY_NAME,
- myUserAvatarUrl = MY_USER_AVATAR_URL,
+ matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL),
useCompleteNotificationFormat = useCompleteNotificationFormat,
roomNotifications = roomNotifications,
invitationNotifications = invitationNotifications,
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
index 7d7812e6cb..09957e2cf2 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt
@@ -16,12 +16,13 @@
package io.element.android.libraries.push.impl.notifications.fake
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.GroupedNotificationEvents
import io.element.android.libraries.push.impl.notifications.NotificationFactory
import io.element.android.libraries.push.impl.notifications.OneShotNotification
import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.SummaryNotification
+import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
@@ -30,9 +31,7 @@ class FakeNotificationFactory {
fun givenNotificationsFor(
groupedEvents: GroupedNotificationEvents,
- sessionId: SessionId,
- myUserDisplayName: String,
- myUserAvatarUrl: String?,
+ matrixUser: MatrixUser,
useCompleteNotificationFormat: Boolean,
roomNotifications: List,
invitationNotifications: List,
@@ -40,13 +39,13 @@ class FakeNotificationFactory {
summaryNotification: SummaryNotification
) {
with(instance) {
- every { groupedEvents.roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) } returns roomNotifications
+ coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications
every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications
every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications
every {
createSummaryNotification(
- sessionId,
+ matrixUser,
roomNotifications,
invitationNotifications,
simpleNotifications,
diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt
index df0b5ad42b..b896737e6f 100644
--- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt
+++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt
@@ -17,11 +17,11 @@
package io.element.android.libraries.push.impl.notifications.fake
import io.element.android.libraries.matrix.api.core.RoomId
-import io.element.android.libraries.matrix.api.core.SessionId
+import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator
import io.element.android.libraries.push.impl.notifications.RoomNotification
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
-import io.mockk.every
+import io.mockk.coEvery
import io.mockk.mockk
class FakeRoomGroupMessageCreator {
@@ -29,14 +29,18 @@ class FakeRoomGroupMessageCreator {
val instance = mockk()
fun givenCreatesRoomMessageFor(
- sessionId: SessionId,
+ matrixUser: MatrixUser,
events: List,
roomId: RoomId,
- userDisplayName: String,
- userAvatarUrl: String?
): RoomNotification.Message {
val mockMessage = mockk()
- every { instance.createRoomMessage(sessionId, events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage
+ coEvery {
+ instance.createRoomMessage(
+ currentUser = matrixUser,
+ events = events,
+ roomId = roomId,
+ )
+ } returns mockMessage
return mockMessage
}
}
diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt
index 052943388e..323aa93bb7 100644
--- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt
+++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt
@@ -34,10 +34,10 @@ object SessionStorageModule {
@SingleIn(AppScope::class)
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
val name = "session_database"
- val secretFile = context.getDatabasePath("$name.key")
+ val secretFile = context.getDatabasePath("${name}.key")
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
val driver = SqlCipherDriverFactory(passphraseProvider)
- .create(SessionDatabase.Schema, "$name.db", context)
+ .create(SessionDatabase.Schema, "${name}.db", context)
return SessionDatabase(driver)
}
}
diff --git a/libraries/textcomposer/src/main/res/values-cs/translations.xml b/libraries/textcomposer/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..85f2a46312
--- /dev/null
+++ b/libraries/textcomposer/src/main/res/values-cs/translations.xml
@@ -0,0 +1,4 @@
+
+
+ "Zpráva…"
+
\ No newline at end of file
diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml
index f016d4bdba..a9f1b3366e 100644
--- a/libraries/textcomposer/src/main/res/values-de/translations.xml
+++ b/libraries/textcomposer/src/main/res/values-de/translations.xml
@@ -1,5 +1,17 @@
+ "Aufzählungsliste ein-/ausschalten"
+ "Codeblock umschalten"
"Nachricht…"
+ "Fettformatierung anwenden"
+ "Kursivformat anwenden"
+ "Durchgestrichenes Format anwenden"
+ "Unterstreichungsformat anwenden"
+ "Vollbildmodus umschalten"
+ "Einrücken"
+ "Inline-Codeformat anwenden"
"Link setzen"
+ "Nummerierte Liste ein-/ausschalten"
+ "Zitat umschalten"
+ "Einrücken aufheben"
\ No newline at end of file
diff --git a/libraries/textcomposer/src/main/res/values-fr/translations.xml b/libraries/textcomposer/src/main/res/values-fr/translations.xml
new file mode 100644
index 0000000000..03143f5059
--- /dev/null
+++ b/libraries/textcomposer/src/main/res/values-fr/translations.xml
@@ -0,0 +1,17 @@
+
+
+ "Afficher une liste à puces"
+ "Afficher le bloc de code"
+ "Envoyer un message…"
+ "Appliquer le format gras"
+ "Appliquer le format italique"
+ "Appliquer le format barré"
+ "Appliquer le format souligné"
+ "Afficher en mode plein écran"
+ "Décaler vers la droite"
+ "Appliquer le formatage de code en ligne"
+ "Définir un lien"
+ "Afficher une liste numérotée"
+ "Afficher une citation"
+ "Décaler vers la gauche"
+
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml
new file mode 100644
index 0000000000..efa4d006d7
--- /dev/null
+++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml
@@ -0,0 +1,143 @@
+
+
+ "Skrýt heslo"
+ "Odeslat soubory"
+ "Zobrazit heslo"
+ "Uživatelské menu"
+ "Přijmout"
+ "Zpět"
+ "Zrušit"
+ "Vybrat fotku"
+ "Vymazat"
+ "Zavřít"
+ "Dokončit ověření"
+ "Potvrdit"
+ "Pokračovat"
+ "Kopírovat"
+ "Kopírovat odkaz"
+ "Vytvořit"
+ "Vytvořit místnost"
+ "Odmítnout"
+ "Zakázat"
+ "Hotovo"
+ "Upravit"
+ "Povolit"
+ "Pozvat"
+ "Pozvat přátele do %1$s"
+ "Pozvánky"
+ "Zjistit více"
+ "Odejít"
+ "Opustit místnost"
+ "Další"
+ "Ne"
+ "Teď ne"
+ "OK"
+ "Rychlá odpověď"
+ "Citovat"
+ "Odstranit"
+ "Odpovědět"
+ "Nahlásit chybu"
+ "Nahlásit obsah"
+ "Zkusit znovu"
+ "Opakovat dešifrování"
+ "Uložit"
+ "Hledat"
+ "Odeslat"
+ "Odeslat zprávu"
+ "Sdílet"
+ "Sdílet odkaz"
+ "Přeskočit"
+ "Začít"
+ "Zahájit chat"
+ "Zahájit ověření"
+ "Vyfotit"
+ "Zobrazit zdroj"
+ "Ano"
+ "O aplikaci"
+ "Analytika"
+ "Zvuk"
+ "Bubliny"
+ "Vytváření místnosti…"
+ "Opustit místnost"
+ "Chyba dešifrování"
+ "Možnosti pro vývojáře"
+ "(upraveno)"
+ "Úpravy"
+ "* %1$s %2$s"
+ "Šifrování povoleno"
+ "Chyba"
+ "Soubor"
+ "GIF"
+ "Obrázek"
+ "Odkaz zkopírován do schránky"
+ "Načítání…"
+ "Zpráva"
+ "Rozložení zprávy"
+ "Zpráva byla odstraněna"
+ "Moderní"
+ "Žádné výsledky"
+ "Offline"
+ "Heslo"
+ "Lidé"
+ "Trvalý odkaz"
+ "Reakce"
+ "Odpověď na %1$s"
+ "Nahlásit chybu"
+ "Zpráva odeslána"
+ "Název místnosti"
+ "Hledat někoho"
+ "Výsledky hledání"
+ "Zabezpečení"
+ "Vyberte svůj server"
+ "Odesílání…"
+ "Server není podporován"
+ "URL serveru"
+ "Nastavení"
+ "Nálepka"
+ "Úspěch"
+ "Návrhy"
+ "Téma"
+ "Nelze dešifrovat"
+ "Nepodporovaná událost"
+ "Uživatelské jméno"
+ "Ověření zrušeno"
+ "Ověření dokončeno"
+ "Video"
+ "Čekání…"
+ "Potvrzení"
+ "Upozornění"
+ "Aktivity"
+ "Vlajky"
+ "Jídlo a nápoje"
+ "Zvířata a příroda"
+ "Předměty"
+ "Smajlíci a lidé"
+ "Cestování a místa"
+ "Symboly"
+ "Vytvoření trvalého odkazu se nezdařilo"
+ "Načítání zpráv se nezdařilo"
+ "Některé zprávy nebyly odeslány"
+ "Omlouváme se, došlo k chybě"
+ "Ahoj, ozvi se mi na %1$s: %2$s"
+ "Opravdu chcete opustit tuto místnost? Jste tu jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás."
+ "Opravdu chcete opustit tuto místnost? Tato místnost není veřejná a bez pozvánky se nebudete moci znovu připojit."
+ "Opravdu chcete opustit místnost?"
+ "%1$s Android"
+
+ - "%1$d člen"
+ - "%1$d členové"
+ - "%1$d členů"
+
+ "Tato zpráva bude nahlášena správci vašeho domovského serveru. Nebude si moci přečíst žádné šifrované zprávy."
+ "Důvod nahlášení tohoto obsahu"
+ "Toto je začátek %1$s."
+ "Toto je začátek této konverzace."
+ "Nové"
+ "Rageshake"
+ "Obecné"
+ "Verze: %1$s (%2$s)"
+ "en"
+ "Chyba"
+ "Úspěch"
+ "Zablokovat uživatele"
+
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml
index 2fb21016e5..2a4558f756 100644
--- a/libraries/ui-strings/src/main/res/values-de/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-de/translations.xml
@@ -4,15 +4,19 @@
"Dateien senden"
"Passwort anzeigen"
"Benutzermenü"
+ "Zustimmen"
"Zurück"
"Abbrechen"
"Foto auswählen"
+ "Löschen"
"Schließen"
"Verifizierung abschließen"
"Bestätigen"
+ "Weiter"
"Kopieren"
"Link kopieren"
"Erstellen"
+ "Raum erstellen"
"Ablehnen"
"Deaktivieren"
"Fertig"
@@ -31,6 +35,7 @@
"Schnellantwort"
"Zitieren"
"Entfernen"
+ "Antworten"
"Fehler melden"
"Inhalt melden"
"Erneut versuchen"
@@ -42,31 +47,50 @@
"Teilen"
"Link teilen"
"Überspringen"
+ "Starten"
"Chat starten"
+ "Verifizierung starten"
"Foto aufnehmen"
+ "Quelltext anzeigen"
"Ja"
"Über"
"Analyse"
"Audio"
"Blasen"
+ "Erstelle Raum…"
+ "Raum verlassen"
"Entschlüsselungsfehler"
"Entwickleroptionen"
"(bearbeitet)"
+ "Bearbeiten"
"Verschlüsselung aktiviert"
"Fehler"
"Datei"
"GIF"
"Bild"
"Link in Zwischenablage kopiert"
+ "Wird geladen…"
"Nachricht"
+ "Nachrichtenlayout"
+ "Nachricht wurde entfernt"
"Modern"
+ "Keine Ergebnisse"
"Offline"
"Passwort"
+ "Personen"
+ "Permalink"
"Reaktionen"
- "Fehler melden"
+ "Auf %1$s antworten"
+ "Melde einen Fehler"
+ "Bericht gesendet"
+ "Raumname"
+ "Suche nach jemandem"
"Suchergebnisse"
"Sicherheit"
+ "Wählen deinen Server"
+ "Senden…"
"Server wird nicht unterstützt"
+ "Server-URL"
"Einstellungen"
"Chat wird gestartet…"
"Sticker"
@@ -80,6 +104,7 @@
"Verifizierung abgeschlossen"
"Video"
"Warten…"
+ "Bestätigung"
"Warnung"
"Aktivitäten"
"Flaggen"
@@ -89,26 +114,45 @@
"Smileys & Personen"
"Reisen & Orte"
"Symbole"
+ "Fehler beim Erstellen des Permalinks"
"Fehler beim Laden der Nachrichten"
"Einige Nachrichten wurden nicht gesendet"
"Entschuldigung, ein Fehler ist aufgetreten."
+ "Hey, sprich mit mir auf %1$s: %2$s"
+ "Bist du sicher, dass du diesen Raum verlassen willst? Du bist die einzige Person hier. Wenn du gehst, kann in Zukunft niemand mehr beitreten, auch du nicht."
+ "Bist du dir sicher, dass du den Raum verlassen möchtest? Dieser Raum ist nicht öffentlich und du kannst ihm ohne eine Einladung nicht mehr beitreten."
+ "Bist du dir sicher, dass du den Raum verlassen möchtest?"
"%1$s Android"
- "%1$d Mitglied"
- "%1$d Mitglieder"
+ "Rageshake zum Melden von Fehlern"
+ "Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?"
+ "Diese Nachricht wird an deinen Heimserver-Admin gemeldet werden. Er wird nicht in der Lage sein, verschlüsselte Nachrichten zu lesen."
"Grund für die Meldung dieses Inhalts"
"Dies ist der Anfang von %1$s."
+ "Dies ist der Beginn dieser Konversation."
"Neu"
"Wir erfassen und analysieren ""keine"" Account-Daten"
+ "Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben."
+ "Sie können alle unsere Nutzerbedingungen %1$s lesen."
+ "hier"
"Sie können die Analyse jederzeit in den Einstellungen deaktivieren"
"Wir geben ""keine"" Informationen an Dritte weiter"
+ "Helfen Sie %1$s zu verbessern"
"Teile Analyse-Daten"
"Medienauswahl fehlgeschlagen, bitte versuche es erneut."
+ "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest"
+ "Rageshake"
"Erkennungsschwelle"
+ "Allgemein"
"Version: %1$s (%2$s)"
"de"
"Fehler"
"Erfolg"
+ "Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben."
+ "Sie können alle unsere Nutzerbedingungen %1$s lesen."
+ "hier"
"Nutzer blockieren"
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml
index de1a80a9f5..76111046f8 100644
--- a/libraries/ui-strings/src/main/res/values-fr/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml
@@ -4,13 +4,140 @@
"Envoyer des fichiers"
"Afficher le mot de passe"
"Menu utilisateur"
+ "Accepter"
"Retour"
"Annuler"
+ "Choisir une photo"
"Effacer"
"Fermer"
+ "Compléter la vérification"
"Confirmer"
"Continuer"
"Copier"
"Copier le lien"
+ "Créer"
+ "Créer une salle"
+ "Refuser"
+ "Désactiver"
+ "Terminé"
+ "Éditer"
+ "Activer"
+ "Inviter"
+ "Inviter des amis à %1$s"
+ "Invitations"
+ "En savoir plus"
+ "Quitter"
+ "Quitter la salle"
+ "Suivant"
+ "Non"
+ "Pas maintenant"
+ "OK"
+ "Réponse rapide"
+ "Citer"
+ "Supprimer"
+ "Répondre"
+ "Signaler un bug"
+ "Signaler le contenu"
+ "Réessayer"
+ "Réessayer le déchiffrement"
+ "Enregistrer"
+ "Chercher"
+ "Envoyer"
+ "Envoyer un message"
+ "Partager"
+ "Partager le lien"
+ "Passer"
+ "Démarrer"
+ "Commencer un chat"
+ "Commencer la vérification"
+ "Prendre une photo"
+ "Voir la source"
+ "Oui"
+ "À propos"
+ "Audio"
+ "Bulles"
+ "Création de la salle…"
+ "La salle a été quittée"
+ "Erreur de déchiffrement"
+ "Options de développement"
+ "(édité)"
+ "Modification en cours"
+ "Chiffrement activé"
+ "Erreur"
+ "Fichier"
+ "GIF"
+ "Image"
+ "Lien copié dans le presse-papiers"
+ "Chargement…"
+ "Message"
+ "Mise en page du message"
+ "Message supprimé"
+ "Moderne"
+ "Aucun résultat"
+ "Hors ligne"
+ "Mot de passe"
+ "Personnes"
+ "Permalien"
+ "Réactions"
+ "En réponse à %1$s"
+ "Signaler un problème"
+ "Rapport envoyé"
+ "Nom de la salle"
+ "Rechercher quelqu\'un"
+ "Sécurité"
+ "Sélectionnez votre serveur"
+ "Envoi en cours…"
+ "Serveur non pris en charge"
+ "URL du serveur"
+ "Paramètres"
+ "Autocollant"
+ "Succès"
+ "Suggestions"
+ "Sujet"
+ "Incapable de décrypter"
+ "Événement non pris en charge"
+ "Nom d\'utilisateur"
+ "Vérification annulée"
+ "Vérification terminée"
+ "Vidéo"
+ "Patientez…"
+ "Confirmation"
+ "Attention"
+ "Activités"
+ "Drapeaux"
+ "Nourriture et boissons"
+ "Animaux et nature"
+ "Objets"
+ "Émoticônes et personnes"
+ "Voyages & lieux"
+ "Symboles"
+ "Échec de la création du permalien"
+ "Échec du chargement des messages"
+ "Certains messages n\'ont pas été envoyés"
+ "Désolé, une erreur est survenue."
+ "Salut, parle-moi sur %1$s : %2$s"
+ "Êtes-vous sûr de vouloir quitter cette salle ? Vous êtes la seule personne ici. Si vous partez, personne ne pourra rejoindre la salle à l\'avenir, y compris vous."
+ "Êtes-vous sûr de vouloir quitter cette salle ? Cette salle n\'est pas publique et vous ne pourrez pas la rejoindre sans invitation."
+ "Êtes-vous sûr de vouloir quitter la salle ?"
+ "%1$s Android"
+
+ - "%1$d membre"
+ - "%1$d membres"
+
+ "Rageshake pour signaler un bug"
+ "Vous semblez secouer le téléphone de frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?"
+ "Ce message sera signalé à l’administrateur de votre serveur d\'accueil. Ils ne pourront lire aucun message crypté."
+ "Raison du signalement de ce contenu"
+ "Ceci est le début de %1$s."
+ "Ceci est le début de cette conversation."
+ "Nouveau"
+ "Cochez si vous souhaitez masquer tous les messages actuels et futurs de cet utilisateur."
+ "Rageshake"
+ "Seuil de détection"
+ "Général"
+ "Version: %1$s ( %2$s )"
"fr"
+ "Erreur"
+ "Succès"
+ "Bloquer l\'utilisateur"
\ No newline at end of file
diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml
index efcd69b3fc..d3e67a98ef 100644
--- a/libraries/ui-strings/src/main/res/values-ro/translations.xml
+++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml
@@ -23,6 +23,7 @@
"Editați"
"Activați"
"Invitați"
+ "Invitați prieteni"
"Invitați prieteni în %1$s"
"Invitații"
"Aflați mai multe"
@@ -63,11 +64,13 @@
"Opțiuni programator"
"(editat)"
"Editare"
+ "* %1$s %2$s"
"Criptare activată"
"Eroare"
"Fişier"
"GIF"
"Imagine"
+ "Se părăsește conversația"
"Linkul a fost copiat în clipboard"
"Se încarcă…"
"Mesaj"
@@ -83,18 +86,23 @@
"Răspuns pentru %1$s"
"Raportați o eroare"
"Raport trimis"
+ "Numele camerei"
"Căutați pe cineva"
+ "Rezultatele căutării"
"Securitate"
"Selectați serverul"
"Se trimite…"
"Serverul nu este compatibil"
"Adresa URL a serverului"
"Setări"
+ "Se începe conversația…"
"Autocolant"
"Succes"
"Sugestii"
"Subiect"
"Nu s-a putut decripta"
+ "Nu am putut trimite cu succes invitații unuia sau mai multor utilizatori."
+ "Nu s-a putut trimite invitația (invitațiile)"
"Eveniment neacceptat"
"Utilizator"
"Verificare anulată"
@@ -115,6 +123,7 @@
"Încărcarea mesajelor a eșuat"
"Unele mesaje nu au fost trimise"
"Ne pare rău, a apărut o eroare"
+ "🔐️ Alăturați-vă mie pe %1$s"
"Hei, vorbește cu mine pe %1$s: %2$s"
"Sunteți sigur că vreți să părăsiți această cameră? Sunteți singura persoană de aici. Dacă o părasiți, nimeni nu se va mai putea alătura în viitor, inclusiv dumneavoastra."
"Sunteți sigur că vrei să părăsiți această cameră? Această cameră nu este publică și nu va veti putea alătura din nou fără o invitație."
@@ -139,6 +148,10 @@
"Puteți dezactiva această opțiune oricând din setări"
"Nu"" împărtășim informații cu terți"
"Ajutați la îmbunătățirea %1$s"
+ "Partajați datele analitice"
+ "Selectarea fișierelor media a eșuat, încercați din nou."
+ "Procesarea datelor media a eșuat, vă rugăm să încercați din nou."
+ "Încărcarea fișierelor media a eșuat, încercați din nou."
"Confirmați că doriți să ascundeți toate mesajele curente și viitoare de la acest utilizator"
"Rageshake"
"Prag de detecție"
diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml
index 163d65f19f..954d078d01 100644
--- a/libraries/ui-strings/src/main/res/values/localazy.xml
+++ b/libraries/ui-strings/src/main/res/values/localazy.xml
@@ -22,6 +22,7 @@
"Done"
"Edit"
"Enable"
+ "Forgot password?"
"Invite"
"Invite friends"
"Invite friends to %1$s"
@@ -70,6 +71,7 @@
"File"
"GIF"
"Image"
+ "We can’t validate this user’s Matrix ID. The invite might not be received."
"Leaving room"
"Link copied to clipboard"
"Loading…"
@@ -86,6 +88,7 @@
"Replying to %1$s"
"Report a bug"
"Report submitted"
+ "Room name"
"Search for someone"
"Search results"
"Security"
@@ -151,6 +154,12 @@
"Failed processing media to upload, please try again."
"Failed uploading media, please try again."
"Check if you want to hide all current and future messages from this user"
+ "Change account provider"
+ "A private server for Element employees."
+ "Matrix is an open network for secure, decentralised communication."
+ "This is where your conversations will live — just like you would use an email provider to keep your emails."
+ "You’re about to sign in to %1$s"
+ "You’re about to create an account on %1$s"
"Rageshake"
"Detection threshold"
"General"
@@ -163,4 +172,4 @@
"You can read all our terms %1$s."
"here"
"Block user"
-
+
\ No newline at end of file
diff --git a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt
index bbb124c3c5..03e9952c92 100644
--- a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt
+++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt
@@ -16,10 +16,9 @@
package io.element.android.libraries.usersearch.api
-import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.coroutines.flow.Flow
interface UserRepository {
- suspend fun search(query: String): Flow>
+ suspend fun search(query: String): Flow>
}
diff --git a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt
new file mode 100644
index 0000000000..e67a7af46f
--- /dev/null
+++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.usersearch.api
+
+import io.element.android.libraries.matrix.api.user.MatrixUser
+
+data class UserSearchResult(
+ val matrixUser: MatrixUser,
+ val isUnresolved: Boolean = false,
+)
diff --git a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt
index 7577e8f6e8..fff9218556 100644
--- a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt
+++ b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt
@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.usersearch.api.UserListDataSource
import io.element.android.libraries.usersearch.api.UserRepository
+import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@@ -33,24 +34,26 @@ class MatrixUserRepository @Inject constructor(
private val dataSource: UserListDataSource
) : UserRepository {
- override suspend fun search(query: String): Flow> = flow {
+ override suspend fun search(query: String): Flow> = flow {
// Manually add a fake result with the matrixId, if any
val isUserId = MatrixPatterns.isUserId(query)
if (isUserId) {
- emit(listOf(MatrixUser(UserId(query))))
+ emit(listOf(UserSearchResult(MatrixUser(UserId(query)))))
}
if (query.length >= MINIMUM_SEARCH_LENGTH) {
// Debounce
delay(DEBOUNCE_TIME_MILLIS)
- val results = dataSource.search(query).toMutableList()
+ val results = dataSource.search(query).map { UserSearchResult(it) }.toMutableList()
// If the query is a user ID and the result doesn't contain that user ID, query the profile information explicitly
- if (isUserId && results.none { it.userId.value == query }) {
- val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query))
- val profile = getProfileResult ?: MatrixUser(UserId(query))
- results.add(0, profile)
+ if (isUserId && results.none { it.matrixUser.userId.value == query }) {
+ results.add(
+ 0,
+ dataSource.getProfile(UserId(query))
+ ?.let { UserSearchResult(it) }
+ ?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true))
}
emit(results)
diff --git a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt
index e2eab067e6..b2327ef750 100644
--- a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt
+++ b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt
@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
+import io.element.android.libraries.usersearch.api.UserSearchResult
import io.element.android.libraries.usersearch.test.FakeUserListDataSource
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -63,7 +64,7 @@ internal class MatrixUserRepositoryTest {
val result = repository.search("some query")
result.test {
- assertThat(awaitItem()).isEqualTo(aMatrixUserList())
+ assertThat(awaitItem()).isEqualTo(aMatrixUserList().toUserSearchResults())
awaitComplete()
}
}
@@ -76,7 +77,7 @@ internal class MatrixUserRepositoryTest {
val result = repository.search(A_USER_ID.value)
result.test {
- assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)))
+ assertThat(awaitItem()).isEqualTo(listOf(placeholderResult()))
skipItems(1)
awaitComplete()
}
@@ -93,7 +94,7 @@ internal class MatrixUserRepositoryTest {
result.test {
skipItems(1)
- assertThat(awaitItem()).isEqualTo(searchResults)
+ assertThat(awaitItem()).isEqualTo(searchResults.toUserSearchResults())
awaitComplete()
}
}
@@ -112,13 +113,13 @@ internal class MatrixUserRepositoryTest {
result.test {
skipItems(1)
- assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults)
+ assertThat(awaitItem()).isEqualTo((listOf(userProfile) + searchResults).toUserSearchResults())
awaitComplete()
}
}
@Test
- fun `search - just shows id if profile can't be loaded`() = runTest {
+ fun `search - returns unresolved user if profile can't be loaded`() = runTest {
val searchResults = aMatrixUserListWithoutUserId(A_USER_ID)
val dataSource = FakeUserListDataSource()
@@ -130,11 +131,15 @@ internal class MatrixUserRepositoryTest {
result.test {
skipItems(1)
- assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults)
+ assertThat(awaitItem()).isEqualTo(listOf(placeholderResult(isUnresolved = true)) + searchResults.toUserSearchResults())
awaitComplete()
}
}
private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId }
+ private fun List.toUserSearchResults() = map { UserSearchResult(it) }
+
+ private fun placeholderResult(id: UserId = A_USER_ID, isUnresolved: Boolean = false) = UserSearchResult(MatrixUser(id), isUnresolved = isUnresolved)
+
}
diff --git a/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt b/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt
index ea0d93a197..0911d86b36 100644
--- a/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt
+++ b/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt
@@ -16,8 +16,8 @@
package io.element.android.libraries.usersearch.test
-import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.usersearch.api.UserRepository
+import io.element.android.libraries.usersearch.api.UserSearchResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -26,14 +26,14 @@ class FakeUserRepository : UserRepository {
var providedQuery: String? = null
private set
- private val flow = MutableSharedFlow>()
+ private val flow = MutableSharedFlow>()
- override suspend fun search(query: String): Flow> {
+ override suspend fun search(query: String): Flow> {
providedQuery = query
return flow
}
- suspend fun emitResult(result: List) {
+ suspend fun emitResult(result: List) {
flow.emit(result)
}
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt
index e7ea426a65..9d9971a842 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt
@@ -19,6 +19,7 @@ package io.element.android.samples.minimal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow
import io.element.android.features.login.impl.root.LoginRootPresenter
import io.element.android.features.login.impl.root.LoginRootView
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
@@ -28,7 +29,10 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService
@Composable
fun Content(modifier: Modifier = Modifier) {
val presenter = remember {
- LoginRootPresenter(authenticationService = authenticationService)
+ LoginRootPresenter(
+ authenticationService = authenticationService,
+ DefaultOidcActionFlow()
+ )
}
val state = presenter.present()
LoginRootView(
diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt
index ef776c613c..12481b81e1 100644
--- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt
+++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt
@@ -30,6 +30,7 @@ import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
+import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
import kotlinx.coroutines.runBlocking
import java.io.File
@@ -43,6 +44,7 @@ class MainActivity : ComponentActivity() {
coroutineScope = Singleton.appScope,
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = InMemorySessionStore(),
+ clock = DefaultSystemClock()
)
}
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
index a19dd629ae..4628f4910c 100644
--- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt
@@ -14,9 +14,12 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package io.element.android.tests.testutils
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.UnconfinedTestDispatcher
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 525854695a..5c4f9b2afa 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:55d774d5e61f8832162859ec9d22299efa5b0728cfc3308b13fb11df31c5130e
-size 13790
+oid sha256:20e9edff7567936627718427349a2ca7bf318a909b6ca206304deab0149a796d
+size 13934
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 596f01f6a6..274045391e 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b96cf938377ee0f02e0cf2d5896eb83935d9416a5b87849ed81caed4db5e90f2
-size 41264
+oid sha256:ab9f1e15c5a071be9c2f63aa28e6fbd57c360960899f808cead275ef67a54ae6
+size 41444
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index ebc59dec6a..fca921c50b 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:439a2d9e9497486e86b63fe42402f1c2ee33b15695e47312e18867530d041dad
-size 96539
+oid sha256:c1c1eedbab868e0c2501220293608572850e052f685f7076ec939b3f1a9abf27
+size 4464
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 8ea3902e3a..419dd3269a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2ed229820d7cbd6b550e98116ec3bf688c52957e2a57ab296bfc5abd212345b1
-size 13784
+oid sha256:0b2350c880e73ce285bfeafafc851c6973e0372330ab6f4daa16d078ed6a81c9
+size 13884
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 339b2adf16..85e99f70b1 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:92fe30f0927b8be4ffc384548e6731e7d5e347dc5caa1c84251f2a932ee1873c
-size 38848
+oid sha256:71d10fb1a2102466ab759442a1e006a3f5f85cb80f9f885cb5d18c8ded570bef
+size 38945
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
index dd593bdc3e..665c8811ac 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:255b99dea5c1da9d466533d510d7c0abe39a79cdc42b579cfa186176e9c6a49d
-size 91991
+oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b
+size 4457
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 508f754a17..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:fb3c154cf9fac2bc17b0ac3575bd2ce7745781cc1f2b9f28c220f98fb359c74d
-size 16284
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 4bd0d46417..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1d932221affde9bf53b06ea0e2b9c4b45dffe6adad7ff92f75775d9fe1d15b77
-size 16024
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index a076ab948b..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:4bd925ebe0257b400ed0b1cb956848622d1b42d8afcc71f915522c4878aaf94b
-size 23447
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemLightPreview_0_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 050457dd05..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemLightPreview_0_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:12b569ad80bb4a4df89d70d9ae4a92da0d9e96deb477e1e437efbef5ec4a1fdd
-size 22302
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2066a90b22
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchMultipleUsersResultItemPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8e7352789223e57f85c597a591ada117c903e06ed2df49719a98e793d128cd7b
+size 106579
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 7833fbac38..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:91de9d54216aac2bf2c4721e5e8ff5e74be43105846bdac16230cb7bea7427af
-size 13777
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemLightPreview_0_null,NEXUS_5,1.0,en].png
deleted file mode 100644
index 6b4f02c4d6..0000000000
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemLightPreview_0_null,NEXUS_5,1.0,en].png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:38fbef57e6fb2860d05ab2b864bab16a30ebc98199eb18b19e422f946c52e163
-size 13096
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e992def7cc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_SearchSingleUserResultItemPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:815864d4c929c5f123bc830d367b4f983851412891a23329d03e58e7726e4280
+size 54198
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 734464a3ef..a12b50f256 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a9da3afa82783ded1e686a36eb969b8e097de853e9c6dadea24884f7975c0720
-size 63729
+oid sha256:533ccd5b4e27b98ae4155e70d8cdde8a3c263ca613cc3e0ce31c2fc1d52758b8
+size 63361
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index dc5306778e..24c6168c3a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e2c200874d6fa8f4fe07da69a7879eb264fb8941c1ab9654162cb76586c52936
-size 103326
+oid sha256:7a036b609372a8c2f20307ca9f13b619973da8da45e81a019af1fdafadda64de
+size 102275
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 3983e46caf..2a7e793b29 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e48bb979a1d328871157847862723b8342cd8c2704750045bbf24592e143a5e0
-size 57874
+oid sha256:4032529e222fb9d14d4dc1fd215c8aae5d63326809df7cec449011d7904a60d2
+size 58355
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index bcd8ebf134..8a33e96fa4 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4320fd1900a120a737ba2a56e6c1f55cf671eb0e8c1791dc605994d333feb0c2
-size 96888
+oid sha256:2bf49968f5b07ca6798c4c8670a9e173ef3a511c6c60ed8d55d652bb042b8a8b
+size 96370
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index de4377ecc9..4c9bab34c8 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6f36c2b4f4266048d5295df08f380e4128630d11cce7ac11cf3a0eaaa5594d61
-size 19924
+oid sha256:4fddc08d41da556719424efbc75a1a80a57202095ce082124bc0bca08e54c74b
+size 21415
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 2a7a10f2c1..7abe9f914a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:56c23bd0880524cc56b713c93c5358e9f3771291c20f7a149bad00465f3f987a
-size 20648
+oid sha256:b13d46300c7cdd3632ee39a6bfce92a5b9bfa44917b32928da313741965addc7
+size 8677
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 74acb423d6..5e201cb8cf 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b7b91a2d05b975d568116615c286568f376ebead49e25ff17f5aae8b75be0e1f
-size 28876
+oid sha256:4d63e57cea65fd8617f19f2775c6cacb58fb1fd933ac26895445b49c8d8638d4
+size 17363
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 0bdbed7968..11d198b819 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:23b9e996b8f0cc2efcb3adb6294bfa7b4a53fefbb7b6ee07add4105da9b9d40e
-size 18984
+oid sha256:2639e6662a1c78ff0f6091fb72b5fc06b486aa08e3beed78e4877b9282ce98d6
+size 20309
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 69d05b132c..309acc5b39 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f12591531cfdce24378003dda48940c20d8c9cf7bfac73ac1e7713e925bfac4d
-size 20214
+oid sha256:255e7caab6e7133239a186a421cf669649133704b15cddebe2dbc4ca9baca372
+size 8731
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
index bb7d1036e4..99c67749fc 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:44a35f28b3a59cc28937fee16eda26c26ef7b7622f929218e40f7537e096b2e8
-size 28120
+oid sha256:4d62a75d2faea119344221414f4442bb0682bf74a1bd71ca1cfc5ddfcaea4635
+size 17342
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..94817469d3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:49e2768b2a111af737a30038913481113f759a20d3cf5df0166964c75926ec1f
+size 6545
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..40f9c06277
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eac9030993d92ef7f57ae5e622d566cd88555a60c462c0299f2a16a0c3e5daa8
+size 6853
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..94817469d3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:49e2768b2a111af737a30038913481113f759a20d3cf5df0166964c75926ec1f
+size 6545
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7ae58a8744
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc.webview_null_DefaultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:74a1a3ac6ae62c3573ce5834f5ae17952049ee77a8be76bfcb99e78b8fc6cb97
+size 6675
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..94817469d3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:49e2768b2a111af737a30038913481113f759a20d3cf5df0166964c75926ec1f
+size 6545
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..40f9c06277
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eac9030993d92ef7f57ae5e622d566cd88555a60c462c0299f2a16a0c3e5daa8
+size 6853
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..94817469d3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:49e2768b2a111af737a30038913481113f759a20d3cf5df0166964c75926ec1f
+size 6545
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7ae58a8744
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.oidc_null_DefaultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:74a1a3ac6ae62c3573ce5834f5ae17952049ee77a8be76bfcb99e78b8fc6cb97
+size 6675
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 5f158821fc..3ace44456c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9d4aefe56727b0752db5a45f3e4444be134ef0ae1dcbc833acd9c914e726421b
-size 34253
+oid sha256:db157015f3b440738db961d5152c693383457562158563fb2beb8ebb6e41feba
+size 31570
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..5197332f89
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:03c71b78e0e64989d64526a245efbf7881a47c5d270e754811575e36d3520f33
+size 25303
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..1536711dde
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:670769b82c2405be6b8360cf36ebab06551f6088899e9e0dc1e63d4102c904cd
+size 24543
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..63690986c3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:42121a3c5151b222724a05bbd6d2c6b624d5211a9365f4c7a2de21a31f0650de
+size 20264
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8f27ef74ed
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ccf9cbb300a4d0c45c4be54124e6e506ca1f53e7fa9c33db3544ab85110d3d35
+size 26052
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 1db3e3234c..b20f7ad12e 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:65aad7d493d9bae55dda4ca2783c0f0a8ed5fbcf67835957b6ede1e8f2d8293e
-size 33210
+oid sha256:e2ef182ba3721fa37c014dff57b49cbb7ee23d6becbc4ee60d81d44aff11a198
+size 30523
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..49257acefc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5cda5716e850b31997b49dafb975112b3a0578391cb39044ca0057de6379530f
+size 24636
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..b20d358a12
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e338c8c2d3f89f3b7fdeafc5afffb36399855155e2b85231349e13e381e4cf1f
+size 23617
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..4313439757
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dea12dfe1c1799b1d2d765214b1bd911154a19a7794bf37c6663d179cec76c81
+size 19556
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..ed81ad1335
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1dc7d39a9b4e0f3eee8157b0290146b6bae5e6d0ea280296ddfc7051f31b95df
+size 24786
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index cd4deb3d0b..792ec762a7 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:372f239e351215dadf0d5c451b8105e93bc86e338f4e564aa4726d482028ae9c
-size 396027
+oid sha256:6e30e5a9ef2371748af9a77806168d5e49185b2e565308f31edb62b94f8060b0
+size 396024
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 5183011e59..90f76a4d29 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:087ef6e2e489e63e1a30793b183915984a32265676fd5a95f02d7ca02821c84b
-size 132174
+oid sha256:036b362d18c690ff516de13f45aa5561902772bb4ff5a5c48383e22b27eb2999
+size 132172
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 62f356b12c..7db0c02d08 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f5e03b31f020201e8a8bd7ad5962022337441c4e83d15e03e95f83c9fa10eaaf
-size 98743
+oid sha256:e4c14c033b2d4961bad5d05ae7eb61e72fc542e497bb09770271980321832607
+size 98742
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 4afab509fc..d400660085 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
-size 393620
+oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
+size 393618
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 4afab509fc..d400660085 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
-size 393620
+oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
+size 393618
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 4afab509fc..d400660085 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
-size 393620
+oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
+size 393618
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
index 4afab509fc..d400660085 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
-size 393620
+oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
+size 393618
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
index 4afab509fc..d400660085 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479
-size 393620
+oid sha256:bc6306a6c951e34884603f71ceae6c6fdbf32889146b60049f41f9451972dd2e
+size 393618
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 4069bda25e..b70f830625 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bb1648a000da198a30d3214adecf0b1266a150a226548ce77ac6d0eb001c65e2
-size 5238
+oid sha256:7ddcd892538faa691ade67de5145392f89eaab4c903cc8fed00b24726753ed4e
+size 6945
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 219643d41a..90c95a6056 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:44487cb1f6115b388bb043081e15d904a090277fca6edea05ccb7b6c54d56de2
-size 5761
+oid sha256:cea39d32d858cd7b28885e634e746aea3652a56b665563e26f62346d9eaa1128
+size 7172
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_10,NEXUS_5,1.0,en].png
index a13df6829d..92f24c2b0a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_10,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_10,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:75ba23e1b82c3a5e4c3f4330a004b00d4eb2a2d3a27ce687ad6876c6e6e129be
-size 5547
+oid sha256:7eb000677f48efbd9dfbe373b221dbc98b2da65404e86aea42e79416cc032205
+size 7107
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_11,NEXUS_5,1.0,en].png
index 0e2b15ff9b..c5f5a8072c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_11,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_11,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:46067cba703ae02846a141dd53b888d48b6e55890cefed4cef4795c0b5910346
-size 5871
+oid sha256:1daab66cf9beb8a5bb9c4cdea0db76fdc690b850bde1ada9679bfaf0167e6982
+size 7150
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_12,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2fa9ef478f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_12,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:37ba26495ac60e2857cfab755675993f44775c8c0bd36b5b7d69ccebf9a71a6d
+size 7269
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_13,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8b3bf8a750
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_13,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dcdb3be85af22f6997b372d268d54ca287ef282a8c1353d4fcb47d44ceffcf8a
+size 7595
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_14,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8d529d0de5
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_14,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:65c434fc772482b1a884bc3c68447e228b470549ff72a510e43ba0aaae69387a
+size 7396
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_15,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..91e0bfc7e0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_15,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:23f468e1737a6a3789fdfd56322e4b15551c1a0d74249f92aee92879d476ea91
+size 7485
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 7c2e9b8c05..5bcbeaf3a9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dbd968203eb9092f6ceab631da735a72bd15a44c3c8faaba45246a58bf3d9837
-size 5566
+oid sha256:7ab56f3950f19ed24fcd2592157046eb878a2dde9e9831b8a3d846f5f3207dc0
+size 7062
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_3,NEXUS_5,1.0,en].png
index b2fbd520c3..bd06f74e2a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d7f03a7e0ac8e63116d1c4806f07266ace800f52c8f089846c515ff12488918f
-size 5875
+oid sha256:2c6e69cd6e3687ef724ff2c8f5151fb6679fd6395d866d7f944507c130e938eb
+size 7084
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_4,NEXUS_5,1.0,en].png
index d6312cb012..d34a9e81f6 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:492b8ec89b02bbba9abb849d6b28f61d77f448d66d1eef5eba5a6f66db725897
-size 5133
+oid sha256:d789e51e1f3c440f8577d465322c982a5a6aea54f7d1a7ab12e183ba841b0f14
+size 7274
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_5,NEXUS_5,1.0,en].png
index 0d2618d910..75c1d45a91 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ccf5cf29bdcb6016203c849ea1013de2ab42ec21c9b202c56dea2d7b2b75434b
-size 5516
+oid sha256:15100ea369eab89e99c4252b88e33f362cca287e6ef0799e1e3e3a8f4be6e227
+size 7327
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_6,NEXUS_5,1.0,en].png
index e25aab0f37..0f6bef4954 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e666a4e785be44883e945e5ab22766c96358381467fc6860c3e8733a359d6801
-size 5462
+oid sha256:92c2dc09ef213e2f55568f036d17e0ba81f9053a61774bd89ea27273c9a0aeb8
+size 7415
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_7,NEXUS_5,1.0,en].png
index 192b3586fc..9bfcee38c9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6fc64090e9c685246a0adc2a2250cb80fcc64cbe7d6578d63bc4827eaa50591e
-size 5682
+oid sha256:a035a78264d396b78c2653b89ab0edbc3ee6400d380eccd587f80cafdd57f165
+size 7335
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_8,NEXUS_5,1.0,en].png
index 8754951421..822ca7afe1 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b584ab84ca09c1fe9c2ce8433a92adb8f1f6c595ccc702f10b976b81951b9048
-size 5253
+oid sha256:012db6849c7458978d11f0c56b171dcc01993ac3bbc940639a609a04e5f591ad
+size 7093
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_9,NEXUS_5,1.0,en].png
index 8996a9fbed..4834e3f8af 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_9,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleDarkPreview_0_null_9,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ab7a53108da1eda0c1f24360d4c4073dca110dd3b368cc1f117cd3f351a385dc
-size 5789
+oid sha256:d429ec724142f73fa7846ec26a21b62e1a3466bd1e85193bdf48e749bf188978
+size 7253
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 33e9fff6b1..625ec13c74 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ace5a91e960c6570647e2b8ad2de428ba474e48531a2f74ab267d07668d7a9e8
-size 4928
+oid sha256:87de1686f0e84c9f00f80dc02a63ad6d0b0c360762a9d4f996b315407aec37d6
+size 6700
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 5c993c220e..859a6eed31 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aec5d74e6f4d316c1ea124dc50e2abbfd22d6ee94b6d82519b3f9cab7a191dfa
-size 5348
+oid sha256:ab4af3d37dc161ccba7366517f60cb50923539615909c4bf76ec667095a4293c
+size 6712
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_10,NEXUS_5,1.0,en].png
index 1e9f02cdef..8f13f13c53 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_10,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_10,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dfaa4d6032641b332b111b20c81ec4bca917391cf122a06718fca7da849a7c94
-size 5124
+oid sha256:a5962c60a9072b96cbd72e8ea9cf6344f715d8469a45f51fcdebacfec5344544
+size 6693
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_11,NEXUS_5,1.0,en].png
index 7b2413937c..597309f4f9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_11,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_11,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ac3eb3ebe9f2f30224389586f9b5901106b73cee9f5b4cdeb0f76b313ba0cd31
-size 5384
+oid sha256:3ed947a2df42bc4cd7b0c181d608c69763897383f4846b2878e3df2705dfae0e
+size 6715
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_12,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e3c939c48b
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_12,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:30f04a04ecde63528df19021a9b43b1bb2d26aaf31fa021e4b83f4503fd73782
+size 6997
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_13,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_13,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..44d40d43dc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_13,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4e390e24a61c5ea42c2469bc71c0d7785de647991dbf5b8d5df360e76adcd0a
+size 7184
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_14,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_14,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..eff7f05a7d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_14,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eba96060e90371916f2e62f550cc0fd4fa44e2872263a353dd3c37c2664c0f47
+size 7001
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_15,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_15,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..dddf73f879
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_15,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:05f55e1df050651b48708dc3f40fe95a8ab68d1a0fea7838023cbadc2a21da57
+size 7078
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_2,NEXUS_5,1.0,en].png
index cb2f53102c..65d5cec935 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8e0eaf65f627b7be068311b0c8f52cfbe847571684a0c63dda3e7c0621eac42c
-size 5133
+oid sha256:544dac21d0af65c94f1e38ee2b0ff5a20dd3f12c40a5c41037c12f3af54df9ca
+size 6598
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_3,NEXUS_5,1.0,en].png
index 1324860ed0..651de32b9c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8359444a65d74682106930a7bbc6fb171acb95446cc5ff67eefa0dadc51e5932
-size 5397
+oid sha256:dca857bfb1b6405488d96214b72e219f9aa4c4abd535cd1dd83b7efd7603c497
+size 6641
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_4,NEXUS_5,1.0,en].png
index 12176c5b98..451a6a11f5 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d774fca7f28623d7e464c233e5e2282b5c7d608d7aa6084b2350a4f71765bc5a
-size 4809
+oid sha256:83613dcfe9968db44cabf918d6dc9ab011869c80d4bf089d5d4c46f7b6bedae4
+size 6968
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_5,NEXUS_5,1.0,en].png
index 34a8c795e7..3a4b4e33b9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:32c81961535a22ce1fccc15865cd76bc9844168552a2205f7f7342398866b622
-size 5109
+oid sha256:8d44801312b785f5c595a988124de6d70d65de01fb18c74876b1f6328e84c222
+size 6914
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_6,NEXUS_5,1.0,en].png
index 4b3f1bfb0c..08df2b5e02 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bd53dc603b4689eabe41e5d90ea0d8a70b769d60082ee57eee556ef9a7ea24ec
-size 4984
+oid sha256:e884fe9fd0c212caf0bfc745206ea277ff77037356e4e94b844e1cd1100a8861
+size 6955
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_7,NEXUS_5,1.0,en].png
index e4a872faf4..2d4ae50749 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:43e08e7c243192571121595483ddc2dd44b56892afff5ae949ac224cad13e73f
-size 5165
+oid sha256:7abd5b98f55ca52bccd791196613029d2932520f062f08fd6356666b5fe1dbe8
+size 6878
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_8,NEXUS_5,1.0,en].png
index 19cc971e97..a760810c62 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7c61846e0037aa7a4d6f146fa4f2b441654570db082b3f506bb6f49aa01f3999
-size 4944
+oid sha256:cfaed204b2cc5ee8d1ec54f84c446346c62e392ad2ae305359a552f5f9a427f1
+size 6755
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_9,NEXUS_5,1.0,en].png
index b4bd02006b..c1a681d637 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_9,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessageEventBubbleLightPreview_0_null_9,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:137bfee9d776c8abff1a2428e3edc1c28cc7ca21d9849640e2aad2d945d7f7f2
-size 5361
+oid sha256:a801ced0c93a02e5fa22ca021f78d3ab1834169745353aaacbaad4bc41bbb112
+size 6834
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 5dcb0e4633..5c15cd7380 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
-size 6302
+oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
+size 6303
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index ce4149128c..f45e646ed2 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
-size 7036
+oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
+size 7035
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 5dcb0e4633..5c15cd7380 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
-size 6302
+oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
+size 6303
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png
index ce4149128c..f45e646ed2 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
-size 7036
+oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
+size 7035
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 4e6eae60dc..7a56bba7ba 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
-size 6161
+oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
+size 6163
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 7b52cf5f6a..639720a366 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
-size 6836
+oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
+size 6834
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png
index 4e6eae60dc..7a56bba7ba 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
-size 6161
+oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
+size 6163
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png
index 7b52cf5f6a..639720a366 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
-size 6836
+oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
+size 6834
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..b244bdb73f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bdf7fe891aa40f4d626733deb130481d64b3315531712128d1b4073e5ccedf19
+size 5394
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..25c4fffbab
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0baf33f0ec99cd8e4b201d72b38208a02b0511da544276a38d17ff5653e2b754
+size 5906
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..5aa7211ab3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c8d9dc01ec07f9e43032e3ec7610eda5588145f3003cc3d329ddf4325e19f239
+size 6674
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2ed9e07432
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5e0f86449d651c50966fe594a46e3a59ae9a5505013e3e36f2458be35ba1844e
+size 7174
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..f640d9af42
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:621facc4799c0069fe83b4be9a5085ecafb042de1cdfbbf4ccf0c15548373aa9
+size 5332
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8ed48c31f6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:986f40750d69639e771c9bf1aac30c49dbf122aa60dc0280164a8868671b638a
+size 5910
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9735300d61
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6d4fb8d68603309963d434f3166821055d818836b1ebfb4fc4c637aba992277d
+size 6617
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d265165330
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1fa641ca8d5c796c858b54ad997052aabf817bc86e254d38d4b33038bc21eedf
+size 7285
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png
index 0006c267ea..f4f6a627b5 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3dc781e3f17f09eb7e0cb8bf753a68ebde850b0b618e8d6bff7c780e365ee778
-size 11853
+oid sha256:ff1f9f65252e855037d6008aea0d884ccaec6419412b26e4ad12a32ab583f5ca
+size 11848
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png
index 0dcb849d60..02c073d5ad 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6d14b21a4723db7ac5ceda97eee44681d5d236007262e0e9e8b5a8d5abf8022b
+oid sha256:37c370bc41527d7b407679c971a35aee051da93c033c2f195b6c3d6c7b2d885a
size 11514
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index ac79e58737..b2a3fa0b56 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0e7e30c460d75815023ed02cd6b0fcff4a15881916f0056e47d9fd034bf3a181
-size 41125
+oid sha256:f99c5fd192b3d56188dc0a14a522b1ea4a05cdbe9b18f63c4a4c99f0914c0df5
+size 41443
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 8ca1e75687..123b94deab 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:81b126a3d322d70f06db15622e560a146cea2c81f5873987550aba0a5c5efffa
-size 53314
+oid sha256:8a524ff4463d72a0adca6a13907d95c6fb2cfe8afeba541cfe03c8fb44202d4f
+size 53427
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png
index 2a2d363120..dce22380c4 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b7e8b8ff0aeeb517c47dfd987579b73a7e862e9a67a5100532f39087d42a4fe4
-size 46056
+oid sha256:6c7090f345c722c0c5d5d415723e1770843d36fef48dc750a893320830b9dfd4
+size 46068
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index cbe31a3f09..e88ba038ca 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b0ede4470399e2c7e6f3c99e59191a857c1e14254789307777359802980c59a1
-size 193735
+oid sha256:b876f9b49579215a6f0cbb73627e6c5ad19e7b71131bee02a2fe4b7f46432a67
+size 197787
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
index 809b1bba99..f923e1c9b4 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c9fe22528aad68a5dffc8489fd1ec3138a8edde44fb23a1391f6e53a07d44fff
-size 193966
+oid sha256:6d358680e2b08ed4d4f709b9f4d908f5cad84b888a7aa74b49609b09297e416e
+size 198126
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
index fc4cbf27fd..e6567f92c5 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d559a454af6dfe367d1256dcd7ceeb32ae6fe168b76b1b5d8860d328e9752fb3
-size 51433
+oid sha256:f8f54ea6a7d158e8f7da56648aabf05c8dbc761db8698891d2d3f1ff863b76fb
+size 51724
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
index cd486afaf0..3451b5ba36 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1e45ec17c6353f96f0f777f8c638da010dbd062c680175d8ae80f6e700a6f928
-size 72941
+oid sha256:4c2845cd6ef914a8062d3685d62d6b861a3963e09aad13efd5a9ac5e2de1a3ea
+size 73507
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
index bd287148b7..85e76e3cb1 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:572d6f11354e2b1f02cb0979b35e2c798abe2d3b63f852ca6b868471ceecaed0
-size 42964
+oid sha256:8dbf4c20504f68324ab8606d84f41390bef28479f5e0f9d76d34c358403b0de8
+size 42954
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png
index 842ea61125..63a6318693 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b25f0497c08515125896756c89c5bc9e1757b25ec50478cd5cb67cdfd00c2dec
-size 55019
+oid sha256:3defc06e2e3d791261e87048fb019d2da7e8372d35550bcb17aacb2bb62370eb
+size 55127
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png
index d83002fedd..4d1821d97b 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3f22479bb2c6bd35c6f927c11ff19e644226df3d2a99b9c6b23f5bd679178d5a
-size 39151
+oid sha256:824b93fd466fff749da6f6314fecbc1a15d316b3cac201adac0137c28660e121
+size 39342
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png
index cc3b898a92..e666ac5893 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6920b50647111a70215c74ca27be736d6c208c0801eb4bcd323abcb33e903aea
-size 56229
+oid sha256:8233a3eafef97e18dcd6b846d78af3e3a6a4fb79b89af276a1a41067f9e9af45
+size 56472
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 8a2c64bd25..d328d59d1a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a01bde6db3ce00e84e9bb5d6a8179c8d97af9dde939f6e31aa81c955dbdbf3a3
-size 40586
+oid sha256:fbe93775a53dc0e06a019d2fd3c72612dfbf9000709f48b4cdcc14ef72c914ad
+size 40934
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index a07f4eb8ed..d462c390a8 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3f12a13dd0fb3b569ce332ebc4e00fb55a6402bbf2b2a927b02abb4056c5e5c3
-size 53043
+oid sha256:4703dc63b3015c2a2c96e731dbabe55f2cff07c5508853f370d4eeb67e67740a
+size 53186
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png
index bdb8e46417..f6c6b2f55e 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c50bb17bf530de6472d44232b0248c2a5761c67fde785b3aee6eb3f77fa4103e
-size 45948
+oid sha256:70a1848c08f7e3ed4fcc2cd0b05344234b2c4a2c26139c256344b102b49d98a1
+size 45847
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
index cdfd4fd6be..35a2f47fac 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c0f6c785a1e20160355c72356744dedba65bda3d453fbd9772390350eaf6cc6e
-size 195504
+oid sha256:4fe993ac7679fef189bcbfdf9e82abfb0263686721fcabb4ddeca8d2a3d4e209
+size 199655
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
index cb6574b5ad..8a508845da 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:37c702936170a0e4b37d608cf62b92c3b60b93950d4e9f32633af77e3ce4fdc9
-size 195775
+oid sha256:fa118c2f28fe2c7bd3a194cf80cb25ece8c61fd15edd407d93ee76816c362fe1
+size 199975
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
index 949cd3ef67..f29c6b9fdd 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e0b885810087916d3fefe699e5abc5125138ad74c6377f560e010e7fe68892da
-size 51388
+oid sha256:022ca7f2b5ce17e0079346778ee3b99b1e235439f88885a2d1bf545cc11864e6
+size 51800
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
index 6a5e4d9ec6..fcbfd4f52b 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:adcb71f4b9639287beabf35c9e91837bebee8f6b070fa77c0e6520fdf24faaae
-size 73679
+oid sha256:c347c51252c8e1305385c6c92936941403c481c792ba3fe10ca4949a1accd051
+size 74080
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
index 07823941c7..db3b41535b 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cb24bbba2e17417f4f3459353ee2c762517acfae2eada90c47b559828bad16a0
-size 42411
+oid sha256:51120566fc91498097a2e68232f04342ade84d4aee702d5cc06ee5ff4bc17460
+size 42551
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png
index 61aaf14dac..5edaa2c19f 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d75c877c55c0f622aabee5ed8428b8a980cbc28daacb2127f2a17cc29db62a9d
-size 55057
+oid sha256:548566862bbb0da3e13b43aee882b28656ea544b9341d6152d4d1f5c526ed3f2
+size 55292
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png
index 97d47f37f0..ad1af8bb53 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a6edb33e04054aba49ea978d6b2c0d0c25332c4dd901cf2cadd6c368076e957d
-size 38687
+oid sha256:092598b99ed8d53381f80352c357b13b159dd65cb23ef54d277bfa862de28263
+size 38928
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png
index 4bca8aaed3..05f925aee9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:996016191220540fde3d8aaa3124a074a6eaaf6e87c43bf0d57d32992faf47fc
-size 56342
+oid sha256:0dd943e910de80c7a36db12acb17b0ee116228181ddb58a45c8ec9252e85e435
+size 56491
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index 3884544dd1..10be1fb5e7 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9c2c642f1595f503a1deb1c241f14543d7f15221baa1d0937595aa023f36a874
-size 44765
+oid sha256:74d763ff8d44e686ee3a10dcadc0e2c1f02e3332c486d62d00e2333d0c3b60cf
+size 45578
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 0dd33fab8e..dac883dcdb 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1afd2c42012017f154fdf031d4a5fc7384fa5f239b56cf4567f42fd579806c9a
-size 45363
+oid sha256:09b29e2e7bb8c38c978d3b56b04a97fc96d752154bb78fddd13a5cde8051a048
+size 45757
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index a934e0c73e..fd526958c5 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9481f2994885a7649aa54d1c0043c44fe390004a7b58db4fc9d0e3e70bfc258a
-size 43537
+oid sha256:a936d98080f35c838adab660ef9b9287fc380d1358335c54e3da56230476721f
+size 43810
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
index a32dc0d376..c8b1900e58 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:220b9ac66b74724887d7b0bbc082b4d0240d9fc25a068d33491eb83fe76e45fd
-size 43958
+oid sha256:43042ccb540b5fa3b8d5d9e273bd1155258d0dc12875b95c7a364492b3985da2
+size 44854
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 6c69a3850e..a07fbf9be9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9aa83ed14fb23495e79500a1e7d9dee8215efe67eec89e65e382d4f695eac68b
-size 44871
+oid sha256:16cf470ec6ff50ee469db7bdc2cbfc1e32c5bcd7835c12c7313aafa495a44825
+size 45282
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
index 0be594f38f..8d97ed2ac1 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aa4970f995c6ee618be3d868ff5a34442352a7f4493cad345093b889daf0d4c2
-size 42042
+oid sha256:e6cc29403f3b3e146e9a637973320f5bd6bf73feb7b042f8844337817e551113
+size 42280
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png
index 9749477ebd..b1f708d57c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c820bd324df729db08710d1a6c17ca34a451a3bb94da2206fc57d6b4efe91e2f
-size 60019
+oid sha256:b53d55b5085673ac3a0a7663f7ff68e7d510fe352f3931b49c7e75e4c3767b93
+size 60007
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png
index f24edcc4a1..d020b1e521 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:da2f9f87d89382b4b18d39a477a8e86f98067e2326adae4def8374b5bf09f316
-size 57588
+oid sha256:a761318f3dfbc2ce6e777cfabc19eeb2f89100ae7631660f4b4d7550cd947c84
+size 57580
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index 07451a0330..01ec8ec0fc 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:628f1f00dca9d15faabd8288a3c54b1b8581a380cf14edf364f0dee9ecc187d5
+oid sha256:147786f2cfcf7674147253cca5e41b4af96587a27216076a1c0802c81b0b1b46
size 180117
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 8146c241cb..a49d11835b 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:827bccb0fdd3106fb24a95d665b4da2cfc15de48e8f508ae809c9f75d6d1bbf0
-size 178915
+oid sha256:789a979f7207531b4b9ba488ae2de52e6046809e788baf6690e1383a933cf624
+size 178916
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..9b2f7eb04e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ec2051ec47f43a88aeb0d56078094fca784c948ce2b3e1a450343943ab7ee951
+size 35408
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..487980822c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:85d54c4c049c16435c4e6bcc8bf6d25dbc3fccc90a0278c2c330c97334eb27d0
+size 28119
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8209d5b40a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6caa0bdf8897ab6d8d73b8dfeac5c8c3cece5945e87123a1f5a1c96de477e623
+size 32262
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..16e677ee95
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df9d8b28c6e98632204e90bccfddcf1e0d0ca6ce1a19af03f5bc491ade32fa36
+size 34793
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..c6c6003d29
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bfed133c28a48fb90aeda89de0154b4ab5c209d08a75147065dd8cc126f955eb
+size 34416
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d1bbb6763c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d3e1f45de5ab8d0bd953bd2a0a0014b7dce3315a82d993ceb633304f2b8c2622
+size 28517
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..0c632d8d45
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02d7335bab9fd2bd753086ceeed886e57e0800b4d36a2cacd136c37bd41ebb9d
+size 27152
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..a1307b1cf9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c61f9803c3313f6e2cceb966942bec7885fcb2efd87e94babdac8e37b95d4da5
+size 35380
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7422209943
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a1a6d6b7fb6f2d97d63773dae499ed096dbf38d16cd34cafcee3b46571717bcb
+size 27828
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..ee9c9329b8
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:31d763b42ffc618bbf1c3272cbd4484edd7ab687f0ba25391955edf8f57c3208
+size 32443
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..077fc03e17
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ca9b7a692c9d5bfa0ab0789f1d33a2f66c6244b1f4b672fab1aa18a71b3c9d0a
+size 33897
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..cda549b019
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bdbeb867e5e009206a55eb1dfb0c478499301baa317a979cb2ffe9b1198871e8
+size 33931
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..e866646102
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dcf0472ccdfd1256eca966c6d29b4aace86f9ddab37164bb865633349a4377f3
+size 28519
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..d3cccbf61e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.edit_null_DefaultGroup_RoomDetailsEditViewLightPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3733e12a63cd72d529f9359f555cda4da65384bd7631f8450451b27095691193
+size 26945
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png
index ee9f0fb113..38aaa95764 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:516e972069292f31625847ca39de06ea402ce1606c97d84f930c18eaa2448cfa
-size 13759
+oid sha256:9ba1f70268339a290e35e2127d1a3fa03840e69dc143631ef2fe29f22ddf646a
+size 15363
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png
index a1094a6fa6..e40d7e6d34 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b387f10903926ba9ed30b7d213314388eebf23743cbbdbcf4b613cf3136f64aa
-size 41524
+oid sha256:01b7d6a33e3591a54bcf602bac34155b85056ebdf600c511858efba55ea43cf4
+size 43034
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png
index 79b48dd535..7c469f37db 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0af8c7149fd4266c68c0e4b8b9a699bc0b3fc0b05fea3f2e2544abc884f70c5b
-size 11988
+oid sha256:cdb5e42bc94ca908c76b5da491ed712169876e8f2ea80534f2e722b7280ba72d
+size 13602
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png
index ff95c9ed19..9648a14380 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8eb2fcdb57b7f1ba28990f9576fad6dce156fe0c5c9ae491797040fc22ae4f7b
-size 39516
+oid sha256:656cec478d23350ed5c6e08b6cd9579e497999d6a7a8aa879af7dd223fefa98b
+size 41088
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png
index f7f16c2b8a..3f7583050a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1e4d3bc53e6a86957218247b614be6bbc78cfb35ce49565f2243f50d6ef94fe3
-size 14216
+oid sha256:456b9c09e0ff314543fa6a0e54d7e6e41f346e3fd2ff33f9ba1ea3ae3168d629
+size 15839
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png
index 0cd4f9c3e0..a604fa2859 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:213777e4ace6f9c33209db37bffff2ca6dba13e540e9d07be82239d261ee8e3f
-size 63951
+oid sha256:d76042bbb94307f96dc99d5077b58204b8f30a195408739271321570c218d0da
+size 65441
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..7041e4be75
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersDarkPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:22bda941040d3ad60d709517f07a27933ae25a5a69464e492fe1d0f9c4d7c405
+size 50488
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png
index 57c7799447..29dd68cf13 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_0,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8ddec11fd636d345c49cf981ce7498ac2d94027d4b223b257b64288bb0254766
-size 13328
+oid sha256:bcc75c47150e577714221b5d574a67c69051cf2f4439f8b7c929620509d87562
+size 14668
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png
index 1db9b42586..7a5b1f7a23 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_1,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:da5ba4637d999c8654e38a86ad0b26c75a6fe10a6988eb821b24cf91da2274e5
-size 39070
+oid sha256:1df29f2bd4090162dcbf7336971c366b53b49390f5f41cc6df6c6f506f01ab30
+size 40464
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png
index 6648389b0e..79171ae80b 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c57c20fe79d0dbb0ca25571fa2ab3ba6e06f3a9c6634e96fac3ed0155c2ea106
-size 11160
+oid sha256:d488d43188f491cc578d30a70d05c90d6c436ca8fad88a36b85a1ef5bc90e88b
+size 12564
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png
index 5b6a14ea66..29762f9bf5 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8e43279132a7536516cf267599f877f7c729f76be51d6319944f74c57af0e282
-size 36493
+oid sha256:900065642b4eb9a90a3a5d1bbbb5f03e9057b08e5e8289e535fd9237ac15e497
+size 37824
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png
index fd948f750d..0a329a78cd 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:933c5ac4478699e9e713af559025ab675a8bdc93b15c42ff27fe5525fa3b668a
-size 13166
+oid sha256:b457fe4bc8ff8a26612819abbbac6440b539e61a8f00fb4171ad38dfb131840a
+size 14553
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png
index a35148eff1..168be9c78c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3a66a39bf18ba35abfc87dea22ca019eea98727bf246142cb034649c5b1772bd
-size 61041
+oid sha256:8384acb8fef843e0f06108d230c40bda5db7a95932568d37cbd610eaf09acc23
+size 62392
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_6,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..53795c55fd
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.invite_null_DefaultGroup_RoomInviteMembersLightPreview_0_null_6,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:307bfb8ffa9088d7d474c0d51e5e8d54bd6598b527357fe67380c755c2af75f0
+size 48893
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png
index d2b6b99b87..eef6fe679e 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c3e225aea9018c660e16ce5ee6e1e343b439b96264b274c5d722b05c6c9a5d48
-size 52064
+oid sha256:53015a9106ad2c3a24b9073e142db6fb3ba0e40ef3e87b1ba2142ec04066eba6
+size 53644
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png
index f056965480..d2b6b99b87 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2372fe63e9fcc2c6529622270592437b880211acdcf7ef8be36dfc9fe41e0e5b
-size 66807
+oid sha256:c3e225aea9018c660e16ce5ee6e1e343b439b96264b274c5d722b05c6c9a5d48
+size 52064
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png
index da20925ed9..f056965480 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dce5d8c8dacb7c095cd22d590fc4fd9bc2cb9b13e0d4edbdd52387226b37b4ec
-size 70253
+oid sha256:2372fe63e9fcc2c6529622270592437b880211acdcf7ef8be36dfc9fe41e0e5b
+size 66807
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_5,NEXUS_5,1.0,en].png
index dd6890cc46..da20925ed9 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d3a1afc10771257294d9f22d26ed8a81d04cd4b1f31d9fb9fc1065a8216230a9
-size 67658
+oid sha256:dce5d8c8dacb7c095cd22d590fc4fd9bc2cb9b13e0d4edbdd52387226b37b4ec
+size 70253
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png
index 29b02fac10..dd6890cc46 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7a3b23895b133b7bba7300fbf31fd70daa83f558e29e8c09dfc7789ce0bc04d6
-size 61861
+oid sha256:d3a1afc10771257294d9f22d26ed8a81d04cd4b1f31d9fb9fc1065a8216230a9
+size 67658
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..3f1e6f2204
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_8,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8c3317c3c03399d5575a089029e237c766b912bb80a96a8feccb005f4e7247f5
+size 63166
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_9,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..98ca67f5aa
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsDarkPreview--1_1_null_9,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f3dd0fc6e0024160c03749948e4d4c6b353e3b5162b6ecc219c8bd16c2ad9848
+size 71157
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png
index 66e16dc49c..aa5bcb484a 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_2,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b22c207ac5f185c1c7eaa30ffe43acb2a1e8da06379f15f66ff96797b8ea9158
-size 48297
+oid sha256:7045a9ced4691ec52451b6a31d2454ddc82c04b767c96a3dd4c3f81addcb8f0c
+size 50010
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png
index e58e13b108..66e16dc49c 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_3,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a7a98c0125cc58a130b8ebfbd5f2b8fcfa65e02b0c6a151c0b594208a8801c96
-size 60894
+oid sha256:b22c207ac5f185c1c7eaa30ffe43acb2a1e8da06379f15f66ff96797b8ea9158
+size 48297
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png
index b8abed4961..e58e13b108 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_4,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:998819b0aeb15712bb53e982e3756cf2e4a4069a2a13f7955e10b873e3e736bd
-size 63698
+oid sha256:a7a98c0125cc58a130b8ebfbd5f2b8fcfa65e02b0c6a151c0b594208a8801c96
+size 60894
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_5,NEXUS_5,1.0,en].png
index 5130e63f43..b8abed4961 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_5,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_5,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cb8dc0ca3fa34ffa40ea43934ee00d0c48c9b7ce721274763c093eee83d8cff9
-size 62187
+oid sha256:998819b0aeb15712bb53e982e3756cf2e4a4069a2a13f7955e10b873e3e736bd
+size 63698
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png
index 81d82fd9d8..5130e63f43 100644
--- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_7,NEXUS_5,1.0,en].png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e403d076b9d28e7fd06493399d4b7c405d99f329fb1a298c41fb445a1e9ec466
-size 57215
+oid sha256:cb8dc0ca3fa34ffa40ea43934ee00d0c48c9b7ce721274763c093eee83d8cff9
+size 62187
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..451dc8f651
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_8,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da8ee4d42e3fcc4b68e06406bee859634c245baeb9ab7850427fffc04f41f1f8
+size 58493
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_9,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..be582845d0
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl_null_DefaultGroup_RoomDetailsLightPreview--0_0_null_9,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1797f9c90a79fc9bcbb1aa8d8ae32452c76d17b12e8ef7f441e1c93e9ac95c3f
+size 64593
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncFailurePreviewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncFailurePreviewDark_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..dea5ca7bfc
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncFailurePreviewDark_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ee096c4234d5f983e84113058518af756ff03696ed5a733ec7540a842ff59ba4
+size 11435
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncFailurePreviewLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncFailurePreviewLight_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..8356990393
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncFailurePreviewLight_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e2f7614737130cb1a23105faab7e46b39580eadcd05b6d7f811ddef525bef11
+size 10640
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncLoadingPreviewDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncLoadingPreviewDark_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..54cbdbcbed
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncLoadingPreviewDark_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:226b5056efcf7371c76729caf4105d94f1d945f40f1d6715d538ed19154ea0f0
+size 4971
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncLoadingPreviewLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncLoadingPreviewLight_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..29817c3dd2
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components.async_null_DefaultGroup_AsyncLoadingPreviewLight_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:59580eab9625ce67761ab8674f2adb0d9e4f0d2b27bbc132f60ae021828fd558
+size 4644
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..1de39b0f80
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7cc0a54fec11686b8ffcba432562cfc1115973fab77693f8983a5b81a64e5d78
+size 15683
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..f40b7dae38
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:738fd87b80036a040ceaddbad06ed4657a0d8d9d8c6052933300e5c194c17f24
+size 15724
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_AvatarActionBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentDarkPreview_0_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_AvatarActionBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_AvatarActionBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom.avatar_null_DefaultGroup_SheetContentLightPreview_0_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_AvatarActionBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_CheckableUnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_CheckableUnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..fd84b77b2d
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_CheckableUnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:33e3ba31ada4522ce7c9b6adafd4c1c37df1927017e4bba8bf3f418a352a0cc9
+size 129286
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png
new file mode 100644
index 0000000000..2b2903bdba
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnresolvedUserRowPreview_0_null,NEXUS_5,1.0,en].png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4bd094880b88662dd76409e4a728eda3736700c59c0721b87ba2568ee4bc0782
+size 39296
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnsavedAvatarDarkPreview_0_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnsavedAvatarDarkPreview_0_null,NEXUS_5,1.0,en].png
diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnsavedAvatarLightPreview_0_null,NEXUS_5,1.0,en].png
similarity index 100%
rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png
rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_UnsavedAvatarLightPreview_0_null,NEXUS_5,1.0,en].png
diff --git a/tools/adb/oidc.sh b/tools/adb/oidc.sh
new file mode 100755
index 0000000000..bcc519f313
--- /dev/null
+++ b/tools/adb/oidc.sh
@@ -0,0 +1,24 @@
+#! /bin/bash
+#
+# Copyright (c) 2023 New Vector Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Format is:
+
+# Error
+# adb shell am start -a android.intent.action.VIEW -d "io.element:/callback?error=access_denied\\&state=IFF1UETGye2ZA8pO"
+
+# Success
+adb shell am start -a android.intent.action.VIEW -d "io.element:/callback?state=IFF1UETGye2ZA8pO\\&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh
new file mode 100755
index 0000000000..9e8c964499
--- /dev/null
+++ b/tools/check/check_code_quality.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+
+#
+# Copyright 2023 New Vector Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#######################################################################################################################
+# Search forbidden pattern
+#######################################################################################################################
+
+searchForbiddenStringsScript=./tmp/search_forbidden_strings.pl
+
+if [[ -f ${searchForbiddenStringsScript} ]]; then
+ echo "${searchForbiddenStringsScript} already there"
+else
+ mkdir tmp
+ echo "Get the script"
+ wget https://raw.githubusercontent.com/matrix-org/matrix-dev-tools/develop/bin/search_forbidden_strings.pl -O ${searchForbiddenStringsScript}
+fi
+
+if [[ -x ${searchForbiddenStringsScript} ]]; then
+ echo "${searchForbiddenStringsScript} is already executable"
+else
+ echo "Make the script executable"
+ chmod u+x ${searchForbiddenStringsScript}
+fi
+
+echo
+echo "Search for forbidden patterns in code..."
+
+# list all Kotlin folders of the project.
+allKotlinDirs=`find . -type d |grep -v build |grep -v \.git |grep -v \.gradle |grep kotlin$`
+
+${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_code.txt $allKotlinDirs
+
+resultForbiddenStringInCode=$?
+
+if [[ ${resultForbiddenStringInCode} -eq 0 ]]; then
+ echo "MAIN OK"
+else
+ echo "❌ MAIN ERROR"
+ exit 1
+fi
diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt
new file mode 100755
index 0000000000..ae346c170d
--- /dev/null
+++ b/tools/check/forbidden_strings_in_code.txt
@@ -0,0 +1,131 @@
+#
+# Copyright 2023 New Vector Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# This file list String which are not allowed in source code.
+# Use Perl regex to write forbidden strings
+# Note: line cannot start with a space. Use \s instead.
+# It is possible to specify an authorized number of occurrence with === suffix. Default is 0
+# Example:
+# AuthorizedStringThreeTimes===3
+
+# Extension:kt
+
+### No import static: use full class name
+import static
+
+### Rubbish from merge. Please delete those lines (sometimes in comment)
+<<<<<<<
+>>>>>>>
+
+### carry return before "}". Please remove empty lines.
+\n\s*\n\s*\}
+
+### typo detected.
+formated
+abtract
+Succes[^s]
+succes[^s]
+
+### Use int instead of Integer
+protected Integer
+
+### Use the interface declaration. Example: use type "Map" instead of type "HashMap" to declare variable or parameter. For Kotlin, use mapOf, setOf, ...
+(private|public|protected| ) (static )?(final )?(HashMap|HashSet|ArrayList)<
+
+### Use int instead of short
+Short\.parseShort
+\(short\)
+private short
+final short
+
+### Line length is limited to 160 chars. Please split long lines
+#[^─]{161}
+
+### "DO NOT COMMIT" has been committed
+DO NOT COMMIT
+
+### invalid formatting
+\s{8}/\*\n \*
+# Now checked by ktlint
+# [^\w]if\(
+# while\(
+# for\(
+
+# Add space after //
+# DISABLED To re-enable when code will be formatted globally
+#^\s*//[^\s]
+
+### invalid formatting (too many space char)
+^ /\*
+
+### unnecessary parenthesis around numbers, example: " (0)"
+ \(\d+\)
+
+### import the package, do not use long class name with package
+android\.os\.Build\.
+
+### Tab char is forbidden. Use only spaces
+\t
+
+# Empty lines and trailing space
+# DISABLED To re-enable when code will be formatted globally
+#[ ]$
+
+### Deprecated, use retrofit2.HttpException
+import retrofit2\.adapter\.rxjava\.HttpException
+
+### This is generally not necessary, no need to reset the padding if there is no drawable
+setCompoundDrawablePadding\(0\)
+
+# Change thread with Rx
+# DISABLED
+#runOnUiThread
+
+### Bad formatting of chain (missing new line)
+\w\.flatMap\(
+
+### In Kotlin, Void has to be null safe, i.e. use 'Void?' instead of 'Void'
+\: Void\)
+
+### Kotlin conversion tools introduce this, but is can be replace by trim()
+trim \{ it \<\= \' \' \}
+
+### Put the operator at the beginning of next line
+ ==$
+
+### Use JsonUtils.getBasicGson()
+new Gson\(\)
+
+### Use TextUtils.formatFileSize
+Formatter\.formatFileSize===1
+
+### Use TextUtils.formatFileSize with short format param to true
+Formatter\.formatShortFileSize===1
+
+### Use `Context#getSystemService` extension function provided by `core-ktx`
+getSystemService\(Context
+
+### Use DefaultSharedPreferences.getInstance() instead for better performance
+PreferenceManager\.getDefaultSharedPreferences==2
+
+### Use the Clock interface, or use `measureTimeMillis`
+System\.currentTimeMillis\(\)===1
+
+### Remove extra space between the name and the description
+\* @\w+ \w+ +
+
+### Suspicious String template. Please check that the string template will behave as expected, i.e. the class field and not the whole object will be used. For instance `Timber.d("$event.type")` is not correct, you should write `Timber.d("${event.type}")`. In the former the whole event content will be logged, since it's a data class. If this is expected (i.e. to fix false positive), please add explicit curly braces (`{` and `}`) around the variable, for instance `"elementLogs.${i}.txt"`
+\$[a-zA-Z_]\w*\??\.[a-zA-Z_]
diff --git a/tools/danger/dangerfile-lint.js b/tools/danger/dangerfile-lint.js
index b0531fef9b..8d704ef5a8 100644
--- a/tools/danger/dangerfile-lint.js
+++ b/tools/danger/dangerfile-lint.js
@@ -26,4 +26,9 @@ schedule(reporter.scan({
* This can be useful if there are multiple reports being parsed to make them distinguishable.
*/
// outputPrefix?: ""
+
+ /**
+ * Optional: If set to true, it will remove duplicate violations.
+ */
+ removeDuplicates: true,
}))
diff --git a/tools/danger/dangerfile.js b/tools/danger/dangerfile.js
index 88e721febb..263c394e25 100644
--- a/tools/danger/dangerfile.js
+++ b/tools/danger/dangerfile.js
@@ -171,11 +171,6 @@ if (hasPngs) {
warn("You seem to have made changes to some images. Please consider using an vector drawable.")
}
-// Check for reviewers
-if (github.requested_reviewers.users.length == 0 && !pr.draft) {
- warn("Please add a reviewer to your PR.")
-}
-
// Check that translations have not been modified by developers
const translationAllowList = [
"RiotTranslateBot",
diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml
index e77443fcab..dc797aace1 100644
--- a/tools/detekt/detekt.yml
+++ b/tools/detekt/detekt.yml
@@ -15,6 +15,12 @@ style:
UnusedPrivateMember:
# TODO Enable it
active: false
+ UnusedParameter:
+ # TODO Enable it
+ active: false
+ UnusedPrivateProperty:
+ # TODO Enable it
+ active: false
ThrowsCount:
active: false
LoopWithTooManyJumpStatements: