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 @@ + + + + + + + + + + \ 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, }))