@ -7,31 +7,50 @@
@@ -7,31 +7,50 @@
package io.element.android.features.joinroom.impl
import androidx.compose.foundation.background
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.heightIn
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.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
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
@ -41,11 +60,13 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
@@ -41,11 +60,13 @@ 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
@ -64,17 +85,20 @@ fun JoinRoomView(
@@ -64,17 +85,20 @@ fun JoinRoomView(
Box (
modifier = modifier . fillMaxSize ( ) ,
) {
var knockMessage by rememberSaveable { mutableStateOf ( " " ) }
LightGradientBackground ( )
HeaderFooterPage (
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 = knockMessage ,
onKnockMessageUpdate = { knockMessage = it } ,
)
} ,
footer = {
@ -90,7 +114,10 @@ fun JoinRoomView(
@@ -90,7 +114,10 @@ fun JoinRoomView(
state . eventSink ( JoinRoomEvents . JoinRoom )
} ,
onKnockRoom = {
state . eventSink ( JoinRoomEvents . KnockRoom )
state . eventSink ( JoinRoomEvents . KnockRoom ( knockMessage ) )
} ,
onCancelKnock = {
state . eventSink ( JoinRoomEvents . CancelKnock ( requiresConfirmation = true ) )
} ,
onRetry = {
state . eventSink ( JoinRoomEvents . RetryFetchingContent )
@ -103,12 +130,27 @@ fun JoinRoomView(
@@ -103,12 +130,27 @@ 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 = { state . eventSink ( JoinRoomEvents . ClearActionStates ) } ,
onErrorDismiss = { state . eventSink ( JoinRoomEvents . ClearActionStates ) } ,
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,29 +161,31 @@ private fun JoinRoomFooter(
@@ -119,29 +161,31 @@ private fun JoinRoomFooter(
onDeclineInvite : ( ) -> Unit ,
onJoinRoom : ( ) -> Unit ,
onKnockRoom : ( ) -> Unit ,
onCancelKnock : ( ) -> Unit ,
onRetry : ( ) -> Unit ,
onGoBack : ( ) -> Unit ,
modifier : Modifier = Modifier ,
) {
Box ( modifier = modifier . fillMaxWidth ( ) . padding ( top = 8. dp ) ) {
if ( state . contentState is ContentState . Failure ) {
Button (
text = stringResource ( CommonStrings . action _retry ) ,
onClick = onRetry ,
modifier = m odifier. fillMaxWidth ( ) ,
modifier = M odifier. 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 = m odifier. fillMaxWidth ( ) ,
modifier = M odifier. fillMaxWidth ( ) ,
size = ButtonSize . Large ,
)
} else {
val joinAuthorisationStatus = state . joinAuthorisationStatus
when ( joinAuthorisationStatus ) {
is JoinAuthorisationStatus . IsInvited -> {
ButtonRowMolecule ( modifier = modifier , horizontalArrangement = Arrangement . spacedBy ( 20. dp ) ) {
ButtonRowMolecule ( horizontalArrangement = Arrangement . spacedBy ( 20. dp ) ) {
OutlinedButton (
text = stringResource ( CommonStrings . action _decline ) ,
onClick = onDeclineInvite ,
@ -159,7 +203,7 @@ private fun JoinRoomFooter(
@@ -159,7 +203,7 @@ private fun JoinRoomFooter(
JoinAuthorisationStatus . CanJoin -> {
SuperButton (
onClick = onJoinRoom ,
modifier = m odifier. fillMaxWidth ( ) ,
modifier = M odifier. fillMaxWidth ( ) ,
buttonSize = ButtonSize . Large ,
) {
Text (
@ -168,85 +212,58 @@ private fun JoinRoomFooter(
@@ -168,85 +212,58 @@ private fun JoinRoomFooter(
}
}
JoinAuthorisationStatus . CanKnock -> {
Button (
text = stringResource ( R . string . screen _join _room _knock _action ) ,
SuperButton (
onClick = onKnockRoom ,
modifier = modifier . fillMaxWidth ( ) ,
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
}
}
}
}
@Composable
private fun JoinRoomContent (
contentState : ContentState ,
applicationName : String ,
knockMessage : String ,
onKnockMessageUpdate : ( String ) -> Unit ,
modifier : Modifier = Modifier ,
) {
Box ( 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 )
}
} ,
description = {
Column (
horizontalAlignment = Alignment . CenterHorizontally ,
verticalArrangement = Arrangement . spacedBy ( 8. dp ) ,
) {
val inviteSender = ( contentState . joinAuthorisationStatus as ? JoinAuthorisationStatus . IsInvited ) ?. inviteSender
if ( inviteSender != null ) {
InviteSenderView ( inviteSender = inviteSender )
when ( contentState . joinAuthorisationStatus ) {
is JoinAuthorisationStatus . IsKnocked -> {
IsKnockedLoadedContent ( )
}
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 ,
else -> {
DefaultLoadedContent (
modifier = Modifier . verticalScroll ( rememberScrollState ( ) ) ,
contentState = contentState ,
applicationName = applicationName ,
knockMessage = knockMessage ,
onKnockMessageUpdate = onKnockMessageUpdate
)
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 )
}
}
)
}
is ContentState . UnknownRoom -> {
RoomPreviewOrganism (
modifier = modifier ,
avatar = {
PlaceholderAtom ( width = AvatarSize . RoomHeader . dp , height = AvatarSize . RoomHeader . dp )
} ,
@ -260,7 +277,6 @@ private fun JoinRoomContent(
@@ -260,7 +277,6 @@ private fun JoinRoomContent(
}
is ContentState . Loading -> {
RoomPreviewOrganism (
modifier = modifier ,
avatar = {
PlaceholderAtom ( width = AvatarSize . RoomHeader . dp , height = AvatarSize . RoomHeader . dp )
} ,
@ -274,7 +290,6 @@ private fun JoinRoomContent(
@@ -274,7 +290,6 @@ private fun JoinRoomContent(
}
is ContentState . Failure -> {
RoomPreviewOrganism (
modifier = modifier ,
avatar = {
PlaceholderAtom ( width = AvatarSize . RoomHeader . dp , height = AvatarSize . RoomHeader . dp )
} ,
@ -298,18 +313,146 @@ private fun JoinRoomContent(
@@ -298,18 +313,146 @@ private fun JoinRoomContent(
)
}
}
}
}
@Composable
fun IsKnockedLoadedContent ( modifier : Modifier = Modifier ) {
BoxWithConstraints (
modifier = modifier
. fillMaxHeight ( )
. padding ( horizontal = 16. dp ) ,
contentAlignment = Alignment . Center ,
) {
IconTitleSubtitleMolecule (
modifier = Modifier . sizeIn ( minHeight = maxHeight * 0.7f ) ,
iconImageVector = CompoundIcons . CheckCircleSolid ( ) ,
title = stringResource ( R . string . screen _join _room _knock _sent _title ) ,
subTitle = stringResource ( R . string . screen _join _room _knock _sent _description ) ,
iconTint = ElementTheme . colors . iconSuccessPrimary ,
iconBackgroundTint = ElementTheme . colors . bgSuccessSubtle ,
)
}
}
@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 ( 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 ,
)
} else if ( contentState . joinAuthorisationStatus is JoinAuthorisationStatus . CanKnock ) {
Spacer ( modifier = Modifier . height ( 24. dp ) )
OutlinedTextField (
value = knockMessage ,
onValueChange = onKnockMessageUpdate ,
modifier = Modifier
. fillMaxWidth ( )
. heightIn ( min = 90. dp )
)
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
)
}
}
} ,
)
}