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

@@ -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,
)