Room navigation : make it working with RoomDirectory

This commit is contained in:
ganfra
2024-04-10 15:14:59 +02:00
parent fc20b7399a
commit bf7a94cc93
20 changed files with 226 additions and 163 deletions

View File

@@ -42,8 +42,8 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
@@ -55,6 +55,7 @@ import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
@@ -83,6 +84,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.util.Optional
@ContributesNode(SessionScope::class)
class LoggedInFlowNode @AssistedInject constructor(
@@ -214,6 +216,7 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
data class Room(
val roomId: RoomId,
val roomDescription: RoomDescription? = null,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages
) : NavTarget
@@ -304,7 +307,11 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Settings(PreferencesEntryPoint.InitialTarget.NotificationSettings))
}
}
val inputs = RoomFlowNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement)
val inputs = RoomFlowNode.Inputs(
roomId = navTarget.roomId,
roomDescription = Optional.ofNullable(navTarget.roomDescription),
initialElement = navTarget.initialElement
)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.Settings -> {
@@ -375,8 +382,12 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.RoomDirectorySearch -> {
roomDirectoryEntryPoint.nodeBuilder(this, buildContext)
.callback(object : RoomDirectoryEntryPoint.Callback {
override fun onOpenRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) }
override fun onRoomJoined(roomId: RoomId) {
backstack.push(NavTarget.Room(roomId))
}
override fun onResultClicked(roomDescription: RoomDescription) {
backstack.push(NavTarget.Room(roomDescription.roomId, roomDescription))
}
})
.build()

View File

@@ -37,6 +37,7 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs
@@ -53,6 +54,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
@ContributesNode(SessionScope::class)
@@ -72,6 +74,7 @@ class RoomFlowNode @AssistedInject constructor(
) {
data class Inputs(
val roomId: RoomId,
val roomDescription: Optional<RoomDescription>,
val initialElement: RoomNavigationTarget = RoomNavigationTarget.Messages,
) : NodeInputs
@@ -102,6 +105,7 @@ class RoomFlowNode @AssistedInject constructor(
}
.launchIn(lifecycleScope)
// When leaving the room from this session only, navigate up.
roomMembershipObserver.updates
.filter { update -> update.roomId == inputs.roomId && !update.isUserInRoom }
.onEach {
@@ -114,7 +118,7 @@ class RoomFlowNode @AssistedInject constructor(
return when (navTarget) {
NavTarget.Loading -> loadingNode(buildContext)
NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId)
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomId, roomDescription = inputs.roomDescription)
joinRoomEntryPoint.createNode(this, buildContext, inputs)
}
NavTarget.JoinedRoom -> {

View File

@@ -44,7 +44,6 @@ 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.CoroutineScope
import kotlinx.coroutines.launch
@@ -60,10 +59,9 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
private val appNavigationStateService: AppNavigationStateService,
private val appCoroutineScope: CoroutineScope,
roomComponentFactory: RoomComponentFactory,
roomMembershipObserver: RoomMembershipObserver,
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
backstack = BackStack(
initialElement = when(plugins.filterIsInstance(Inputs::class.java).first().initialElement){
initialElement = when (plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
RoomNavigationTarget.Messages -> NavTarget.Messages
RoomNavigationTarget.Details -> NavTarget.RoomDetails
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings

View File

@@ -25,4 +25,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.features.roomdirectory.api)
}

View File

@@ -18,9 +18,11 @@ package io.element.android.features.joinroom.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.RoomId
import java.util.Optional
interface JoinRoomEntryPoint : FeatureEntryPoint {
@@ -28,6 +30,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint {
data class Inputs(
val roomId: RoomId,
val roomDescription: Optional<RoomDescription>,
) : NodeInputs
}

View File

@@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.features.invite.api)
implementation(projects.features.roomdirectory.api)
implementation(projects.libraries.uiStrings)

View File

@@ -38,7 +38,7 @@ class JoinRoomNode @AssistedInject constructor(
) : Node(buildContext, plugins = plugins) {
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
private val presenter = presenterFactory.create(inputs.roomId)
private val presenter = presenterFactory.create(inputs.roomId, inputs.roomDescription)
@Composable
override fun View(modifier: Modifier) {

View File

@@ -25,6 +25,7 @@ import dagger.assisted.AssistedInject
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient
@@ -36,34 +37,29 @@ import kotlin.jvm.optionals.getOrNull
class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId,
@Assisted private val roomDescription: Optional<RoomDescription>,
private val matrixClient: MatrixClient,
private val acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter,
) : Presenter<JoinRoomState> {
interface Factory {
fun create(roomId: RoomId): JoinRoomPresenter
fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter
}
@Composable
override fun present(): JoinRoomState {
val mxRoomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val joinAuthorisationStatus = joinAuthorisationStatus(mxRoomInfo)
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val joinAuthorisationStatus = joinAuthorisationStatus(roomInfo)
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
val roomInfo by produceState<AsyncData<RoomInfo>>(initialValue = AsyncData.Uninitialized, key1 = mxRoomInfo) {
val contentState by produceState<AsyncData<ContentState>>(initialValue = AsyncData.Uninitialized, key1 = roomInfo) {
value = when {
mxRoomInfo.isPresent -> {
val roomInfo = mxRoomInfo.get().let {
RoomInfo(
roomId = roomId,
roomName = it.name,
roomAlias = it.canonicalAlias,
memberCount = it.activeMembersCount,
isDirect = it.isDirect,
topic = it.topic,
roomAvatarUrl = it.avatarUrl
)
}
AsyncData.Success(roomInfo)
roomInfo.isPresent -> {
val contentState = roomInfo.get().toContentState()
AsyncData.Success(contentState)
}
roomDescription.isPresent -> {
val contentState = roomDescription.get().toContentState()
AsyncData.Success(contentState)
}
else -> AsyncData.Uninitialized
}
@@ -73,30 +69,68 @@ class JoinRoomPresenter @AssistedInject constructor(
when (event) {
JoinRoomEvents.AcceptInvite, JoinRoomEvents.JoinRoom -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(roomInfo.toInviteData())
AcceptDeclineInviteEvents.AcceptInvite(contentState.toInviteData())
)
}
JoinRoomEvents.DeclineInvite -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(roomInfo.toInviteData())
AcceptDeclineInviteEvents.DeclineInvite(contentState.toInviteData())
)
}
}
}
return JoinRoomState(
roomInfo = roomInfo,
contentState = contentState,
joinAuthorisationStatus = joinAuthorisationStatus,
acceptDeclineInviteState = acceptDeclineInviteState,
eventSink = ::handleEvents
)
}
private fun AsyncData<RoomInfo>.toInviteData(): InviteData {
private fun RoomDescription.toContentState(): ContentState {
return ContentState(
roomId = roomId,
name = name,
description = description,
numberOfMembers = numberOfMembers,
isDirect = false,
roomAvatarUrl = avatarUrl
)
}
private fun MatrixRoomInfo.toContentState(): ContentState {
fun title(): String {
return name ?: canonicalAlias ?: roomId.value
}
fun description(): String? {
val topic = topic
val alias = canonicalAlias
val name = name
return when {
topic != null -> topic
name != null && alias != null -> alias
name == null && alias == null -> null
else -> roomId.value
}
}
return ContentState(
roomId = roomId,
name = title(),
description = description(),
numberOfMembers = activeMembersCount,
isDirect = isDirect,
roomAvatarUrl = avatarUrl
)
}
private fun AsyncData<ContentState>.toInviteData(): InviteData {
return dataOrNull().let {
InviteData(
roomId = roomId,
roomName = it?.roomName ?: "",
roomName = it?.name ?: "",
isDirect = it?.isDirect ?: false
)
}

View File

@@ -25,27 +25,27 @@ import io.element.android.libraries.matrix.api.core.RoomId
@Immutable
data class JoinRoomState(
val roomInfo: AsyncData<RoomInfo>,
val contentState: AsyncData<ContentState>,
val joinAuthorisationStatus: JoinAuthorisationStatus,
val acceptDeclineInviteState: AcceptDeclineInviteState,
val eventSink: (JoinRoomEvents) -> Unit
) {
val showMemberCount = roomInfo.dataOrNull()?.memberCount != null
}
)
data class RoomInfo(
data class ContentState(
val roomId: RoomId,
val roomName: String?,
val roomAlias: String?,
val memberCount: Long?,
val topic: String?,
val name: String,
val description: String?,
val numberOfMembers: Long?,
val isDirect: Boolean,
val roomAvatarUrl: String?,
) {
val showMemberCount = numberOfMembers != null
fun avatarData(size: AvatarSize): AvatarData {
return AvatarData(
id = roomId.value,
name = roomName,
name = name,
url = roomAvatarUrl,
size = size,
)

View File

@@ -26,7 +26,7 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState>
get() = sequenceOf(
aJoinRoomState(
roomInfo = AsyncData.Uninitialized
contentState = AsyncData.Uninitialized
),
aJoinRoomState(
joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin
@@ -41,14 +41,13 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
}
fun aJoinRoomState(
roomInfo: AsyncData<RoomInfo> = AsyncData.Success(
RoomInfo(
contentState: AsyncData<ContentState> = AsyncData.Success(
ContentState(
roomId = RoomId("@exa:matrix.org"),
roomName = "Element x android",
roomAlias = "#exa:matrix.org",
memberCount = null,
name = "Element x android",
description = "#exa:matrix.org",
numberOfMembers = null,
isDirect = false,
topic = null,
roomAvatarUrl = null
)
),
@@ -56,7 +55,7 @@ fun aJoinRoomState(
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState(
roomInfo = roomInfo,
contentState = contentState,
joinAuthorisationStatus = joinAuthorisationStatus,
acceptDeclineInviteState = acceptDeclineInviteState,
eventSink = eventSink

View File

@@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -33,7 +32,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -42,10 +40,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
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.ElementPreview
@@ -66,10 +62,10 @@ fun JoinRoomView(
HeaderFooterPage(
modifier = modifier,
topBar = {
JoinRoomTopBar(asyncRoomInfo = state.roomInfo, onBackClicked = onBackPressed)
JoinRoomTopBar(onBackClicked = onBackPressed)
},
content = {
JoinRoomContent(state = state)
JoinRoomContent(asyncContentState = state.contentState)
},
footer = {
JoinRoomFooter(
@@ -127,40 +123,65 @@ private fun JoinRoomFooter(
@Composable
private fun JoinRoomContent(
state: JoinRoomState,
asyncContentState: AsyncData<ContentState>,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
@Composable
fun ContentScaffold(
avatar: @Composable () -> Unit,
title: String,
description: String,
memberCount: @Composable (() -> Unit)? = null
) {
when (state.roomInfo) {
is AsyncData.Success -> {
val roomInfo = state.roomInfo.data
Avatar(avatarData = roomInfo.avatarData(AvatarSize.RoomHeader))
}
else -> {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
}
}
avatar()
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.screen_join_room_title_no_preview),
text = title,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.screen_join_room_subtitle_no_preview),
text = description,
style = ElementTheme.typography.fontBodyMdRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
)
if (state.showMemberCount) {
JoinRoomMembersCount(memberCount = state.roomInfo.dataOrNull()?.memberCount ?: 0)
memberCount?.invoke()
}
Column(
modifier = modifier
.fillMaxWidth()
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
when (asyncContentState) {
is AsyncData.Success -> {
val contentState = asyncContentState.data
ContentScaffold(
avatar = {
Avatar(contentState.avatarData(AvatarSize.RoomHeader))
},
title = contentState.name,
description = contentState.description ?: stringResource(R.string.screen_join_room_subtitle_no_preview)
) {
if (contentState.showMemberCount) {
JoinRoomMembersCount(memberCount = contentState.numberOfMembers ?: 0)
}
}
}
else -> {
ContentScaffold(
avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
},
title = stringResource(R.string.screen_join_room_title_no_preview),
description = stringResource(R.string.screen_join_room_subtitle_no_preview),
)
}
}
}
}
@@ -192,7 +213,6 @@ fun JoinRoomMembersCount(memberCount: Long) {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun JoinRoomTopBar(
asyncRoomInfo: AsyncData<RoomInfo>,
onBackClicked: () -> Unit,
) {
TopAppBar(
@@ -200,44 +220,11 @@ private fun JoinRoomTopBar(
BackButton(onClick = onBackClicked)
},
title = {
when (asyncRoomInfo) {
is AsyncData.Success -> {
val roomInfo = asyncRoomInfo.data
if(roomInfo.roomName == null){
IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
}else {
RoomAvatarAndNameRow(roomName = roomInfo.roomName, roomAvatar = roomInfo.avatarData(AvatarSize.TimelineRoom))
}
}
else -> {
IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp)
}
}
},
)
}
@Composable
private fun RoomAvatarAndNameRow(
roomName: String,
roomAvatar: AvatarData,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically
) {
Avatar(roomAvatar)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = roomName,
style = ElementTheme.typography.fontBodyLgMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@PreviewLightDark
@Composable
fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {

View File

@@ -21,10 +21,12 @@ import dagger.Module
import dagger.Provides
import io.element.android.features.invite.api.response.AcceptDeclineInvitePresenter
import io.element.android.features.joinroom.impl.JoinRoomPresenter
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import java.util.Optional
@Module
@ContributesTo(SessionScope::class)
@@ -36,9 +38,10 @@ object JoinRoomModule {
acceptDeclineInvitePresenter: AcceptDeclineInvitePresenter,
): JoinRoomPresenter.Factory {
return object : JoinRoomPresenter.Factory {
override fun create(roomId: RoomId): JoinRoomPresenter {
override fun create(roomId: RoomId, roomDescription: Optional<RoomDescription>): JoinRoomPresenter {
return JoinRoomPresenter(
roomId = roomId,
roomDescription = roomDescription,
matrixClient = client,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
)

View File

@@ -16,6 +16,7 @@
plugins {
id("io.element.android-library")
id("kotlin-parcelize")
}
android {

View File

@@ -16,13 +16,26 @@
package io.element.android.features.roomdirectory.api
import android.os.Parcelable
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize
@Parcelize
data class RoomDescription(
val roomId: RoomId,
val name: String,
val description: String,
val avatarData: AvatarData,
val avatarUrl: String?,
val canBeJoined: Boolean,
)
val numberOfMembers: Long,
) : Parcelable {
fun avatarData(size: AvatarSize) = AvatarData(
id = roomId.value,
name = name,
url = avatarUrl,
size = size,
)
}

View File

@@ -31,6 +31,7 @@ interface RoomDirectoryEntryPoint : FeatureEntryPoint {
}
interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId)
fun onRoomJoined(roomId: RoomId)
fun onResultClicked(roomDescription: RoomDescription)
}
}

View File

@@ -25,6 +25,7 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@@ -35,9 +36,16 @@ class RoomDirectoryNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>,
private val presenter: RoomDirectoryPresenter,
) : Node(buildContext, plugins = plugins) {
private fun onResultClicked(roomDescription: RoomDescription) {
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
it.onResultClicked(roomDescription)
}
}
private fun onRoomJoined(roomId: RoomId) {
plugins<RoomDirectoryEntryPoint.Callback>().forEach {
it.onOpenRoom(roomId)
it.onRoomJoined(roomId)
}
}
@@ -47,6 +55,7 @@ class RoomDirectoryNode @AssistedInject constructor(
RoomDirectoryView(
state = state,
onRoomJoined = ::onRoomJoined,
onResultClicked = ::onResultClicked,
onBackPressed = ::navigateUp,
modifier = modifier
)

View File

@@ -19,8 +19,6 @@ package io.element.android.features.roomdirectory.impl.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -71,25 +69,17 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
roomId = RoomId("!exa:matrix.org"),
name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "!exa:matrix.org",
name = "Element X Android",
url = null,
size = AvatarSize.RoomDirectoryItem
),
avatarUrl = null,
canBeJoined = true,
numberOfMembers = 2765,
),
RoomDescription(
roomId = RoomId("!exi:matrix.org"),
name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.",
avatarData = AvatarData(
id = "!exi:matrix.org",
name = "Element X iOS",
url = null,
size = AvatarSize.RoomDirectoryItem
),
avatarUrl = null,
canBeJoined = false,
numberOfMembers = 356,
)
)
}

View File

@@ -51,6 +51,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.impl.R
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.avatar.Avatar
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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -70,12 +71,13 @@ import kotlinx.collections.immutable.ImmutableList
@Composable
fun RoomDirectoryView(
state: RoomDirectoryState,
onResultClicked: (RoomDescription) -> Unit,
onRoomJoined: (RoomId) -> Unit,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
fun joinRoom(roomId: RoomId) {
state.eventSink(RoomDirectoryEvents.JoinRoom(roomId))
fun joinRoom(roomDescription: RoomDescription) {
state.eventSink(RoomDirectoryEvents.JoinRoom(roomDescription.roomId))
}
Scaffold(
@@ -86,10 +88,11 @@ fun RoomDirectoryView(
content = { padding ->
RoomDirectoryContent(
state = state,
onResultClicked = ::joinRoom,
onResultClicked = onResultClicked,
onJoinClicked = ::joinRoom,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.padding(padding)
.consumeWindowInsets(padding)
)
}
)
@@ -128,7 +131,8 @@ private fun RoomDirectoryTopBar(
@Composable
private fun RoomDirectoryContent(
state: RoomDirectoryState,
onResultClicked: (RoomId) -> Unit,
onResultClicked: (RoomDescription) -> Unit,
onJoinClicked: (RoomDescription) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@@ -143,6 +147,7 @@ private fun RoomDirectoryContent(
displayLoadMoreIndicator = state.displayLoadMoreIndicator,
displayEmptyState = state.displayEmptyState,
onResultClicked = onResultClicked,
onJoinClicked = onJoinClicked,
onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) },
)
}
@@ -153,7 +158,8 @@ private fun RoomDirectoryRoomList(
roomDescriptions: ImmutableList<RoomDescription>,
displayLoadMoreIndicator: Boolean,
displayEmptyState: Boolean,
onResultClicked: (RoomId) -> Unit,
onResultClicked: (RoomDescription) -> Unit,
onJoinClicked: (RoomDescription) -> Unit,
onReachedLoadMore: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -161,7 +167,12 @@ private fun RoomDirectoryRoomList(
items(roomDescriptions) { roomDescription ->
RoomDirectoryRoomRow(
roomDescription = roomDescription,
onClick = onResultClicked,
onClick = {
onResultClicked(roomDescription)
},
onJoinClick = {
onJoinClicked(roomDescription)
},
)
}
if (displayEmptyState) {
@@ -188,10 +199,10 @@ private fun RoomDirectoryRoomList(
@Composable
private fun LoadMoreIndicator(modifier: Modifier = Modifier) {
Box(
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(24.dp),
modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(24.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
@@ -256,30 +267,29 @@ private fun SearchTextField(
@Composable
private fun RoomDirectoryRoomRow(
roomDescription: RoomDescription,
onClick: (RoomId) -> Unit,
onClick: () -> Unit,
onJoinClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(enabled = roomDescription.canBeJoined) {
onClick(roomDescription.roomId)
}
.padding(
top = 12.dp,
bottom = 12.dp,
start = 16.dp,
)
.height(IntrinsicSize.Min),
.fillMaxWidth()
.clickable(enabled = roomDescription.canBeJoined, onClick = onClick)
.padding(
top = 12.dp,
bottom = 12.dp,
start = 16.dp,
)
.height(IntrinsicSize.Min),
) {
Avatar(
avatarData = roomDescription.avatarData,
avatarData = roomDescription.avatarData(AvatarSize.RoomDirectoryItem),
modifier = Modifier.align(Alignment.CenterVertically)
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
.weight(1f)
.padding(start = 16.dp)
) {
Text(
text = roomDescription.name,
@@ -301,8 +311,9 @@ private fun RoomDirectoryRoomRow(
text = stringResource(id = CommonStrings.action_join),
color = ElementTheme.colors.textSuccessPrimary,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 4.dp, end = 12.dp)
.align(Alignment.CenterVertically)
.clickable(onClick = onJoinClick)
.padding(start = 4.dp, end = 12.dp)
)
} else {
Spacer(modifier = Modifier.width(24.dp))
@@ -315,6 +326,7 @@ private fun RoomDirectoryRoomRow(
internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview {
RoomDirectoryView(
state = state,
onResultClicked = {},
onRoomJoined = {},
onBackPressed = {},
)

View File

@@ -42,12 +42,8 @@ fun MatrixRoomDescription.toFeatureModel(): RoomDescription {
roomId = roomId,
name = name(),
description = description(),
avatarData = AvatarData(
id = roomId.value,
name = name,
url = avatarUrl,
size = AvatarSize.RoomDirectoryItem,
),
avatarUrl = avatarUrl,
numberOfMembers = numberOfMembers,
canBeJoined = joinRule == MatrixRoomDescription.JoinRule.PUBLIC,
)
}

View File

@@ -105,7 +105,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
setContent {
RoomDirectoryView(
state = state,
onRoomJoined = onRoomJoined,
onRoomClicked = onRoomJoined,
onBackPressed = onBackPressed,
)
}