Join room : change state and view

This commit is contained in:
ganfra 2024-04-12 14:09:57 +02:00
parent 038d8e3334
commit c0918bd965
11 changed files with 228 additions and 165 deletions

View file

@ -26,7 +26,6 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.roomdirectory.api.RoomDescription 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.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -49,18 +48,16 @@ class JoinRoomPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): JoinRoomState { override fun present(): JoinRoomState {
val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty()) val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val contentState by produceState<AsyncData<ContentState>>(initialValue = AsyncData.Uninitialized, key1 = roomInfo) { val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomId), key1 = roomInfo) {
value = when { value = when {
roomInfo.isPresent -> { roomInfo.isPresent -> {
val contentState = roomInfo.get().toContentState() roomInfo.get().toContentState()
AsyncData.Success(contentState)
} }
roomDescription.isPresent -> { roomDescription.isPresent -> {
val contentState = roomDescription.get().toContentState() roomDescription.get().toContentState()
AsyncData.Success(contentState)
} }
else -> { else -> {
AsyncData.Uninitialized ContentState.UnknownRoom(roomId)
} }
} }
} }
@ -93,10 +90,11 @@ class JoinRoomPresenter @AssistedInject constructor(
@VisibleForTesting @VisibleForTesting
internal fun RoomDescription.toContentState(): ContentState { internal fun RoomDescription.toContentState(): ContentState {
return ContentState( return ContentState.Loaded(
roomId = roomId, roomId = roomId,
name = name, name = name,
description = description, topic = topic,
alias = alias,
numberOfMembers = numberOfMembers, numberOfMembers = numberOfMembers,
isDirect = false, isDirect = false,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
@ -110,26 +108,11 @@ internal fun RoomDescription.toContentState(): ContentState {
@VisibleForTesting @VisibleForTesting
internal fun MatrixRoomInfo.toContentState(): ContentState { internal fun MatrixRoomInfo.toContentState(): ContentState {
fun title(): String { return ContentState.Loaded(
return name ?: canonicalAlias ?: id
}
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 -> id
}
}
return ContentState(
roomId = RoomId(id), roomId = RoomId(id),
name = title(), name = name,
description = description(), topic = topic,
alias = canonicalAlias,
numberOfMembers = activeMembersCount, numberOfMembers = activeMembersCount,
isDirect = isDirect, isDirect = isDirect,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
@ -142,12 +125,13 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
} }
@VisibleForTesting @VisibleForTesting
internal fun AsyncData<ContentState>.toInviteData(): InviteData? { internal fun ContentState.toInviteData(): InviteData? {
return dataOrNull()?.let { contentState -> return when (this) {
InviteData( is ContentState.Loaded -> InviteData(
roomId = contentState.roomId, roomId = roomId,
roomName = contentState.name, roomName = computedTitle,
isDirect = contentState.isDirect isDirect = isDirect
) )
else -> null
} }
} }

View file

@ -18,39 +18,53 @@ package io.element.android.features.joinroom.impl
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@Immutable @Immutable
data class JoinRoomState( data class JoinRoomState(
val contentState: AsyncData<ContentState>, val contentState: ContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val eventSink: (JoinRoomEvents) -> Unit val eventSink: (JoinRoomEvents) -> Unit
) { ) {
val joinAuthorisationStatus = contentState.dataOrNull()?.joinAuthorisationStatus ?: JoinAuthorisationStatus.Unknown val joinAuthorisationStatus = when(contentState) {
is ContentState.Loaded -> contentState.joinAuthorisationStatus
else -> JoinAuthorisationStatus.Unknown
}
} }
data class ContentState( sealed interface ContentState {
val roomId: RoomId, data class Loading(val roomId: RoomId) : ContentState
val name: String, data class UnknownRoom(val roomId: RoomId) : ContentState
val description: String?, data class Loaded(
val numberOfMembers: Long?, val roomId: RoomId,
val isDirect: Boolean, val name: String?,
val roomAvatarUrl: String?, val topic: String?,
val joinAuthorisationStatus: JoinAuthorisationStatus, val alias: String?,
) { val numberOfMembers: Long?,
val isDirect: Boolean,
val roomAvatarUrl: String?,
val joinAuthorisationStatus: JoinAuthorisationStatus,
) : ContentState {
val computedTitle = name ?: roomId.value
val showMemberCount = numberOfMembers != null val computedSubtitle = when {
alias != null -> alias
name == null -> ""
else -> roomId.value
}
fun avatarData(size: AvatarSize): AvatarData { val showMemberCount = numberOfMembers != null
return AvatarData(
id = roomId.value, fun avatarData(size: AvatarSize): AvatarData {
name = name, return AvatarData(
url = roomAvatarUrl, id = roomId.value,
size = size, name = name,
) url = roomAvatarUrl,
size = size,
)
}
} }
} }

View file

@ -19,45 +19,47 @@ package io.element.android.features.joinroom.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> { open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
override val values: Sequence<JoinRoomState> override val values: Sequence<JoinRoomState>
get() = sequenceOf( get() = sequenceOf(
aJoinRoomState( aJoinRoomState(
contentState = AsyncData.Uninitialized contentState = anUninitializedContentState()
), ),
aJoinRoomState( aJoinRoomState(
contentState = AsyncData.Success( contentState = anUnknownContentState()
aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
)
), ),
aJoinRoomState( aJoinRoomState(
contentState = AsyncData.Success( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin)
aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock)
)
), ),
aJoinRoomState( aJoinRoomState(
contentState = AsyncData.Success( contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock)
aContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited) ),
) aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited)
), ),
) )
} }
fun aContentState( fun anUnknownContentState(roomId: RoomId = RoomId("@exa:matrix.org")) = ContentState.UnknownRoom(roomId)
fun anUninitializedContentState(roomId: RoomId = RoomId("@exa:matrix.org")) = ContentState.Loading(roomId)
fun aLoadedContentState(
roomId: RoomId = RoomId("@exa:matrix.org"), roomId: RoomId = RoomId("@exa:matrix.org"),
name: String = "Element x android", name: String = "Element x android",
description: String? = "#exa:matrix.org", alias: String? = "#exa:matrix.org",
topic: String? = "Element X is a secure, private and decentralized messenger.",
numberOfMembers: Long? = null, numberOfMembers: Long? = null,
isDirect: Boolean = false, isDirect: Boolean = false,
roomAvatarUrl: String? = null, roomAvatarUrl: String? = null,
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
) = ContentState( ) = ContentState.Loaded(
roomId = roomId, roomId = roomId,
name = name, name = name,
description = description, alias = alias,
topic = topic,
numberOfMembers = numberOfMembers, numberOfMembers = numberOfMembers,
isDirect = isDirect, isDirect = isDirect,
roomAvatarUrl = roomAvatarUrl, roomAvatarUrl = roomAvatarUrl,
@ -65,9 +67,7 @@ fun aContentState(
) )
fun aJoinRoomState( fun aJoinRoomState(
contentState: AsyncData<ContentState> = AsyncData.Success( contentState: ContentState = aLoadedContentState(),
aContentState()
),
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
eventSink: (JoinRoomEvents) -> Unit = {} eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState( ) = JoinRoomState(

View file

@ -16,9 +16,11 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -30,14 +32,15 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign 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.PreviewLightDark
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.compound.tokens.generated.CompoundIcons 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.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@ -61,11 +64,12 @@ fun JoinRoomView(
) { ) {
HeaderFooterPage( HeaderFooterPage(
modifier = modifier, modifier = modifier,
paddingValues = PaddingValues(16.dp),
topBar = { topBar = {
JoinRoomTopBar(onBackClicked = onBackPressed) JoinRoomTopBar(onBackClicked = onBackPressed)
}, },
content = { content = {
JoinRoomContent(asyncContentState = state.contentState) JoinRoomContent(contentState = state.contentState)
}, },
footer = { footer = {
JoinRoomFooter( JoinRoomFooter(
@ -111,22 +115,19 @@ private fun JoinRoomFooter(
} }
JoinAuthorisationStatus.CanJoin -> { JoinAuthorisationStatus.CanJoin -> {
Button( Button(
text = stringResource(CommonStrings.action_join), text = stringResource(R.string.screen_join_room_join_action),
onClick = onJoinRoom, onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium, size = ButtonSize.Medium,
) )
} }
JoinAuthorisationStatus.CanKnock -> { JoinAuthorisationStatus.CanKnock -> {
//TODO knock
/*
Button( Button(
text = stringResource(CommonStrings.action_knock), text = stringResource(R.string.screen_join_room_knock_action),
onClick = onJoinRoom, onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
size = ButtonSize.Medium, size = ButtonSize.Medium,
) )
*/
} }
JoinAuthorisationStatus.Unknown -> Unit JoinAuthorisationStatus.Unknown -> Unit
} }
@ -134,63 +135,60 @@ private fun JoinRoomFooter(
@Composable @Composable
private fun JoinRoomContent( private fun JoinRoomContent(
asyncContentState: AsyncData<ContentState>, contentState: ContentState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@Composable
fun ContentScaffold(
avatar: @Composable () -> Unit,
title: String,
description: String,
memberCount: @Composable (() -> Unit)? = null
) {
avatar()
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textPrimary,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = description,
style = ElementTheme.typography.fontBodyMdRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
)
memberCount?.invoke()
}
Column( Column(
modifier = modifier modifier = modifier.fillMaxWidth(),
.fillMaxWidth()
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
when (asyncContentState) { when (contentState) {
is AsyncData.Success -> { is ContentState.Loaded -> {
val contentState = asyncContentState.data
ContentScaffold( ContentScaffold(
avatar = { avatar = {
Avatar(contentState.avatarData(AvatarSize.RoomHeader)) Avatar(contentState.avatarData(AvatarSize.RoomHeader))
}, },
title = contentState.name, title = {
description = contentState.description ?: stringResource(R.string.screen_join_room_subtitle_no_preview) Title(contentState.computedTitle)
) { },
if (contentState.showMemberCount) { subtitle = {
JoinRoomMembersCount(memberCount = contentState.numberOfMembers ?: 0) Subtitle(contentState.computedSubtitle)
},
description = {
Description(contentState.topic ?: "")
},
memberCount = {
if (contentState.showMemberCount) {
MembersCount(memberCount = contentState.numberOfMembers ?: 0)
}
} }
} )
} }
else -> { is ContentState.UnknownRoom -> {
ContentScaffold( ContentScaffold(
avatar = { avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
}, },
title = stringResource(R.string.screen_join_room_title_no_preview), title = {
description = stringResource(R.string.screen_join_room_subtitle_no_preview), Title(stringResource(R.string.screen_join_room_title_no_preview))
},
subtitle = {
Subtitle(stringResource(R.string.screen_join_room_subtitle_no_preview))
},
)
}
is ContentState.Loading -> {
ContentScaffold(
avatar = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
},
title = {
PlaceholderAtom(width = 200.dp, height = 22.dp)
},
subtitle = {
PlaceholderAtom(width = 140.dp, height = 20.dp)
},
) )
} }
} }
@ -198,13 +196,72 @@ private fun JoinRoomContent(
} }
@Composable @Composable
private fun JoinRoomMembersCount(memberCount: Long) { private fun ContentScaffold(
avatar: @Composable () -> Unit,
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit,
description: @Composable (() -> Unit)? = null,
memberCount: @Composable (() -> Unit)? = null,
) {
avatar()
Spacer(modifier = Modifier.height(16.dp))
title()
Spacer(modifier = Modifier.height(8.dp))
subtitle()
Spacer(modifier = Modifier.height(8.dp))
if (memberCount != null) {
memberCount()
}
Spacer(modifier = Modifier.height(8.dp))
if (description != null) {
description()
}
Spacer(modifier = Modifier.height(24.dp))
}
@Composable
private fun Title(title: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = title,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textPrimary,
)
}
@Composable
private fun Subtitle(subtitle: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
)
}
@Composable
private fun Description(description: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = description,
style = ElementTheme.typography.fontBodySmRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
@Composable
private fun MembersCount(memberCount: Long) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(
modifier = Modifier modifier = Modifier
.background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape) .background(color = ElementTheme.colors.bgSubtleSecondary, shape = CircleShape)
.widthIn(min = 48.dp) .widthIn(min = 48.dp)
.padding(all = 2.dp), .padding(all = 2.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
@ -230,9 +287,7 @@ private fun JoinRoomTopBar(
navigationIcon = { navigationIcon = {
BackButton(onClick = onBackClicked) BackButton(onClick = onBackClicked)
}, },
title = { title = {},
},
) )
} }

View file

@ -21,7 +21,6 @@ import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.roomdirectory.api.RoomDescription 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.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -51,10 +50,13 @@ class JoinRoomPresenterTest {
val presenter = createJoinRoomPresenter() val presenter = createJoinRoomPresenter()
presenter.test { presenter.test {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isInstanceOf(AsyncData.Uninitialized::class.java) assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID))
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
} }
awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.UnknownRoom(A_ROOM_ID))
}
} }
} }
@ -72,11 +74,11 @@ class JoinRoomPresenterTest {
presenter.test { presenter.test {
skipItems(1) skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isInstanceOf(AsyncData.Success::class.java) val contentState = state.contentState as ContentState.Loaded
val contentState = state.contentState.dataOrNull()!!
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
assertThat(contentState.name).isEqualTo(roomInfo.name) assertThat(contentState.name).isEqualTo(roomInfo.name)
assertThat(contentState.description).isEqualTo(roomInfo.topic) assertThat(contentState.topic).isEqualTo(roomInfo.topic)
assertThat(contentState.alias).isEqualTo(roomInfo.canonicalAlias)
assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount) assertThat(contentState.numberOfMembers).isEqualTo(roomInfo.activeMembersCount)
assertThat(contentState.isDirect).isEqualTo(roomInfo.isDirect) assertThat(contentState.isDirect).isEqualTo(roomInfo.isDirect)
assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl) assertThat(contentState.roomAvatarUrl).isEqualTo(roomInfo.avatarUrl)
@ -186,11 +188,11 @@ class JoinRoomPresenterTest {
presenter.test { presenter.test {
skipItems(1) skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isInstanceOf(AsyncData.Success::class.java) val contentState = state.contentState as ContentState.Loaded
val contentState = state.contentState.dataOrNull()!!
assertThat(contentState.roomId).isEqualTo(A_ROOM_ID) assertThat(contentState.roomId).isEqualTo(A_ROOM_ID)
assertThat(contentState.name).isEqualTo(roomDescription.name) assertThat(contentState.name).isEqualTo(roomDescription.name)
assertThat(contentState.description).isEqualTo(roomDescription.description) assertThat(contentState.topic).isEqualTo(roomDescription.topic)
assertThat(contentState.alias).isEqualTo(roomDescription.alias)
assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers) assertThat(contentState.numberOfMembers).isEqualTo(roomDescription.numberOfMembers)
assertThat(contentState.isDirect).isFalse() assertThat(contentState.isDirect).isFalse()
assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl) assertThat(contentState.roomAvatarUrl).isEqualTo(roomDescription.avatarUrl)
@ -256,8 +258,9 @@ class JoinRoomPresenterTest {
private fun aRoomDescription( private fun aRoomDescription(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME, name: String? = A_ROOM_NAME,
description: String = "A room about something", topic: String? = "A room about something",
alias: String? = "#alias:matrix.org",
avatarUrl: String? = null, avatarUrl: String? = null,
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
numberOfMembers: Long = 2L numberOfMembers: Long = 2L
@ -265,7 +268,8 @@ class JoinRoomPresenterTest {
return RoomDescription( return RoomDescription(
roomId = roomId, roomId = roomId,
name = name, name = name,
description = description, topic = topic,
alias = alias,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
joinRule = joinRule, joinRule = joinRule,
numberOfMembers = numberOfMembers numberOfMembers = numberOfMembers

View file

@ -17,16 +17,19 @@
package io.element.android.features.roomdirectory.api package io.element.android.features.roomdirectory.api
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.avatar.AvatarSize
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@Immutable
data class RoomDescription( data class RoomDescription(
val roomId: RoomId, val roomId: RoomId,
val name: String, val name: String?,
val description: String, val alias: String?,
val topic: String?,
val avatarUrl: String?, val avatarUrl: String?,
val joinRule: JoinRule, val joinRule: JoinRule,
val numberOfMembers: Long, val numberOfMembers: Long,
@ -38,6 +41,18 @@ data class RoomDescription(
UNKNOWN UNKNOWN
} }
val computedName = name ?: alias ?: roomId.value
val computedDescription: String
get() {
return when {
topic != null -> topic
name != null && alias != null -> alias
name == null && alias == null -> ""
else -> roomId.value
}
}
fun canBeJoined() = joinRule == JoinRule.PUBLIC || joinRule == JoinRule.KNOCK fun canBeJoined() = joinRule == JoinRule.PUBLIC || joinRule == JoinRule.KNOCK
fun avatarData(size: AvatarSize) = AvatarData( fun avatarData(size: AvatarSize) = AvatarData(

View file

@ -68,7 +68,8 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
RoomDescription( RoomDescription(
roomId = RoomId("!exa:matrix.org"), roomId = RoomId("!exa:matrix.org"),
name = "Element X Android", name = "Element X Android",
description = "Element X is a secure, private and decentralized messenger.", topic = "Element X is a secure, private and decentralized messenger.",
alias = "#element-x-android:matrix.org",
avatarUrl = null, avatarUrl = null,
joinRule = RoomDescription.JoinRule.PUBLIC, joinRule = RoomDescription.JoinRule.PUBLIC,
numberOfMembers = 2765, numberOfMembers = 2765,
@ -76,7 +77,8 @@ fun aRoomDescriptionList(): ImmutableList<RoomDescription> {
RoomDescription( RoomDescription(
roomId = RoomId("!exi:matrix.org"), roomId = RoomId("!exi:matrix.org"),
name = "Element X iOS", name = "Element X iOS",
description = "Element X is a secure, private and decentralized messenger.", topic = "Element X is a secure, private and decentralized messenger.",
alias = "#element-x-ios:matrix.org",
avatarUrl = null, avatarUrl = null,
joinRule = RoomDescription.JoinRule.UNKNOWN, joinRule = RoomDescription.JoinRule.UNKNOWN,
numberOfMembers = 356, numberOfMembers = 356,

View file

@ -292,14 +292,14 @@ private fun RoomDirectoryRoomRow(
.padding(start = 16.dp) .padding(start = 16.dp)
) { ) {
Text( Text(
text = roomDescription.name, text = roomDescription.computedName,
maxLines = 1, maxLines = 1,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Text( Text(
text = roomDescription.description, text = roomDescription.computedDescription,
maxLines = 1, maxLines = 1,
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,

View file

@ -17,31 +17,15 @@
package io.element.android.features.roomdirectory.impl.root.model package io.element.android.features.roomdirectory.impl.root.model
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
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.roomdirectory.RoomDescription as MatrixRoomDescription import io.element.android.libraries.matrix.api.roomdirectory.RoomDescription as MatrixRoomDescription
fun MatrixRoomDescription.toFeatureModel(): RoomDescription { fun MatrixRoomDescription.toFeatureModel(): RoomDescription {
fun name(): String {
return name ?: alias ?: roomId.value
}
fun description(): String {
val topic = topic
val alias = alias
val name = name
return when {
topic != null -> topic
name != null && alias != null -> alias
name == null && alias == null -> ""
else -> roomId.value
}
}
return RoomDescription( return RoomDescription(
roomId = roomId, roomId = roomId,
name = name(), name = name,
description = description(), alias = alias,
topic = topic,
avatarUrl = avatarUrl, avatarUrl = avatarUrl,
numberOfMembers = numberOfMembers, numberOfMembers = numberOfMembers,
joinRule = when (joinRule) { joinRule = when (joinRule) {

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.designsystem.atomic.pages
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -43,6 +44,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable @Composable
fun HeaderFooterPage( fun HeaderFooterPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(20.dp),
background: @Composable () -> Unit = {}, background: @Composable () -> Unit = {},
topBar: @Composable () -> Unit = {}, topBar: @Composable () -> Unit = {},
header: @Composable () -> Unit = {}, header: @Composable () -> Unit = {},
@ -57,7 +59,7 @@ fun HeaderFooterPage(
background() background()
Column( Column(
modifier = Modifier modifier = Modifier
.padding(all = 20.dp) .padding(paddingValues = paddingValues)
.padding(padding) .padding(padding)
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
) { ) {

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.util package io.element.android.libraries.matrix.impl.util
import io.element.android.libraries.core.data.tryOrNull
import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
@ -23,7 +24,9 @@ import org.matrix.rustcomponents.sdk.TaskHandle
internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle?) = internal fun <T> mxCallbackFlow(block: suspend ProducerScope<T>.() -> TaskHandle?) =
callbackFlow { callbackFlow {
val taskHandle: TaskHandle? = block(this) val taskHandle: TaskHandle? = tryOrNull {
block(this)
}
awaitClose { awaitClose {
taskHandle?.cancelAndDestroy() taskHandle?.cancelAndDestroy()
} }