Join room : change state and view
This commit is contained in:
parent
038d8e3334
commit
c0918bd965
11 changed files with 228 additions and 165 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue