Merge pull request #5614 from element-hq/feature/fga/space_flow_inject_room

Misc : space flow inject room
This commit is contained in:
ganfra 2025-10-28 10:05:04 +01:00 committed by GitHub
commit d4de8224c0
29 changed files with 163 additions and 74 deletions

View file

@ -62,7 +62,6 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.features.startchat.api.StartChatEntryPoint import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@ -256,7 +255,7 @@ class LoggedInFlowNode(
val serverNames: List<String> = emptyList(), val serverNames: List<String> = emptyList(),
val trigger: JoinedRoom.Trigger? = null, val trigger: JoinedRoom.Trigger? = null,
val roomDescription: RoomDescription? = null, val roomDescription: RoomDescription? = null,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages(), val initialElement: RoomNavigationTarget = RoomNavigationTarget.Root(),
val targetId: UUID = UUID.randomUUID(), val targetId: UUID = UUID.randomUUID(),
) : NavTarget ) : NavTarget
@ -358,7 +357,7 @@ class LoggedInFlowNode(
roomIdOrAlias = data.roomIdOrAlias, roomIdOrAlias = data.roomIdOrAlias,
serverNames = data.viaParameters, serverNames = data.viaParameters,
trigger = JoinedRoom.Trigger.Timeline, trigger = JoinedRoom.Trigger.Timeline,
initialElement = RoomNavigationTarget.Messages(data.eventId), initialElement = RoomNavigationTarget.Root(data.eventId),
) )
if (pushToBackstack) { if (pushToBackstack) {
backstack.push(target) backstack.push(target)
@ -377,11 +376,6 @@ class LoggedInFlowNode(
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings)) backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
} }
} }
val spaceCallback = object : SpaceEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), serverNames = viaParameters))
}
}
val inputs = RoomFlowNode.Inputs( val inputs = RoomFlowNode.Inputs(
roomIdOrAlias = navTarget.roomIdOrAlias, roomIdOrAlias = navTarget.roomIdOrAlias,
roomDescription = Optional.ofNullable(navTarget.roomDescription), roomDescription = Optional.ofNullable(navTarget.roomDescription),
@ -389,7 +383,7 @@ class LoggedInFlowNode(
trigger = Optional.ofNullable(navTarget.trigger), trigger = Optional.ofNullable(navTarget.trigger),
initialElement = navTarget.initialElement initialElement = navTarget.initialElement
) )
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, joinedRoomCallback, spaceCallback)) createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, joinedRoomCallback))
} }
is NavTarget.UserProfile -> { is NavTarget.UserProfile -> {
val callback = object : UserProfileEntryPoint.Callback { val callback = object : UserProfileEntryPoint.Callback {
@ -421,7 +415,7 @@ class LoggedInFlowNode(
} }
override fun navigateTo(roomId: RoomId, eventId: EventId) { override fun navigateTo(roomId: RoomId, eventId: EventId) {
backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Messages(eventId))) backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias(), initialElement = RoomNavigationTarget.Root(eventId)))
} }
} }
val inputs = PreferencesEntryPoint.Params(navTarget.initialElement) val inputs = PreferencesEntryPoint.Params(navTarget.initialElement)
@ -516,9 +510,7 @@ class LoggedInFlowNode(
roomIdOrAlias = roomIdOrAlias, roomIdOrAlias = roomIdOrAlias,
serverNames = serverNames, serverNames = serverNames,
trigger = trigger, trigger = trigger,
initialElement = RoomNavigationTarget.Messages( initialElement = RoomNavigationTarget.Root(eventId = eventId)
focusedEventId = eventId
)
) )
backstack.accept(AttachRoomOperation(roomNavTarget, clearBackstack)) backstack.accept(AttachRoomOperation(roomNavTarget, clearBackstack))
} }

View file

@ -9,7 +9,6 @@ package io.element.android.appnav.room
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@ -37,7 +36,6 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.coroutine.withPreviousValue import io.element.android.libraries.core.coroutine.withPreviousValue
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -49,11 +47,11 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -130,7 +128,6 @@ class RoomFlowNode(
private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) { private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List<String>) {
val roomInfoFlow = client.getRoomInfoFlow(roomId) val roomInfoFlow = client.getRoomInfoFlow(roomId)
val isSpaceFlow = roomInfoFlow.map { it.getOrNull()?.isSpace.orFalse() }.distinctUntilChanged()
// This observes the local membership changes for the room // This observes the local membership changes for the room
val membershipUpdateFlow = membershipObserver.updates val membershipUpdateFlow = membershipObserver.updates
@ -143,14 +140,10 @@ class RoomFlowNode(
.map { it.getOrNull()?.currentUserMembership } .map { it.getOrNull()?.currentUserMembership }
.distinctUntilChanged() .distinctUntilChanged()
.withPreviousValue() .withPreviousValue()
combine(currentMembershipFlow, isSpaceFlow) { (previousMembership, membership), isSpace -> currentMembershipFlow.onEach { (previousMembership, membership) ->
Timber.d("Room membership: $membership") Timber.d("Room membership: $membership")
if (membership == CurrentUserMembership.JOINED) { if (membership == CurrentUserMembership.JOINED) {
if (isSpace) { backstack.newRoot(NavTarget.JoinedRoom(roomId))
backstack.newRoot(NavTarget.JoinedSpace(spaceId = roomId))
} else {
backstack.newRoot(NavTarget.JoinedRoom(roomId))
}
} else { } else {
val leavingFromCurrentDevice = val leavingFromCurrentDevice =
membership == CurrentUserMembership.LEFT && membership == CurrentUserMembership.LEFT &&

View file

@ -13,7 +13,7 @@ import kotlinx.parcelize.Parcelize
sealed interface RoomNavigationTarget : Parcelable { sealed interface RoomNavigationTarget : Parcelable {
@Parcelize @Parcelize
data class Messages(val focusedEventId: EventId? = null) : RoomNavigationTarget data class Root(val eventId: EventId? = null) : RoomNavigationTarget
@Parcelize @Parcelize
data object Details : RoomNavigationTarget data object Details : RoomNavigationTarget

View file

@ -22,8 +22,11 @@ import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.RoomGraphFactory import io.element.android.appnav.di.RoomGraphFactory
import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode.Inputs
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode.NavTarget
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
@ -51,6 +54,7 @@ class JoinedRoomLoadedFlowNode(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val messagesEntryPoint: MessagesEntryPoint, private val messagesEntryPoint: MessagesEntryPoint,
private val roomDetailsEntryPoint: RoomDetailsEntryPoint, private val roomDetailsEntryPoint: RoomDetailsEntryPoint,
private val spaceEntryPoint: SpaceEntryPoint,
private val appNavigationStateService: AppNavigationStateService, private val appNavigationStateService: AppNavigationStateService,
@SessionCoroutineScope @SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
@ -59,11 +63,7 @@ class JoinedRoomLoadedFlowNode(
roomGraphFactory: RoomGraphFactory, roomGraphFactory: RoomGraphFactory,
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>( ) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) { initialElement = initialElement(plugins),
is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId)
RoomNavigationTarget.Details -> NavTarget.RoomDetails
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
},
savedStateMap = buildContext.savedStateMap, savedStateMap = buildContext.savedStateMap,
), ),
buildContext = buildContext, buildContext = buildContext,
@ -154,9 +154,28 @@ class JoinedRoomLoadedFlowNode(
NavTarget.RoomNotificationSettings -> { NavTarget.RoomNotificationSettings -> {
createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings) createRoomDetailsNode(buildContext, RoomDetailsEntryPoint.InitialTarget.RoomNotificationSettings)
} }
NavTarget.Space -> {
createSpaceNode(buildContext)
}
} }
} }
private fun createSpaceNode(buildContext: BuildContext): Node {
val callback = object : SpaceEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) {
callbacks.forEach { it.onOpenRoom(roomId, viaParameters) }
}
override fun onOpenDetails() {
backstack.push(NavTarget.RoomDetails)
}
}
return spaceEntryPoint.nodeBuilder(this, buildContext)
.inputs(SpaceEntryPoint.Inputs(roomId = inputs.room.roomId))
.callback(callback)
.build()
}
private fun createMessagesNode( private fun createMessagesNode(
buildContext: BuildContext, buildContext: BuildContext,
navTarget: NavTarget.Messages, navTarget: NavTarget.Messages,
@ -188,6 +207,9 @@ class JoinedRoomLoadedFlowNode(
} }
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize
data object Space : NavTarget
@Parcelize @Parcelize
data class Messages(val focusedEventId: EventId? = null) : NavTarget data class Messages(val focusedEventId: EventId? = null) : NavTarget
@ -206,3 +228,18 @@ class JoinedRoomLoadedFlowNode(
BackstackView() BackstackView()
} }
} }
private fun initialElement(plugins: List<Plugin>): NavTarget {
val input = plugins.filterIsInstance<Inputs>().single()
return when (input.initialElement) {
is RoomNavigationTarget.Root -> {
if (input.room.roomInfoFlow.value.isSpace) {
NavTarget.Space
} else {
NavTarget.Messages(input.initialElement.eventId)
}
}
RoomNavigationTarget.Details -> NavTarget.RoomDetails
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
}
}

View file

@ -19,8 +19,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -76,7 +74,6 @@ private fun LoadingRoomTopBar(
BackButton(onClick = onBackClick) BackButton(onClick = onBackClick)
}, },
title = { title = {
IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
}, },
) )
} }

View file

@ -22,12 +22,14 @@ import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.features.space.api.SpaceEntryPoint
import io.element.android.libraries.architecture.childNode import io.element.android.libraries.architecture.childNode
import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.services.appnavstate.api.ActiveRoomsHolder import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.FakeAppNavigationStateService
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -98,16 +100,40 @@ class JoinedRoomLoadedFlowNodeTest {
} }
} }
private class FakeSpaceEntryPoint : SpaceEntryPoint {
var nodeId: String? = null
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): SpaceEntryPoint.NodeBuilder {
return object : SpaceEntryPoint.NodeBuilder {
override fun inputs(inputs: SpaceEntryPoint.Inputs): SpaceEntryPoint.NodeBuilder {
return this
}
override fun callback(callback: SpaceEntryPoint.Callback): SpaceEntryPoint.NodeBuilder {
return this
}
override fun build(): Node {
return node(buildContext) {}.also {
nodeId = it.id
}
}
}
}
}
private fun TestScope.createJoinedRoomLoadedFlowNode( private fun TestScope.createJoinedRoomLoadedFlowNode(
plugins: List<Plugin>, plugins: List<Plugin>,
messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(), messagesEntryPoint: MessagesEntryPoint = FakeMessagesEntryPoint(),
roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(), roomDetailsEntryPoint: RoomDetailsEntryPoint = FakeRoomDetailsEntryPoint(),
spaceEntryPoint: SpaceEntryPoint = FakeSpaceEntryPoint(),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(), activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
) = JoinedRoomLoadedFlowNode( ) = JoinedRoomLoadedFlowNode(
buildContext = BuildContext.root(savedStateMap = null), buildContext = BuildContext.root(savedStateMap = null),
plugins = plugins, plugins = plugins,
messagesEntryPoint = messagesEntryPoint, messagesEntryPoint = messagesEntryPoint,
roomDetailsEntryPoint = roomDetailsEntryPoint, roomDetailsEntryPoint = roomDetailsEntryPoint,
spaceEntryPoint = spaceEntryPoint,
appNavigationStateService = FakeAppNavigationStateService(), appNavigationStateService = FakeAppNavigationStateService(),
sessionCoroutineScope = this, sessionCoroutineScope = this,
roomGraphFactory = FakeRoomGraphFactory(), roomGraphFactory = FakeRoomGraphFactory(),
@ -116,11 +142,11 @@ class JoinedRoomLoadedFlowNodeTest {
) )
@Test @Test
fun `given a room flow node when initialized then it loads messages entry point`() = runTest { fun `given a room flow node when initialized then it loads messages entry point if room is not space`() = runTest {
// GIVEN // GIVEN
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}, initialRoomInfo = aRoomInfo(isSpace = false)))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
@ -135,13 +161,33 @@ class JoinedRoomLoadedFlowNodeTest {
assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId) assertThat(messagesNode.id).isEqualTo(fakeMessagesEntryPoint.nodeId)
} }
@Test
fun `given a room flow node when initialized then it loads space entry point if room is space`() = runTest {
// GIVEN
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}, initialRoomInfo = aRoomInfo(isSpace = true)))
val spaceEntryPoint = FakeSpaceEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs),
spaceEntryPoint = spaceEntryPoint,
)
// WHEN
val roomFlowNodeTestHelper = roomFlowNode.parentNodeTestHelper()
// THEN
assertThat(roomFlowNode.backstack.activeElement).isEqualTo(JoinedRoomLoadedFlowNode.NavTarget.Space)
roomFlowNodeTestHelper.assertChildHasLifecycle(JoinedRoomLoadedFlowNode.NavTarget.Space, Lifecycle.State.CREATED)
val spaceNode = roomFlowNode.childNode(JoinedRoomLoadedFlowNode.NavTarget.Space)!!
assertThat(spaceNode.id).isEqualTo(spaceEntryPoint.nodeId)
}
@Test @Test
fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest { fun `given a room flow node when callback on room details is triggered then it loads room details entry point`() = runTest {
// GIVEN // GIVEN
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
@ -162,7 +208,7 @@ class JoinedRoomLoadedFlowNodeTest {
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val activeRoomsHolder = ActiveRoomsHolder() val activeRoomsHolder = ActiveRoomsHolder()
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
@ -185,7 +231,7 @@ class JoinedRoomLoadedFlowNodeTest {
val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {})) val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(updateMembersResult = {}))
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Root())
val activeRoomsHolder = ActiveRoomsHolder().apply { val activeRoomsHolder = ActiveRoomsHolder().apply {
addRoom(room) addRoom(room)
} }

View file

@ -32,5 +32,6 @@ interface SpaceEntryPoint : FeatureEntryPoint {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
fun onOpenDetails()
} }
} }

View file

@ -31,12 +31,12 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.RoomScope
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.spaces.SpaceService import io.element.android.libraries.matrix.api.spaces.SpaceService
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ContributesNode(SessionScope::class) @ContributesNode(RoomScope::class)
@AssistedInject @AssistedInject
class SpaceFlowNode( class SpaceFlowNode(
@Assisted val buildContext: BuildContext, @Assisted val buildContext: BuildContext,
@ -84,6 +84,10 @@ class SpaceFlowNode(
callback.onOpenRoom(roomId, viaParameters) callback.onOpenRoom(roomId, viaParameters)
} }
override fun onOpenDetails() {
callback.onOpenDetails()
}
override fun onLeaveSpace() { override fun onLeaveSpace() {
backstack.push(NavTarget.Leave) backstack.push(NavTarget.Leave)
} }

View file

@ -11,12 +11,12 @@ import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.GraphExtension import dev.zacsweers.metro.GraphExtension
import dev.zacsweers.metro.Provides import dev.zacsweers.metro.Provides
import io.element.android.libraries.architecture.NodeFactoriesBindings import io.element.android.libraries.architecture.NodeFactoriesBindings
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.spaces.SpaceRoomList import io.element.android.libraries.matrix.api.spaces.SpaceRoomList
@GraphExtension(SpaceFlowScope::class) @GraphExtension(SpaceFlowScope::class)
interface SpaceFlowGraph : NodeFactoriesBindings { interface SpaceFlowGraph : NodeFactoriesBindings {
@ContributesTo(SessionScope::class) @ContributesTo(RoomScope::class)
@GraphExtension.Factory @GraphExtension.Factory
interface Factory { interface Factory {
fun create(@Provides spaceRoomList: SpaceRoomList): SpaceFlowGraph fun create(@Provides spaceRoomList: SpaceRoomList): SpaceFlowGraph

View file

@ -41,6 +41,7 @@ class SpaceNode(
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) fun onOpenRoom(roomId: RoomId, viaParameters: List<String>)
fun onOpenDetails()
fun onLeaveSpace() fun onLeaveSpace()
} }
@ -76,6 +77,9 @@ class SpaceNode(
onRoomClick = { spaceRoom -> onRoomClick = { spaceRoom ->
callback.onOpenRoom(spaceRoom.roomId, spaceRoom.via) callback.onOpenRoom(spaceRoom.roomId, spaceRoom.via)
}, },
onDetailsClick = {
callback.onOpenDetails()
},
onShareSpace = { onShareSpace = {
onShareRoom(context) onShareRoom(context)
}, },

View file

@ -7,6 +7,7 @@
package io.element.android.features.space.impl.root package io.element.android.features.space.impl.root
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -25,6 +27,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@ -74,6 +77,7 @@ fun SpaceView(
onRoomClick: (spaceRoom: SpaceRoom) -> Unit, onRoomClick: (spaceRoom: SpaceRoom) -> Unit,
onShareSpace: () -> Unit, onShareSpace: () -> Unit,
onLeaveSpaceClick: () -> Unit, onLeaveSpaceClick: () -> Unit,
onDetailsClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
acceptDeclineInviteView: @Composable () -> Unit, acceptDeclineInviteView: @Composable () -> Unit,
) { ) {
@ -85,6 +89,7 @@ fun SpaceView(
onBackClick = onBackClick, onBackClick = onBackClick,
onLeaveSpaceClick = onLeaveSpaceClick, onLeaveSpaceClick = onLeaveSpaceClick,
onShareSpace = onShareSpace, onShareSpace = onShareSpace,
onDetailsClick = onDetailsClick
) )
}, },
content = { padding -> content = { padding ->
@ -249,6 +254,7 @@ private fun SpaceViewTopBar(
currentSpace: SpaceRoom?, currentSpace: SpaceRoom?,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onLeaveSpaceClick: () -> Unit, onLeaveSpaceClick: () -> Unit,
onDetailsClick: () -> Unit,
onShareSpace: () -> Unit, onShareSpace: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -259,9 +265,14 @@ private fun SpaceViewTopBar(
}, },
title = { title = {
if (currentSpace != null) { if (currentSpace != null) {
val roundedCornerShape = RoundedCornerShape(8.dp)
SpaceAvatarAndNameRow( SpaceAvatarAndNameRow(
name = currentSpace.displayName, name = currentSpace.displayName,
avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom), avatarData = currentSpace.getAvatarData(AvatarSize.TimelineRoom),
modifier = Modifier
.clip(roundedCornerShape)
// TODO enable when screen ready for space
.clickable(enabled = false, onClick = onDetailsClick)
) )
} }
}, },
@ -391,6 +402,7 @@ internal fun SpaceViewPreview(
onShareSpace = {}, onShareSpace = {},
onLeaveSpaceClick = {}, onLeaveSpaceClick = {},
acceptDeclineInviteView = {}, acceptDeclineInviteView = {},
onDetailsClick = {},
onBackClick = {}, onBackClick = {},
) )
} }

View file

@ -45,6 +45,7 @@ class DefaultSpaceEntryPointTest {
} }
val callback = object : SpaceEntryPoint.Callback { val callback = object : SpaceEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) = lambdaError() override fun onOpenRoom(roomId: RoomId, viaParameters: List<String>) = lambdaError()
override fun onOpenDetails() = lambdaError()
} }
val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null)) val result = entryPoint.nodeBuilder(parentNode, BuildContext.root(null))
.inputs(nodeInputs) .inputs(nodeInputs)

View file

@ -139,6 +139,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(), onRoomClick: (SpaceRoom) -> Unit = EnsureNeverCalledWithParam(),
onShareSpace: () -> Unit = EnsureNeverCalled(), onShareSpace: () -> Unit = EnsureNeverCalled(),
onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(), onLeaveSpaceClick: () -> Unit = EnsureNeverCalled(),
onDetailsClick: () -> Unit = EnsureNeverCalled(),
acceptDeclineInviteView: @Composable () -> Unit = {}, acceptDeclineInviteView: @Composable () -> Unit = {},
) { ) {
setContent { setContent {
@ -148,6 +149,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setSpace
onRoomClick = onRoomClick, onRoomClick = onRoomClick,
onShareSpace = onShareSpace, onShareSpace = onShareSpace,
onLeaveSpaceClick = onLeaveSpaceClick, onLeaveSpaceClick = onLeaveSpaceClick,
onDetailsClick = onDetailsClick,
acceptDeclineInviteView = acceptDeclineInviteView, acceptDeclineInviteView = acceptDeclineInviteView,
) )
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c9583a29e680bee98819258e5b7356dd8677144957d5869da72c47685d986e87 oid sha256:071914fc3e7bbc2fc6ef8a1b4544b9126c05dd08b2123666d320223582423226
size 6494 size 5632

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f5c19a8b26c9187c9bfd630242b15370ec8f0b874f9d3913ce53c743c0e0bdcd oid sha256:5e9df6017bd0771e1337f82b75d13a0a8a572156e6900ae72f208e9bb2aaddb7
size 8574 size 7717

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:74ad11143fc70164f67a8326cb87ce90a2c8730317540810ea138e39fc9093fa oid sha256:811bd4534f4f16530cea1489b62d393e2fe7bf1b18b859a924333cd9d309f8dd
size 6306 size 5566

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bde612ade7298aeef44bbc480ee9d742ccb8c94108e21dcae9e804c744032a08 oid sha256:030971f2c5f8c19f47fed2fa207c6d84d536c603fa7705ea6c288736684b7868
size 8308 size 7553

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ba78cafc20c3d865fec9c7ab92f90e2565f233b224f99fb0665ad0d0c3c2be4e oid sha256:c55277089a4447b618a0e8c058718ecf9d3da6d437322f0e23e5fd70019f6b00
size 34607 size 34585

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:fe62bb5d157ba1b4c7a4e4f443f4b1b3e7d68bc0ac59ce7edb6fbc99e2abbdf7 oid sha256:241f5500cb7212fac174466bbe7855ccf39de3e3764a83202388b947d90ae807
size 34795 size 34770

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:88db0bd5d05963761bd3b3a5a97834f8937a9a0e10723238f2c104c5d03eb81a oid sha256:92785cf3a4010779b0fbcd58be3437a22808b0a2f02a19a5cfd50eb3bd58ed26
size 35089 size 35058

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7e8b65396dedff81056157620e2390c8d69954ee288266b575ac61aba16c2bec oid sha256:de26882f13bac98b2cb5365d98e06e781d516d179adb8328cf22cf524e6fd79e
size 62590 size 62568

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8e1fb3b8b62ea8583a5bc9a18f39dbe71684e7d019bf63b22b873851b219209 oid sha256:a121fdb9473512b0264e48294df1799a7a6bf9b469df973fbe41f31bbf98f1d0
size 63270 size 63248

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3e60857b4c0d801d3a6ad7f7383b8ff8428157763ca46d30c2a559a4957cb71e oid sha256:a3506b4f646408262450ae51b612f86c1171ed972c1d7ea8871c4dc090556c7a
size 59706 size 59702

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:53b3736b922b746a996f71aa38b3937ebc16e37eb40bb57cd1991d6f9d98ea33 oid sha256:f2407444889af236ef21a90c47a5d3e05df8b15b9cc9483e84377e3af8794772
size 34022 size 33996

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4f2d6b368d4a8eaa8f3a396ba6edf8252d52b0e7ebd3b3f416c60050d3cd3c57 oid sha256:fee41efefc2ca1d6670d8455ac756c6b314aab54510eab8a4e597f1cc1edf3f8
size 34170 size 34141

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4b0b347f48ac09d05a2348383580d26e3725af2ef48558be86541afa239f3b06 oid sha256:ea955839cbd1aeba5de2780cee413628c7d46383398b10125cd3a900fb41d5a5
size 34485 size 34459

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9ff5ca08d441240de9e5adc35e41bceeb0f462979777345da4b2865e9a80012e oid sha256:cba2c99744aeb2a869ae2ed700d7241b1d0b6ed979b16d2be9774ddbc5f8f28a
size 61405 size 61381

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2f35b63ac49c799a5b3b5ad47652fe0f199efbbd2a285f782170c1edcd9ae723 oid sha256:b32d65accabc357208efeb2ec61374182479541299ade28184f82938e59bfdd5
size 61955 size 61932

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f470577ca0f0de530db1ab0d531f5b14630f9082cc4c34ad4e3fcbc1ab9ee530 oid sha256:7c161ff55e8a235fe403e53ee179b299fd2563d85ef64bfe6d0dd9295228685b
size 57954 size 57925