diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 613316a2e74..df86219cd14 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -37,6 +37,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.replace import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.room.RoomFlowNode @@ -197,6 +198,8 @@ class LoggedInFlowNode @AssistedInject constructor( @Parcelize data class Room( val roomIdOrAlias: RoomIdOrAlias, + val serverNames: List = emptyList(), + val trigger: JoinedRoom.Trigger? = null, val roomDescription: RoomDescription? = null, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages() ) : NavTarget @@ -292,8 +295,9 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push( NavTarget.Room( roomIdOrAlias = data.roomIdOrAlias, + serverNames = data.viaParameters, + trigger = JoinedRoom.Trigger.Timeline, initialElement = RoomNavigationTarget.Messages(data.eventId), - // TODO Use the viaParameters ) ) } @@ -311,6 +315,8 @@ class LoggedInFlowNode @AssistedInject constructor( val inputs = RoomFlowNode.Inputs( roomIdOrAlias = navTarget.roomIdOrAlias, roomDescription = Optional.ofNullable(navTarget.roomDescription), + serverNames = navTarget.serverNames, + trigger = Optional.ofNullable(navTarget.trigger), initialElement = navTarget.initialElement ) createNode(buildContext, plugins = listOf(inputs, callback)) @@ -371,7 +377,13 @@ class LoggedInFlowNode @AssistedInject constructor( roomDirectoryEntryPoint.nodeBuilder(this, buildContext) .callback(object : RoomDirectoryEntryPoint.Callback { override fun onResultClicked(roomDescription: RoomDescription) { - backstack.push(NavTarget.Room(roomDescription.roomId.toRoomIdOrAlias(), roomDescription)) + backstack.push( + NavTarget.Room( + roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(), + roomDescription = roomDescription, + trigger = JoinedRoom.Trigger.RoomDirectory, + ) + ) } }) .build() @@ -379,7 +391,12 @@ class LoggedInFlowNode @AssistedInject constructor( } } - suspend fun attachRoom(roomIdOrAlias: RoomIdOrAlias, eventId: EventId? = null) { + suspend fun attachRoom( + roomIdOrAlias: RoomIdOrAlias, + serverNames: List = emptyList(), + trigger: JoinedRoom.Trigger? = null, + eventId: EventId? = null, + ) { waitForNavTargetAttached { navTarget -> navTarget is NavTarget.RoomList } @@ -387,6 +404,8 @@ class LoggedInFlowNode @AssistedInject constructor( backstack.push( NavTarget.Room( roomIdOrAlias = roomIdOrAlias, + serverNames = serverNames, + trigger = trigger, initialElement = RoomNavigationTarget.Messages( focusedEventId = eventId ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index db04345a4ef..4b886d9c992 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -34,6 +34,7 @@ import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.di.MatrixClientsHolder import io.element.android.appnav.intent.IntentResolver @@ -295,6 +296,8 @@ class RootFlowNode @AssistedInject constructor( is PermalinkData.RoomLink -> { attachRoom( roomIdOrAlias = permalinkData.roomIdOrAlias, + trigger = JoinedRoom.Trigger.MobilePermalink, + serverNames = permalinkData.viaParameters, eventId = permalinkData.eventId, ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index 800b7037cff..70337e8d339 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -32,6 +32,7 @@ import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode @@ -54,6 +55,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.room.CurrentUserMembership +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn @@ -83,6 +85,8 @@ class RoomFlowNode @AssistedInject constructor( data class Inputs( val roomIdOrAlias: RoomIdOrAlias, val roomDescription: Optional, + val serverNames: List, + val trigger: Optional, val initialElement: RoomNavigationTarget, ) : NodeInputs @@ -96,7 +100,11 @@ class RoomFlowNode @AssistedInject constructor( data class Resolving(val roomAlias: RoomAlias) : NavTarget @Parcelize - data class JoinRoom(val roomId: RoomId) : NavTarget + data class JoinRoom( + val roomId: RoomId, + val serverNames: List, + val trigger: im.vector.app.features.analytics.plan.JoinedRoom.Trigger, + ) : NavTarget @Parcelize data class JoinedRoom(val roomId: RoomId) : NavTarget @@ -114,13 +122,13 @@ class RoomFlowNode @AssistedInject constructor( backstack.newRoot(NavTarget.Resolving(i.roomAlias)) } is RoomIdOrAlias.Id -> { - subscribeToRoomInfoFlow(i.roomId) + subscribeToRoomInfoFlow(i.roomId, inputs.serverNames) } } } } - private fun subscribeToRoomInfoFlow(roomId: RoomId) { + private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List) { val roomInfoFlow = client.getRoomInfoFlow( roomId = roomId ).map { it.getOrNull() } @@ -136,7 +144,13 @@ class RoomFlowNode @AssistedInject constructor( // we can have a space here in case the space has just been joined. // So navigate to the JoinRoom target for now, which will // handle the space not supported screen - backstack.newRoot(NavTarget.JoinRoom(roomId)) + backstack.newRoot( + NavTarget.JoinRoom( + roomId = roomId, + serverNames = serverNames, + trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, + ) + ) } else { backstack.newRoot(NavTarget.JoinedRoom(roomId)) } @@ -147,7 +161,13 @@ class RoomFlowNode @AssistedInject constructor( } else -> { // Was invited or the room is not known, display the join room screen - backstack.newRoot(NavTarget.JoinRoom(roomId)) + backstack.newRoot( + NavTarget.JoinRoom( + roomId = roomId, + serverNames = serverNames, + trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, + ) + ) } } }.launchIn(lifecycleScope) @@ -158,8 +178,11 @@ class RoomFlowNode @AssistedInject constructor( is NavTarget.Loading -> loadingNode(buildContext) is NavTarget.Resolving -> { val callback = object : RoomAliasResolverEntryPoint.Callback { - override fun onAliasResolved(roomId: RoomId) { - subscribeToRoomInfoFlow(roomId) + override fun onAliasResolved(data: ResolvedRoomAlias) { + subscribeToRoomInfoFlow( + roomId = data.roomId, + serverNames = data.servers, + ) } } val params = RoomAliasResolverEntryPoint.Params(navTarget.roomAlias) @@ -173,6 +196,8 @@ class RoomFlowNode @AssistedInject constructor( roomId = navTarget.roomId, roomIdOrAlias = inputs.roomIdOrAlias, roomDescription = inputs.roomDescription, + serverNames = navTarget.serverNames, + trigger = navTarget.trigger, ) joinRoomEntryPoint.createNode(this, buildContext, inputs) } diff --git a/changelog.d/2843.misc b/changelog.d/2843.misc new file mode 100644 index 00000000000..7141e460d68 --- /dev/null +++ b/changelog.d/2843.misc @@ -0,0 +1 @@ +Use via parameters when joining a room from permalink. diff --git a/features/invite/api/build.gradle.kts b/features/invite/api/build.gradle.kts index 52df82e38a6..95fceb41f25 100644 --- a/features/invite/api/build.gradle.kts +++ b/features/invite/api/build.gradle.kts @@ -25,4 +25,5 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.services.analytics.api) } diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt index ec18aea0459..05ba21b98d7 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenter.kt @@ -33,9 +33,8 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.push.api.notifications.NotificationDrawerManager -import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.util.Optional @@ -44,7 +43,7 @@ import kotlin.jvm.optionals.getOrNull class AcceptDeclineInvitePresenter @Inject constructor( private val client: MatrixClient, - private val analyticsService: AnalyticsService, + private val joinRoom: JoinRoom, private val notificationDrawerManager: NotificationDrawerManager, ) : Presenter { @Composable @@ -59,9 +58,11 @@ class AcceptDeclineInvitePresenter @Inject constructor( fun handleEvents(event: AcceptDeclineInviteEvents) { when (event) { is AcceptDeclineInviteEvents.AcceptInvite -> { - currentInvite = Optional.of(event.invite) - localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) + // currentInvite is used to render the decline confirmation dialog + // and to reuse the roomId when the user confirm the rejection of the invitation. + // Just set it to empty here. currentInvite = Optional.empty() + localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) } is AcceptDeclineInviteEvents.DeclineInvite -> { @@ -100,14 +101,18 @@ class AcceptDeclineInvitePresenter @Inject constructor( ) } - private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState>) = launch { + private fun CoroutineScope.acceptInvite( + roomId: RoomId, + acceptedAction: MutableState>, + ) = launch { acceptedAction.runUpdatingState { - client.joinRoom(roomId) + joinRoom( + roomId = roomId, + serverNames = emptyList(), + trigger = JoinedRoom.Trigger.Invite, + ) .onSuccess { notificationDrawerManager.clearMembershipNotificationForRoom(client.sessionId, roomId, doRender = true) - client.getRoom(roomId)?.use { room -> - analyticsService.capture(room.toAnalyticsJoinedRoom(JoinedRoom.Trigger.Invite)) - } } .map { roomId } } diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt index 90dcc104d4f..247df956e64 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.invite.impl.response import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.InviteData import io.element.android.libraries.architecture.AsyncAction @@ -26,13 +27,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager -import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -163,13 +164,10 @@ class AcceptDeclineInvitePresenterTest { @Test fun `present - accepting invite error flow`() = runTest { - val joinRoomFailure = lambdaRecorder { roomId: RoomId -> + val joinRoomFailure = lambdaRecorder { roomId: RoomId, _: List, _: JoinedRoom.Trigger -> Result.failure(RuntimeException("Failed to join room $roomId")) } - val client = FakeMatrixClient().apply { - joinRoomLambda = joinRoomFailure - } - val presenter = createAcceptDeclineInvitePresenter(client = client) + val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomFailure) presenter.test { val inviteData = anInviteData() awaitItem().also { state -> @@ -177,33 +175,35 @@ class AcceptDeclineInvitePresenterTest { AcceptDeclineInviteEvents.AcceptInvite(inviteData) ) } - skipItems(1) awaitItem().also { state -> - assertThat(state.invite).isEqualTo(Optional.of(inviteData)) + assertThat(state.invite).isEqualTo(Optional.empty()) + assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java) state.eventSink( InternalAcceptDeclineInviteEvents.DismissAcceptError ) } - skipItems(1) awaitItem().also { state -> assertThat(state.invite).isEqualTo(Optional.empty()) assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java) } cancelAndConsumeRemainingEvents() } - assert(joinRoomFailure).isCalledOnce() + assert(joinRoomFailure) + .isCalledExactly(1) + .withSequence( + listOf(value(A_ROOM_ID), value(emptyList()), value(JoinedRoom.Trigger.Invite)) + ) } @Test fun `present - accepting invite success flow`() = runTest { - val joinRoomSuccess = lambdaRecorder { _: RoomId -> + val joinRoomSuccess = lambdaRecorder { _: RoomId, _: List, _: JoinedRoom.Trigger -> Result.success(Unit) } - val client = FakeMatrixClient().apply { - joinRoomLambda = joinRoomSuccess - } - val presenter = createAcceptDeclineInvitePresenter(client = client) + val presenter = createAcceptDeclineInvitePresenter(joinRoomLambda = joinRoomSuccess) presenter.test { val inviteData = anInviteData() awaitItem().also { state -> @@ -211,14 +211,20 @@ class AcceptDeclineInvitePresenterTest { AcceptDeclineInviteEvents.AcceptInvite(inviteData) ) } - skipItems(1) awaitItem().also { state -> - assertThat(state.invite).isEqualTo(Optional.of(inviteData)) + assertThat(state.invite).isEqualTo(Optional.empty()) + assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java) } cancelAndConsumeRemainingEvents() } - assert(joinRoomSuccess).isCalledOnce() + assert(joinRoomSuccess) + .isCalledExactly(1) + .withSequence( + listOf(value(A_ROOM_ID), value(emptyList()), value(JoinedRoom.Trigger.Invite)) + ) } private fun anInviteData( @@ -235,12 +241,14 @@ class AcceptDeclineInvitePresenterTest { private fun createAcceptDeclineInvitePresenter( client: MatrixClient = FakeMatrixClient(), - analyticsService: AnalyticsService = FakeAnalyticsService(), + joinRoomLambda: (RoomId, List, JoinedRoom.Trigger) -> Result = { _, _, _ -> + Result.success(Unit) + }, notificationDrawerManager: NotificationDrawerManager = FakeNotificationDrawerManager(), ): AcceptDeclineInvitePresenter { return AcceptDeclineInvitePresenter( client = client, - analyticsService = analyticsService, + joinRoom = FakeJoinRoom(joinRoomLambda), notificationDrawerManager = notificationDrawerManager, ) } diff --git a/features/joinroom/api/build.gradle.kts b/features/joinroom/api/build.gradle.kts index a016c2d195a..461723f785b 100644 --- a/features/joinroom/api/build.gradle.kts +++ b/features/joinroom/api/build.gradle.kts @@ -26,4 +26,5 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.features.roomdirectory.api) + implementation(projects.services.analytics.api) } diff --git a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt index d62a9819c9a..04ca23340ea 100644 --- a/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt +++ b/features/joinroom/api/src/main/kotlin/io/element/android/features/joinroom/api/JoinRoomEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.joinroom.api import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.NodeInputs @@ -32,5 +33,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint { val roomId: RoomId, val roomIdOrAlias: RoomIdOrAlias, val roomDescription: Optional, + val serverNames: List, + val trigger: JoinedRoom.Trigger, ) : NodeInputs } diff --git a/features/joinroom/impl/build.gradle.kts b/features/joinroom/impl/build.gradle.kts index 33fdbb6b476..319f15eea42 100644 --- a/features/joinroom/impl/build.gradle.kts +++ b/features/joinroom/impl/build.gradle.kts @@ -44,9 +44,10 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) implementation(projects.features.invite.api) implementation(projects.features.roomdirectory.api) - implementation(projects.libraries.uiStrings) + implementation(projects.services.analytics.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt index 5d302abc8bb..2cddd9d45b6 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomNode.kt @@ -41,6 +41,8 @@ class JoinRoomNode @AssistedInject constructor( inputs.roomId, inputs.roomIdOrAlias, inputs.roomDescription, + inputs.serverNames, + inputs.trigger, ) @Composable @@ -49,6 +51,7 @@ class JoinRoomNode @AssistedInject constructor( JoinRoomView( state = state, onBackPressed = ::navigateUp, + onJoinSuccess = ::navigateUp, onKnockSuccess = ::navigateUp, modifier = modifier ) diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt index ea88150c786..2019cd77d01 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.InviteData @@ -45,6 +46,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomType +import io.element.android.libraries.matrix.api.room.join.JoinRoom import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.ui.model.toInviteSender import kotlinx.coroutines.CoroutineScope @@ -55,7 +57,10 @@ class JoinRoomPresenter @AssistedInject constructor( @Assisted private val roomId: RoomId, @Assisted private val roomIdOrAlias: RoomIdOrAlias, @Assisted private val roomDescription: Optional, + @Assisted private val serverNames: List, + @Assisted private val trigger: JoinedRoom.Trigger, private val matrixClient: MatrixClient, + private val joinRoom: JoinRoom, private val knockRoom: KnockRoom, private val acceptDeclineInvitePresenter: Presenter, private val buildMeta: BuildMeta, @@ -65,6 +70,8 @@ class JoinRoomPresenter @AssistedInject constructor( roomId: RoomId, roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional, + serverNames: List, + trigger: JoinedRoom.Trigger, ): JoinRoomPresenter } @@ -73,6 +80,7 @@ class JoinRoomPresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() var retryCount by remember { mutableIntStateOf(0) } val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) + val joinAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val contentState by produceState( initialValue = ContentState.Loading(roomIdOrAlias), @@ -108,16 +116,14 @@ class JoinRoomPresenter @AssistedInject constructor( fun handleEvents(event: JoinRoomEvents) { when (event) { - JoinRoomEvents.AcceptInvite, - JoinRoomEvents.JoinRoom -> { + JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction) + JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction) + JoinRoomEvents.AcceptInvite -> { val inviteData = contentState.toInviteData() ?: return acceptDeclineInviteState.eventSink( AcceptDeclineInviteEvents.AcceptInvite(inviteData) ) } - JoinRoomEvents.KnockRoom -> { - coroutineScope.knockRoom(roomId, knockAction) - } JoinRoomEvents.DeclineInvite -> { val inviteData = contentState.toInviteData() ?: return acceptDeclineInviteState.eventSink( @@ -129,6 +135,7 @@ class JoinRoomPresenter @AssistedInject constructor( } JoinRoomEvents.ClearError -> { knockAction.value = AsyncAction.Uninitialized + joinAction.value = AsyncAction.Uninitialized } } } @@ -136,13 +143,24 @@ class JoinRoomPresenter @AssistedInject constructor( return JoinRoomState( contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, + joinAction = joinAction.value, knockAction = knockAction.value, applicationName = buildMeta.applicationName, eventSink = ::handleEvents ) } - private fun CoroutineScope.knockRoom(roomId: RoomId, knockAction: MutableState>) = launch { + private fun CoroutineScope.joinRoom(joinAction: MutableState>) = launch { + joinAction.runUpdatingState { + joinRoom.invoke( + roomId = roomId, + serverNames = serverNames, + trigger = trigger + ) + } + } + + private fun CoroutineScope.knockRoom(knockAction: MutableState>) = launch { knockAction.runUpdatingState { knockRoom(roomId) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt index 9146b13513d..0f849aac246 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.ui.model.InviteSender data class JoinRoomState( val contentState: ContentState, val acceptDeclineInviteState: AcceptDeclineInviteState, + val joinAction: AsyncAction, val knockAction: AsyncAction, val applicationName: String, val eventSink: (JoinRoomEvents) -> Unit diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt index ee08ee954ab..f8970266004 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt @@ -125,11 +125,13 @@ fun aLoadedContentState( fun aJoinRoomState( contentState: ContentState = aLoadedContentState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), + joinAction: AsyncAction = AsyncAction.Uninitialized, knockAction: AsyncAction = AsyncAction.Uninitialized, eventSink: (JoinRoomEvents) -> Unit = {} ) = JoinRoomState( contentState = contentState, acceptDeclineInviteState = acceptDeclineInviteState, + joinAction = joinAction, knockAction = knockAction, applicationName = "AppName", eventSink = eventSink diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt index 49541938d23..435cd1bf8b5 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt @@ -66,6 +66,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun JoinRoomView( state: JoinRoomState, onBackPressed: () -> Unit, + onJoinSuccess: () -> Unit, onKnockSuccess: () -> Unit, modifier: Modifier = Modifier, ) { @@ -108,7 +109,11 @@ fun JoinRoomView( } ) } - + AsyncActionView( + async = state.joinAction, + onSuccess = { onJoinSuccess() }, + onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) }, + ) AsyncActionView( async = state.knockAction, onSuccess = { onKnockSuccess() }, @@ -323,6 +328,7 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) JoinRoomView( state = state, onBackPressed = { }, + onJoinSuccess = { }, onKnockSuccess = { }, ) } diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt index c288021cdfd..d1154eedaed 100644 --- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt +++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt @@ -19,6 +19,7 @@ package io.element.android.features.joinroom.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.joinroom.impl.JoinRoomPresenter import io.element.android.features.roomdirectory.api.RoomDescription @@ -28,6 +29,7 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias +import io.element.android.libraries.matrix.api.room.join.JoinRoom import java.util.Optional @Module @@ -36,6 +38,7 @@ object JoinRoomModule { @Provides fun providesJoinRoomPresenterFactory( client: MatrixClient, + joinRoom: JoinRoom, knockRoom: KnockRoom, acceptDeclineInvitePresenter: Presenter, buildMeta: BuildMeta, @@ -45,12 +48,17 @@ object JoinRoomModule { roomId: RoomId, roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional, + serverNames: List, + trigger: JoinedRoom.Trigger, ): JoinRoomPresenter { return JoinRoomPresenter( roomId = roomId, roomIdOrAlias = roomIdOrAlias, roomDescription = roomDescription, + serverNames = serverNames, + trigger = trigger, matrixClient = client, + joinRoom = joinRoom, knockRoom = knockRoom, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, buildMeta = buildMeta, diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt index 3928ca31e86..16a8c1ecd1e 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt @@ -17,6 +17,7 @@ package io.element.android.features.joinroom.impl import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState @@ -36,10 +37,12 @@ import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.assert @@ -174,6 +177,59 @@ class JoinRoomPresenterTest { } } + @Test + fun `present - when room is joined with success, all the parameters are provided`() = runTest { + val aTrigger = JoinedRoom.Trigger.MobilePermalink + val joinRoomLambda = lambdaRecorder { _: RoomId, _: List, _: JoinedRoom.Trigger -> + Result.success(Unit) + } + val presenter = createJoinRoomPresenter( + trigger = aTrigger, + serverNames = A_SERVER_LIST, + joinRoomLambda = joinRoomLambda, + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + state.eventSink(JoinRoomEvents.JoinRoom) + } + awaitItem().also { state -> + assertThat(state.joinAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.joinAction).isEqualTo(AsyncAction.Success(Unit)) + } + joinRoomLambda.assertions() + .isCalledOnce() + .with(value(A_ROOM_ID), value(A_SERVER_LIST), value(aTrigger)) + } + } + + @Test + fun `present - when room is joined with error, it is possible to clear the error`() = runTest { + val presenter = createJoinRoomPresenter( + joinRoomLambda = { _, _, _ -> + Result.failure(AN_EXCEPTION) + }, + ) + presenter.test { + skipItems(1) + awaitItem().also { state -> + state.eventSink(JoinRoomEvents.JoinRoom) + } + awaitItem().also { state -> + assertThat(state.joinAction).isEqualTo(AsyncAction.Loading) + } + awaitItem().also { state -> + assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) + state.eventSink(JoinRoomEvents.ClearError) + } + awaitItem().also { state -> + assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized) + } + } + } + @Test fun `present - when room is left and public then join authorization is equal to canJoin`() = runTest { val roomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.LEFT, isPublic = true) @@ -415,7 +471,12 @@ class JoinRoomPresenterTest { private fun createJoinRoomPresenter( roomId: RoomId = A_ROOM_ID, roomDescription: Optional = Optional.empty(), + serverNames: List = emptyList(), + trigger: JoinedRoom.Trigger = JoinedRoom.Trigger.Invite, matrixClient: MatrixClient = FakeMatrixClient(), + joinRoomLambda: (RoomId, List, JoinedRoom.Trigger) -> Result = { _, _, _ -> + Result.success(Unit) + }, knockRoom: KnockRoom = FakeKnockRoom(), buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"), acceptDeclineInvitePresenter: Presenter = Presenter { anAcceptDeclineInviteState() } @@ -424,7 +485,10 @@ class JoinRoomPresenterTest { roomId = roomId, roomIdOrAlias = roomId.toRoomIdOrAlias(), roomDescription = roomDescription, + serverNames = serverNames, + trigger = trigger, matrixClient = matrixClient, + joinRoom = FakeJoinRoom(joinRoomLambda), knockRoom = knockRoom, buildMeta = buildMeta, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt index b4bd7882868..bf4449be412 100644 --- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt +++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt @@ -91,6 +91,34 @@ class JoinRoomViewTest { eventsRecorder.assertSingle(JoinRoomEvents.ClearError) } + @Test + fun `clicking on closing Join error emits the expected Event`() { + val eventsRecorder = EventsRecorder() + rule.setJoinRoomView( + aJoinRoomState( + contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), + joinAction = AsyncAction.Failure(Exception("Error")), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_ok) + eventsRecorder.assertSingle(JoinRoomEvents.ClearError) + } + + @Test + fun `when joining room is successful, the expected callback is invoked`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setJoinRoomView( + aJoinRoomState( + joinAction = AsyncAction.Success(Unit), + eventSink = eventsRecorder, + ), + onJoinSuccess = it + ) + } + } + @Test fun `clicking on Accept invitation IsInvited room emits the expected Event`() { val eventsRecorder = EventsRecorder() @@ -149,12 +177,14 @@ class JoinRoomViewTest { private fun AndroidComposeTestRule.setJoinRoomView( state: JoinRoomState, onBackPressed: () -> Unit = EnsureNeverCalled(), + onJoinSuccess: () -> Unit = EnsureNeverCalled(), onKnockSuccess: () -> Unit = EnsureNeverCalled(), ) { setContent { JoinRoomView( state = state, onBackPressed = onBackPressed, + onJoinSuccess = onJoinSuccess, onKnockSuccess = onKnockSuccess, ) } 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 f3c3462cd89..cbdd20c7700 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 @@ -47,6 +47,7 @@ import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.analytics.toAnalyticsViewRoom import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId @@ -57,7 +58,6 @@ import io.element.android.libraries.matrix.api.room.alias.matches import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.services.analytics.api.AnalyticsService -import io.element.android.services.analytics.api.extensions.toAnalyticsViewRoom import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) diff --git a/features/roomaliasresolver/api/src/main/kotlin/io/element/android/features/roomaliasesolver/api/RoomAliasResolverEntryPoint.kt b/features/roomaliasresolver/api/src/main/kotlin/io/element/android/features/roomaliasesolver/api/RoomAliasResolverEntryPoint.kt index 8c0cc10f64f..b22a9680358 100644 --- a/features/roomaliasresolver/api/src/main/kotlin/io/element/android/features/roomaliasesolver/api/RoomAliasResolverEntryPoint.kt +++ b/features/roomaliasresolver/api/src/main/kotlin/io/element/android/features/roomaliasesolver/api/RoomAliasResolverEntryPoint.kt @@ -22,7 +22,7 @@ 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.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias interface RoomAliasResolverEntryPoint : FeatureEntryPoint { fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder @@ -34,7 +34,7 @@ interface RoomAliasResolverEntryPoint : FeatureEntryPoint { } interface Callback : Plugin { - fun onAliasResolved(roomId: RoomId) + fun onAliasResolved(data: ResolvedRoomAlias) } data class Params( diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt index a4dbcc40b57..d6d497e62a6 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverNode.kt @@ -28,7 +28,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias @ContributesNode(SessionScope::class) class RoomAliasResolverNode @AssistedInject constructor( @@ -42,8 +42,8 @@ class RoomAliasResolverNode @AssistedInject constructor( inputs.roomAlias ) - private fun onAliasResolved(roomId: RoomId) { - plugins().forEach { it.onAliasResolved(roomId) } + private fun onAliasResolved(data: ResolvedRoomAlias) { + plugins().forEach { it.onAliasResolved(data) } } @Composable diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt index 775be7d6ff4..5e3671f564f 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt @@ -29,7 +29,7 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -46,7 +46,7 @@ class RoomAliasResolverPresenter @AssistedInject constructor( @Composable override fun present(): RoomAliasResolverState { val coroutineScope = rememberCoroutineScope() - val resolveState: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } + val resolveState: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } LaunchedEffect(Unit) { resolveAlias(resolveState) } @@ -64,7 +64,7 @@ class RoomAliasResolverPresenter @AssistedInject constructor( ) } - private fun CoroutineScope.resolveAlias(resolveState: MutableState>) = launch { + private fun CoroutineScope.resolveAlias(resolveState: MutableState>) = launch { suspend { matrixClient.resolveRoomAlias(roomAlias).getOrThrow() }.runCatchingUpdatingState(resolveState) diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt index 638214da3fe..12cbb64c00e 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt @@ -19,11 +19,11 @@ package io.element.android.features.roomaliasresolver.impl import androidx.compose.runtime.Immutable import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias @Immutable data class RoomAliasResolverState( val roomAlias: RoomAlias, - val resolveState: AsyncData, + val resolveState: AsyncData, val eventSink: (RoomAliasResolverEvents) -> Unit ) diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt index 3c5599628cf..e7e5cfc6868 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt @@ -19,7 +19,7 @@ package io.element.android.features.roomaliasresolver.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.RoomAlias -import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias open class RoomAliasResolverStateProvider : PreviewParameterProvider { override val values: Sequence @@ -36,7 +36,7 @@ open class RoomAliasResolverStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + resolveState: AsyncData = AsyncData.Uninitialized, eventSink: (RoomAliasResolverEvents) -> Unit = {} ) = RoomAliasResolverState( roomAlias = roomAlias, diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt index cd5bd042c8e..722eb866504 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt @@ -49,14 +49,14 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator 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.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.ui.strings.CommonStrings @Composable fun RoomAliasResolverView( state: RoomAliasResolverState, onBackPressed: () -> Unit, - onAliasResolved: (RoomId) -> Unit, + onAliasResolved: (ResolvedRoomAlias) -> Unit, modifier: Modifier = Modifier, ) { val latestOnAliasResolved by rememberUpdatedState(onAliasResolved) diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenterTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenterTest.kt index 2c64690600f..c3cd566432f 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenterTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenterTest.kt @@ -22,9 +22,12 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomAlias +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ALIAS import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest @@ -48,8 +51,9 @@ class RoomAliasResolverPresenterTest { @Test fun `present - resolve alias to roomId`() = runTest { + val result = aResolvedRoomAlias() val client = FakeMatrixClient( - resolveRoomAliasResult = { Result.success(A_ROOM_ID) } + resolveRoomAliasResult = { Result.success(result) } ) val presenter = createPresenter(matrixClient = client) moleculeFlow(RecompositionMode.Immediate) { @@ -59,7 +63,7 @@ class RoomAliasResolverPresenterTest { assertThat(awaitItem().resolveState.isLoading()).isTrue() val resultState = awaitItem() assertThat(resultState.roomAlias).isEqualTo(A_ROOM_ALIAS) - assertThat(resultState.resolveState.dataOrNull()).isEqualTo(A_ROOM_ID) + assertThat(resultState.resolveState.dataOrNull()).isEqualTo(result) } } @@ -92,3 +96,11 @@ class RoomAliasResolverPresenterTest { matrixClient = matrixClient, ) } + +internal fun aResolvedRoomAlias( + roomId: RoomId = A_ROOM_ID, + servers: List = A_SERVER_LIST, +) = ResolvedRoomAlias( + roomId = roomId, + servers = servers, +) diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverViewTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverViewTest.kt index 6df8a7849ec..990bbfe46af 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverViewTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverViewTest.kt @@ -21,8 +21,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam @@ -69,11 +68,12 @@ class RoomAliasResolverViewTest { @Test fun `success state invokes the expected Callback`() { + val result = aResolvedRoomAlias() val eventsRecorder = EventsRecorder(expectEvents = false) - ensureCalledOnceWithParam(A_ROOM_ID) { + ensureCalledOnceWithParam(result) { rule.setRoomAliasResolverView( aRoomAliasResolverState( - resolveState = AsyncData.Success(A_ROOM_ID), + resolveState = AsyncData.Success(result), eventSink = eventsRecorder, ), onAliasResolved = it, @@ -85,7 +85,7 @@ class RoomAliasResolverViewTest { private fun AndroidComposeTestRule.setRoomAliasResolverView( state: RoomAliasResolverState, onBackPressed: () -> Unit = EnsureNeverCalled(), - onAliasResolved: (RoomId) -> Unit = EnsureNeverCalledWithParam(), + onAliasResolved: (ResolvedRoomAlias) -> Unit = EnsureNeverCalledWithParam(), ) { setContent { RoomAliasResolverView( diff --git a/features/roomdirectory/impl/build.gradle.kts b/features/roomdirectory/impl/build.gradle.kts index 49638ece40f..c88df44d150 100644 --- a/features/roomdirectory/impl/build.gradle.kts +++ b/features/roomdirectory/impl/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) + implementation(projects.services.analytics.api) testImplementation(libs.test.junit) testImplementation(libs.androidx.compose.ui.test.junit) diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index 62192967324..b551dfa9194 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(libs.dagger) implementation(projects.libraries.androidutils) implementation(projects.libraries.core) + implementation(projects.services.analytics.api) implementation(libs.serialization.json) api(projects.libraries.sessionStorage.api) implementation(libs.coroutines.core) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index fb20e16c9db..fc57e690050 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -32,6 +32,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.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService @@ -66,6 +67,7 @@ interface MatrixClient : Closeable { suspend fun uploadAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result suspend fun joinRoom(roomId: RoomId): Result + suspend fun joinRoomByIdOrAlias(roomId: RoomId, serverNames: List): Result suspend fun knockRoom(roomId: RoomId): Result fun syncService(): SyncService fun sessionVerificationService(): SessionVerificationService @@ -102,6 +104,6 @@ interface MatrixClient : Closeable { suspend fun trackRecentlyVisitedRoom(roomId: RoomId): Result suspend fun getRecentlyVisitedRooms(): Result> - suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result + suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result suspend fun getRoomPreview(roomIdOrAlias: RoomIdOrAlias): Result } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/ViewRoomExt.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt similarity index 81% rename from services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/ViewRoomExt.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt index 2845129fa85..ecc5ce8d7c1 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/ViewRoomExt.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/analytics/ViewRoomExt.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2024 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. @@ -14,12 +14,16 @@ * limitations under the License. */ -package io.element.android.services.analytics.api.extensions +package io.element.android.libraries.matrix.api.analytics import im.vector.app.features.analytics.plan.ViewRoom import io.element.android.libraries.matrix.api.room.MatrixRoom -fun MatrixRoom.toAnalyticsViewRoom(trigger: ViewRoom.Trigger? = null, selectedSpace: MatrixRoom? = null, viaKeyboard: Boolean? = null): ViewRoom { +fun MatrixRoom.toAnalyticsViewRoom( + trigger: ViewRoom.Trigger? = null, + selectedSpace: MatrixRoom? = null, + viaKeyboard: Boolean? = null, +): ViewRoom { val activeSpace = selectedSpace?.toActiveSpace() ?: ViewRoom.ActiveSpace.Home return ViewRoom( diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/ResolvedRoomAlias.kt similarity index 54% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/ResolvedRoomAlias.kt index 477b49e4f6d..90466649172 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/di/JoinRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/alias/ResolvedRoomAlias.kt @@ -14,19 +14,20 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.root.di +package io.element.android.libraries.matrix.api.room.alias -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId -import javax.inject.Inject -interface JoinRoom { - suspend operator fun invoke(roomId: RoomId): Result -} - -@ContributesBinding(SessionScope::class) -class DefaultJoinRoom @Inject constructor(private val client: MatrixClient) : JoinRoom { - override suspend fun invoke(roomId: RoomId) = client.joinRoom(roomId) -} +/** + * Information about a room, that was resolved from a room alias. + */ +data class ResolvedRoomAlias( + /** + * The room ID that the alias resolved to. + */ + val roomId: RoomId, + /** + * A list of servers that can be used to find the room by its room ID. + */ + val servers: List +) diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt similarity index 69% rename from features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt index 6251bcaefaf..fe6a2d9e471 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/FakeJoinRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRoom.kt @@ -14,13 +14,15 @@ * limitations under the License. */ -package io.element.android.features.roomdirectory.impl.root +package io.element.android.libraries.matrix.api.room.join -import io.element.android.features.roomdirectory.impl.root.di.JoinRoom +import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.libraries.matrix.api.core.RoomId -class FakeJoinRoom( - var lambda: (RoomId) -> Result = { Result.success(Unit) } -) : JoinRoom { - override suspend fun invoke(roomId: RoomId) = lambda(roomId) +interface JoinRoom { + suspend operator fun invoke( + roomId: RoomId, + serverNames: List, + trigger: JoinedRoom.Trigger, + ): Result } 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 7b6ec132681..8a370df09be 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 @@ -39,6 +39,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.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService @@ -443,6 +444,23 @@ class RustMatrixClient( } } + override suspend fun joinRoomByIdOrAlias( + roomId: RoomId, + serverNames: List, + ): Result = withContext(sessionDispatcher) { + runCatching { + client.joinRoomByIdOrAlias( + roomIdOrAlias = roomId.value, + serverNames = serverNames, + ).destroy() + try { + awaitRoom(roomId, 10.seconds) + } catch (e: Exception) { + Timber.e(e, "Timeout waiting for the room to be available in the room list") + } + } + } + override suspend fun knockRoom(roomId: RoomId): Result { return Result.failure(NotImplementedError("Not yet implemented")) } @@ -459,9 +477,13 @@ class RustMatrixClient( } } - override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result = withContext(sessionDispatcher) { + override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result = withContext(sessionDispatcher) { runCatching { - client.resolveRoomAlias(roomAlias.value).roomId.let(::RoomId) + val result = client.resolveRoomAlias(roomAlias.value) + ResolvedRoomAlias( + roomId = RoomId(result.roomId), + servers = result.servers, + ) } } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/JoinedRoomExt.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt similarity index 88% rename from services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/JoinedRoomExt.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt index c8ab1fad06b..955a6b0f8a5 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/extensions/JoinedRoomExt.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/analytics/JoinedRoomExt.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2024 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. @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.services.analytics.api.extensions +package io.element.android.libraries.matrix.impl.analytics import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.libraries.matrix.api.room.MatrixRoom -fun Long?.toAnalyticsRoomSize(): JoinedRoom.RoomSize { +private fun Long?.toAnalyticsRoomSize(): JoinedRoom.RoomSize { return when (this) { null, 2L -> JoinedRoom.RoomSize.Two diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt new file mode 100644 index 00000000000..d2ce4e61d54 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.room.join + +import com.squareup.anvil.annotations.ContributesBinding +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.libraries.matrix.impl.analytics.toAnalyticsJoinedRoom +import io.element.android.services.analytics.api.AnalyticsService +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultJoinRoom @Inject constructor( + private val client: MatrixClient, + private val analyticsService: AnalyticsService, +) : JoinRoom { + override suspend fun invoke( + roomId: RoomId, + serverNames: List, + trigger: JoinedRoom.Trigger, + ): Result { + return if (serverNames.isEmpty()) { + client.joinRoom(roomId) + } else { + client.joinRoomByIdOrAlias(roomId, serverNames) + }.onSuccess { + client.getRoom(roomId)?.use { room -> + analyticsService.capture(room.toAnalyticsJoinedRoom(trigger)) + } + } + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt new file mode 100644 index 00000000000..8bbdf47e21b --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.room.join + +import com.google.common.truth.Truth.assertThat +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.impl.analytics.toAnalyticsJoinedRoom +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SERVER_LIST +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.services.analytics.test.FakeAnalyticsService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultJoinRoomTest { + @Test + fun `when there is no server names, the classic join room API is used`() = runTest { + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(Unit) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomId, _: List -> Result.success(Unit) } + val roomResult = FakeMatrixRoom() + val aTrigger = JoinedRoom.Trigger.MobilePermalink + val client: MatrixClient = FakeMatrixClient().also { + it.joinRoomLambda = joinRoomLambda + it.joinRoomByIdOrAliasLambda = joinRoomByIdOrAliasLambda + it.givenGetRoomResult( + roomId = A_ROOM_ID, + result = roomResult + ) + } + val analyticsService = FakeAnalyticsService() + val sut = DefaultJoinRoom( + client = client, + analyticsService = analyticsService, + ) + sut.invoke(A_ROOM_ID, emptyList(), aTrigger) + joinRoomByIdOrAliasLambda + .assertions() + .isNeverCalled() + joinRoomLambda + .assertions() + .isCalledExactly(1) + .withSequence( + listOf(value(A_ROOM_ID)) + ) + assertThat(analyticsService.capturedEvents).containsExactly( + roomResult.toAnalyticsJoinedRoom(aTrigger) + ) + } + + @Test + fun `when server names are available, joinRoomByIdOrAlias API is used`() = runTest { + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(Unit) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomId, _: List -> Result.success(Unit) } + val roomResult = FakeMatrixRoom() + val aTrigger = JoinedRoom.Trigger.MobilePermalink + val client: MatrixClient = FakeMatrixClient().also { + it.joinRoomLambda = joinRoomLambda + it.joinRoomByIdOrAliasLambda = joinRoomByIdOrAliasLambda + it.givenGetRoomResult( + roomId = A_ROOM_ID, + result = roomResult + ) + } + val analyticsService = FakeAnalyticsService() + val sut = DefaultJoinRoom( + client = client, + analyticsService = analyticsService, + ) + sut.invoke(A_ROOM_ID, A_SERVER_LIST, aTrigger) + joinRoomByIdOrAliasLambda + .assertions() + .isCalledExactly(1) + .withSequence( + listOf(value(A_ROOM_ID), value(A_SERVER_LIST)) + ) + joinRoomLambda + .assertions() + .isNeverCalled() + assertThat(analyticsService.capturedEvents).containsExactly( + roomResult.toAnalyticsJoinedRoom(aTrigger) + ) + } +} diff --git a/libraries/matrix/test/build.gradle.kts b/libraries/matrix/test/build.gradle.kts index 9c41948bd70..8e06adeece9 100644 --- a/libraries/matrix/test/build.gradle.kts +++ b/libraries/matrix/test/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { api(projects.libraries.matrix.api) api(libs.coroutines.core) implementation(libs.coroutines.test) + implementation(projects.services.analytics.api) implementation(projects.tests.testutils) implementation(libs.kotlinx.collections.immutable) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 015f764ff59..def7bc01fdc 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -33,6 +33,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.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService @@ -76,7 +77,7 @@ class FakeMatrixClient( private val encryptionService: FakeEncryptionService = FakeEncryptionService(), private val roomDirectoryService: RoomDirectoryService = FakeRoomDirectoryService(), private val accountManagementUrlString: Result = Result.success(null), - private val resolveRoomAliasResult: (RoomAlias) -> Result = { Result.success(A_ROOM_ID) }, + private val resolveRoomAliasResult: (RoomAlias) -> Result = { Result.success(ResolvedRoomAlias(A_ROOM_ID, emptyList())) }, private val getRoomPreviewResult: (RoomIdOrAlias) -> Result = { Result.failure(AN_EXCEPTION) }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false @@ -106,6 +107,9 @@ class FakeMatrixClient( var joinRoomLambda: (RoomId) -> Result = { Result.success(Unit) } + var joinRoomByIdOrAliasLambda: (RoomId, List) -> Result = { _, _ -> + Result.success(Unit) + } var knockRoomLambda: (RoomId) -> Result = { Result.success(Unit) } @@ -201,6 +205,10 @@ class FakeMatrixClient( override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) + override suspend fun joinRoomByIdOrAlias(roomId: RoomId, serverNames: List): Result { + return joinRoomByIdOrAliasLambda(roomId, serverNames) + } + override suspend fun knockRoom(roomId: RoomId): Result = knockRoomLambda(roomId) override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService @@ -285,7 +293,7 @@ class FakeMatrixClient( return Result.success(Unit) } - override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result = simulateLongTask { + override suspend fun resolveRoomAlias(roomAlias: RoomAlias): Result = simulateLongTask { resolveRoomAliasResult(roomAlias) } 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 96346574ce3..9196bf364f9 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 @@ -76,3 +76,5 @@ val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) const val A_RECOVERY_KEY = "1234 5678" + +val A_SERVER_LIST = listOf("server1", "server2") diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/join/FakeJoinRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/join/FakeJoinRoom.kt new file mode 100644 index 00000000000..55eade69d4c --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/join/FakeJoinRoom.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 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.test.room.join + +import im.vector.app.features.analytics.plan.JoinedRoom +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.join.JoinRoom +import io.element.android.tests.testutils.simulateLongTask + +class FakeJoinRoom( + var lambda: (RoomId, List, JoinedRoom.Trigger) -> Result +) : JoinRoom { + override suspend fun invoke( + roomId: RoomId, + serverNames: List, + trigger: JoinedRoom.Trigger, + ): Result = simulateLongTask { + lambda(roomId, serverNames, trigger) + } +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index b96adefb6e5..7dc75904e3a 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.impl.room.join.DefaultJoinRoom import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager import io.element.android.services.analytics.noop.NoopAnalyticsService @@ -134,7 +135,7 @@ class RoomListScreen( ), acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter( client = matrixClient, - analyticsService = NoopAnalyticsService(), + joinRoom = DefaultJoinRoom(matrixClient, NoopAnalyticsService()), notificationDrawerManager = FakeNotificationDrawerManager(), ), analyticsService = NoopAnalyticsService(), diff --git a/services/analytics/api/build.gradle.kts b/services/analytics/api/build.gradle.kts index c78a050571b..76309c0179f 100644 --- a/services/analytics/api/build.gradle.kts +++ b/services/analytics/api/build.gradle.kts @@ -25,6 +25,5 @@ dependencies { api(projects.services.analyticsproviders.api) api(projects.services.toolbox.api) implementation(libs.coroutines.core) - implementation(projects.libraries.matrix.api) implementation(projects.libraries.core) }