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/.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/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/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/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 9abae4d2cb..cd73a6c6c2 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
@@ -30,12 +30,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,
@@ -69,16 +70,16 @@ fun SearchUserBar(
resultHandler = { users ->
LazyColumn {
if (isMultiSelectionEnabled) {
- itemsIndexed(users) { index, matrixUser ->
+ itemsIndexed(users) { index, 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)
}
}
)
@@ -87,11 +88,11 @@ fun SearchUserBar(
}
}
} else {
- itemsIndexed(users) { index, matrixUser ->
+ itemsIndexed(users) { index, searchResult ->
SearchSingleUserResultItem(
modifier = Modifier.fillMaxWidth(),
- matrixUser = matrixUser,
- onClick = { onUserSelected(matrixUser) }
+ searchResult = searchResult,
+ onClick = { onUserSelected(searchResult.matrixUser) }
)
if (index < users.lastIndex) {
Divider()
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-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml
index 9a3030ba71..9a5d672fd4 100644
--- a/features/createroom/impl/src/main/res/values-es/translations.xml
+++ b/features/createroom/impl/src/main/res/values-es/translations.xml
@@ -5,4 +5,4 @@
"Añadir personas"
"Se ha producido un error al intentar iniciar un chat"
"Crear una sala"
-
\ 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 214cd9406d..ceddb71154 100644
--- a/features/createroom/impl/src/main/res/values-it/translations.xml
+++ b/features/createroom/impl/src/main/res/values-it/translations.xml
@@ -5,4 +5,4 @@
"Aggiungi persone"
"Si è verificato un errore durante il tentativo di avviare una chat"
"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 55b9f6d692..e546e55587 100644
--- a/features/createroom/impl/src/main/res/values-ro/translations.xml
+++ b/features/createroom/impl/src/main/res/values-ro/translations.xml
@@ -14,4 +14,4 @@
"Despre ce este această cameră?"
"A apărut o eroare la încercarea începerii conversației"
"Creați o cameră"
-
\ No newline at end of file
+
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/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/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/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 1e10a553f1..00e74b026c 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
@@ -39,6 +39,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
@@ -89,6 +90,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 8d08b77a49..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,
) {
@@ -203,6 +205,7 @@ fun MessagesView(
.consumeWindowInsets(padding),
onMessageClicked = ::onMessageClicked,
onMessageLongClicked = ::onMessageLongClicked,
+ onUserDataClicked = onUserDataClicked,
)
},
snackbarHost = {
@@ -240,6 +243,7 @@ fun MessagesViewContent(
state: MessagesState,
modifier: Modifier = Modifier,
onMessageClicked: (TimelineItem.Event) -> Unit = {},
+ onUserDataClicked: (UserId) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) {
Column(
@@ -255,6 +259,7 @@ fun MessagesViewContent(
modifier = Modifier.weight(1f),
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
+ onUserDataClicked = onUserDataClicked,
)
}
MessageComposerView(
@@ -354,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/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
index cf7c3c9bf1..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
@@ -87,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
@@ -95,6 +97,7 @@ import kotlinx.coroutines.launch
fun TimelineView(
state: TimelineState,
modifier: Modifier = Modifier,
+ onUserDataClicked: (UserId) -> Unit = {},
onMessageClicked: (TimelineItem.Event) -> Unit = {},
onMessageLongClicked: (TimelineItem.Event) -> Unit = {},
) {
@@ -120,6 +123,7 @@ fun TimelineView(
highlightedItem = state.highlightedEventId?.value,
onClick = onMessageClicked,
onLongClick = onMessageLongClicked,
+ onUserDataClick = onUserDataClicked,
)
if (index == state.timelineItems.lastIndex) {
onReachedLoadMore()
@@ -139,6 +143,7 @@ fun TimelineView(
fun TimelineItemRow(
timelineItem: TimelineItem,
highlightedItem: String?,
+ onUserDataClick: (UserId) -> Unit,
onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier
@@ -173,6 +178,7 @@ fun TimelineItemRow(
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = ::onClick,
onLongClick = ::onLongClick,
+ onUserDataClick = onUserDataClick,
modifier = modifier,
)
}
@@ -203,6 +209,7 @@ fun TimelineItemRow(
highlightedItem = highlightedItem,
onClick = onClick,
onLongClick = onLongClick,
+ 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()
@@ -257,6 +270,7 @@ fun TimelineItemEventRow(
Modifier
.zIndex(1f)
.offset(y = 12.dp)
+ .clickable(onClick = ::onUserDataClicked)
)
}
val bubbleState = BubbleState(
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/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/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
index 97fb3311cb..02783775bf 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,7 @@ 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.invite.RoomInviteMembersNode
import io.element.android.features.roomdetails.impl.members.RoomMemberListNode
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode
@@ -44,7 +45,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,
@@ -95,7 +96,8 @@ class RoomDetailsFlowNode @AssistedInject constructor(
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/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt
index 7d90342f65..78c26c4ae5 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
@@ -45,9 +45,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
@@ -67,6 +67,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.matrix.api.room.RoomMember
+import io.element.android.libraries.ui.strings.R as StringR
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
@@ -185,7 +186,7 @@ internal fun RoomHeaderSection(
@Composable
internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) {
- PreferenceCategory(title = stringResource(R.string.screen_room_details_topic_title), modifier = modifier) {
+ PreferenceCategory(title = stringResource(StringR.string.common_topic), modifier = modifier) {
Text(
roomTopic,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 150f35c885..5aa00f991a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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"
@@ -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"
@@ -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.1.0.3113"
+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/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/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/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/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/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
index 55576c7b96..c21ceb5076 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
@@ -31,6 +31,7 @@ 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.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -54,6 +55,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 +79,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 {
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/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/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/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/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..de168090f4 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
@@ -71,32 +71,32 @@ 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 = clock.epochMillis(),
+ 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
+ )
+ }
}
/**
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..d7af528ce3 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,7 +53,7 @@ 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 ?: ""
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/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/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 ea60503459..2a693ff9a4 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, MAXIMUM_SEARCH_RESULTS).toMutableList()
+ val results = dataSource.search(query, MAXIMUM_SEARCH_RESULTS).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/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_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_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.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_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_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_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 105b94a19e..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:121e9eb7013123e5647e351eda7d331ade8032ae5aa56bd1be943ee6c6acad53
-size 41444
+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 b579039d11..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:e19a24fa27305822af8ec3a4dfac727eb1297ccbbeffec726664c6bc29b1a2a4
-size 53421
+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 2a39d3fa2d..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:9ab84b59725483b7187e91f3df18d1d55decbef7da37e186099027c6c98e3430
+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 ec8687992b..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:5b388b6664ea0e5a86d6506a954cbcdc36c39054204739365eb0228b2e1d4fd4
-size 197778
+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 9467cabc80..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:dbeca10cc6a489dd20390bbc3bf5f2fa4f726342083621b8161a2a4448622ffc
-size 198122
+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 22b7e81257..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:23ed23fbc4a47152c4d9b6373111c7de5497900e9ccb3382f99364bde2444032
+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 c71a28244b..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:8a715939660ec1a2bea4c5a3e9d0a3b4b0bc45f137fae4c3210476145346a3a9
+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 2fbafcf338..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:f3094dab56dd2c46a5c34162e717afcf674823be080959ec5e852227946237ff
+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 d89fe8b6dd..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:44534d8513114e666e7faa341a81184420fd84e8c3723c2d17236949f31e2fdb
-size 55118
+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 fc9cacdcf3..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:f988cd33ae74aa292f26f0c6f11b5fcec8ff298005423161921e912a5e416c97
-size 39341
+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 61e274ef18..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:9a65842cd12943594ed28ffba0a958ff076d6c6492533bfc5ffd395b915b2b30
-size 56467
+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 d33815af64..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:f7822ba6ecc9e7cf7bfdb4446b6796dce685d2417f6ed29b6ef5015438e09ace
-size 40935
+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 ff256d7186..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:d3d8fae3d49c61850eef15c762d1f9615a735ca1943e78aa4dc116eaa830c02a
-size 53182
+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 b2207d8ccb..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:ebc4e13d35c918274f35445db57955780bfdc037d5bd85a98eddb9b3bac42d5e
-size 45846
+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 dabf581ac8..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:ed3f266ce286216deca3747ded2b46bd7ef37c707145d1ae676e8afe227850b2
-size 199651
+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 ca627ffc7f..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:685a607ef2c644b8844a1a83bc9427040f3890b4b9f777aa80592c98f0a96d87
-size 199963
+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 1ddc9bb4b0..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:d5975a89ee4cf263317079be95b6f6cc199bfbbcf75ac88d622989bfd859c813
-size 51801
+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 213b36fafb..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:0375a01da0c470b8379d2edcfb282b4b55f5cee86dc9da3efbe66fbadf7019a5
-size 74079
+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 4360de3629..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:f085dfb5b0147374af21b8df3f4cd2b9930e638188add6f3a4fa94008747c7ee
-size 42553
+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 0f0e1ba1aa..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:7ec8c64bb4500bce08725d74753f45db93a369095626064ecdf9a83122aea326
-size 55285
+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 ecf8b2e64c..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:54046d58d89dd8f44e8e2fdb83306682fd6305a32ba91c06c9bcd042e99bbdd3
+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 6d8f83611f..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:a96004c7012bd1b83eb395511472a6c93f410f9504656f4e3621d7809eccca9f
-size 56483
+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 f1bc47d485..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:49759fbb46a966d0e0f1c793545e320e7588ac482399a12ac45762474e6781f8
-size 45579
+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 c28a1d3ac7..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:cdca9e94c071c20753aa2076f6c3a7ed931a2c1e90a78c29e00f27f7af99dfe8
-size 45758
+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 6864f509ba..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:4e47966820a6a77efece51aaac5adbe3dd1dad1880b1feb7da2e86e36b386f7e
-size 43815
+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 7009f86be7..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:bc3fc4c97a43c5786a75328e35828ed0e0cfd74ffab673832053ecc11db9b5e0
-size 44852
+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 823b19bf1c..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:7f82a51b6b6ed4ac030c4602da9da2629010169e998aaba16f9e05c652f6010d
-size 45283
+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 49d46e6e23..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:5d18843f61267ae7385c23113be5c5c3d4376d95a843eef126ed670877f69b80
-size 42281
+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.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..84abdcb0e5
--- /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:cb1178f063eafb21ba01b93310d84335afbc4e6ad1fc99d1776578fe3f7eaa07
+size 49237
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..4d09e6d3d2
--- /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:ca9a896e2dd3cf47792467f052f2d45b555d99ccbd91436ef4c03dfca66836ab
+size 47542
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.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/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,
}))