Room : rename the flows
This commit is contained in:
@@ -28,7 +28,7 @@ import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.node.ParentNode
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.appnav.LoggedInFlowNode
|
||||
import io.element.android.appnav.room.RoomFlowNode
|
||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||
import io.element.android.appnav.RootFlowNode
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
@@ -67,7 +67,7 @@ class MainNode(
|
||||
}
|
||||
}
|
||||
|
||||
private val roomFlowNodeCallback = object : RoomFlowNode.LifecycleCallback {
|
||||
private val roomFlowNodeCallback = object : RoomLoadedFlowNode.LifecycleCallback {
|
||||
override fun onFlowCreated(identifier: String, room: MatrixRoom) {
|
||||
val component = bindings<RoomComponent.ParentBindings>().roomComponentBuilder().room(room).build()
|
||||
mainDaggerComponentOwner.addComponent(identifier, component)
|
||||
|
||||
@@ -42,8 +42,8 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.loggedin.LoggedInNode
|
||||
import io.element.android.appnav.room.AwaitRoomNode
|
||||
import io.element.android.appnav.room.RoomFlowNode
|
||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||
import io.element.android.features.analytics.api.AnalyticsEntryPoint
|
||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||
@@ -208,7 +208,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
@Parcelize
|
||||
data class Room(
|
||||
val roomId: RoomId,
|
||||
val initialElement: RoomFlowNode.NavTarget = RoomFlowNode.NavTarget.Messages
|
||||
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
@@ -256,7 +256,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomFlowNode.NavTarget.RoomDetails))
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomLoadedFlowNode.NavTarget.RoomDetails))
|
||||
}
|
||||
|
||||
override fun onReportBugClicked() {
|
||||
@@ -270,13 +270,13 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
is NavTarget.Room -> {
|
||||
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||
val callback = object : RoomFlowNode.Callback {
|
||||
val callback = object : RoomLoadedFlowNode.Callback {
|
||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||
coroutineScope.launch { attachRoom(roomId) }
|
||||
}
|
||||
}
|
||||
val inputs = AwaitRoomNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement)
|
||||
createNode<AwaitRoomNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
||||
val inputs = RoomFlowNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement)
|
||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
|
||||
}
|
||||
NavTarget.Settings -> {
|
||||
val callback = object : PreferencesEntryPoint.Callback {
|
||||
@@ -334,7 +334,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun attachRoom(roomId: RoomId): AwaitRoomNode {
|
||||
suspend fun attachRoom(roomId: RoomId): RoomFlowNode {
|
||||
return attachChild {
|
||||
backstack.singleTop(NavTarget.RoomList)
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
|
||||
@@ -1,177 +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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.appnav.room
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
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.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.NodeLifecycleCallback
|
||||
import io.element.android.appnav.safeRoot
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.placeholderBackground
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class AwaitRoomNode @AssistedInject constructor(
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
awaitRoomStateFlowFactory: AwaitRoomStateFlowFactory,
|
||||
) :
|
||||
BackstackNode<AwaitRoomNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Loading,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
|
||||
data class Inputs(
|
||||
val roomId: RoomId,
|
||||
val initialElement: RoomFlowNode.NavTarget = RoomFlowNode.NavTarget.Messages,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val awaitRoomStateFlow = awaitRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Loading : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Loaded : NavTarget
|
||||
}
|
||||
|
||||
init {
|
||||
awaitRoomStateFlow.onEach { awaitRoomState ->
|
||||
when (awaitRoomState) {
|
||||
is AwaitRoomState.Loaded -> backstack.safeRoot(NavTarget.Loaded)
|
||||
else -> backstack.safeRoot(NavTarget.Loading)
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Loaded -> {
|
||||
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||
val roomFlowNodeCallback = plugins<RoomFlowNode.Callback>()
|
||||
val awaitRoomState = awaitRoomStateFlow.value
|
||||
if (awaitRoomState is AwaitRoomState.Loaded) {
|
||||
val inputs = RoomFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
|
||||
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback + nodeLifecycleCallbacks)
|
||||
} else {
|
||||
loadingNode(buildContext, this::navigateUp)
|
||||
}
|
||||
}
|
||||
NavTarget.Loading -> {
|
||||
loadingNode(buildContext, this::navigateUp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadingNode(buildContext: BuildContext, onBackPressed: () -> Unit) = node(buildContext) { modifier ->
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets.systemBars,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackPressed)
|
||||
},
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(AvatarSize.TimelineRoom.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
PlaceholderAtom(width = 20.dp, height = 7.dp)
|
||||
Spacer(modifier = Modifier.width(7.dp))
|
||||
PlaceholderAtom(width = 45.dp, height = 7.dp)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding), contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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.room
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.appnav.loggedin.LoggedInState
|
||||
import io.element.android.appnav.loggedin.LoggedInStateProvider
|
||||
import io.element.android.appnav.loggedin.LoggedInView
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.theme.placeholderBackground
|
||||
import io.element.android.libraries.theme.ElementTheme
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoadingRoomNodeView(
|
||||
state: LoadingRoomState,
|
||||
onBackClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets.systemBars,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier,
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClicked)
|
||||
},
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(AvatarSize.TimelineRoom.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
.background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
PlaceholderAtom(width = 20.dp, height = 7.dp)
|
||||
Spacer(modifier = Modifier.width(7.dp))
|
||||
PlaceholderAtom(width = 45.dp, height = 7.dp)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (state is LoadingRoomState.Error) {
|
||||
Text(
|
||||
text = stringResource(id = CommonStrings.error_unknown),
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
} else {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoadingRoomNodeViewLightPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
|
||||
ElementPreviewLight { ContentToPreview(state) }
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoadingRoomNodeViewDarkPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: LoadingRoomState) {
|
||||
LoadingRoomNodeView(
|
||||
state = state,
|
||||
onBackClicked = {}
|
||||
)
|
||||
}
|
||||
@@ -16,12 +16,14 @@
|
||||
|
||||
package io.element.android.appnav.room
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
@@ -29,25 +31,36 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed interface AwaitRoomState {
|
||||
object Loading : AwaitRoomState
|
||||
object Error : AwaitRoomState
|
||||
data class Loaded(val room: MatrixRoom) : AwaitRoomState
|
||||
sealed interface LoadingRoomState {
|
||||
object Loading : LoadingRoomState
|
||||
object Error : LoadingRoomState
|
||||
data class Loaded(val room: MatrixRoom) : LoadingRoomState
|
||||
}
|
||||
|
||||
open class LoadingRoomStateProvider : PreviewParameterProvider<LoadingRoomState> {
|
||||
override val values: Sequence<LoadingRoomState>
|
||||
get() = sequenceOf(
|
||||
LoadingRoomState.Loading,
|
||||
LoadingRoomState.Error
|
||||
)
|
||||
}
|
||||
|
||||
@SingleIn(SessionScope::class)
|
||||
class AwaitRoomStateFlowFactory @Inject constructor(private val matrixClient: MatrixClient) {
|
||||
|
||||
fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow<AwaitRoomState> = suspend {
|
||||
fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow<LoadingRoomState> =
|
||||
getRoomFlow(roomId)
|
||||
.map { room ->
|
||||
if (room != null) {
|
||||
LoadingRoomState.Loaded(room)
|
||||
} else {
|
||||
LoadingRoomState.Error
|
||||
}
|
||||
}
|
||||
.stateIn(lifecycleScope, SharingStarted.Eagerly, LoadingRoomState.Loading)
|
||||
|
||||
private fun getRoomFlow(roomId: RoomId): Flow<MatrixRoom?> = suspend {
|
||||
matrixClient.getRoom(roomId = roomId)
|
||||
}
|
||||
.asFlow()
|
||||
.map { room ->
|
||||
if (room != null) {
|
||||
AwaitRoomState.Loaded(room)
|
||||
} else {
|
||||
AwaitRoomState.Error
|
||||
}
|
||||
}
|
||||
.stateIn(lifecycleScope, SharingStarted.Eagerly, AwaitRoomState.Loading)
|
||||
}
|
||||
@@ -14,166 +14,122 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.element.android.appnav.room
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
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.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.NodeLifecycleCallback
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.appnav.safeRoot
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class RoomFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted val buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
) : BackstackNode<RoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||
}
|
||||
|
||||
interface LifecycleCallback : NodeLifecycleCallback {
|
||||
fun onFlowCreated(identifier: String, room: MatrixRoom) = Unit
|
||||
fun onFlowReleased(identifier: String, room: MatrixRoom) = Unit
|
||||
}
|
||||
awaitRoomStateFlowFactory: AwaitRoomStateFlowFactory,
|
||||
) :
|
||||
BackstackNode<RoomFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Loading,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins
|
||||
) {
|
||||
|
||||
data class Inputs(
|
||||
val room: MatrixRoom,
|
||||
val initialElement: NavTarget = NavTarget.Messages,
|
||||
val roomId: RoomId,
|
||||
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val callbacks = plugins.filterIsInstance<Callback>()
|
||||
private val loadingRoomStateStateFlow = awaitRoomStateFlowFactory.create(lifecycleScope, inputs.roomId)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
Timber.v("OnCreate")
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.room) }
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
fetchRoomMembers()
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.room) }
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
}
|
||||
)
|
||||
roomMembershipObserver.updates
|
||||
.filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom }
|
||||
.onEach {
|
||||
navigateUp()
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
inputs<Inputs>()
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Loading : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object Loaded : NavTarget
|
||||
}
|
||||
|
||||
private fun fetchRoomMembers() = lifecycleScope.launch {
|
||||
val room = inputs.room
|
||||
room.updateMembers()
|
||||
.onFailure {
|
||||
Timber.e(it, "Fail to fetch members for room ${room.roomId}")
|
||||
}.onSuccess {
|
||||
Timber.v("Success fetching members for room ${room.roomId}")
|
||||
override fun onBuilt() {
|
||||
super.onBuilt()
|
||||
loadingRoomStateStateFlow
|
||||
.map {
|
||||
it is LoadingRoomState.Loaded
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { isLoaded ->
|
||||
if (isLoaded) {
|
||||
backstack.safeRoot(NavTarget.Loaded)
|
||||
} else {
|
||||
backstack.safeRoot(NavTarget.Loading)
|
||||
}
|
||||
}.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Messages -> {
|
||||
val callback = object : MessagesEntryPoint.Callback {
|
||||
override fun onRoomDetailsClicked() {
|
||||
backstack.push(NavTarget.RoomDetails)
|
||||
}
|
||||
|
||||
override fun onUserDataClicked(userId: UserId) {
|
||||
backstack.push(NavTarget.RoomMemberDetails(userId))
|
||||
}
|
||||
|
||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
||||
}
|
||||
NavTarget.Loaded -> {
|
||||
val nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
|
||||
val roomFlowNodeCallback = plugins<RoomLoadedFlowNode.Callback>()
|
||||
val awaitRoomState = loadingRoomStateStateFlow.value
|
||||
if (awaitRoomState is LoadingRoomState.Loaded) {
|
||||
val inputs = RoomLoadedFlowNode.Inputs(awaitRoomState.room, initialElement = inputs.initialElement)
|
||||
createNode<RoomLoadedFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback + nodeLifecycleCallbacks)
|
||||
} else {
|
||||
loadingNode(buildContext, this::navigateUp)
|
||||
}
|
||||
messagesEntryPoint.createNode(this, buildContext, callback)
|
||||
}
|
||||
NavTarget.RoomDetails -> {
|
||||
val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomDetails)
|
||||
roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
|
||||
}
|
||||
is NavTarget.RoomMemberDetails -> {
|
||||
val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId))
|
||||
roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
|
||||
NavTarget.Loading -> {
|
||||
loadingNode(buildContext, this::navigateUp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Messages : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomDetails : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||
private fun loadingNode(buildContext: BuildContext, onBackClicked: () -> Unit) = node(buildContext) { modifier ->
|
||||
val loadingRoomState by loadingRoomStateStateFlow.collectAsState()
|
||||
LoadingRoomNodeView(
|
||||
state = loadingRoomState,
|
||||
modifier = modifier,
|
||||
onBackClicked = onBackClicked
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
// Rely on the View Lifecycle instead of the Node Lifecycle,
|
||||
// because this node enters 'onDestroy' before his children, so it can leads to
|
||||
// using the room in a child node where it's already closed.
|
||||
DisposableEffect(Unit) {
|
||||
inputs.room.open()
|
||||
onDispose {
|
||||
inputs.room.close()
|
||||
}
|
||||
}
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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.room
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appnav.NodeLifecycleCallback
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.NodeInputs
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
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
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class RoomLoadedFlowNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
roomMembershipObserver: RoomMembershipObserver,
|
||||
) : BackstackNode<RoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = plugins.filterIsInstance(Inputs::class.java).first().initialElement,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onForwardedToSingleRoom(roomId: RoomId)
|
||||
}
|
||||
|
||||
interface LifecycleCallback : NodeLifecycleCallback {
|
||||
fun onFlowCreated(identifier: String, room: MatrixRoom) = Unit
|
||||
fun onFlowReleased(identifier: String, room: MatrixRoom) = Unit
|
||||
}
|
||||
|
||||
data class Inputs(
|
||||
val room: MatrixRoom,
|
||||
val initialElement: NavTarget = NavTarget.Messages,
|
||||
) : NodeInputs
|
||||
|
||||
private val inputs: Inputs = inputs()
|
||||
private val callbacks = plugins.filterIsInstance<Callback>()
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
Timber.v("OnCreate")
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.room) }
|
||||
appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId)
|
||||
fetchRoomMembers()
|
||||
},
|
||||
onDestroy = {
|
||||
Timber.v("OnDestroy")
|
||||
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.room) }
|
||||
appNavigationStateService.onLeavingRoom(id)
|
||||
}
|
||||
)
|
||||
roomMembershipObserver.updates
|
||||
.filter { update -> update.roomId == inputs.room.roomId && !update.isUserInRoom }
|
||||
.onEach {
|
||||
navigateUp()
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
inputs<Inputs>()
|
||||
}
|
||||
|
||||
private fun fetchRoomMembers() = lifecycleScope.launch {
|
||||
val room = inputs.room
|
||||
room.updateMembers()
|
||||
.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 -> {
|
||||
val callback = object : MessagesEntryPoint.Callback {
|
||||
override fun onRoomDetailsClicked() {
|
||||
backstack.push(NavTarget.RoomDetails)
|
||||
}
|
||||
|
||||
override fun onUserDataClicked(userId: UserId) {
|
||||
backstack.push(NavTarget.RoomMemberDetails(userId))
|
||||
}
|
||||
|
||||
override fun onForwardedToSingleRoom(roomId: RoomId) {
|
||||
callbacks.forEach { it.onForwardedToSingleRoom(roomId) }
|
||||
}
|
||||
}
|
||||
messagesEntryPoint.createNode(this, buildContext, callback)
|
||||
}
|
||||
NavTarget.RoomDetails -> {
|
||||
val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomDetails)
|
||||
roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
|
||||
}
|
||||
is NavTarget.RoomMemberDetails -> {
|
||||
val inputs = RoomDetailsEntryPoint.Inputs(RoomDetailsEntryPoint.InitialTarget.RoomMemberDetails(navTarget.userId))
|
||||
roomDetailsEntryPoint.createNode(this, buildContext, inputs, emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Messages : NavTarget
|
||||
|
||||
@Parcelize
|
||||
object RoomDetails : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class RoomMemberDetails(val userId: UserId) : NavTarget
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
// Rely on the View Lifecycle instead of the Node Lifecycle,
|
||||
// because this node enters 'onDestroy' before his children, so it can leads to
|
||||
// using the room in a child node where it's already closed.
|
||||
DisposableEffect(Unit) {
|
||||
inputs.room.open()
|
||||
onDispose {
|
||||
inputs.room.close()
|
||||
}
|
||||
}
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import com.bumble.appyx.navmodel.backstack.activeElement
|
||||
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.appnav.room.RoomFlowNode
|
||||
import io.element.android.appnav.room.RoomLoadedFlowNode
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
import io.element.android.libraries.architecture.childNode
|
||||
@@ -77,7 +77,7 @@ class RoomFlowNodeTest {
|
||||
plugins: List<Plugin>,
|
||||
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
|
||||
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
|
||||
) = RoomFlowNode(
|
||||
) = RoomLoadedFlowNode(
|
||||
buildContext = BuildContext.root(savedStateMap = null),
|
||||
plugins = plugins,
|
||||
messagesEntryPoint = messagesEntryPoint,
|
||||
@@ -91,15 +91,15 @@ class RoomFlowNodeTest {
|
||||
// GIVEN
|
||||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val inputs = RoomFlowNode.Inputs(room)
|
||||
val inputs = RoomLoadedFlowNode.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(roomFlowNode.backstack.activeElement).isEqualTo(RoomLoadedFlowNode.NavTarget.Messages)
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.Messages, Lifecycle.State.CREATED)
|
||||
val messagesNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.Messages)!!
|
||||
Truth.assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
|
||||
}
|
||||
|
||||
@@ -109,14 +109,14 @@ class RoomFlowNodeTest {
|
||||
val room = FakeMatrixRoom()
|
||||
val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
|
||||
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
|
||||
val inputs = RoomFlowNode.Inputs(room)
|
||||
val inputs = RoomLoadedFlowNode.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)!!
|
||||
roomFlowNodeTestHelper.assertChildHasLifecycle(RoomLoadedFlowNode.NavTarget.RoomDetails, Lifecycle.State.CREATED)
|
||||
val roomDetailsNode = roomFlowNode.childNode(RoomLoadedFlowNode.NavTarget.RoomDetails)!!
|
||||
Truth.assertThat(roomDetailsNode.id).isEqualTo(fakeRoomDetailsEntryPoint.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user