Merge pull request #319 from vector-im/feature/fga/clean_up

Feature/fga/clean up
This commit is contained in:
ganfra 2023-04-14 17:49:09 +02:00 committed by GitHub
commit d32fbd1d09
28 changed files with 361 additions and 218 deletions

View file

@ -68,4 +68,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.rageshake.impl)
testImplementation(projects.services.appnavstate.test)
testImplementation(libs.test.appyx.junit)
testImplementation(libs.test.arch.core)
} }

View file

@ -108,17 +108,17 @@ class LoggedInFlowNode @AssistedInject constructor(
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory() val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory) Coil.setImageLoader(imageLoaderFactory)
inputs.matrixClient.startSync() 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 // 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) loggedInFlowProcessor.observeEvents(coroutineScope)
}, },
onDestroy = { onDestroy = {
val imageLoaderFactory = bindings<MatrixUIBindings>().notLoggedInImageLoaderFactory() val imageLoaderFactory = bindings<MatrixUIBindings>().notLoggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory) Coil.setImageLoader(imageLoaderFactory)
plugins<LifecycleCallback>().forEach { it.onFlowReleased(inputs.matrixClient) } plugins<LifecycleCallback>().forEach { it.onFlowReleased(inputs.matrixClient) }
appNavigationStateService.onLeavingSpace() appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession() appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving() loggedInFlowProcessor.stopObserving()
} }
) )

View file

@ -19,6 +19,7 @@ package io.element.android.appnav
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@ -44,6 +45,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
@ -55,7 +57,6 @@ class RoomFlowNode @AssistedInject constructor(
private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
private val appNavigationStateService: AppNavigationStateService, private val appNavigationStateService: AppNavigationStateService,
roomMembershipObserver: RoomMembershipObserver, roomMembershipObserver: RoomMembershipObserver,
coroutineScope: CoroutineScope,
) : BackstackNode<RoomFlowNode.NavTarget>( ) : BackstackNode<RoomFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Messages, initialElement = NavTarget.Messages,
@ -75,41 +76,49 @@ class RoomFlowNode @AssistedInject constructor(
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val timeline = inputs.room.timeline()
private val roomFlowPresenter = RoomFlowPresenter(inputs.room)
init { init {
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
Timber.v("OnCreate") Timber.v("OnCreate")
plugins<LifecycleCallback>().forEach { it.onFlowCreated(inputs.room) } plugins<LifecycleCallback>().forEach { it.onFlowCreated(inputs.room) }
appNavigationStateService.onNavigateToRoom(inputs.room.roomId) appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
fetchRoomMembers()
}, },
onDestroy = { onDestroy = {
Timber.v("OnDestroy") Timber.v("OnDestroy")
inputs.room.close() inputs.room.close()
plugins<LifecycleCallback>().forEach { it.onFlowReleased(inputs.room) } plugins<LifecycleCallback>().forEach { it.onFlowReleased(inputs.room) }
appNavigationStateService.onLeavingRoom() appNavigationStateService.onLeavingRoom(id)
} }
) )
roomMembershipObserver.updates roomMembershipObserver.updates
.filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom } .filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom }
.onEach { .onEach {
navigateUp() 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Messages -> { NavTarget.Messages -> {
messagesEntryPoint.createNode(this, buildContext, object : MessagesEntryPoint.Callback { val callback = object : MessagesEntryPoint.Callback {
override fun onRoomDetailsClicked() { override fun onRoomDetailsClicked() {
backstack.push(NavTarget.RoomDetails) backstack.push(NavTarget.RoomDetails)
} }
}) }
messagesEntryPoint.createNode(this, buildContext, callback)
} }
NavTarget.RoomDetails -> { NavTarget.RoomDetails -> {
roomDetailsEntryPoint.createNode(this, buildContext, emptyList()) roomDetailsEntryPoint.createNode(this, buildContext, emptyList())
@ -127,7 +136,6 @@ class RoomFlowNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
roomFlowPresenter.present()
Children( Children(
navModel = backstack, navModel = backstack,
modifier = modifier, modifier = modifier,

View file

@ -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<RoomFlowState> {
@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

View file

@ -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<Plugin>): Node {
return node(buildContext) {}.also {
nodeId = it.id
}
}
}
private fun aRoomFlowNode(
plugins: List<Plugin>,
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)
}
}

View file

@ -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()
}
}
}

View file

@ -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.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.RoomScope 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 io.element.android.libraries.matrix.api.room.RoomMember
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -62,19 +61,16 @@ class RoomDetailsFlowNode @AssistedInject constructor(
data class RoomMemberDetails(val roomMember: RoomMember) : NavTarget 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.RoomDetails -> createNode<RoomDetailsNode>(buildContext, listOf(callback)) NavTarget.RoomDetails -> {
val callback = object : RoomDetailsNode.Callback {
override fun openRoomMemberList() {
backstack.push(NavTarget.RoomMemberList)
}
}
createNode<RoomDetailsNode>(buildContext, listOf(callback))
}
NavTarget.RoomMemberList -> { NavTarget.RoomMemberList -> {
val callback = object : RoomMemberListNode.Callback { val callback = object : RoomMemberListNode.Callback {
override fun openRoomMemberDetails(roomMember: RoomMember) { override fun openRoomMemberDetails(roomMember: RoomMember) {
@ -84,7 +80,8 @@ class RoomDetailsFlowNode @AssistedInject constructor(
createNode<RoomMemberListNode>(buildContext, listOf(callback)) createNode<RoomMemberListNode>(buildContext, listOf(callback))
} }
is NavTarget.RoomMemberDetails -> { is NavTarget.RoomMemberDetails -> {
createNode<RoomMemberDetailsNode>(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMember))) val inputs = RoomMemberDetailsNode.Inputs(navTarget.roomMember)
createNode<RoomMemberDetailsNode>(buildContext, listOf(inputs))
} }
} }
} }

View file

@ -40,7 +40,11 @@ class RoomDetailsNode @AssistedInject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val callback = plugins<RoomDetailsFlowNode.Callback>().firstOrNull() interface Callback : Plugin {
fun openRoomMemberList()
}
private val callback = plugins<Callback>().firstOrNull()
private fun openRoomMemberList() { private fun openRoomMemberList() {
callback?.openRoomMemberList() callback?.openRoomMemberList()

View file

@ -16,7 +16,6 @@
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.roomdetails.impl.members.details
import android.content.Context
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -51,30 +50,32 @@ class RoomMemberDetailsNode @AssistedInject constructor(
private val inputs = inputs<Inputs>() private val inputs = inputs<Inputs>()
private val presenter = presenterFactory.create(inputs.member) 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 @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val context = LocalContext.current 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() val state = presenter.present()
RoomMemberDetailsView( RoomMemberDetailsView(
state = state, state = state,
modifier = modifier, modifier = modifier,
goBack = { navigateUp() }, goBack = this::navigateUp,
onShareUser = { onShareUser(context) } onShareUser = ::onShareUser
) )
} }
} }

View file

@ -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.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.test.A_ROOM_ID 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_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.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -45,7 +44,7 @@ import org.junit.Test
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class RoomDetailsPresenterTests { class RoomDetailsPresenterTests {
private val roomMembershipObserver = RoomMembershipObserver(A_SESSION_ID) private val roomMembershipObserver = RoomMembershipObserver()
@Test @Test
fun `present - initial state is created from room info`() = runTest { fun `present - initial state is created from room info`() = runTest {

View file

@ -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.designsystem.utils.handleSnackbarMessage
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.UserId 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.room.RoomSummary
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
@ -61,11 +60,9 @@ class RoomListPresenter @Inject constructor(
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
) : Presenter<RoomListState> { ) : Presenter<RoomListState> {
private val roomMembershipObserver: RoomMembershipObserver = client.roomMembershipObserver()
@Composable @Composable
override fun present(): RoomListState { override fun present(): RoomListState {
val matrixUser: MutableState<MatrixUser?> = remember { val matrixUser: MutableState<MatrixUser?> = rememberSaveable {
mutableStateOf(null) mutableStateOf(null)
} }
var filter by rememberSaveable { mutableStateOf("") } var filter by rememberSaveable { mutableStateOf("") }

View file

@ -103,6 +103,7 @@ network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-k
# Test # Test
test_core = { module = "androidx.test:core", version.ref = "test_core" } test_core = { module = "androidx.test:core", version.ref = "test_core" }
test_corektx = { module = "androidx.test:core-ktx", 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_junit = "junit:junit:4.13.2"
test_runner = "androidx.test:runner:1.5.2" test_runner = "androidx.test:runner:1.5.2"
test_uiautomator = "androidx.test.uiautomator:uiautomator:2.2.0" 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_truth = "com.google.truth:truth:1.1.3"
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11"
test_robolectric = "org.robolectric:robolectric:4.9.2" test_robolectric = "org.robolectric:robolectric:4.9.2"
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
# Others # Others
coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil = { module = "io.coil-kt:coil", version.ref = "coil" }

View file

@ -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 <NavTarget : Any> ParentNode<NavTarget>.childNode(navTarget: NavTarget): Node? {
val childMap = children.value
val key = childMap.keys.find { it.navTarget == navTarget }
return childMap[key]?.nodeOrNull
}

View file

@ -59,5 +59,5 @@ interface MatrixRoom: Closeable {
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit> suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
fun leave(): Result<Unit> suspend fun leave(): Result<Unit>
} }

View file

@ -31,7 +31,6 @@ data class RoomMember(
val isIgnored: Boolean, val isIgnored: Boolean,
) : Parcelable ) : Parcelable
@Parcelize enum class RoomMembershipState {
enum class RoomMembershipState : Parcelable {
BAN, INVITE, JOIN, KNOCK, LEAVE BAN, INVITE, JOIN, KNOCK, LEAVE
} }

View file

@ -17,14 +17,11 @@
package io.element.android.libraries.matrix.api.room 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.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
class RoomMembershipObserver( class RoomMembershipObserver {
private val sessionId: SessionId,
) {
data class RoomMembershipUpdate( data class RoomMembershipUpdate(
val roomId: RoomId, val roomId: RoomId,
val isUserInRoom: Boolean, val isUserInRoom: Boolean,

View file

@ -145,7 +145,7 @@ class RustMatrixClient constructor(
private val mediaResolver = RustMediaResolver(this) private val mediaResolver = RustMediaResolver(this)
private val isSyncing = AtomicBoolean(false) private val isSyncing = AtomicBoolean(false)
private val roomMembershipObserver = RoomMembershipObserver(sessionId) private val roomMembershipObserver = RoomMembershipObserver()
init { init {
client.setDelegate(clientDelegate) client.setDelegate(clientDelegate)

View file

@ -93,6 +93,7 @@ class RustMatrixRoom(
coroutineDispatchers = coroutineDispatchers coroutineDispatchers = coroutineDispatchers
) )
} }
override fun close() { override fun close() {
innerRoom.destroy() innerRoom.destroy()
slidingSyncRoom.destroy() slidingSyncRoom.destroy()
@ -188,7 +189,9 @@ class RustMatrixRoom(
} }
} }
override fun leave(): Result<Unit> { override suspend fun leave(): Result<Unit> = withContext(coroutineDispatchers.io) {
return runCatching { innerRoom.leave() } runCatching {
innerRoom.leave()
}
} }
} }

View file

@ -110,7 +110,7 @@ class FakeMatrixClient(
override fun onSlidingSyncUpdate() {} override fun onSlidingSyncUpdate() {}
override fun roomMembershipObserver(): RoomMembershipObserver { override fun roomMembershipObserver(): RoomMembershipObserver {
return RoomMembershipObserver(A_SESSION_ID) return RoomMembershipObserver()
} }
// Mocks // Mocks

View file

@ -122,7 +122,7 @@ class FakeMatrixRoom(
return Result.success(Unit) return Result.success(Unit)
} }
override fun leave(): Result<Unit> = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) override suspend fun leave(): Result<Unit> = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit)
override fun close() = Unit override fun close() = Unit

View file

@ -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.SpaceId
import io.element.android.libraries.matrix.api.core.ThreadId 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( data class Session(
override val owner: String,
val sessionId: SessionId, val sessionId: SessionId,
) : AppNavigationState ) : AppNavigationState(owner)
data class Space( data class Space(
override val owner: String,
// Can be fake value, if no space is selected // Can be fake value, if no space is selected
val spaceId: SpaceId, val spaceId: SpaceId,
val parentSession: Session, val parentSession: Session,
) : AppNavigationState ) : AppNavigationState(owner)
data class Room( data class Room(
override val owner: String,
val roomId: RoomId, val roomId: RoomId,
val parentSpace: Space, val parentSpace: Space,
) : AppNavigationState ) : AppNavigationState(owner)
data class Thread( data class Thread(
override val owner: String,
val threadId: ThreadId, val threadId: ThreadId,
val parentRoom: Room, val parentRoom: Room,
) : AppNavigationState ) : AppNavigationState(owner)
} }

View file

@ -25,15 +25,15 @@ import kotlinx.coroutines.flow.StateFlow
interface AppNavigationStateService { interface AppNavigationStateService {
val appNavigationStateFlow: StateFlow<AppNavigationState> val appNavigationStateFlow: StateFlow<AppNavigationState>
fun onNavigateToSession(sessionId: SessionId) fun onNavigateToSession(owner: String, sessionId: SessionId)
fun onLeavingSession() fun onLeavingSession(owner: String)
fun onNavigateToSpace(spaceId: SpaceId) fun onNavigateToSpace(owner: String, spaceId: SpaceId)
fun onLeavingSpace() fun onLeavingSpace(owner: String)
fun onNavigateToRoom(roomId: RoomId) fun onNavigateToRoom(owner: String, roomId: RoomId)
fun onLeavingRoom() fun onLeavingRoom(owner: String)
fun onNavigateToThread(threadId: ThreadId) fun onNavigateToThread(owner: String, threadId: ThreadId)
fun onLeavingThread() fun onLeavingThread(owner: String)
} }

View file

@ -47,4 +47,5 @@ dependencies {
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.appnavstate.test)
} }

View file

@ -40,11 +40,10 @@ private val loggerTag = LoggerTag("Navigation")
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStateService { class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStateService {
private val currentAppNavigationState = MutableStateFlow<AppNavigationState>(AppNavigationState.Root) private val currentAppNavigationState: MutableStateFlow<AppNavigationState> = MutableStateFlow(AppNavigationState.Root)
override val appNavigationStateFlow: StateFlow<AppNavigationState> = currentAppNavigationState override val appNavigationStateFlow: StateFlow<AppNavigationState> = currentAppNavigationState
override fun onNavigateToSession(sessionId: SessionId) { override fun onNavigateToSession(owner: String, sessionId: SessionId) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue") Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue")
val newValue: AppNavigationState.Session = when (currentValue) { val newValue: AppNavigationState.Session = when (currentValue) {
@ -52,53 +51,54 @@ class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStat
is AppNavigationState.Space, is AppNavigationState.Space,
is AppNavigationState.Room, is AppNavigationState.Room,
is AppNavigationState.Thread, is AppNavigationState.Thread,
AppNavigationState.Root -> AppNavigationState.Session(sessionId) is AppNavigationState.Root -> AppNavigationState.Session(owner, sessionId)
} }
currentAppNavigationState.value = newValue currentAppNavigationState.value = newValue
} }
override fun onNavigateToSpace(spaceId: SpaceId) { override fun onNavigateToSpace(owner: String, spaceId: SpaceId) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue") Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue")
val newValue: AppNavigationState.Space = when (currentValue) { val newValue: AppNavigationState.Space = when (currentValue) {
AppNavigationState.Root -> error("onNavigateToSession() must be called first") AppNavigationState.Root -> error("onNavigateToSession() must be called first")
is AppNavigationState.Session -> AppNavigationState.Space(spaceId, currentValue) is AppNavigationState.Session -> AppNavigationState.Space(owner, spaceId, currentValue)
is AppNavigationState.Space -> AppNavigationState.Space(spaceId, currentValue.parentSession) is AppNavigationState.Space -> AppNavigationState.Space(owner, spaceId, currentValue.parentSession)
is AppNavigationState.Room -> AppNavigationState.Space(spaceId, currentValue.parentSpace.parentSession) is AppNavigationState.Room -> AppNavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession)
is AppNavigationState.Thread -> AppNavigationState.Space(spaceId, currentValue.parentRoom.parentSpace.parentSession) is AppNavigationState.Thread -> AppNavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession)
} }
currentAppNavigationState.value = newValue currentAppNavigationState.value = newValue
} }
override fun onNavigateToRoom(roomId: RoomId) { override fun onNavigateToRoom(owner: String, roomId: RoomId) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue") Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue")
val newValue: AppNavigationState.Room = when (currentValue) { val newValue: AppNavigationState.Room = when (currentValue) {
AppNavigationState.Root -> error("onNavigateToSession() must be called first") AppNavigationState.Root -> error("onNavigateToSession() must be called first")
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
is AppNavigationState.Space -> AppNavigationState.Room(roomId, currentValue) is AppNavigationState.Space -> AppNavigationState.Room(owner, roomId, currentValue)
is AppNavigationState.Room -> AppNavigationState.Room(roomId, currentValue.parentSpace) is AppNavigationState.Room -> AppNavigationState.Room(owner, roomId, currentValue.parentSpace)
is AppNavigationState.Thread -> AppNavigationState.Room(roomId, currentValue.parentRoom.parentSpace) is AppNavigationState.Thread -> AppNavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace)
} }
currentAppNavigationState.value = newValue currentAppNavigationState.value = newValue
} }
override fun onNavigateToThread(threadId: ThreadId) { override fun onNavigateToThread(owner: String, threadId: ThreadId) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue") Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue")
val newValue: AppNavigationState.Thread = when (currentValue) { val newValue: AppNavigationState.Thread = when (currentValue) {
AppNavigationState.Root -> error("onNavigateToSession() must be called first") AppNavigationState.Root -> error("onNavigateToSession() must be called first")
is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") is AppNavigationState.Session -> error("onNavigateToSpace() must be called first")
is AppNavigationState.Space -> error("onNavigateToRoom() must be called first") is AppNavigationState.Space -> error("onNavigateToRoom() must be called first")
is AppNavigationState.Room -> AppNavigationState.Thread(threadId, currentValue) is AppNavigationState.Room -> AppNavigationState.Thread(owner, threadId, currentValue)
is AppNavigationState.Thread -> AppNavigationState.Thread(threadId, currentValue.parentRoom) is AppNavigationState.Thread -> AppNavigationState.Thread(owner, threadId, currentValue.parentRoom)
} }
currentAppNavigationState.value = newValue currentAppNavigationState.value = newValue
} }
override fun onLeavingThread() { override fun onLeavingThread(owner: String) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue") Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: AppNavigationState.Room = when (currentValue) { val newValue: AppNavigationState.Room = when (currentValue) {
AppNavigationState.Root -> error("onNavigateToSession() must be called first") AppNavigationState.Root -> error("onNavigateToSession() must be called first")
is AppNavigationState.Session -> error("onNavigateToSpace() 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 currentAppNavigationState.value = newValue
} }
override fun onLeavingRoom() { override fun onLeavingRoom(owner: String) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue") Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: AppNavigationState.Space = when (currentValue) { val newValue: AppNavigationState.Space = when (currentValue) {
AppNavigationState.Root -> error("onNavigateToSession() must be called first") AppNavigationState.Root -> error("onNavigateToSession() must be called first")
is AppNavigationState.Session -> error("onNavigateToSpace() 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 currentAppNavigationState.value = newValue
} }
override fun onLeavingSpace() { override fun onLeavingSpace(owner: String) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue") Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
val newValue: AppNavigationState.Session = when (currentValue) { val newValue: AppNavigationState.Session = when (currentValue) {
AppNavigationState.Root -> error("onNavigateToSession() must be called first") AppNavigationState.Root -> error("onNavigateToSession() must be called first")
is AppNavigationState.Session -> error("onNavigateToSpace() 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 currentAppNavigationState.value = newValue
} }
override fun onLeavingSession() { override fun onLeavingSession(owner: String) {
val currentValue = currentAppNavigationState.value val currentValue = currentAppNavigationState.value
Timber.tag(loggerTag.value).d("Leaving session. Current state: $currentValue") Timber.tag(loggerTag.value).d("Leaving session. Current state: $currentValue")
if (!currentValue.assertOwner(owner)) return
currentAppNavigationState.value = AppNavigationState.Root 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
}
} }

View file

@ -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_SPACE_ID
import io.element.android.libraries.matrix.test.A_THREAD_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.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.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
import org.junit.Test import org.junit.Test
class DefaultAppNavigationStateServiceTest { class DefaultAppNavigationStateServiceTest {
@Test @Test
fun testNavigation() = runTest { fun testNavigation() = runTest {
val service = DefaultAppNavigationStateService() val service = DefaultAppNavigationStateService()
service.onNavigateToSession(A_SESSION_ID) service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID)
service.onNavigateToSpace(A_SPACE_ID) service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID)
service.onNavigateToRoom(A_ROOM_ID) service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID)
service.onNavigateToThread(A_THREAD_ID) service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID)
assertThat(service.appNavigationStateFlow.first()).isEqualTo( assertThat(service.appNavigationStateFlow.first()).isEqualTo(
AppNavigationState.Thread( AppNavigationState.Thread(
A_THREAD_ID, A_THREAD_OWNER, A_THREAD_ID,
AppNavigationState.Room( AppNavigationState.Room(
A_ROOM_OWNER,
A_ROOM_ID, A_ROOM_ID,
AppNavigationState.Space( AppNavigationState.Space(
A_SPACE_OWNER,
A_SPACE_ID, A_SPACE_ID,
AppNavigationState.Session( AppNavigationState.Session(
A_SESSION_OWNER,
A_SESSION_ID A_SESSION_ID
) )
) )
@ -58,6 +64,6 @@ class DefaultAppNavigationStateServiceTest {
@Test @Test
fun testFailure() = runTest { fun testFailure() = runTest {
val service = DefaultAppNavigationStateService() val service = DefaultAppNavigationStateService()
assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_ID) } assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) }
} }
} }

View file

@ -27,4 +27,5 @@ android {
dependencies { dependencies {
api(projects.libraries.matrix.api) api(projects.libraries.matrix.api)
api(projects.services.appnavstate.api) api(projects.services.appnavstate.api)
implementation(libs.coroutines.core)
} }

View file

@ -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.libraries.matrix.api.core.ThreadId
import io.element.android.services.appnavstate.api.AppNavigationState 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( fun anAppNavigationState(
sessionId: SessionId? = null, sessionId: SessionId? = null,
spaceId: SpaceId? = MAIN_SPACE, spaceId: SpaceId? = MAIN_SPACE,
@ -32,17 +37,17 @@ fun anAppNavigationState(
if (sessionId == null) { if (sessionId == null) {
return AppNavigationState.Root return AppNavigationState.Root
} }
val session = AppNavigationState.Session(sessionId) val session = AppNavigationState.Session(A_SESSION_OWNER, sessionId)
if (spaceId == null) { if (spaceId == null) {
return session return session
} }
val space = AppNavigationState.Space(spaceId, session) val space = AppNavigationState.Space(A_SPACE_OWNER, spaceId, session)
if (roomId == null) { if (roomId == null) {
return space return space
} }
val room = AppNavigationState.Room(roomId, space) val room = AppNavigationState.Room(A_ROOM_OWNER, roomId, space)
if (threadId == null) { if (threadId == null) {
return room return room
} }
return AppNavigationState.Thread(threadId, room) return AppNavigationState.Thread(A_THREAD_OWNER, threadId, room)
} }

View file

@ -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<AppNavigationState> =
MutableStateFlow(AppNavigationState.Root)
override val appNavigationStateFlow: StateFlow<AppNavigationState> = 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
}