From 8a750f3ae49a0143ff1e8445880881e046c42fc3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 May 2023 12:05:25 +0200 Subject: [PATCH 01/34] Comment out hard-coded sliding sync url. --- .../element/android/libraries/matrix/impl/RustMatrixClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..8f4b353e2e 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 @@ -153,7 +153,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) From 111cca99bff104c9af05099245ae3796f47f5a9c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 May 2023 14:11:20 +0200 Subject: [PATCH 02/34] Add `EventType` to avoid using hard-coding strings. --- .../api/timeline/item/event/EventType.kt | 99 +++++++++++++++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 13 +-- .../impl/timeline/RustMatrixTimeline.kt | 9 +- .../notifications/NotifiableEventProcessor.kt | 3 +- .../model/NotifiableMessageEvent.kt | 3 +- .../NotifiableEventProcessorTest.kt | 3 +- 6 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventType.kt 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/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 8f4b353e2e..4c3e895100 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 @@ -114,9 +115,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 +137,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) 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/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/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()) From f1d2f566bcdc2304b5659f0be438040faca67fb3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Apr 2023 21:24:07 +0200 Subject: [PATCH 03/34] Add OIDC support --- docs/oidc.md | 45 +++++++ .../features/login/impl/LoginFlowNode.kt | 14 ++ .../features/login/impl/oidc/OidcEvents.kt | 23 ++++ .../features/login/impl/oidc/OidcNode.kt | 58 +++++++++ .../features/login/impl/oidc/OidcPresenter.kt | 97 ++++++++++++++ .../features/login/impl/oidc/OidcState.kt | 26 ++++ .../login/impl/oidc/OidcStateProvider.kt | 39 ++++++ .../features/login/impl/oidc/OidcUrlParser.kt | 47 +++++++ .../features/login/impl/oidc/OidcView.kt | 121 ++++++++++++++++++ .../login/impl/oidc/OidcWebViewClient.kt | 42 ++++++ .../login/impl/oidc/WebViewEventListener.kt | 29 +++++ .../features/login/impl/root/LoginRootNode.kt | 11 +- .../login/impl/root/LoginRootPresenter.kt | 28 +++- .../login/impl/root/LoginRootState.kt | 9 +- .../login/impl/root/LoginRootStateProvider.kt | 8 +- .../features/login/impl/root/LoginRootView.kt | 65 ++++++---- .../login/impl/util/LoginConstants.kt | 3 +- .../api/auth/AuthenticationException.kt | 1 + .../api/auth/MatrixAuthenticationService.kt | 19 +++ .../api/auth/MatrixHomeServerDetails.kt | 2 +- .../libraries/matrix/api/auth/OidcConfig.kt | 21 +++ .../libraries/matrix/api/auth/OidcDetails.kt | 25 ++++ .../impl/auth/AuthenticationException.kt | 7 + .../matrix/impl/auth/HomeserverDetails.kt | 2 +- .../libraries/matrix/impl/auth/OidcConfig.kt | 29 +++++ .../auth/RustMatrixAuthenticationService.kt | 62 ++++++++- 26 files changed, 789 insertions(+), 44 deletions(-) create mode 100644 docs/oidc.md create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcConfig.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/auth/OidcDetails.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt diff --git a/docs/oidc.md b/docs/oidc.md new file mode 100644 index 0000000000..7feae3ce83 --- /dev/null +++ b/docs/oidc.md @@ -0,0 +1,45 @@ +[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/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..37cfec229b 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 @@ -29,11 +29,13 @@ 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.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.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) @@ -55,6 +57,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,10 +69,19 @@ class LoginFlowNode @AssistedInject constructor( override fun onChangeHomeServer() { backstack.push(NavTarget.ChangeServer) } + + override fun onOidcDetails(oidcDetails: OidcDetails) { + 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)) + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt new file mode 100644 index 0000000000..0c40f457bf --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.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.impl.oidc + +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/OidcNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt new file mode 100644 index 0000000000..0122ead10c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt @@ -0,0 +1,58 @@ +/* + * 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 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 + +/** + * TODO Transmit back press to the webview + */ +@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/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt new file mode 100644 index 0000000000..76faa090d1 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt @@ -0,0 +1,97 @@ +/* + * 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 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.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 { + requestState = try { + authenticationService.cancelOidcLogin() + // Then go back + Async.Success(Unit) + } catch (throwable: Throwable) { + Async.Failure(throwable) + } + } + } + + fun handleSuccess(url: String) { + requestState = Async.Loading() + localCoroutineScope.launch { + try { + authenticationService.loginWithOidc(url) + // Then the node tree will be updated, there is nothing to do + } catch (throwable: Throwable) { + requestState = Async.Failure(throwable) + } + } + } + + 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/OidcState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt new file mode 100644 index 0000000000..e9b2ac2355 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/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 + +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/OidcStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt new file mode 100644 index 0000000000..7a5552e719 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/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 + +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/OidcUrlParser.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.kt new file mode 100644 index 0000000000..eb55368f47 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParser.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.features.login.impl.oidc + +import io.element.android.libraries.matrix.api.auth.OidcConfig + +/** + * Simple parser for oidc url interception. + * TODO Find documentation about the format. + */ +class OidcUrlParser { + + // 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)) 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") + } +} + +sealed interface OidcAction { + object GoBack : OidcAction + data class Success(val url: String) : OidcAction +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt new file mode 100644 index 0000000000..06591409c9 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt @@ -0,0 +1,121 @@ +/* + * 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.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.remember +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.libraries.architecture.Async +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.bool.orTrue +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: WebView? = 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(eventListener = object : WebViewEventListener { + override fun shouldOverrideUrlLoading(url: String): Boolean { + return shouldOverrideUrl(url) + } + }) + } + + BackHandler { + if (webView?.canGoBack().orFalse()) { + webView?.goBack() + } else { + // To properly cancel Oidc login + state.eventSink.invoke(OidcEvents.Cancel) + } + } + + Box(modifier = modifier.statusBarsPadding()) { + AndroidView( + modifier = modifier + .statusBarsPadding(), + 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 -> { + // Indeterminate indicator, to avoid the freeze effect if the connection takes time to initialize. + 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/OidcWebViewClient.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt new file mode 100644 index 0000000000..bd7ab99ad6 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt @@ -0,0 +1,42 @@ +/* + * 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.annotation.TargetApi +import android.os.Build +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import timber.log.Timber + +// TODO Move to a dedicated module +class OidcWebViewClient(private val eventListener: WebViewEventListener) : WebViewClient() { + @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/WebViewEventListener.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt new file mode 100644 index 0000000000..91fde6c311 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt @@ -0,0 +1,29 @@ +/* + * 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 + +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 { + return false + } +} 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..fd3d293870 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 @@ -33,7 +33,11 @@ import javax.inject.Inject class LoginRootPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter { - private val defaultHomeserver = MatrixHomeServerDetails(LoginConstants.DEFAULT_HOMESERVER_URL, true, null) + private val defaultHomeserver = MatrixHomeServerDetails( + url = LoginConstants.DEFAULT_HOMESERVER_URL, + supportsPasswordLogin = true, + supportsOidc = false, + ) @Composable override fun present(): LoginRootState { @@ -54,7 +58,12 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: is LoginRootEvents.SetPassword -> updateFormState(formState) { copy(password = event.password) } - LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState) + LoginRootEvents.Submit -> { + when { + homeserver.supportsOidc -> localCoroutineScope.submitOidc(homeserver.url, loggedInState) + homeserver.supportsPasswordLogin -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState) + } + } LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn } } @@ -67,9 +76,22 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: ) } + private fun CoroutineScope.submitOidc(homeserver: String, loggedInState: MutableState) = launch { + loggedInState.value = LoggedInState.LoggingIn + // TODO rework the setHomeserver flow + authenticationService.setHomeserver(homeserver) + authenticationService.getOidcUrl() + .onSuccess { + loggedInState.value = LoggedInState.OidcStarted(it) + } + .onFailure { failure -> + loggedInState.value = LoggedInState.ErrorLoggingIn(failure) + } + } + private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState) = launch { loggedInState.value = LoggedInState.LoggingIn - //TODO rework the setHomeserver flow + // TODO rework the setHomeserver flow authenticationService.setHomeserver(homeserver) authenticationService.login(formState.login.trim(), formState.password) .onSuccess { sessionId -> 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..884a3037df 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 @@ -18,6 +18,7 @@ package io.element.android.features.login.impl.root import android.os.Parcelable 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 @@ -27,13 +28,17 @@ data class LoginRootState( val formState: LoginFormState, val eventSink: (LoginRootEvents) -> Unit ) { - val submitEnabled: Boolean get() = - formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState !is LoggedInState.ErrorLoggingIn + val supportPasswordLogin = homeserverDetails.supportsPasswordLogin + val supportOidcLogin = homeserverDetails.supportsOidc + 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..cdb92bcfaf 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 @@ -24,16 +24,20 @@ open class LoginRootStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLoginRootState(), - aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", true, null)), + aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", supportsPasswordLogin = true, supportsOidc = 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(homeserverDetails = MatrixHomeServerDetails("server-with-oidc.org", supportsPasswordLogin = false, supportsOidc = true)), + // No password, no oidc support + aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("wrong.org", supportsPasswordLogin = false, supportsOidc = false)), ) } fun aLoginRootState() = LoginRootState( - homeserverDetails = MatrixHomeServerDetails("matrix.org", true, null), + homeserverDetails = MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidc = 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..c8c85ed50f 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 @@ -83,7 +83,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 +94,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 +102,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( @@ -143,13 +152,37 @@ fun LoginRootView( Spacer(Modifier.height(32.dp)) - LoginForm(state = state, isLoading = isLoading) + 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) + } + else -> { + Text(text = "No supported login flow") + } + } - Spacer(modifier = Modifier.height(32.dp)) + 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)) + } } when (val loggedInState = state.loggedInState) { - is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId) + is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail) else -> Unit } } @@ -217,6 +250,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 +259,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 +345,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..c481fdf927 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 = "synapse-oidc.lab.element.dev" // "matrix.org" const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" - } 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..90c54cfe2e 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 supportsOidc: 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/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..b98ecc193f 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,13 @@ 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!!) + + 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..9ca3d78456 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() + supportsOidc = 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..774c22781b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt @@ -0,0 +1,29 @@ +/* + * 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 +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..b0409cfba3 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,8 +24,8 @@ 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 @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder +import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.use import java.io.File @@ -51,7 +52,12 @@ class RustMatrixAuthenticationService @Inject constructor( private val sessionStore: SessionStore, ) : MatrixAuthenticationService { - private val authService: RustAuthenticationService = RustAuthenticationService(baseDirectory.absolutePath, null, null) + private val authService: RustAuthenticationService = RustAuthenticationService( + basePath = baseDirectory.absolutePath, + passphrase = null, + oidcClientMetadata = oidcClientMetadata, + customSlidingSyncProxy = null + ) private var currentHomeserver = MutableStateFlow(null) override fun isLoggedIn(): Flow { @@ -91,9 +97,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 +109,55 @@ 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() } + private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null + + override suspend fun getOidcUrl(): Result { + 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 { + 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 { + 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, From 9551a5e6f8fe8f1d4d27c9e83b3e822f113891d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Apr 2023 16:13:02 +0200 Subject: [PATCH 04/34] Rework the set homeserver part: get the info, instead of hard-coded value, and implement retry in case of error. --- .../login/impl/root/LoginRootEvents.kt | 1 + .../login/impl/root/LoginRootPresenter.kt | 58 ++++++++++---- .../login/impl/root/LoginRootState.kt | 9 ++- .../login/impl/root/LoginRootStateProvider.kt | 40 +++++++++- .../features/login/impl/root/LoginRootView.kt | 77 ++++++++++++------- .../components/async/AsyncFailure.kt | 70 +++++++++++++++++ .../components/async/AsyncLoading.kt | 54 +++++++++++++ ...lurePreviewDark_0_null,NEXUS_5,1.0,en].png | 3 + ...urePreviewLight_0_null,NEXUS_5,1.0,en].png | 3 + ...dingPreviewDark_0_null,NEXUS_5,1.0,en].png | 3 + ...ingPreviewLight_0_null,NEXUS_5,1.0,en].png | 3 + 11 files changed, 270 insertions(+), 51 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncLoading.kt create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt index fd3d293870..0d1b8de7fd 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,6 +17,7 @@ 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 @@ -24,25 +25,38 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable 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( - url = LoginConstants.DEFAULT_HOMESERVER_URL, - supportsPasswordLogin = true, - supportsOidc = false, - ) +class LoginRootPresenter @Inject constructor( + private val authenticationService: MatrixAuthenticationService, +) : 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) } @@ -52,6 +66,7 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: fun handleEvents(event: LoginRootEvents) { when (event) { + LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction) is LoginRootEvents.SetLogin -> updateFormState(formState) { copy(login = event.login) } @@ -59,9 +74,10 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: copy(password = event.password) } LoginRootEvents.Submit -> { + val homeServerDetails = getHomeServerDetailsAction.value.dataOrNull() ?: return when { - homeserver.supportsOidc -> localCoroutineScope.submitOidc(homeserver.url, loggedInState) - homeserver.supportsPasswordLogin -> localCoroutineScope.submit(homeserver.url, formState.value, loggedInState) + homeServerDetails.supportsOidc -> localCoroutineScope.submitOidc(loggedInState) + homeServerDetails.supportsPasswordLogin -> localCoroutineScope.submit(formState.value, loggedInState) } } LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn @@ -69,17 +85,27 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: } return LoginRootState( - homeserverDetails = homeserver, + homeserverUrl = homeserver, + homeserverDetails = getHomeServerDetailsAction.value, loggedInState = loggedInState.value, formState = formState.value, eventSink = ::handleEvents ) } - private fun CoroutineScope.submitOidc(homeserver: String, loggedInState: MutableState) = launch { + private fun CoroutineScope.getHomeServerDetails( + homeserver: String, + state: MutableState>, + ) = launch { + state.value = Async.Loading() + suspend { + authenticationService.setHomeserver(homeserver) + authenticationService.getHomeserverDetails().value!! + }.execute(state) + } + + private fun CoroutineScope.submitOidc(loggedInState: MutableState) = launch { loggedInState.value = LoggedInState.LoggingIn - // TODO rework the setHomeserver flow - authenticationService.setHomeserver(homeserver) authenticationService.getOidcUrl() .onSuccess { loggedInState.value = LoggedInState.OidcStarted(it) @@ -89,10 +115,8 @@ class LoginRootPresenter @Inject constructor(private val authenticationService: } } - private fun CoroutineScope.submit(homeserver: String, formState: LoginFormState, loggedInState: MutableState) = launch { + 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) 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 884a3037df..ccf7523b1b 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,19 +17,22 @@ 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 supportPasswordLogin = homeserverDetails.supportsPasswordLogin - val supportOidcLogin = homeserverDetails.supportsOidc + val supportPasswordLogin = (homeserverDetails as? Async.Success)?.state?.supportsPasswordLogin.orFalse() + val supportOidcLogin = (homeserverDetails as? Async.Success)?.state?.supportsOidc.orFalse() val submitEnabled: Boolean get() = loggedInState !is LoggedInState.ErrorLoggingIn && ((formState.login.isNotEmpty() && formState.password.isNotEmpty()) || supportOidcLogin) 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 cdb92bcfaf..cffa395ea0 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,20 +25,51 @@ open class LoginRootStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLoginRootState(), - aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("some-custom-server.com", supportsPasswordLogin = true, supportsOidc = false)), + aLoginRootState().copy( + homeserverDetails = Async.Success( + MatrixHomeServerDetails( + "some-custom-server.com", + supportsPasswordLogin = true, + supportsOidc = 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(homeserverDetails = MatrixHomeServerDetails("server-with-oidc.org", supportsPasswordLogin = false, supportsOidc = true)), + aLoginRootState().copy( + homeserverUrl = "server-with-oidc.org", + homeserverDetails = Async.Success( + MatrixHomeServerDetails( + "server-with-oidc.org", + supportsPasswordLogin = false, + supportsOidc = true + ) + ) + ), // No password, no oidc support - aLoginRootState().copy(homeserverDetails = MatrixHomeServerDetails("wrong.org", supportsPasswordLogin = false, supportsOidc = false)), + aLoginRootState().copy( + homeserverUrl = "wrong.org", + homeserverDetails = Async.Success( + MatrixHomeServerDetails( + "wrong.org", + supportsPasswordLogin = false, + supportsOidc = false + ) + ) + ), + // Loading + aLoginRootState().copy(homeserverDetails = Async.Loading()), + //Error + aLoginRootState().copy(homeserverDetails = Async.Failure(Exception("An error occurred"))), ) } fun aLoginRootState() = LoginRootState( - homeserverDetails = MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidc = false), + homeserverUrl = "matrix.org", + homeserverDetails = Async.Success(MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidc = 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 c8c85ed50f..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 @@ -146,39 +149,22 @@ fun LoginRootView( ChangeServerSection( interactionEnabled = !isLoading, - homeserver = state.homeserverDetails.url, + homeserver = state.homeserverUrl, onChangeServer = onChangeServer ) Spacer(Modifier.height(32.dp)) - 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) - } - else -> { - Text(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) + when (state.homeserverDetails) { + Async.Uninitialized, + is Async.Loading -> AsyncLoading() + is Async.Failure -> AsyncFailure( + throwable = state.homeserverDetails.error, + onRetry = { + state.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo) + } ) - Spacer(modifier = Modifier.height(32.dp)) + is Async.Success -> ServerDetailForm(state, isLoading, ::submit) } } when (val loggedInState = state.loggedInState) { @@ -195,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, 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/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 From f8dbd31c117d3daf5978114eb175f6d5769dc531 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Apr 2023 21:15:46 +0200 Subject: [PATCH 05/34] Test for Oidc --- .../features/login/impl/oidc/OidcPresenter.kt | 28 ++-- .../features/login/impl/oidc/OidcUrlParser.kt | 2 +- .../login/impl/root/LoginRootPresenter.kt | 6 +- .../changeserver/ChangeServerPresenterTest.kt | 4 +- .../login/impl/oidc/OidcPresenterTest.kt | 145 ++++++++++++++++++ .../login/impl/oidc/OidcUrlParserTest.kt | 59 +++++++ .../login/impl/root/LoginRootPresenterTest.kt | 116 +++++++++++++- .../android/libraries/matrix/test/TestData.kt | 3 +- .../test/auth/FakeAuthenticationService.kt | 26 ++++ 9 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt index 76faa090d1..1c7debfd6f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt @@ -51,25 +51,27 @@ class OidcPresenter @AssistedInject constructor( fun handleCancel() { requestState = Async.Loading() localCoroutineScope.launch { - requestState = try { - authenticationService.cancelOidcLogin() - // Then go back - Async.Success(Unit) - } catch (throwable: Throwable) { - Async.Failure(throwable) - } + 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 { - try { - authenticationService.loginWithOidc(url) - // Then the node tree will be updated, there is nothing to do - } catch (throwable: Throwable) { - requestState = Async.Failure(throwable) - } + authenticationService.loginWithOidc(url) + .onFailure { + requestState = Async.Failure(it) + } + // On success, the node tree will be updated, there is nothing to do } } 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 index eb55368f47..a28e63ede8 100644 --- 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 @@ -32,7 +32,7 @@ class OidcUrlParser { * Return a OidcAction, or null if the url is not a OidcUrl */ fun parse(url: String): OidcAction? { - if (!url.startsWith(OidcConfig.redirectUri)) return null + 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) 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 0d1b8de7fd..cf00075c4e 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 @@ -97,10 +97,12 @@ class LoginRootPresenter @Inject constructor( homeserver: String, state: MutableState>, ) = launch { - state.value = Async.Loading() suspend { authenticationService.setHomeserver(homeserver) - authenticationService.getHomeserverDetails().value!! + .map { + authenticationService.getHomeserverDetails().value!! + } + .getOrThrow() }.execute(state) } 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/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt new file mode 100644 index 0000000000..0fba87f2e3 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt @@ -0,0 +1,145 @@ +/* + * 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 + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.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/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..d13f24c885 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcUrlParserTest.kt @@ -0,0 +1,59 @@ +/* + * 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.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/root/LoginRootPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt index be940feacf..c072656e36 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,16 @@ 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.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 @@ -39,18 +44,79 @@ class LoginRootPresenterTest { 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 presenter = LoginRootPresenter( + authenticationService, + ) + 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 presenter = LoginRootPresenter( + authenticationService, + ) + 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 presenter = LoginRootPresenter( - FakeAuthenticationService(), + authenticationService, ) + authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -67,10 +133,49 @@ class LoginRootPresenterTest { } @Test - fun `present - submit`() = runTest { + fun `present - oidc login`() = runTest { + val authenticationService = FakeAuthenticationService() val presenter = LoginRootPresenter( - FakeAuthenticationService(), + authenticationService, ) + 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 presenter = LoginRootPresenter( + authenticationService, + ) + 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 - submit`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = LoginRootPresenter( + authenticationService, + ) + authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -93,6 +198,7 @@ class LoginRootPresenterTest { val presenter = LoginRootPresenter( authenticationService, ) + authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -116,11 +222,11 @@ class LoginRootPresenterTest { val presenter = LoginRootPresenter( authenticationService, ) + 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/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..be9f41dd6c 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, supportsOidc = false) +val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidc = 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..5b6b8b1e28 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,6 +19,7 @@ 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 kotlinx.coroutines.delay @@ -27,8 +28,12 @@ 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 @@ -62,6 +67,27 @@ class FakeAuthenticationService : MatrixAuthenticationService { 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(100) + 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 } From 4cc305087175d46970ca22fda1010c03dd6ed504 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Apr 2023 21:38:45 +0200 Subject: [PATCH 06/34] Cleanup --- .../element/android/features/login/impl/oidc/OidcNode.kt | 3 --- .../element/android/features/login/impl/oidc/OidcView.kt | 1 - .../features/login/impl/root/LoginRootPresenter.kt | 2 +- .../android/features/login/impl/root/LoginRootState.kt | 2 +- .../features/login/impl/root/LoginRootStateProvider.kt | 8 ++++---- .../libraries/matrix/api/auth/MatrixHomeServerDetails.kt | 2 +- .../libraries/matrix/impl/auth/HomeserverDetails.kt | 2 +- .../io/element/android/libraries/matrix/test/TestData.kt | 4 ++-- 8 files changed, 10 insertions(+), 14 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt index 0122ead10c..b1f9b45237 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt @@ -29,9 +29,6 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails -/** - * TODO Transmit back press to the webview - */ @ContributesNode(AppScope::class) class OidcNode @AssistedInject constructor( @Assisted buildContext: BuildContext, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt index 06591409c9..110678a708 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt @@ -92,7 +92,6 @@ fun OidcView( ) } is Async.Loading -> { - // Indeterminate indicator, to avoid the freeze effect if the connection takes time to initialize. CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) 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 cf00075c4e..5dec3b6537 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 @@ -76,7 +76,7 @@ class LoginRootPresenter @Inject constructor( LoginRootEvents.Submit -> { val homeServerDetails = getHomeServerDetailsAction.value.dataOrNull() ?: return when { - homeServerDetails.supportsOidc -> localCoroutineScope.submitOidc(loggedInState) + homeServerDetails.supportsOidcLogin -> localCoroutineScope.submitOidc(loggedInState) homeServerDetails.supportsPasswordLogin -> localCoroutineScope.submit(formState.value, loggedInState) } } 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 ccf7523b1b..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 @@ -32,7 +32,7 @@ data class LoginRootState( val eventSink: (LoginRootEvents) -> Unit ) { val supportPasswordLogin = (homeserverDetails as? Async.Success)?.state?.supportsPasswordLogin.orFalse() - val supportOidcLogin = (homeserverDetails as? Async.Success)?.state?.supportsOidc.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) 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 cffa395ea0..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 @@ -30,7 +30,7 @@ open class LoginRootStateProvider : PreviewParameterProvider { MatrixHomeServerDetails( "some-custom-server.com", supportsPasswordLogin = true, - supportsOidc = false + supportsOidcLogin = false ) ) ), @@ -45,7 +45,7 @@ open class LoginRootStateProvider : PreviewParameterProvider { MatrixHomeServerDetails( "server-with-oidc.org", supportsPasswordLogin = false, - supportsOidc = true + supportsOidcLogin = true ) ) ), @@ -56,7 +56,7 @@ open class LoginRootStateProvider : PreviewParameterProvider { MatrixHomeServerDetails( "wrong.org", supportsPasswordLogin = false, - supportsOidc = false + supportsOidcLogin = false ) ) ), @@ -69,7 +69,7 @@ open class LoginRootStateProvider : PreviewParameterProvider { fun aLoginRootState() = LoginRootState( homeserverUrl = "matrix.org", - homeserverDetails = Async.Success(MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidc = false)), + homeserverDetails = Async.Success(MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidcLogin = false)), loggedInState = LoggedInState.NotLoggedIn, formState = LoginFormState.Default, eventSink = {} 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 90c54cfe2e..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 supportsOidc: Boolean, + val supportsOidcLogin: Boolean, ): Parcelable 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 9ca3d78456..f1d3e34bf8 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(), - supportsOidc = supportsOidcLogin(), + supportsOidcLogin = supportsOidcLogin(), ) } 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 be9f41dd6c..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,8 +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, supportsPasswordLogin = true, supportsOidc = false) -val A_HOMESERVER_OIDC = MatrixHomeServerDetails(A_HOMESERVER_URL, supportsPasswordLogin = false, supportsOidc = true) +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" From d2b73dd6bc2f29a18f72f785f5c6dbcae47cf986 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 21 Apr 2023 09:49:11 +0200 Subject: [PATCH 07/34] Quality checks --- .../element/android/features/login/impl/oidc/OidcUrlParser.kt | 2 +- .../io/element/android/features/login/impl/oidc/OidcView.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index a28e63ede8..090fd62501 100644 --- 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 @@ -29,7 +29,7 @@ class OidcUrlParser { // On success, we get: // `io.element:/callback?state=IFF1UETGye2ZA8pO&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB` /** - * Return a OidcAction, or null if the url is not a OidcUrl + * 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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt index 110678a708..10834f5afa 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt @@ -71,7 +71,7 @@ fun OidcView( Box(modifier = modifier.statusBarsPadding()) { AndroidView( - modifier = modifier + modifier = Modifier .statusBarsPadding(), factory = { context -> WebView(context).apply { From d2f969252dc99e6dd51be82f49167703c8d36ee6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 12:50:35 +0200 Subject: [PATCH 08/34] Oidc with CustomTab --- app/src/main/AndroidManifest.xml | 8 ++ .../io/element/android/appnav/RootFlowNode.kt | 19 +++- .../android/appnav/intent/IntentResolver.kt | 47 ++++++++++ .../features/login/api/oidc/OidcAction.kt | 22 +++++ .../features/login/api/oidc/OidcActionFlow.kt | 21 +++++ .../login/api/oidc/OidcIntentResolver.kt | 23 +++++ features/login/impl/build.gradle.kts | 1 + .../login/impl/src/main/AndroidManifest.xml | 26 ++++++ .../features/login/impl/LoginFlowNode.kt | 10 ++- .../login/impl/oidc/CustomTabHandler.kt | 87 +++++++++++++++++++ .../impl/oidc/DefaultOidcIntentResolver.kt | 33 +++++++ .../features/login/impl/oidc/OidcEvents.kt | 2 + .../features/login/impl/oidc/OidcPresenter.kt | 1 + .../features/login/impl/oidc/OidcUrlParser.kt | 9 +- .../features/login/impl/oidc/web/CustomTab.kt | 65 ++++++++++++++ .../impl/oidc/web/DefaultOidcActionFlow.kt | 39 +++++++++ .../login/impl/root/LoginRootPresenter.kt | 36 ++++++++ .../login/impl/oidc/OidcPresenterTest.kt | 1 + .../login/impl/oidc/OidcUrlParserTest.kt | 1 + gradle/libs.versions.toml | 2 + libraries/deeplink/build.gradle.kts | 1 + tools/adb/oidc.sh | 24 +++++ 22 files changed, 466 insertions(+), 12 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcAction.kt create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcActionFlow.kt create mode 100644 features/login/api/src/main/kotlin/io/element/android/features/login/api/oidc/OidcIntentResolver.kt create mode 100644 features/login/impl/src/main/AndroidManifest.xml create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/DefaultOidcIntentResolver.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt create mode 100755 tools/adb/oidc.sh diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 22a63b60c9..875f9f0435 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -49,6 +49,14 @@ android:host="open" android:scheme="elementx" /> + + + + + + + + ( 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/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 37cfec229b..f1f56888bb 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 @@ -29,6 +29,7 @@ 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.CustomTabHandler import io.element.android.features.login.impl.oidc.OidcNode import io.element.android.features.login.impl.root.LoginRootNode import io.element.android.libraries.architecture.BackstackNode @@ -42,6 +43,7 @@ import kotlinx.parcelize.Parcelize class LoginFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val customTabHandler: CustomTabHandler, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -50,7 +52,6 @@ class LoginFlowNode @AssistedInject constructor( buildContext = buildContext, plugins = plugins, ) { - sealed interface NavTarget : Parcelable { @Parcelize object Root : NavTarget @@ -71,7 +72,12 @@ class LoginFlowNode @AssistedInject constructor( } override fun onOidcDetails(oidcDetails: OidcDetails) { - backstack.push(NavTarget.OidcView(oidcDetails)) + if (customTabHandler.supportCustomTab()) { + customTabHandler.open(oidcDetails.url) + } else { + // Fallback to WebView mode + backstack.push(NavTarget.OidcView(oidcDetails)) + } } } createNode(buildContext, plugins = listOf(callback)) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt new file mode 100644 index 0000000000..593469a12d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt @@ -0,0 +1,87 @@ +/* + * 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.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.features.login.impl.oidc.web.openUrlInChromeCustomTab +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 + + /** + * 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 + } + + 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(url: String) { + openUrlInChromeCustomTab(context, customTabsSession, false, url) + } +} 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/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt index 0c40f457bf..4f62c6476d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt @@ -16,6 +16,8 @@ package io.element.android.features.login.impl.oidc +import io.element.android.features.login.api.oidc.OidcAction + sealed interface OidcEvents { object Cancel : OidcEvents data class OidcActionEvent(val oidcAction: OidcAction): OidcEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt index 1c7debfd6f..f66caef5b2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt @@ -25,6 +25,7 @@ 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 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 index 090fd62501..487df70253 100644 --- 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 @@ -16,13 +16,15 @@ 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 { +class OidcUrlParser @Inject constructor() { // When user press button "Cancel", we get the url: // `io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO` @@ -40,8 +42,3 @@ class OidcUrlParser { error("Not supported: $url") } } - -sealed interface OidcAction { - object GoBack : OidcAction - data class Success(val url: String) : OidcAction -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt new file mode 100644 index 0000000000..d6c9de7e68 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt @@ -0,0 +1,65 @@ +/* + * 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.web + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +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 openUrlInChromeCustomTab( + context: Context, + 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() + .apply { + intent.flags += Intent.FLAG_ACTIVITY_NEW_TASK + } + .launchUrl(context, 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/web/DefaultOidcActionFlow.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt new file mode 100644 index 0000000000..be4ec88ca2 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.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.web + +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.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(lambda: suspend (OidcAction?) -> Unit) { + mutableStateFlow.collect(lambda) + } +} 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 5dec3b6537..5bf9f2ad05 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 @@ -24,6 +24,8 @@ 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.web.DefaultOidcActionFlow import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter @@ -36,6 +38,7 @@ import javax.inject.Inject class LoginRootPresenter @Inject constructor( private val authenticationService: MatrixAuthenticationService, + private val defaultOidcActionFlow: DefaultOidcActionFlow, ) : Presenter { @Composable @@ -64,6 +67,14 @@ class LoginRootPresenter @Inject constructor( mutableStateOf(LoginFormState.Default) } + LaunchedEffect(Unit) { + launch { + defaultOidcActionFlow.collect { + onOidcAction(it, loggedInState) + } + } + } + fun handleEvents(event: LoginRootEvents) { when (event) { LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction) @@ -131,4 +142,29 @@ class LoginRootPresenter @Inject constructor( 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) + } + } + } + } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt index 0fba87f2e3..f69cecd0f2 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt @@ -22,6 +22,7 @@ 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 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 index d13f24c885..a0275f8f47 100644 --- 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 @@ -17,6 +17,7 @@ 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9783cb1cf7..8e3522055c 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" 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/tools/adb/oidc.sh b/tools/adb/oidc.sh new file mode 100755 index 0000000000..52be556cbb --- /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 From 4951a94af59c128f3cc70b44d734b0edeb1a3b87 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 14:49:15 +0200 Subject: [PATCH 09/34] Create sub packages. --- .../features/login/impl/LoginFlowNode.kt | 9 +++-- .../impl/oidc/CustomTabAvailabilityChecker.kt | 35 +++++++++++++++++++ .../oidc/{ => customtab}/CustomTabHandler.kt | 14 ++------ .../DefaultOidcActionFlow.kt | 2 +- .../CustomTab.kt => customtab/Extensions.kt} | 7 ++-- .../impl/oidc/{ => webview}/OidcEvents.kt | 2 +- .../login/impl/oidc/{ => webview}/OidcNode.kt | 2 +- .../impl/oidc/{ => webview}/OidcPresenter.kt | 2 +- .../impl/oidc/{ => webview}/OidcState.kt | 2 +- .../oidc/{ => webview}/OidcStateProvider.kt | 2 +- .../login/impl/oidc/{ => webview}/OidcView.kt | 4 +-- .../oidc/{ => webview}/OidcWebViewClient.kt | 3 +- .../{ => webview}/WebViewEventListener.kt | 2 +- .../login/impl/root/LoginRootPresenter.kt | 2 +- .../oidc/{ => webview}/OidcPresenterTest.kt | 2 +- 15 files changed, 58 insertions(+), 32 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabAvailabilityChecker.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => customtab}/CustomTabHandler.kt (83%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{web => customtab}/DefaultOidcActionFlow.kt (95%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{web/CustomTab.kt => customtab/Extensions.kt} (94%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcEvents.kt (93%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcNode.kt (96%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcPresenter.kt (98%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcState.kt (93%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcStateProvider.kt (95%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcView.kt (96%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcWebViewClient.kt (94%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/WebViewEventListener.kt (93%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/{ => webview}/OidcPresenterTest.kt (98%) 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 f1f56888bb..833bf4367b 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 @@ -29,8 +29,9 @@ 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.CustomTabHandler -import io.element.android.features.login.impl.oidc.OidcNode +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 @@ -43,6 +44,7 @@ import kotlinx.parcelize.Parcelize class LoginFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, private val customTabHandler: CustomTabHandler, ) : BackstackNode( backstack = BackStack( @@ -72,7 +74,8 @@ class LoginFlowNode @AssistedInject constructor( } override fun onOidcDetails(oidcDetails: OidcDetails) { - if (customTabHandler.supportCustomTab()) { + if (customTabAvailabilityChecker.supportCustomTab()) { + // In this case open a Chrome Custom tab customTabHandler.open(oidcDetails.url) } else { // Fallback to WebView mode 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/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt similarity index 83% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt index 593469a12d..4a84dc76a7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/CustomTabHandler.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.customtab import android.content.ComponentName import android.content.Context @@ -22,7 +22,6 @@ import android.net.Uri import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession -import io.element.android.features.login.impl.oidc.web.openUrlInChromeCustomTab import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject @@ -33,15 +32,6 @@ class CustomTabHandler @Inject constructor( private var customTabsClient: CustomTabsClient? = null private var customTabsServiceConnection: CustomTabsServiceConnection? = null - /** - * 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 - } - fun prepareCustomTab(url: String) { val packageName = CustomTabsClient.getPackageName(context, null) @@ -82,6 +72,6 @@ class CustomTabHandler @Inject constructor( } fun open(url: String) { - openUrlInChromeCustomTab(context, customTabsSession, false, url) + context.openUrlInChromeCustomTab(customTabsSession, false, url) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt index be4ec88ca2..87c22629a0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/DefaultOidcActionFlow.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/DefaultOidcActionFlow.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.web +package io.element.android.features.login.impl.oidc.customtab import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.login.api.oidc.OidcAction diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt index d6c9de7e68..3321c8979a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/web/CustomTab.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.web +package io.element.android.features.login.impl.oidc.customtab import android.content.ActivityNotFoundException import android.content.Context @@ -29,8 +29,7 @@ import androidx.browser.customtabs.CustomTabsSession * If several compatible browsers are installed, the user will be proposed to choose one. * Ref: https://developer.chrome.com/multidevice/android/customtabs. */ -fun openUrlInChromeCustomTab( - context: Context, +fun Context.openUrlInChromeCustomTab( session: CustomTabsSession?, darkTheme: Boolean, url: String @@ -58,7 +57,7 @@ fun openUrlInChromeCustomTab( .apply { intent.flags += Intent.FLAG_ACTIVITY_NEW_TASK } - .launchUrl(context, Uri.parse(url)) + .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/OidcEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt index 4f62c6476d..6265cfc85a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview import io.element.android.features.login.api.oidc.OidcAction diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt index b1f9b45237..dd16b5e57b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt similarity index 98% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt index f66caef5b2..66926b3734 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt index e9b2ac2355..fc9507a89d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt index 7a5552e719..80878cf8f8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.Async diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt index 10834f5afa..47dd4f7a28 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview import android.webkit.WebView import androidx.activity.compose.BackHandler @@ -27,9 +27,9 @@ 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.core.bool.orTrue 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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt index bd7ab99ad6..78c44dcf27 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/OidcWebViewClient.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/OidcWebViewClient.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview import android.annotation.TargetApi import android.os.Build @@ -23,7 +23,6 @@ import android.webkit.WebView import android.webkit.WebViewClient import timber.log.Timber -// TODO Move to a dedicated module class OidcWebViewClient(private val eventListener: WebViewEventListener) : WebViewClient() { @TargetApi(Build.VERSION_CODES.N) override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt index 91fde6c311..acb5c082dc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/WebViewEventListener.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/webview/WebViewEventListener.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview interface WebViewEventListener { /** 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 5bf9f2ad05..ecf6533929 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 @@ -25,7 +25,7 @@ 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.web.DefaultOidcActionFlow +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 diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt similarity index 98% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt index f69cecd0f2..5756cd13d2 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/OidcPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/oidc/webview/OidcPresenterTest.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.features.login.impl.oidc +package io.element.android.features.login.impl.oidc.webview import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow From 0e9c7bc15a5a906c50b981e974665c0e29871b47 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 14:58:25 +0200 Subject: [PATCH 10/34] Start CustomTab from Activity --- .../android/features/login/impl/LoginFlowNode.kt | 13 ++++++++++++- .../login/impl/oidc/customtab/CustomTabHandler.kt | 5 +++-- .../login/impl/oidc/customtab/Extensions.kt | 8 ++------ 3 files changed, 17 insertions(+), 9 deletions(-) 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 833bf4367b..d8f05832f9 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,12 @@ 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.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 @@ -54,6 +57,8 @@ class LoginFlowNode @AssistedInject constructor( buildContext = buildContext, plugins = plugins, ) { + private var activity: Activity? = null + sealed interface NavTarget : Parcelable { @Parcelize object Root : NavTarget @@ -76,7 +81,7 @@ class LoginFlowNode @AssistedInject constructor( override fun onOidcDetails(oidcDetails: OidcDetails) { if (customTabAvailabilityChecker.supportCustomTab()) { // In this case open a Chrome Custom tab - customTabHandler.open(oidcDetails.url) + activity?.let { customTabHandler.open(it, oidcDetails.url) } } else { // Fallback to WebView mode backstack.push(NavTarget.OidcView(oidcDetails)) @@ -96,6 +101,12 @@ class LoginFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { + activity = LocalContext.current as? Activity + DisposableEffect(lifecycle) { + onDispose { + activity = null + } + } Children( navModel = backstack, modifier = modifier, 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 index 4a84dc76a7..059757657d 100644 --- 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 @@ -16,6 +16,7 @@ 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 @@ -71,7 +72,7 @@ class CustomTabHandler @Inject constructor( customTabsServiceConnection = null } - fun open(url: String) { - context.openUrlInChromeCustomTab(customTabsSession, false, url) + fun open(activity: Activity, url: String) { + activity.openUrlInChromeCustomTab(customTabsSession, false, url) } } 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 index 3321c8979a..be98566e7c 100644 --- 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 @@ -16,9 +16,8 @@ package io.element.android.features.login.impl.oidc.customtab +import android.app.Activity import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent @@ -29,7 +28,7 @@ import androidx.browser.customtabs.CustomTabsSession * If several compatible browsers are installed, the user will be proposed to choose one. * Ref: https://developer.chrome.com/multidevice/android/customtabs. */ -fun Context.openUrlInChromeCustomTab( +fun Activity.openUrlInChromeCustomTab( session: CustomTabsSession?, darkTheme: Boolean, url: String @@ -54,9 +53,6 @@ fun Context.openUrlInChromeCustomTab( // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) .apply { session?.let { setSession(it) } } .build() - .apply { - intent.flags += Intent.FLAG_ACTIVITY_NEW_TASK - } .launchUrl(this, Uri.parse(url)) } catch (activityNotFoundException: ActivityNotFoundException) { // TODO context.toast(R.string.error_no_external_application_found) From 0766ec456ce1a9bc53de3ca4d078e57d92256eb0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 15:08:56 +0200 Subject: [PATCH 11/34] Oidc custom tab: avoid replay. --- .../login/impl/oidc/customtab/DefaultOidcActionFlow.kt | 4 ++++ .../android/features/login/impl/root/LoginRootPresenter.kt | 1 + 2 files changed, 5 insertions(+) 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 index 87c22629a0..41ec484298 100644 --- 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 @@ -36,4 +36,8 @@ class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow { suspend fun collect(lambda: suspend (OidcAction?) -> Unit) { mutableStateFlow.collect(lambda) } + + fun reset() { + mutableStateFlow.value = null + } } 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 ecf6533929..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 @@ -166,5 +166,6 @@ class LoginRootPresenter @Inject constructor( } } } + defaultOidcActionFlow.reset() } } From 47af53049e9994282b43f0c7b7e373b7af1cdb12 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 15:21:39 +0200 Subject: [PATCH 12/34] Avoid Custom Chrome tab to appear as recent activity. --- app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 875f9f0435..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"> From 4ecd7a241712952e9e6b96ed9761628cfef0597d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 15:26:03 +0200 Subject: [PATCH 13/34] Fix test script --- tools/adb/oidc.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/adb/oidc.sh b/tools/adb/oidc.sh index 52be556cbb..bcc519f313 100755 --- a/tools/adb/oidc.sh +++ b/tools/adb/oidc.sh @@ -18,7 +18,7 @@ # Format is: # Error -# adb shell am start -a android.intent.action.VIEW -d io.element:/callback?error=access_denied&state=IFF1UETGye2ZA8pO +# 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 +adb shell am start -a android.intent.action.VIEW -d "io.element:/callback?state=IFF1UETGye2ZA8pO\\&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB" From 142c459861aa64123ce5d8842b33ff401338f1af Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 15:37:37 +0200 Subject: [PATCH 14/34] Fix compilation and test --- .../login/impl/root/LoginRootPresenterTest.kt | 18 ++++++++++++++++++ .../android/samples/minimal/LoginScreen.kt | 6 +++++- 2 files changed, 23 insertions(+), 1 deletion(-) 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 c072656e36..7548d50c57 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,6 +20,7 @@ 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.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 @@ -39,6 +40,7 @@ class LoginRootPresenterTest { fun `present - initial state`() = runTest { val presenter = LoginRootPresenter( FakeAuthenticationService(), + DefaultOidcActionFlow(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -56,8 +58,10 @@ class LoginRootPresenterTest { @Test fun `present - initial state server load`() = runTest { val authenticationService = FakeAuthenticationService() + val oidcActionFlow = DefaultOidcActionFlow() val presenter = LoginRootPresenter( authenticationService, + oidcActionFlow, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -80,8 +84,10 @@ class LoginRootPresenterTest { @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() @@ -113,8 +119,10 @@ class LoginRootPresenterTest { @Test fun `present - enter login and password`() = runTest { val authenticationService = FakeAuthenticationService() + val oidcActionFlow = DefaultOidcActionFlow() val presenter = LoginRootPresenter( authenticationService, + oidcActionFlow, ) authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { @@ -135,8 +143,10 @@ class LoginRootPresenterTest { @Test fun `present - oidc login`() = runTest { val authenticationService = FakeAuthenticationService() + val oidcActionFlow = DefaultOidcActionFlow() val presenter = LoginRootPresenter( authenticationService, + oidcActionFlow, ) authenticationService.givenHomeserver(A_HOMESERVER_OIDC) moleculeFlow(RecompositionClock.Immediate) { @@ -153,8 +163,10 @@ class LoginRootPresenterTest { @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) @@ -172,8 +184,10 @@ class LoginRootPresenterTest { @Test fun `present - submit`() = runTest { val authenticationService = FakeAuthenticationService() + val oidcActionFlow = DefaultOidcActionFlow() val presenter = LoginRootPresenter( authenticationService, + oidcActionFlow, ) authenticationService.givenHomeserver(A_HOMESERVER) moleculeFlow(RecompositionClock.Immediate) { @@ -195,8 +209,10 @@ 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) { @@ -219,8 +235,10 @@ 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) { 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( From 5ae2a199dc1465e33b405b2e543cf4b2a1071f84 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 16:17:46 +0200 Subject: [PATCH 15/34] Add test for oidc with custom tab. --- .../login/impl/root/LoginRootPresenterTest.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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 7548d50c57..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,6 +20,7 @@ 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 @@ -181,6 +182,50 @@ class LoginRootPresenterTest { } } + @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() From f4809100d072077479d4d31062d8e7c1fbb71e7d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 17:36:17 +0200 Subject: [PATCH 16/34] Quality --- .../matrix/impl/auth/RustMatrixAuthenticationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b0409cfba3..aaf85ce8fc 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 @@ -141,7 +141,7 @@ class RustMatrixAuthenticationService @Inject constructor( } /** - * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters) + * callbackUrl should be the uriRedirect from OidcClientMetadata (with all the parameters). */ override suspend fun loginWithOidc(callbackUrl: String): Result { return withContext(coroutineDispatchers.io) { From e3c781ad766a0998119687b9576bd9537cc76624 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 18:00:53 +0200 Subject: [PATCH 17/34] Custom tab: dark theme support. --- .../io/element/android/features/login/impl/LoginFlowNode.kt | 5 ++++- .../features/login/impl/oidc/customtab/CustomTabHandler.kt | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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 d8f05832f9..3b9f374bc4 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 @@ -39,6 +39,7 @@ 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 @@ -58,6 +59,7 @@ class LoginFlowNode @AssistedInject constructor( plugins = plugins, ) { private var activity: Activity? = null + private var darkTheme: Boolean = false sealed interface NavTarget : Parcelable { @Parcelize @@ -81,7 +83,7 @@ class LoginFlowNode @AssistedInject constructor( override fun onOidcDetails(oidcDetails: OidcDetails) { if (customTabAvailabilityChecker.supportCustomTab()) { // In this case open a Chrome Custom tab - activity?.let { customTabHandler.open(it, oidcDetails.url) } + activity?.let { customTabHandler.open(it, darkTheme, oidcDetails.url) } } else { // Fallback to WebView mode backstack.push(NavTarget.OidcView(oidcDetails)) @@ -102,6 +104,7 @@ class LoginFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { activity = LocalContext.current as? Activity + darkTheme = !ElementTheme.colors.isLight DisposableEffect(lifecycle) { onDispose { activity = null 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 index 059757657d..407459c5bf 100644 --- 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 @@ -72,7 +72,7 @@ class CustomTabHandler @Inject constructor( customTabsServiceConnection = null } - fun open(activity: Activity, url: String) { - activity.openUrlInChromeCustomTab(customTabsSession, false, url) + fun open(activity: Activity, darkTheme: Boolean, url: String) { + activity.openUrlInChromeCustomTab(customTabsSession, darkTheme, url) } } From ad4141fcc989344268d95f038ee16cfbb3bac1d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Apr 2023 18:04:06 +0200 Subject: [PATCH 18/34] Cleanup --- .../io/element/android/features/login/impl/LoginFlowNode.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 3b9f374bc4..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 @@ -20,6 +20,7 @@ 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 @@ -105,7 +106,7 @@ class LoginFlowNode @AssistedInject constructor( override fun View(modifier: Modifier) { activity = LocalContext.current as? Activity darkTheme = !ElementTheme.colors.isLight - DisposableEffect(lifecycle) { + DisposableEffect(Unit) { onDispose { activity = null } From f6302e4ed3dbf3504de26dbb0b8826ce336782a9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 25 Apr 2023 16:02:38 +0200 Subject: [PATCH 19/34] better api --- .../login/impl/oidc/customtab/DefaultOidcActionFlow.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 41ec484298..17dfa8418f 100644 --- 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 @@ -21,6 +21,7 @@ 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 @@ -33,8 +34,8 @@ class DefaultOidcActionFlow @Inject constructor() : OidcActionFlow { mutableStateFlow.value = oidcAction } - suspend fun collect(lambda: suspend (OidcAction?) -> Unit) { - mutableStateFlow.collect(lambda) + suspend fun collect(collector: FlowCollector) { + mutableStateFlow.collect(collector) } fun reset() { From d9831cb8ab88593bcbcc7b8538c9a9ac78646a5d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 May 2023 15:50:25 +0200 Subject: [PATCH 20/34] Make the application compile with a SDK with no support for Oidc. --- .../features/login/impl/util/LoginConstants.kt | 2 +- .../impl/auth/AuthenticationException.kt | 2 ++ .../matrix/impl/auth/HomeserverDetails.kt | 2 +- .../libraries/matrix/impl/auth/OidcConfig.kt | 5 ++++- .../auth/RustMatrixAuthenticationService.kt | 18 +++++++++++++++--- 5 files changed, 23 insertions(+), 6 deletions(-) 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 c481fdf927..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,6 +18,6 @@ package io.element.android.features.login.impl.util object LoginConstants { - const val DEFAULT_HOMESERVER_URL = "synapse-oidc.lab.element.dev" // "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/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 b98ecc193f..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 @@ -27,11 +27,13 @@ fun Throwable.mapAuthenticationException(): Throwable { 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 f1d3e34bf8..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(), - supportsOidcLogin = supportsOidcLogin(), + 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 index 774c22781b..1ba5063df9 100644 --- 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 @@ -17,8 +17,10 @@ package io.element.android.libraries.matrix.impl.auth import io.element.android.libraries.matrix.api.auth.OidcConfig -import org.matrix.rustcomponents.sdk.OidcClientMetadata +// TODO Oidc +// import org.matrix.rustcomponents.sdk.OidcClientMetadata +/* val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata( clientName = "Element", redirectUri = OidcConfig.redirectUri, @@ -26,4 +28,5 @@ val oidcClientMetadata: OidcClientMetadata = OidcClientMetadata( 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 aaf85ce8fc..4aea938e42 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 @@ -36,7 +36,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder -import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl +// TODO Oidc +// import org.matrix.rustcomponents.sdk.OidcAuthenticationUrl import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.use import java.io.File @@ -55,7 +56,8 @@ class RustMatrixAuthenticationService @Inject constructor( private val authService: RustAuthenticationService = RustAuthenticationService( basePath = baseDirectory.absolutePath, passphrase = null, - oidcClientMetadata = oidcClientMetadata, + // TODO Oidc + // oidcClientMetadata = oidcClientMetadata, customSlidingSyncProxy = null ) private var currentHomeserver = MutableStateFlow(null) @@ -114,9 +116,12 @@ class RustMatrixAuthenticationService @Inject constructor( } } - private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null + // TODO Oidc + // private var pendingUrlForOidcLogin: OidcAuthenticationUrl? = null override suspend fun getOidcUrl(): Result { + TODO("Oidc") + /* return withContext(coroutineDispatchers.io) { runCatching { val urlForOidcLogin = authService.urlForOidcLogin() @@ -127,9 +132,12 @@ class RustMatrixAuthenticationService @Inject constructor( failure.mapAuthenticationException() } } + */ } override suspend fun cancelOidcLogin(): Result { + TODO("Oidc") + /* return withContext(coroutineDispatchers.io) { runCatching { pendingUrlForOidcLogin?.close() @@ -138,12 +146,15 @@ class RustMatrixAuthenticationService @Inject constructor( 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") @@ -156,6 +167,7 @@ class RustMatrixAuthenticationService @Inject constructor( failure.mapAuthenticationException() } } + */ } private fun createMatrixClient(client: Client): MatrixClient { From 44bc6f5ef55188df5ec2307d5e4c6e659b7e1a01 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 May 2023 16:00:37 +0200 Subject: [PATCH 21/34] Record screenshots --- ...aultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...aultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...aultGroup_OidcViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...aultGroup_OidcViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_OidcViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_OidcViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...up_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 3 +++ ...p_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...p_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 +++ ...p_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png | 3 +++ ...p_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png | 3 +++ ...p_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png | 3 +++ 18 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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 From 31e0120f46f972d57c7a0b5681f68a0a8e4e56d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 30 May 2023 16:38:46 +0200 Subject: [PATCH 22/34] Ignore temporary error. --- .../features/login/impl/oidc/webview/OidcWebViewClient.kt | 3 +++ 1 file changed, 3 insertions(+) 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 index 78c44dcf27..2f3a0aee9a 100644 --- 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 @@ -16,6 +16,7 @@ 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 @@ -24,6 +25,8 @@ import android.webkit.WebViewClient import timber.log.Timber 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()) From bbacda4b451d5ccb616660ee87b6335172839b18 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 May 2023 10:34:03 +0200 Subject: [PATCH 23/34] Cleanup after PR review. --- docs/oidc.md | 2 ++ .../features/login/impl/oidc/webview/OidcView.kt | 13 +++++-------- .../login/impl/oidc/webview/OidcWebViewClient.kt | 7 ++++--- .../login/impl/oidc/webview/WebViewEventListener.kt | 6 ++---- .../matrix/test/auth/FakeAuthenticationService.kt | 7 ++++--- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/oidc.md b/docs/oidc.md index 7feae3ce83..5f9e70268d 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -1,3 +1,5 @@ +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 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 index 47dd4f7a28..c1235b76c5 100644 --- 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 @@ -21,7 +21,10 @@ 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 @@ -42,7 +45,7 @@ fun OidcView( modifier: Modifier = Modifier, ) { val oidcUrlParser = remember { OidcUrlParser() } - var webView: WebView? = null + var webView by remember { mutableStateOf(null) } fun shouldOverrideUrl(url: String): Boolean { val action = oidcUrlParser.parse(url) if (action != null) { @@ -53,11 +56,7 @@ fun OidcView( } val oidcWebViewClient = remember { - OidcWebViewClient(eventListener = object : WebViewEventListener { - override fun shouldOverrideUrlLoading(url: String): Boolean { - return shouldOverrideUrl(url) - } - }) + OidcWebViewClient(::shouldOverrideUrl) } BackHandler { @@ -71,8 +70,6 @@ fun OidcView( Box(modifier = modifier.statusBarsPadding()) { AndroidView( - modifier = Modifier - .statusBarsPadding(), factory = { context -> WebView(context).apply { webViewClient = oidcWebViewClient 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 index 2f3a0aee9a..7d8e789715 100644 --- 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 @@ -22,9 +22,10 @@ import android.os.Build import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient -import timber.log.Timber -class OidcWebViewClient(private val eventListener: WebViewEventListener) : 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) @@ -38,7 +39,7 @@ class OidcWebViewClient(private val eventListener: WebViewEventListener) : WebVi } private fun shouldOverrideUrl(url: String): Boolean { - Timber.d("shouldOverrideUrl: $url") + // 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 index acb5c082dc..446754aced 100644 --- 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 @@ -16,14 +16,12 @@ package io.element.android.features.login.impl.oidc.webview -interface WebViewEventListener { +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 { - return false - } + fun shouldOverrideUrlLoading(url: String): Boolean } 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 5b6b8b1e28..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 @@ -22,6 +22,7 @@ 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 @@ -58,12 +59,12 @@ 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) } @@ -76,7 +77,7 @@ class FakeAuthenticationService : MatrixAuthenticationService { } override suspend fun loginWithOidc(callbackUrl: String): Result { - delay(100) + delay(FAKE_DELAY_IN_MS) return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) } From b83dc0fd61c8b2e3834308a731e6f91e82223c3d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 May 2023 11:40:32 +0200 Subject: [PATCH 24/34] Add codeStyles generated file from Android Studio to VCS. --- .idea/codeStyles/Project.xml | 123 +++++++++++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 ++ 2 files changed, 128 insertions(+) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000..7643783a82 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ 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 From fccf194e7bab7507c66875a42b3a19c18bbe18e2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 May 2023 11:46:01 +0200 Subject: [PATCH 25/34] Formatting: set LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY to `false`. New default value for Android Studio is `true`. --- .idea/codeStyles/Project.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7643783a82..cdef735570 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,7 @@ + From f49e2dd4e387774ea93cc01159f341dac9ca6ef3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 May 2023 11:48:26 +0200 Subject: [PATCH 26/34] Add a shared dictionary to the project. --- .gitignore | 1 - .idea/dictionaries/shared.xml | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .idea/dictionaries/shared.xml 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/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 + + + From 7f71b2d81df8e24f6406297f26042dda14a75bb5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 May 2023 15:18:42 +0200 Subject: [PATCH 27/34] Search for forbidden patterns in Kotlin files. --- .github/workflows/quality.yml | 8 ++ tools/check/check_code_quality.sh | 55 +++++++++ tools/check/forbidden_strings_in_code.txt | 131 ++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100755 tools/check/check_code_quality.sh create mode 100755 tools/check/forbidden_strings_in_code.txt 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/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_] From becbc6607d256a9b107cf51d2ecc91f856189d79 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 May 2023 15:31:05 +0200 Subject: [PATCH 28/34] Fix detected forbidden pattern. --- libraries/matrix/impl/build.gradle.kts | 1 + .../libraries/matrix/impl/RustMatrixClient.kt | 3 ++ .../auth/RustMatrixAuthenticationService.kt | 4 +- .../matrix/impl/room/RustMatrixRoom.kt | 6 ++- .../libraries/push/impl/PushersManager.kt | 3 +- .../notifications/NotifiableEventResolver.kt | 50 +++++++++---------- .../impl/di/SessionStorageModule.kt | 4 +- 7 files changed, 39 insertions(+), 32 deletions(-) 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..ce76980a6f 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 @@ -44,6 +44,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 +78,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()) @@ -215,6 +217,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/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index aff83c3b5a..f03ba4562c 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 @@ -25,10 +25,10 @@ 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.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 @@ -49,6 +49,7 @@ 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) @@ -115,6 +116,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/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/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/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) } } From d4284f167ac2af3aa4c7f40114186be5f4f1d773 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 31 May 2023 15:47:24 +0200 Subject: [PATCH 29/34] Fix compilation issue. --- .../kotlin/io/element/android/samples/minimal/MainActivity.kt | 2 ++ 1 file changed, 2 insertions(+) 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() ) } From c74978867721a007588748958ce8a8cdaf6763a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 31 May 2023 16:46:58 +0200 Subject: [PATCH 30/34] Enable gif support for Coil --- gradle/libs.versions.toml | 1 + libraries/matrixui/build.gradle.kts | 1 + .../libraries/matrix/ui/media/ImageLoaderFactories.kt | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 150f35c885..7663a06c78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -127,6 +127,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" } 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/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)) From 6bc48084b83c3fbac3fc02ccf902e5c34320e1ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 17:57:50 +0000 Subject: [PATCH 31/34] Update plugin sonarqube to v4.2.0.3129 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 43d18e320c..6e949900a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -180,6 +180,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" } From 473bfd1e23dc59a5888a68063315e84ef8d24745 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 1 Jun 2023 09:40:45 +0100 Subject: [PATCH 32/34] Display a notice if Matrix ID isn't resolved (#461) Display a notice if Matrix ID isn't resolved If we can't get the profile of a user after an mxid was searched for, show a warning under their ID to say the invite probably won't be delivered. Closes #424 --- changelog.d/424.feature | 1 + .../SearchMultipleUsersResultItem.kt | 45 +++--- .../components/SearchSingleUserResultItem.kt | 39 +++-- .../impl/components/SearchUserBar.kt | 19 +-- .../impl/userlist/DefaultUserListPresenter.kt | 4 +- .../createroom/impl/userlist/UserListState.kt | 3 +- .../impl/userlist/UserListStateProvider.kt | 5 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../userlist/DefaultUserListPresenterTests.kt | 9 +- .../roomdetails/impl/RoomDetailsView.kt | 5 +- .../impl/invite/RoomInviteMembersPresenter.kt | 9 +- .../impl/invite/RoomInviteMembersState.kt | 1 + .../invite/RoomInviteMembersStateProvider.kt | 14 ++ .../impl/invite/RoomInviteMembersView.kt | 43 +++-- .../invite/RoomInviteMembersPresenterTest.kt | 57 ++++++- .../matrix/ui/components/UnresolvedUserRow.kt | 150 ++++++++++++++++++ .../usersearch/api/UserRepository.kt | 3 +- .../usersearch/api/UserSearchResult.kt | 24 +++ .../usersearch/impl/MatrixUserRepository.kt | 17 +- .../impl/MatrixUserRepositoryTest.kt | 17 +- .../usersearch/test/FakeUserRepository.kt | 8 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ItemDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 - ...temLightPreview_0_null,NEXUS_5,1.0,en].png | 3 - ...sultItemPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ItemDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 - ...temLightPreview_0_null,NEXUS_5,1.0,en].png | 3 - ...sultItemPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...onDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...onDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...onDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...onDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...nLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...nLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...nLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...nLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...ViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 +- ...iewLightPreview_0_null,NEXUS_5,1.0,en].png | 2 +- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wDarkPreview_0_null_10,NEXUS_5,1.0,en].png | 2 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 2 +- ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 2 +- ...ewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 2 +- ...ewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...LightPreview_0_null_10,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_6,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_7,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_8,NEXUS_5,1.0,en].png | 2 +- ...wLightPreview_0_null_9,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...reenDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 +- ...eenLightPreview_0_null,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 2 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...rsDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + ...sLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + ...dUserRowPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...dUserRowPreview_0_null,NEXUS_5,1.0,en].png | 3 + 89 files changed, 503 insertions(+), 216 deletions(-) create mode 100644 changelog.d/424.feature create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnresolvedUserRow.kt create mode 100644 libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt delete mode 100644 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 delete mode 100644 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 create mode 100644 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 delete mode 100644 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 delete mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt index cf13768a82..7dd0ce3e3c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchMultipleUsersResultItem.kt @@ -22,40 +22,49 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow +import io.element.android.libraries.matrix.ui.components.CheckableUnresolvedUserRow import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.usersearch.api.UserSearchResult @Composable fun SearchMultipleUsersResultItem( - matrixUser: MatrixUser, + searchResult: UserSearchResult, isUserSelected: Boolean, modifier: Modifier = Modifier, onCheckedChange: (Boolean) -> Unit = {}, ) { - CheckableMatrixUserRow( - checked = isUserSelected, - modifier = modifier, - matrixUser = matrixUser, - avatarSize = AvatarSize.Custom(36.dp), - onCheckedChange = onCheckedChange, - ) + if (searchResult.isUnresolved) { + CheckableUnresolvedUserRow( + checked = isUserSelected, + modifier = modifier, + avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), + id = searchResult.matrixUser.userId.value, + onCheckedChange = onCheckedChange, + ) + } else { + CheckableMatrixUserRow( + checked = isUserSelected, + modifier = modifier, + matrixUser = searchResult.matrixUser, + avatarSize = AvatarSize.Custom(36.dp), + onCheckedChange = onCheckedChange, + ) + } } @Preview @Composable -internal fun SearchMultipleUsersResultItemLightPreview() = ElementPreviewLight { ContentToPreview() } - -@Preview -@Composable -internal fun SearchMultipleUsersResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() } +internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { ContentToPreview() } @Composable private fun ContentToPreview() { Column { - SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = true) - SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = false) + SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = false) + SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false), isUserSelected = true) + SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = false) + SearchMultipleUsersResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true), isUserSelected = true) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt index 8dcaeb2bdf..f1279cfdec 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchSingleUserResultItem.kt @@ -17,39 +17,48 @@ package io.element.android.features.createroom.impl.components import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.matrix.ui.components.UnresolvedUserRow import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.model.getAvatarData +import io.element.android.libraries.usersearch.api.UserSearchResult @Composable fun SearchSingleUserResultItem( - matrixUser: MatrixUser, + searchResult: UserSearchResult, modifier: Modifier = Modifier, onClick: () -> Unit = {}, ) { - MatrixUserRow( - modifier = modifier.clickable(onClick = onClick), - matrixUser = matrixUser, - avatarSize = AvatarSize.Custom(36.dp), - ) + if (searchResult.isUnresolved) { + UnresolvedUserRow( + modifier = modifier.clickable(onClick = onClick), + avatarData = searchResult.matrixUser.getAvatarData(AvatarSize.Custom(36.dp)), + id = searchResult.matrixUser.userId.value, + ) + } else { + MatrixUserRow( + modifier = modifier.clickable(onClick = onClick), + matrixUser = searchResult.matrixUser, + avatarSize = AvatarSize.Custom(36.dp), + ) + } } @Preview @Composable -internal fun SearchSingleUserResultItemLightPreview() = ElementPreviewLight { ContentToPreview() } - -@Preview -@Composable -internal fun SearchSingleUserResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() } +internal fun SearchSingleUserResultItemPreview() = ElementThemedPreview{ ContentToPreview() } @Composable private fun ContentToPreview() { - SearchSingleUserResultItem(matrixUser = aMatrixUser()) + Column { + SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = false)) + SearchSingleUserResultItem(searchResult = UserSearchResult(aMatrixUser(), isUnresolved = true)) + } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt index be87e1cf51..bbbcd715e5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/SearchUserBar.kt @@ -29,12 +29,13 @@ import io.element.android.libraries.designsystem.theme.components.SearchBarResul import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.SelectedUsersList import io.element.android.libraries.ui.strings.R +import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList @Composable fun SearchUserBar( query: String, - state: SearchBarResultState>, + state: SearchBarResultState>, selectedUsers: ImmutableList, active: Boolean, isMultiSelectionEnabled: Boolean, @@ -68,26 +69,26 @@ fun SearchUserBar( resultHandler = { users -> LazyColumn { if (isMultiSelectionEnabled) { - items(users) { matrixUser -> + items(users) { searchResult -> SearchMultipleUsersResultItem( modifier = Modifier.fillMaxWidth(), - matrixUser = matrixUser, - isUserSelected = selectedUsers.find { it.userId == matrixUser.userId } != null, + searchResult = searchResult, + isUserSelected = selectedUsers.find { it.userId == searchResult.matrixUser.userId } != null, onCheckedChange = { checked -> if (checked) { - onUserSelected(matrixUser) + onUserSelected(searchResult.matrixUser) } else { - onUserDeselected(matrixUser) + onUserDeselected(searchResult.matrixUser) } } ) } } else { - items(users) { matrixUser -> + items(users) { searchResult -> SearchSingleUserResultItem( modifier = Modifier.fillMaxWidth(), - matrixUser = matrixUser, - onClick = { onUserSelected(matrixUser) } + searchResult = searchResult, + onClick = { onUserSelected(searchResult.matrixUser) } ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/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/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/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/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/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt index bbb124c3c5..03e9952c92 100644 --- a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt +++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserRepository.kt @@ -16,10 +16,9 @@ package io.element.android.libraries.usersearch.api -import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.coroutines.flow.Flow interface UserRepository { - suspend fun search(query: String): Flow> + suspend fun search(query: String): Flow> } diff --git a/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt new file mode 100644 index 0000000000..e67a7af46f --- /dev/null +++ b/libraries/usersearch/api/src/main/kotlin/io/element/android/libraries/usersearch/api/UserSearchResult.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.usersearch.api + +import io.element.android.libraries.matrix.api.user.MatrixUser + +data class UserSearchResult( + val matrixUser: MatrixUser, + val isUnresolved: Boolean = false, +) diff --git a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt index 7577e8f6e8..fff9218556 100644 --- a/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt +++ b/libraries/usersearch/impl/src/main/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepository.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.usersearch.api.UserListDataSource import io.element.android.libraries.usersearch.api.UserRepository +import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -33,24 +34,26 @@ class MatrixUserRepository @Inject constructor( private val dataSource: UserListDataSource ) : UserRepository { - override suspend fun search(query: String): Flow> = flow { + override suspend fun search(query: String): Flow> = flow { // Manually add a fake result with the matrixId, if any val isUserId = MatrixPatterns.isUserId(query) if (isUserId) { - emit(listOf(MatrixUser(UserId(query)))) + emit(listOf(UserSearchResult(MatrixUser(UserId(query))))) } if (query.length >= MINIMUM_SEARCH_LENGTH) { // Debounce delay(DEBOUNCE_TIME_MILLIS) - val results = dataSource.search(query).toMutableList() + val results = dataSource.search(query).map { UserSearchResult(it) }.toMutableList() // If the query is a user ID and the result doesn't contain that user ID, query the profile information explicitly - if (isUserId && results.none { it.userId.value == query }) { - val getProfileResult: MatrixUser? = dataSource.getProfile(UserId(query)) - val profile = getProfileResult ?: MatrixUser(UserId(query)) - results.add(0, profile) + if (isUserId && results.none { it.matrixUser.userId.value == query }) { + results.add( + 0, + dataSource.getProfile(UserId(query)) + ?.let { UserSearchResult(it) } + ?: UserSearchResult(MatrixUser(UserId(query)), isUnresolved = true)) } emit(results) diff --git a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt index e2eab067e6..b2327ef750 100644 --- a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt +++ b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserRepositoryTest.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.usersearch.api.UserSearchResult import io.element.android.libraries.usersearch.test.FakeUserListDataSource import kotlinx.coroutines.test.runTest import org.junit.Test @@ -63,7 +64,7 @@ internal class MatrixUserRepositoryTest { val result = repository.search("some query") result.test { - assertThat(awaitItem()).isEqualTo(aMatrixUserList()) + assertThat(awaitItem()).isEqualTo(aMatrixUserList().toUserSearchResults()) awaitComplete() } } @@ -76,7 +77,7 @@ internal class MatrixUserRepositoryTest { val result = repository.search(A_USER_ID.value) result.test { - assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID))) + assertThat(awaitItem()).isEqualTo(listOf(placeholderResult())) skipItems(1) awaitComplete() } @@ -93,7 +94,7 @@ internal class MatrixUserRepositoryTest { result.test { skipItems(1) - assertThat(awaitItem()).isEqualTo(searchResults) + assertThat(awaitItem()).isEqualTo(searchResults.toUserSearchResults()) awaitComplete() } } @@ -112,13 +113,13 @@ internal class MatrixUserRepositoryTest { result.test { skipItems(1) - assertThat(awaitItem()).isEqualTo(listOf(userProfile) + searchResults) + assertThat(awaitItem()).isEqualTo((listOf(userProfile) + searchResults).toUserSearchResults()) awaitComplete() } } @Test - fun `search - just shows id if profile can't be loaded`() = runTest { + fun `search - returns unresolved user if profile can't be loaded`() = runTest { val searchResults = aMatrixUserListWithoutUserId(A_USER_ID) val dataSource = FakeUserListDataSource() @@ -130,11 +131,15 @@ internal class MatrixUserRepositoryTest { result.test { skipItems(1) - assertThat(awaitItem()).isEqualTo(listOf(MatrixUser(userId = A_USER_ID)) + searchResults) + assertThat(awaitItem()).isEqualTo(listOf(placeholderResult(isUnresolved = true)) + searchResults.toUserSearchResults()) awaitComplete() } } private fun aMatrixUserListWithoutUserId(userId: UserId) = aMatrixUserList().filterNot { it.userId == userId } + private fun List.toUserSearchResults() = map { UserSearchResult(it) } + + private fun placeholderResult(id: UserId = A_USER_ID, isUnresolved: Boolean = false) = UserSearchResult(MatrixUser(id), isUnresolved = isUnresolved) + } diff --git a/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt b/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt index ea0d93a197..0911d86b36 100644 --- a/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt +++ b/libraries/usersearch/test/src/main/kotlin/io/element/android/libraries/usersearch/test/FakeUserRepository.kt @@ -16,8 +16,8 @@ package io.element.android.libraries.usersearch.test -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.usersearch.api.UserRepository +import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -26,14 +26,14 @@ class FakeUserRepository : UserRepository { var providedQuery: String? = null private set - private val flow = MutableSharedFlow>() + private val flow = MutableSharedFlow>() - override suspend fun search(query: String): Flow> { + override suspend fun search(query: String): Flow> { providedQuery = query return flow } - suspend fun emitResult(result: List) { + suspend fun emitResult(result: List) { flow.emit(result) } diff --git a/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.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.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 From 9efbe52c0d8fb286b1c8646fdf2a81f8a5108e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Thu, 1 Jun 2023 07:35:20 +0200 Subject: [PATCH 33/34] Remove duplicate issue reporting by Danger --- tools/danger/dangerfile-lint.js | 5 +++++ 1 file changed, 5 insertions(+) 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, })) From a36f4b794748338a832b3379283ba9083e02b893 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 1 Jun 2023 12:03:27 +0200 Subject: [PATCH 34/34] [Room details] Open room member details when clicking on user data in timeline (#482) --- .../io/element/android/appnav/RoomFlowNode.kt | 15 ++++++++++++++- .../element/android/appnav/RoomFlowNodeTest.kt | 7 ++++++- changelog.d/480.feature | 1 + .../features/messages/api/MessagesEntryPoint.kt | 2 ++ .../features/messages/impl/MessagesFlowNode.kt | 5 +++++ .../features/messages/impl/MessagesNode.kt | 7 +++++++ .../features/messages/impl/MessagesView.kt | 8 +++++++- .../messages/impl/timeline/TimelineView.kt | 14 ++++++++++++++ features/roomdetails/api/build.gradle.kts | 1 + .../roomdetails/api/RoomDetailsEntryPoint.kt | 17 ++++++++++++++++- .../impl/DefaultRoomDetailsEntryPoint.kt | 16 ++++++++++++++-- .../roomdetails/impl/RoomDetailsFlowNode.kt | 6 ++++-- .../members/details/RoomMemberDetailsNode.kt | 6 +++--- .../details/RoomMemberDetailsPresenter.kt | 4 ++-- .../matrix/ui/room/MatrixRoomMembers.kt | 6 +++--- 15 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 changelog.d/480.feature diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 0493ae7db1..fb3a8d566e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.services.appnavstate.api.AppNavigationStateService @@ -119,11 +120,20 @@ class RoomFlowNode @AssistedInject constructor( override fun onRoomDetailsClicked() { backstack.push(NavTarget.RoomDetails) } + + override fun onUserDataClicked(userId: UserId) { + backstack.push(NavTarget.RoomMemberDetails(userId)) + } } messagesEntryPoint.createNode(this, buildContext, callback) } NavTarget.RoomDetails -> { - 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/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/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/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/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/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 {