@ -9,21 +9,31 @@ package io.element.android.features.joinroom.impl
@@ -9,21 +9,31 @@ package io.element.android.features.joinroom.impl
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
@ -32,20 +42,25 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescrip
@@ -32,20 +42,25 @@ import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescrip
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
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.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.molecules.RoomPreviewMembersCountMolecule
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.LightGradientBackground
import io.element.android.libraries.designsystem.components.BigIcon
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.components.button.SuperButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@ -59,6 +74,7 @@ fun JoinRoomView(
@@ -59,6 +74,7 @@ fun JoinRoomView(
onBackClick : ( ) -> Unit ,
onJoinSuccess : ( ) -> Unit ,
onKnockSuccess : ( ) -> Unit ,
onCancelKnockSuccess : ( ) -> Unit ,
modifier : Modifier = Modifier ,
) {
Box (
@ -69,12 +85,14 @@ fun JoinRoomView(
@@ -69,12 +85,14 @@ fun JoinRoomView(
containerColor = Color . Transparent ,
paddingValues = PaddingValues ( 16. dp ) ,
topBar = {
JoinRoomTopBar ( onBackClick = onBackClick )
JoinRoomTopBar ( contentState = state . contentState , onBackClick = onBackClick )
} ,
content = {
JoinRoomContent (
contentState = state . contentState ,
applicationName = state . applicationName ,
knockMessage = state . knockMessage ,
onKnockMessageUpdate = { state . eventSink ( JoinRoomEvents . UpdateKnockMessage ( it ) ) } ,
)
} ,
footer = {
@ -92,6 +110,9 @@ fun JoinRoomView(
@@ -92,6 +110,9 @@ fun JoinRoomView(
onKnockRoom = {
state . eventSink ( JoinRoomEvents . KnockRoom )
} ,
onCancelKnock = {
state . eventSink ( JoinRoomEvents . CancelKnock ( requiresConfirmation = true ) )
} ,
onRetry = {
state . eventSink ( JoinRoomEvents . RetryFetchingContent )
} ,
@ -103,12 +124,30 @@ fun JoinRoomView(
@@ -103,12 +124,30 @@ fun JoinRoomView(
AsyncActionView (
async = state . joinAction ,
onSuccess = { onJoinSuccess ( ) } ,
onErrorDismiss = { state . eventSink ( JoinRoomEvents . ClearError ) } ,
onErrorDismiss = { state . eventSink ( JoinRoomEvents . ClearActionStates ) } ,
)
AsyncActionView (
async = state . knockAction ,
onSuccess = { onKnockSuccess ( ) } ,
onErrorDismiss = { state . eventSink ( JoinRoomEvents . ClearError ) } ,
onErrorDismiss = { state . eventSink ( JoinRoomEvents . ClearActionStates ) } ,
)
AsyncActionView (
async = state . cancelKnockAction ,
onSuccess = { onCancelKnockSuccess ( ) } ,
onErrorDismiss = { state . eventSink ( JoinRoomEvents . ClearActionStates ) } ,
errorMessage = {
stringResource ( CommonStrings . error _unknown )
} ,
confirmationDialog = {
ConfirmationDialog (
content = stringResource ( R . string . screen _join _room _cancel _knock _alert _description ) ,
title = stringResource ( R . string . screen _join _room _cancel _knock _alert _title ) ,
submitText = stringResource ( R . string . screen _join _room _cancel _knock _alert _confirmation ) ,
cancelText = stringResource ( CommonStrings . action _no ) ,
onSubmitClick = { state . eventSink ( JoinRoomEvents . CancelKnock ( requiresConfirmation = false ) ) } ,
onDismiss = { state . eventSink ( JoinRoomEvents . ClearActionStates ) } ,
)
} ,
)
}
@ -119,63 +158,81 @@ private fun JoinRoomFooter(
@@ -119,63 +158,81 @@ private fun JoinRoomFooter(
onDeclineInvite : ( ) -> Unit ,
onJoinRoom : ( ) -> Unit ,
onKnockRoom : ( ) -> Unit ,
onCancelKnock : ( ) -> Unit ,
onRetry : ( ) -> Unit ,
onGoBack : ( ) -> Unit ,
modifier : Modifier = Modifier ,
) {
if ( state . contentState is ContentState . Failure ) {
Button (
text = stringResource ( CommonStrings . action _retry ) ,
onClick = onRetry ,
modifier = modifier . fillMaxWidth ( ) ,
size = ButtonSize . Large ,
)
} else if ( state . contentState is ContentState . Loaded && state . contentState . roomType == RoomType . Space ) {
Button (
text = stringResource ( CommonStrings . action _go _back ) ,
onClick = onGoBack ,
modifier = modifier . fillMaxWidth ( ) ,
size = ButtonSize . Large ,
)
} else {
val joinAuthorisationStatus = state . joinAuthorisationStatus
when ( joinAuthorisationStatus ) {
is JoinAuthorisationStatus . IsInvited -> {
ButtonRowMolecule ( modifier = modifier , horizontalArrangement = Arrangement . spacedBy ( 20. dp ) ) {
OutlinedButton (
text = stringResource ( CommonStrings . action _decline ) ,
onClick = onDeclineInvite ,
modifier = Modifier . weight ( 1f ) ,
size = ButtonSize . LargeLowPadding ,
)
Button (
text = stringResource ( CommonStrings . action _accept ) ,
onClick = onAcceptInvite ,
modifier = Modifier . weight ( 1f ) ,
size = ButtonSize . LargeLowPadding ,
)
Box (
modifier = modifier
. fillMaxWidth ( )
. padding ( top = 8. dp )
) {
if ( state . contentState is ContentState . Failure ) {
Button (
text = stringResource ( CommonStrings . action _retry ) ,
onClick = onRetry ,
modifier = Modifier . fillMaxWidth ( ) ,
size = ButtonSize . Large ,
)
} else if ( state . contentState is ContentState . Loaded && state . contentState . roomType == RoomType . Space ) {
Button (
text = stringResource ( CommonStrings . action _go _back ) ,
onClick = onGoBack ,
modifier = Modifier . fillMaxWidth ( ) ,
size = ButtonSize . Large ,
)
} else {
val joinAuthorisationStatus = state . joinAuthorisationStatus
when ( joinAuthorisationStatus ) {
is JoinAuthorisationStatus . IsInvited -> {
ButtonRowMolecule ( horizontalArrangement = Arrangement . spacedBy ( 20. dp ) ) {
OutlinedButton (
text = stringResource ( CommonStrings . action _decline ) ,
onClick = onDeclineInvite ,
modifier = Modifier . weight ( 1f ) ,
size = ButtonSize . LargeLowPadding ,
)
Button (
text = stringResource ( CommonStrings . action _accept ) ,
onClick = onAcceptInvite ,
modifier = Modifier . weight ( 1f ) ,
size = ButtonSize . LargeLowPadding ,
)
}
}
}
JoinAuthorisationStatus . CanJoin -> {
SuperButton (
onClick = onJoinRoom ,
modifier = modifier . fillMaxWidth ( ) ,
buttonSize = ButtonSize . Large ,
) {
Text (
text = stringResource ( R . string . screen _join _room _join _action ) ,
JoinAuthorisationStatus . CanJoin -> {
SuperButton (
onClick = onJoinRoom ,
modifier = Modifier . fillMaxWidth ( ) ,
buttonSize = ButtonSize . Large ,
) {
Text (
text = stringResource ( R . string . screen _join _room _join _action ) ,
)
}
}
JoinAuthorisationStatus . CanKnock -> {
SuperButton (
onClick = onKnockRoom ,
modifier = Modifier . fillMaxWidth ( ) ,
buttonSize = ButtonSize . Large ,
) {
Text (
text = stringResource ( R . string . screen _join _room _knock _action ) ,
)
}
}
JoinAuthorisationStatus . IsKnocked -> {
OutlinedButton (
text = stringResource ( R . string . screen _join _room _cancel _knock _action ) ,
onClick = onCancelKnock ,
modifier = Modifier . fillMaxWidth ( ) ,
size = ButtonSize . Large ,
)
}
JoinAuthorisationStatus . Unknown -> Unit
}
JoinAuthorisationStatus . CanKnock -> {
Button (
text = stringResource ( R . string . screen _join _room _knock _action ) ,
onClick = onKnockRoom ,
modifier = modifier . fillMaxWidth ( ) ,
size = ButtonSize . Large ,
)
}
JoinAuthorisationStatus . Unknown -> Unit
}
}
}
@ -184,132 +241,217 @@ private fun JoinRoomFooter(
@@ -184,132 +241,217 @@ private fun JoinRoomFooter(
private fun JoinRoomContent (
contentState : ContentState ,
applicationName : String ,
knockMessage : String ,
onKnockMessageUpdate : ( String ) -> Unit ,
modifier : Modifier = Modifier ,
) {
when ( contentState ) {
is ContentState . Loaded -> {
RoomPreviewOrganism (
modifier = modifier ,
avatar = {
Avatar ( contentState . avatarData ( AvatarSize . RoomHeader ) )
} ,
title = {
if ( contentState . name != null ) {
RoomPreviewTitleAtom (
title = contentState . name ,
)
} else {
RoomPreviewTitleAtom (
title = stringResource ( id = CommonStrings . common _no _room _name ) ,
fontStyle = FontStyle . Italic
)
}
} ,
subtitle = {
if ( contentState . alias != null ) {
RoomPreviewSubtitleAtom ( contentState . alias . value )
Box ( modifier = modifier ) {
when ( contentState ) {
is ContentState . Loaded -> {
when ( contentState . joinAuthorisationStatus ) {
is JoinAuthorisationStatus . IsKnocked -> {
IsKnockedLoadedContent ( )
}
} ,
description = {
Column (
horizontalAlignment = Alignment . CenterHorizontally ,
verticalArrangement = Arrangement . spacedBy ( 8. dp ) ,
) {
val inviteSender = ( contentState . joinAuthorisationStatus as ? JoinAuthorisationStatus . IsInvited ) ?. inviteSender
if ( inviteSender != null ) {
InviteSenderView ( inviteSender = inviteSender )
}
RoomPreviewDescriptionAtom ( contentState . topic ?: " " )
if ( contentState . roomType == RoomType . Space ) {
Spacer ( modifier = Modifier . height ( 24. dp ) )
Text (
text = stringResource ( R . string . screen _join _room _space _not _supported _title ) ,
textAlign = TextAlign . Center ,
style = ElementTheme . typography . fontBodyLgMedium ,
color = MaterialTheme . colorScheme . primary ,
)
Text (
text = stringResource ( R . string . screen _join _room _space _not _supported _description , applicationName ) ,
textAlign = TextAlign . Center ,
style = ElementTheme . typography . fontBodyMdRegular ,
color = MaterialTheme . colorScheme . secondary ,
)
}
}
} ,
memberCount = {
if ( contentState . showMemberCount ) {
RoomPreviewMembersCountMolecule ( memberCount = contentState . numberOfMembers ?: 0 )
else -> {
DefaultLoadedContent (
modifier = Modifier . verticalScroll ( rememberScrollState ( ) ) ,
contentState = contentState ,
applicationName = applicationName ,
knockMessage = knockMessage ,
onKnockMessageUpdate = onKnockMessageUpdate
)
}
}
)
}
is ContentState . UnknownRoom -> {
RoomPreviewOrganism (
modifier = modifier ,
avatar = {
PlaceholderAtom ( width = AvatarSize . RoomHeader . dp , height = AvatarSize . RoomHeader . dp )
} ,
title = {
RoomPreviewTitleAtom ( stringResource ( R . string . screen _join _room _title _no _preview ) )
} ,
subtitle = {
RoomPreviewSubtitleAtom ( stringResource ( R . string . screen _join _room _subtitle _no _preview ) )
} ,
)
}
is ContentState . Loading -> {
RoomPreviewOrganism (
modifier = modifier ,
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 )
} ,
)
}
is ContentState . Failure -> {
RoomPreviewOrganism (
modifier = modifier ,
avatar = {
PlaceholderAtom ( width = AvatarSize . RoomHeader . dp , height = AvatarSize . RoomHeader . dp )
} ,
title = {
when ( contentState . roomIdOrAlias ) {
is RoomIdOrAlias . Alias -> {
RoomPreviewTitleAtom ( contentState . roomIdOrAlias . identifier )
}
is RoomIdOrAlias . Id -> {
PlaceholderAtom ( width = 200. dp , height = 22. dp )
}
is ContentState . UnknownRoom -> {
RoomPreviewOrganism (
avatar = {
PlaceholderAtom ( width = AvatarSize . RoomHeader . dp , height = AvatarSize . RoomHeader . dp )
} ,
title = {
RoomPreviewTitleAtom ( stringResource ( R . string . screen _join _room _title _no _preview ) )
} ,
subtitle = {
RoomPreviewSubtitleAtom ( stringResource ( R . string . screen _join _room _subtitle _no _preview ) )
} ,
)
}
is ContentState . Loading -> {
RoomPreviewOrganism (
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 )
} ,
)
}
is ContentState . Failure -> {
RoomPreviewOrganism (
avatar = {
PlaceholderAtom ( width = AvatarSize . RoomHeader . dp , height = AvatarSize . RoomHeader . dp )
} ,
title = {
when ( contentState . roomIdOrAlias ) {
is RoomIdOrAlias . Alias -> {
RoomPreviewTitleAtom ( contentState . roomIdOrAlias . identifier )
}
is RoomIdOrAlias . Id -> {
PlaceholderAtom ( width = 200. dp , height = 22. dp )
}
}
}
} ,
subtitle = {
} ,
subtitle = {
Text (
text = stringResource ( id = CommonStrings . error _unknown ) ,
textAlign = TextAlign . Center ,
color = MaterialTheme . colorScheme . error ,
)
} ,
)
}
}
}
}
@Composable
private fun IsKnockedLoadedContent ( modifier : Modifier = Modifier ) {
BoxWithConstraints (
modifier = modifier
. fillMaxHeight ( )
. padding ( horizontal = 16. dp ) ,
contentAlignment = Alignment . Center ,
) {
IconTitleSubtitleMolecule (
modifier = Modifier . sizeIn ( minHeight = maxHeight * 0.7f ) ,
iconStyle = BigIcon . Style . SuccessSolid ,
title = stringResource ( R . string . screen _join _room _knock _sent _title ) ,
subTitle = stringResource ( R . string . screen _join _room _knock _sent _description ) ,
)
}
}
@Composable
private fun DefaultLoadedContent (
contentState : ContentState . Loaded ,
applicationName : String ,
knockMessage : String ,
onKnockMessageUpdate : ( String ) -> Unit ,
modifier : Modifier = Modifier ,
) {
RoomPreviewOrganism (
modifier = modifier ,
avatar = {
Avatar ( contentState . avatarData ( AvatarSize . RoomHeader ) )
} ,
title = {
if ( contentState . name != null ) {
RoomPreviewTitleAtom (
title = contentState . name ,
)
} else {
RoomPreviewTitleAtom (
title = stringResource ( id = CommonStrings . common _no _room _name ) ,
fontStyle = FontStyle . Italic
)
}
} ,
subtitle = {
if ( contentState . alias != null ) {
RoomPreviewSubtitleAtom ( contentState . alias . value )
}
} ,
description = {
Column (
horizontalAlignment = Alignment . CenterHorizontally ,
verticalArrangement = Arrangement . spacedBy ( 8. dp ) ,
) {
val inviteSender = ( contentState . joinAuthorisationStatus as ? JoinAuthorisationStatus . IsInvited ) ?. inviteSender
if ( inviteSender != null ) {
InviteSenderView ( inviteSender = inviteSender )
}
RoomPreviewDescriptionAtom ( contentState . topic ?: " " )
if ( contentState . roomType == RoomType . Space ) {
Spacer ( modifier = Modifier . height ( 24. dp ) )
Text (
text = stringResource ( id = CommonStrings . error _unknown ) ,
text = stringResource ( R . string . screen _join _room _space _not _supported _title ) ,
textAlign = TextAlign . Center ,
color = MaterialTheme . colorScheme . error ,
style = ElementTheme . typography . fontBodyLgMedium ,
color = MaterialTheme . colorScheme . primary ,
)
} ,
)
Text (
text = stringResource ( R . string . screen _join _room _space _not _supported _description , applicationName ) ,
textAlign = TextAlign . Center ,
style = ElementTheme . typography . fontBodyMdRegular ,
color = MaterialTheme . colorScheme . secondary ,
)
} else if ( contentState . joinAuthorisationStatus is JoinAuthorisationStatus . CanKnock ) {
Spacer ( modifier = Modifier . height ( 24. dp ) )
OutlinedTextField (
value = knockMessage ,
onValueChange = onKnockMessageUpdate ,
maxLines = 3 ,
minLines = 3 ,
modifier = Modifier . fillMaxWidth ( )
)
Text (
text = stringResource ( R . string . screen _join _room _knock _message _description ) ,
style = ElementTheme . typography . fontBodySmRegular ,
color = ElementTheme . colors . textPlaceholder ,
textAlign = TextAlign . Start ,
modifier = Modifier . fillMaxWidth ( )
)
}
}
} ,
memberCount = {
if ( contentState . showMemberCount ) {
RoomPreviewMembersCountMolecule ( memberCount = contentState . numberOfMembers ?: 0 )
}
}
}
)
}
@OptIn ( ExperimentalMaterial3Api :: class )
@Composable
private fun JoinRoomTopBar (
contentState : ContentState ,
onBackClick : ( ) -> Unit ,
) {
TopAppBar (
navigationIcon = {
BackButton ( onClick = onBackClick )
} ,
title = { } ,
title = {
if ( contentState is ContentState . Loaded && contentState . joinAuthorisationStatus is JoinAuthorisationStatus . IsKnocked ) {
val roundedCornerShape = RoundedCornerShape ( 8. dp )
val titleModifier = Modifier
. clip ( roundedCornerShape )
if ( contentState . name != null ) {
Row (
modifier = titleModifier ,
verticalAlignment = Alignment . CenterVertically
) {
Avatar ( avatarData = contentState . avatarData ( AvatarSize . TimelineRoom ) )
Text (
modifier = Modifier . padding ( horizontal = 8. dp ) ,
text = contentState . name ,
style = ElementTheme . typography . fontBodyLgMedium ,
maxLines = 1 ,
overflow = TextOverflow . Ellipsis
)
}
} else {
IconTitlePlaceholdersRowMolecule (
iconSize = AvatarSize . TimelineRoom . dp ,
modifier = titleModifier
)
}
}
} ,
)
}
@ -321,5 +463,6 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class)
@@ -321,5 +463,6 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class)
onBackClick = { } ,
onJoinSuccess = { } ,
onKnockSuccess = { } ,
onCancelKnockSuccess = { } ,
)
}