diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index b672f582f9..71c382a6b2 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -68,4 +68,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.impl) + testImplementation(projects.services.appnavstate.test) + testImplementation(libs.test.appyx.junit) + testImplementation(libs.test.arch.core) } 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 fe2d8aa0dc..f3eac99a02 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -108,17 +108,17 @@ class LoggedInFlowNode @AssistedInject constructor( val imageLoaderFactory = bindings().loggedInImageLoaderFactory() Coil.setImageLoader(imageLoaderFactory) inputs.matrixClient.startSync() - appNavigationStateService.onNavigateToSession(inputs.matrixClient.sessionId) + appNavigationStateService.onNavigateToSession(id, inputs.matrixClient.sessionId) // TODO We do not support Space yet, so directly navigate to main space - appNavigationStateService.onNavigateToSpace(MAIN_SPACE) + appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE) loggedInFlowProcessor.observeEvents(coroutineScope) }, onDestroy = { val imageLoaderFactory = bindings().notLoggedInImageLoaderFactory() Coil.setImageLoader(imageLoaderFactory) plugins().forEach { it.onFlowReleased(inputs.matrixClient) } - appNavigationStateService.onLeavingSpace() - appNavigationStateService.onLeavingSession() + appNavigationStateService.onLeavingSpace(id) + appNavigationStateService.onLeavingSession(id) loggedInFlowProcessor.stopObserving() } ) 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 3609dbf57e..22929e3d66 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -19,6 +19,7 @@ package io.element.android.appnav import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -44,6 +45,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import timber.log.Timber @@ -55,7 +57,6 @@ class RoomFlowNode @AssistedInject constructor( private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val appNavigationStateService: AppNavigationStateService, roomMembershipObserver: RoomMembershipObserver, - coroutineScope: CoroutineScope, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Messages, @@ -75,41 +76,49 @@ class RoomFlowNode @AssistedInject constructor( ) : NodeInputs private val inputs: Inputs = inputs() - private val timeline = inputs.room.timeline() - - private val roomFlowPresenter = RoomFlowPresenter(inputs.room) init { lifecycle.subscribe( onCreate = { Timber.v("OnCreate") plugins().forEach { it.onFlowCreated(inputs.room) } - appNavigationStateService.onNavigateToRoom(inputs.room.roomId) + appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) + fetchRoomMembers() }, onDestroy = { Timber.v("OnDestroy") inputs.room.close() plugins().forEach { it.onFlowReleased(inputs.room) } - appNavigationStateService.onLeavingRoom() + appNavigationStateService.onLeavingRoom(id) } ) - roomMembershipObserver.updates .filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom } .onEach { navigateUp() } - .launchIn(coroutineScope) + .launchIn(lifecycleScope) + } + + private fun fetchRoomMembers() = lifecycleScope.launch { + val room = inputs.room + room.fetchMembers() + .onFailure { + Timber.e(it, "Fail to fetch members for room ${room.roomId}") + }.onSuccess { + Timber.v("Success fetching members for room ${room.roomId}") + } } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Messages -> { - messagesEntryPoint.createNode(this, buildContext, object : MessagesEntryPoint.Callback { + val callback = object : MessagesEntryPoint.Callback { override fun onRoomDetailsClicked() { backstack.push(NavTarget.RoomDetails) } - }) + } + messagesEntryPoint.createNode(this, buildContext, callback) } NavTarget.RoomDetails -> { roomDetailsEntryPoint.createNode(this, buildContext, emptyList()) @@ -127,7 +136,6 @@ class RoomFlowNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - roomFlowPresenter.present() Children( navModel = backstack, modifier = modifier, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt deleted file mode 100644 index 0a2b066da4..0000000000 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowPresenter.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.room.MatrixRoom -import timber.log.Timber - -class RoomFlowPresenter( - private val room: MatrixRoom, -) : Presenter { - - @Composable - override fun present(): RoomFlowState { - // Preload room members so we can quickly detect if the room is a DM room - LaunchedEffect(Unit) { - room.fetchMembers() - .onFailure { - Timber.e(it, "Fail to fetch members for room ${room.roomId}") - }.onSuccess { - Timber.v("Success fetching members for room ${room.roomId}") - } - } - - return RoomFlowState - } -} - -// At first the return type was Unit, but detekt complained about it -object RoomFlowState diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt new file mode 100644 index 0000000000..ef611a2f4b --- /dev/null +++ b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowNodeTest.kt @@ -0,0 +1,130 @@ +/* + * 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 + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.activeElement +import com.bumble.appyx.testing.junit4.util.MainDispatcherRule +import com.bumble.appyx.testing.unit.common.helper.nodeTestHelper +import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper +import com.google.common.truth.Truth +import io.element.android.features.messages.api.MessagesEntryPoint +import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint +import io.element.android.libraries.architecture.childNode +import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.services.appnavstate.test.NoopAppNavigationStateService +import org.junit.Rule +import org.junit.Test + +class RoomFlowNodeTest { + + @get:Rule + val instantTaskExecutorRule = InstantTaskExecutorRule() + + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + + private class FakeMessagesEntryPoint : MessagesEntryPoint { + + var nodeId: String? = null + var callback: MessagesEntryPoint.Callback? = null + + override fun createNode(parentNode: Node, buildContext: BuildContext, callback: MessagesEntryPoint.Callback): Node { + return node(buildContext) {}.also { + nodeId = it.id + this.callback = callback + } + } + } + + private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint { + + var nodeId: String? = null + + override fun createNode(parentNode: Node, buildContext: BuildContext, plugins: List): Node { + return node(buildContext) {}.also { + nodeId = it.id + } + } + } + + private fun aRoomFlowNode( + plugins: List, + messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), + roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), + ) = RoomFlowNode( + buildContext = BuildContext.root(savedStateMap = null), + plugins = plugins, + messagesEntryPoint = messagesEntryPoint, + roomDetailsEntryPoint = roomDetailsEntryPoint, + appNavigationStateService = NoopAppNavigationStateService(), + roomMembershipObserver = RoomMembershipObserver() + ) + + @Test + fun `given a room flow node when initialized then it fetches room members`() { + // GIVEN + val room = FakeMatrixRoom() + val inputs = RoomFlowNode.Inputs(room) + val roomFlowNode = aRoomFlowNode(listOf(inputs)) + Truth.assertThat(room.areMembersFetched).isFalse() + // WHEN + roomFlowNode.nodeTestHelper() + // THEN + Truth.assertThat(room.areMembersFetched).isTrue() + } + + @Test + fun `given a room flow node when initialized then it loads messages entry point`() { + // GIVEN + val room = FakeMatrixRoom() + val fakeMessagesEntryPoint = FakeMessagesEntryPoint() + val inputs = RoomFlowNode.Inputs(room) + val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint) + // WHEN + val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() + + // THEN + Truth.assertThat(roomFlowNode.backstack.activeElement).isEqualTo(RoomFlowNode.NavTarget.Messages) + roomFlowNodeTestHelper.assertChildHasLifecycle(RoomFlowNode.NavTarget.Messages, Lifecycle.State.CREATED) + val messagesNode = roomFlowNode.childNode(RoomFlowNode.NavTarget.Messages)!! + Truth.assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) + } + + @Test + fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() { + // GIVEN + val room = FakeMatrixRoom() + val fakeMessagesEntryPoint = FakeMessagesEntryPoint() + val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() + val inputs = RoomFlowNode.Inputs(room) + val roomFlowNode = aRoomFlowNode(listOf(inputs), fakeMessagesEntryPoint, fakeRoomDetailsEntryPoint) + val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper() + // WHEN + fakeMessagesEntryPoint.callback?.onRoomDetailsClicked() + // THEN + roomFlowNodeTestHelper.assertChildHasLifecycle(RoomFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED) + val roomDetailsNode = roomFlowNode.childNode(RoomFlowNode.NavTarget.RoomDetails)!! + Truth.assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId) + } +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt deleted file mode 100644 index 1347b0b24c..0000000000 --- a/appnav/src/test/kotlin/io/element/android/appnav/RoomFlowPresenterTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 - -import app.cash.molecule.RecompositionClock -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.lang.IllegalStateException - -class RoomFlowPresenterTest { - - @Test - fun `present - fetches room members`() = runTest { - val fakeTimeline = FakeMatrixTimeline() - val room = FakeMatrixRoom(matrixTimeline = fakeTimeline) - val presenter = RoomFlowPresenter(room) - - Truth.assertThat(room.areMembersFetched).isFalse() - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - Truth.assertThat(room.areMembersFetched).isTrue() - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - recovers from error while fetching room members`() = runTest { - val fakeTimeline = FakeMatrixTimeline() - val room = FakeMatrixRoom(matrixTimeline = fakeTimeline).apply { - givenFetchMemberResult(Result.failure(IllegalStateException("Some error"))) - } - val presenter = RoomFlowPresenter(room) - - Truth.assertThat(room.areMembersFetched).isFalse() - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - Truth.assertThat(room.areMembersFetched).isFalse() - cancelAndIgnoreRemainingEvents() - } - } -} 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 5778eae96e..67038001e9 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 @@ -34,7 +34,6 @@ 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.RoomScope -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.parcelize.Parcelize @@ -62,19 +61,16 @@ class RoomDetailsFlowNode @AssistedInject constructor( data class RoomMemberDetails(val roomMember: RoomMember) : NavTarget } - interface Callback : Plugin { - fun openRoomMemberList() - } - - val callback = object : Callback { - override fun openRoomMemberList() { - backstack.push(NavTarget.RoomMemberList) - } - } - override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.RoomDetails -> createNode(buildContext, listOf(callback)) + NavTarget.RoomDetails -> { + val callback = object : RoomDetailsNode.Callback { + override fun openRoomMemberList() { + backstack.push(NavTarget.RoomMemberList) + } + } + createNode(buildContext, listOf(callback)) + } NavTarget.RoomMemberList -> { val callback = object : RoomMemberListNode.Callback { override fun openRoomMemberDetails(roomMember: RoomMember) { @@ -84,7 +80,8 @@ class RoomDetailsFlowNode @AssistedInject constructor( createNode(buildContext, listOf(callback)) } is NavTarget.RoomMemberDetails -> { - createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMember))) + val inputs = RoomMemberDetailsNode.Inputs(navTarget.roomMember) + createNode(buildContext, listOf(inputs)) } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index f0e1f28cf3..f20a8e9509 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -40,7 +40,11 @@ class RoomDetailsNode @AssistedInject constructor( private val room: MatrixRoom, ) : Node(buildContext, plugins = plugins) { - private val callback = plugins().firstOrNull() + interface Callback : Plugin { + fun openRoomMemberList() + } + + private val callback = plugins().firstOrNull() private fun openRoomMemberList() { callback?.openRoomMemberList() 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 fe7f816a5a..cf1266099a 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 @@ -16,7 +16,6 @@ package io.element.android.features.roomdetails.impl.members.details -import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -51,30 +50,32 @@ class RoomMemberDetailsNode @AssistedInject constructor( private val inputs = inputs() private val presenter = presenterFactory.create(inputs.member) - private fun onShareUser(context: Context) { - val permalinkResult = PermalinkBuilder.permalinkForUser(UserId(inputs.member.userId)) - permalinkResult.onSuccess { permalink -> - startSharePlainTextIntent( - context = context, - activityResultLauncher = null, - chooserTitle = context.getString(R.string.screen_room_details_share_room_title), - text = permalink, - noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found) - ) - }.onFailure { - Timber.e(it) - } - } - @Composable override fun View(modifier: Modifier) { + val context = LocalContext.current + + fun onShareUser() { + val permalinkResult = PermalinkBuilder.permalinkForUser(UserId(inputs.member.userId)) + permalinkResult.onSuccess { permalink -> + startSharePlainTextIntent( + context = context, + activityResultLauncher = null, + chooserTitle = context.getString(R.string.screen_room_details_share_room_title), + text = permalink, + noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found) + ) + }.onFailure { + Timber.e(it) + } + } + val state = presenter.present() RoomMemberDetailsView( state = state, modifier = modifier, - goBack = { navigateUp() }, - onShareUser = { onShareUser(context) } + goBack = this::navigateUp, + onShareUser = ::onShareUser ) } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 6292f877d7..0dedd71c06 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,7 +44,7 @@ import org.junit.Test @ExperimentalCoroutinesApi class RoomDetailsPresenterTests { - private val roomMembershipObserver = RoomMembershipObserver(A_SESSION_ID) + private val roomMembershipObserver = RoomMembershipObserver() @Test fun `present - initial state is created from room info`() = runTest { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index ac37dfe30d..6f77ddf55a 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -38,7 +38,6 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.handleSnackbarMessage 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.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus @@ -61,11 +60,9 @@ class RoomListPresenter @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { - private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver() - @Composable override fun present(): RoomListState { - val matrixUser: MutableState = remember { + val matrixUser: MutableState = rememberSaveable { mutableStateOf(null) } var filter by rememberSaveable { mutableStateOf("") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34fd4d36da..5783cff201 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -103,6 +103,7 @@ network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-k # Test test_core = { module = "androidx.test:core", version.ref = "test_core" } test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" } +test_arch_core = "androidx.arch.core:core-testing:2.2.0" test_junit = "junit:junit:4.13.2" test_runner = "androidx.test:runner:1.5.2" test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0" @@ -115,6 +116,7 @@ test_turbine = "app.cash.turbine:turbine:0.12.1" test_truth = "com.google.truth:truth:1.1.3" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11" test_robolectric = "org.robolectric:robolectric:4.9.2" +test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } # Others coil = { module = "io.coil-kt:coil", version.ref = "coil" } diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/ParentNodeExt.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/ParentNodeExt.kt new file mode 100644 index 0000000000..b8284ff7b9 --- /dev/null +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/ParentNodeExt.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.architecture + +import com.bumble.appyx.core.children.nodeOrNull +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.ParentNode + +fun ParentNode.childNode(navTarget: NavTarget): Node? { + val childMap = children.value + val key = childMap.keys.find { it.navTarget == navTarget } + return childMap[key]?.nodeOrNull +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 9a97fcc9cf..f9726f5670 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -59,5 +59,5 @@ interface MatrixRoom: Closeable { suspend fun redactEvent(eventId: EventId, reason: String? = null): Result - fun leave(): Result + suspend fun leave(): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index fcb83553c5..9c3e771f22 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -31,7 +31,6 @@ data class RoomMember( val isIgnored: Boolean, ) : Parcelable -@Parcelize -enum class RoomMembershipState : Parcelable { +enum class RoomMembershipState { BAN, INVITE, JOIN, KNOCK, LEAVE } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt index 42b0996bb1..ed6f3fae26 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMembershipObserver.kt @@ -17,14 +17,11 @@ package io.element.android.libraries.matrix.api.room 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.timeline.item.event.MembershipChange import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -class RoomMembershipObserver( - private val sessionId: SessionId, -) { +class RoomMembershipObserver { data class RoomMembershipUpdate( val roomId: RoomId, val isUserInRoom: Boolean, 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 bcac6cebd1..fe3dbfb911 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 @@ -145,7 +145,7 @@ class RustMatrixClient constructor( private val mediaResolver = RustMediaResolver(this) private val isSyncing = AtomicBoolean(false) - private val roomMembershipObserver = RoomMembershipObserver(sessionId) + private val roomMembershipObserver = RoomMembershipObserver() init { client.setDelegate(clientDelegate) 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 67cddeee00..8498aa1376 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 @@ -93,6 +93,7 @@ class RustMatrixRoom( coroutineDispatchers = coroutineDispatchers ) } + override fun close() { innerRoom.destroy() slidingSyncRoom.destroy() @@ -188,7 +189,9 @@ class RustMatrixRoom( } } - override fun leave(): Result { - return runCatching { innerRoom.leave() } + override suspend fun leave(): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.leave() + } } } 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 a6741982ea..5cb79564ba 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 @@ -110,7 +110,7 @@ class FakeMatrixClient( override fun onSlidingSyncUpdate() {} override fun roomMembershipObserver(): RoomMembershipObserver { - return RoomMembershipObserver(A_SESSION_ID) + return RoomMembershipObserver() } // Mocks diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index f213da2358..97f06bcabf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -122,7 +122,7 @@ class FakeMatrixRoom( return Result.success(Unit) } - override fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) + override suspend fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) override fun close() = Unit diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt index af6d1d7b0a..5ead00c976 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt @@ -21,26 +21,38 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId -sealed interface AppNavigationState { - object Root : AppNavigationState +/** + * Can represent the current global app navigation state. + * @param owner mostly a Node identifier associated with the state. + * We are using the owner parameter to check when calling onLeaving methods is still using the same owner than his companion onNavigate. + * Why this is needed : for now we rely on lifecycle methods of the node, which are async. + * If you navigate quickly between nodes, onCreate of the new node is called before onDestroy of the previous node. + * So we assume if we don't get the same owner, we can skip the onLeaving action as we already replaced it. + */ +sealed class AppNavigationState(open val owner: String) { + object Root : AppNavigationState("ROOT") data class Session( + override val owner: String, val sessionId: SessionId, - ) : AppNavigationState + ) : AppNavigationState(owner) data class Space( + override val owner: String, // Can be fake value, if no space is selected val spaceId: SpaceId, val parentSession: Session, - ) : AppNavigationState + ) : AppNavigationState(owner) data class Room( + override val owner: String, val roomId: RoomId, val parentSpace: Space, - ) : AppNavigationState + ) : AppNavigationState(owner) data class Thread( + override val owner: String, val threadId: ThreadId, val parentRoom: Room, - ) : AppNavigationState + ) : AppNavigationState(owner) } diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt index c68db8afb7..4bb40b7b75 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt @@ -25,15 +25,15 @@ import kotlinx.coroutines.flow.StateFlow interface AppNavigationStateService { val appNavigationStateFlow: StateFlow - fun onNavigateToSession(sessionId: SessionId) - fun onLeavingSession() + fun onNavigateToSession(owner: String, sessionId: SessionId) + fun onLeavingSession(owner: String) - fun onNavigateToSpace(spaceId: SpaceId) - fun onLeavingSpace() + fun onNavigateToSpace(owner: String, spaceId: SpaceId) + fun onLeavingSpace(owner: String) - fun onNavigateToRoom(roomId: RoomId) - fun onLeavingRoom() + fun onNavigateToRoom(owner: String, roomId: RoomId) + fun onLeavingRoom(owner: String) - fun onNavigateToThread(threadId: ThreadId) - fun onLeavingThread() + fun onNavigateToThread(owner: String, threadId: ThreadId) + fun onLeavingThread(owner: String) } diff --git a/services/appnavstate/impl/build.gradle.kts b/services/appnavstate/impl/build.gradle.kts index 253ddec2fb..eb34a3116a 100644 --- a/services/appnavstate/impl/build.gradle.kts +++ b/services/appnavstate/impl/build.gradle.kts @@ -47,4 +47,5 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.appnavstate.test) } diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt index 4e0071cf46..bf20a04b11 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt @@ -40,11 +40,10 @@ private val loggerTag = LoggerTag("Navigation") @SingleIn(AppScope::class) class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStateService { - private val currentAppNavigationState = MutableStateFlow(AppNavigationState.Root) - + private val currentAppNavigationState: MutableStateFlow = MutableStateFlow(AppNavigationState.Root) override val appNavigationStateFlow: StateFlow = currentAppNavigationState - override fun onNavigateToSession(sessionId: SessionId) { + override fun onNavigateToSession(owner: String, sessionId: SessionId) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue") val newValue: AppNavigationState.Session = when (currentValue) { @@ -52,53 +51,54 @@ class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStat is AppNavigationState.Space, is AppNavigationState.Room, is AppNavigationState.Thread, - AppNavigationState.Root -> AppNavigationState.Session(sessionId) + is AppNavigationState.Root -> AppNavigationState.Session(owner, sessionId) } currentAppNavigationState.value = newValue } - override fun onNavigateToSpace(spaceId: SpaceId) { + override fun onNavigateToSpace(owner: String, spaceId: SpaceId) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue") val newValue: AppNavigationState.Space = when (currentValue) { AppNavigationState.Root -> error("onNavigateToSession() must be called first") - is AppNavigationState.Session -> AppNavigationState.Space(spaceId, currentValue) - is AppNavigationState.Space -> AppNavigationState.Space(spaceId, currentValue.parentSession) - is AppNavigationState.Room -> AppNavigationState.Space(spaceId, currentValue.parentSpace.parentSession) - is AppNavigationState.Thread -> AppNavigationState.Space(spaceId, currentValue.parentRoom.parentSpace.parentSession) + is AppNavigationState.Session -> AppNavigationState.Space(owner, spaceId, currentValue) + is AppNavigationState.Space -> AppNavigationState.Space(owner, spaceId, currentValue.parentSession) + is AppNavigationState.Room -> AppNavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession) + is AppNavigationState.Thread -> AppNavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession) } currentAppNavigationState.value = newValue } - override fun onNavigateToRoom(roomId: RoomId) { + override fun onNavigateToRoom(owner: String, roomId: RoomId) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue") val newValue: AppNavigationState.Room = when (currentValue) { AppNavigationState.Root -> error("onNavigateToSession() must be called first") is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") - is AppNavigationState.Space -> AppNavigationState.Room(roomId, currentValue) - is AppNavigationState.Room -> AppNavigationState.Room(roomId, currentValue.parentSpace) - is AppNavigationState.Thread -> AppNavigationState.Room(roomId, currentValue.parentRoom.parentSpace) + is AppNavigationState.Space -> AppNavigationState.Room(owner, roomId, currentValue) + is AppNavigationState.Room -> AppNavigationState.Room(owner, roomId, currentValue.parentSpace) + is AppNavigationState.Thread -> AppNavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace) } currentAppNavigationState.value = newValue } - override fun onNavigateToThread(threadId: ThreadId) { + override fun onNavigateToThread(owner: String, threadId: ThreadId) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue") val newValue: AppNavigationState.Thread = when (currentValue) { AppNavigationState.Root -> error("onNavigateToSession() must be called first") is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") is AppNavigationState.Space -> error("onNavigateToRoom() must be called first") - is AppNavigationState.Room -> AppNavigationState.Thread(threadId, currentValue) - is AppNavigationState.Thread -> AppNavigationState.Thread(threadId, currentValue.parentRoom) + is AppNavigationState.Room -> AppNavigationState.Thread(owner, threadId, currentValue) + is AppNavigationState.Thread -> AppNavigationState.Thread(owner, threadId, currentValue.parentRoom) } currentAppNavigationState.value = newValue } - override fun onLeavingThread() { + override fun onLeavingThread(owner: String) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue") + if (!currentValue.assertOwner(owner)) return val newValue: AppNavigationState.Room = when (currentValue) { AppNavigationState.Root -> error("onNavigateToSession() must be called first") is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") @@ -109,9 +109,10 @@ class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStat currentAppNavigationState.value = newValue } - override fun onLeavingRoom() { + override fun onLeavingRoom(owner: String) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue") + if (!currentValue.assertOwner(owner)) return val newValue: AppNavigationState.Space = when (currentValue) { AppNavigationState.Root -> error("onNavigateToSession() must be called first") is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") @@ -122,9 +123,10 @@ class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStat currentAppNavigationState.value = newValue } - override fun onLeavingSpace() { + override fun onLeavingSpace(owner: String) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue") + if (!currentValue.assertOwner(owner)) return val newValue: AppNavigationState.Session = when (currentValue) { AppNavigationState.Root -> error("onNavigateToSession() must be called first") is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") @@ -135,9 +137,18 @@ class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStat currentAppNavigationState.value = newValue } - override fun onLeavingSession() { + override fun onLeavingSession(owner: String) { val currentValue = currentAppNavigationState.value Timber.tag(loggerTag.value).d("Leaving session. Current state: $currentValue") + if (!currentValue.assertOwner(owner)) return currentAppNavigationState.value = AppNavigationState.Root } + + private fun AppNavigationState.assertOwner(owner: String): Boolean { + if (this.owner != owner) { + Timber.tag(loggerTag.value).d("Can't leave current state as the owner is not the same (current = ${this.owner}, new = $owner)") + return false + } + return true + } } diff --git a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateServiceTest.kt b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateServiceTest.kt index 9162bdaf75..1ccd4c5b7a 100644 --- a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateServiceTest.kt +++ b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateServiceTest.kt @@ -24,29 +24,35 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SPACE_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.test.A_ROOM_OWNER +import io.element.android.services.appnavstate.test.A_SESSION_OWNER +import io.element.android.services.appnavstate.test.A_SPACE_OWNER +import io.element.android.services.appnavstate.test.A_THREAD_OWNER import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Test - class DefaultAppNavigationStateServiceTest { @Test fun testNavigation() = runTest { val service = DefaultAppNavigationStateService() - service.onNavigateToSession(A_SESSION_ID) - service.onNavigateToSpace(A_SPACE_ID) - service.onNavigateToRoom(A_ROOM_ID) - service.onNavigateToThread(A_THREAD_ID) + service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) + service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) + service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) + service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) assertThat(service.appNavigationStateFlow.first()).isEqualTo( AppNavigationState.Thread( - A_THREAD_ID, + A_THREAD_OWNER, A_THREAD_ID, AppNavigationState.Room( + A_ROOM_OWNER, A_ROOM_ID, AppNavigationState.Space( + A_SPACE_OWNER, A_SPACE_ID, AppNavigationState.Session( + A_SESSION_OWNER, A_SESSION_ID ) ) @@ -58,6 +64,6 @@ class DefaultAppNavigationStateServiceTest { @Test fun testFailure() = runTest { val service = DefaultAppNavigationStateService() - assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_ID) } + assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) } } } diff --git a/services/appnavstate/test/build.gradle.kts b/services/appnavstate/test/build.gradle.kts index cda023b236..57dda0d29e 100644 --- a/services/appnavstate/test/build.gradle.kts +++ b/services/appnavstate/test/build.gradle.kts @@ -27,4 +27,5 @@ android { dependencies { api(projects.libraries.matrix.api) api(projects.services.appnavstate.api) + implementation(libs.coroutines.core) } diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt index 20e872b803..aa0b351220 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt @@ -23,6 +23,11 @@ import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.services.appnavstate.api.AppNavigationState +const val A_SESSION_OWNER = "aSessionOwner" +const val A_SPACE_OWNER = "aSpaceOwner" +const val A_ROOM_OWNER = "aRoomOwner" +const val A_THREAD_OWNER = "aThreadOwner" + fun anAppNavigationState( sessionId: SessionId? = null, spaceId: SpaceId? = MAIN_SPACE, @@ -32,17 +37,17 @@ fun anAppNavigationState( if (sessionId == null) { return AppNavigationState.Root } - val session = AppNavigationState.Session(sessionId) + val session = AppNavigationState.Session(A_SESSION_OWNER, sessionId) if (spaceId == null) { return session } - val space = AppNavigationState.Space(spaceId, session) + val space = AppNavigationState.Space(A_SPACE_OWNER, spaceId, session) if (roomId == null) { return space } - val room = AppNavigationState.Room(roomId, space) + val room = AppNavigationState.Room(A_ROOM_OWNER, roomId, space) if (threadId == null) { return room } - return AppNavigationState.Thread(threadId, room) + return AppNavigationState.Thread(A_THREAD_OWNER, threadId, room) } diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/NoopAppNavigationStateService.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/NoopAppNavigationStateService.kt new file mode 100644 index 0000000000..c31d74ec18 --- /dev/null +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/NoopAppNavigationStateService.kt @@ -0,0 +1,48 @@ +/* + * 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.services.appnavstate.test + +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.SpaceId +import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class NoopAppNavigationStateService : AppNavigationStateService { + + private val currentAppNavigationState: MutableStateFlow = + MutableStateFlow(AppNavigationState.Root) + override val appNavigationStateFlow: StateFlow = currentAppNavigationState + + override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit + override fun onLeavingSession(owner: String) = Unit + + override fun onNavigateToSpace(owner: String, spaceId: SpaceId) = Unit + + override fun onLeavingSpace(owner: String) = Unit + + override fun onNavigateToRoom(owner: String, roomId: RoomId) = Unit + + override fun onLeavingRoom(owner: String) = Unit + + override fun onNavigateToThread(owner: String, threadId: ThreadId) = Unit + + override fun onLeavingThread(owner: String) = Unit +}