knock : start adding ui to the JoinRoomView

This commit is contained in:
ganfra 2024-10-18 20:59:56 +02:00
parent 1424296811
commit 928b1af702
9 changed files with 346 additions and 171 deletions

View file

@ -10,8 +10,9 @@ package io.element.android.features.joinroom.impl
sealed interface JoinRoomEvents { sealed interface JoinRoomEvents {
data object RetryFetchingContent : JoinRoomEvents data object RetryFetchingContent : JoinRoomEvents
data object JoinRoom : JoinRoomEvents data object JoinRoom : JoinRoomEvents
data object KnockRoom : JoinRoomEvents data class KnockRoom(val message: String) : JoinRoomEvents
data object ClearError : JoinRoomEvents data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
data object ClearActionStates : JoinRoomEvents
data object AcceptInvite : JoinRoomEvents data object AcceptInvite : JoinRoomEvents
data object DeclineInvite : JoinRoomEvents data object DeclineInvite : JoinRoomEvents
} }

View file

@ -43,7 +43,7 @@ class JoinRoomNode @AssistedInject constructor(
state = state, state = state,
onBackClick = ::navigateUp, onBackClick = ::navigateUp,
onJoinSuccess = ::navigateUp, onJoinSuccess = ::navigateUp,
onKnockSuccess = ::navigateUp, onKnockSuccess = { },
modifier = modifier modifier = modifier
) )
acceptDeclineInviteView.Render( acceptDeclineInviteView.Render(

View file

@ -75,6 +75,7 @@ class JoinRoomPresenter @AssistedInject constructor(
val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty()) val roomInfo by matrixClient.getRoomInfoFlow(roomId.toRoomIdOrAlias()).collectAsState(initial = Optional.empty())
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val contentState by produceState<ContentState>( val contentState by produceState<ContentState>(
initialValue = ContentState.Loading(roomIdOrAlias), initialValue = ContentState.Loading(roomIdOrAlias),
key1 = roomInfo, key1 = roomInfo,
@ -110,7 +111,7 @@ class JoinRoomPresenter @AssistedInject constructor(
fun handleEvents(event: JoinRoomEvents) { fun handleEvents(event: JoinRoomEvents) {
when (event) { when (event) {
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction) JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction) is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, event.message)
JoinRoomEvents.AcceptInvite -> { JoinRoomEvents.AcceptInvite -> {
val inviteData = contentState.toInviteData() ?: return val inviteData = contentState.toInviteData() ?: return
acceptDeclineInviteState.eventSink( acceptDeclineInviteState.eventSink(
@ -123,12 +124,14 @@ class JoinRoomPresenter @AssistedInject constructor(
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(inviteData)
) )
} }
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
JoinRoomEvents.RetryFetchingContent -> { JoinRoomEvents.RetryFetchingContent -> {
retryCount++ retryCount++
} }
JoinRoomEvents.ClearError -> { JoinRoomEvents.ClearActionStates -> {
knockAction.value = AsyncAction.Uninitialized knockAction.value = AsyncAction.Uninitialized
joinAction.value = AsyncAction.Uninitialized joinAction.value = AsyncAction.Uninitialized
cancelKnockAction.value = AsyncAction.Uninitialized
} }
} }
} }
@ -138,6 +141,7 @@ class JoinRoomPresenter @AssistedInject constructor(
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
joinAction = joinAction.value, joinAction = joinAction.value,
knockAction = knockAction.value, knockAction = knockAction.value,
cancelKnockAction = cancelKnockAction.value,
applicationName = buildMeta.applicationName, applicationName = buildMeta.applicationName,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
@ -153,11 +157,23 @@ class JoinRoomPresenter @AssistedInject constructor(
} }
} }
private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>) = launch { private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>, message: String) = launch {
knockAction.runUpdatingState { knockAction.runUpdatingState {
knockRoom(roomId) knockRoom(roomId)
} }
} }
private fun CoroutineScope.cancelKnockRoom(requiresConfirmation: Boolean, cancelKnockAction: MutableState<AsyncAction<Unit>>) = launch {
if (requiresConfirmation) {
cancelKnockAction.value = AsyncAction.ConfirmingNoParams
} else {
matrixClient.getPendingRoom(roomId)?.use { room ->
cancelKnockAction.runUpdatingState {
room.leave()
}
}
}
}
} }
private fun RoomPreview.toContentState(): ContentState { private fun RoomPreview.toContentState(): ContentState {
@ -206,7 +222,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
name = name, name = name,
topic = topic, topic = topic,
alias = canonicalAlias, alias = canonicalAlias,
numberOfMembers = activeMembersCount.toLong(), numberOfMembers = activeMembersCount,
isDm = isDm, isDm = isDm,
roomType = if (isSpace) RoomType.Space else RoomType.Room, roomType = if (isSpace) RoomType.Space else RoomType.Room,
roomAvatarUrl = avatarUrl, roomAvatarUrl = avatarUrl,
@ -214,6 +230,7 @@ internal fun MatrixRoomInfo.toContentState(): ContentState {
currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited( currentUserMembership == CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
inviteSender = inviter?.toInviteSender() inviteSender = inviter?.toInviteSender()
) )
currentUserMembership == CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
isPublic -> JoinAuthorisationStatus.CanJoin isPublic -> JoinAuthorisationStatus.CanJoin
else -> JoinAuthorisationStatus.Unknown else -> JoinAuthorisationStatus.Unknown
} }

View file

@ -24,6 +24,7 @@ data class JoinRoomState(
val acceptDeclineInviteState: AcceptDeclineInviteState, val acceptDeclineInviteState: AcceptDeclineInviteState,
val joinAction: AsyncAction<Unit>, val joinAction: AsyncAction<Unit>,
val knockAction: AsyncAction<Unit>, val knockAction: AsyncAction<Unit>,
val cancelKnockAction: AsyncAction<Unit>,
val applicationName: String, val applicationName: String,
val eventSink: (JoinRoomEvents) -> Unit val eventSink: (JoinRoomEvents) -> Unit
) { ) {
@ -68,6 +69,7 @@ sealed interface ContentState {
sealed interface JoinAuthorisationStatus { sealed interface JoinAuthorisationStatus {
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
data object IsKnocked : JoinAuthorisationStatus
data object CanKnock : JoinAuthorisationStatus data object CanKnock : JoinAuthorisationStatus
data object CanJoin : JoinAuthorisationStatus data object CanJoin : JoinAuthorisationStatus
data object Unknown : JoinAuthorisationStatus data object Unknown : JoinAuthorisationStatus

View file

@ -81,6 +81,12 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
isDm = true, isDm = true,
) )
), ),
aJoinRoomState(
contentState = aLoadedContentState(
name = "A knocked Room",
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
)
)
) )
} }
@ -124,12 +130,14 @@ fun aJoinRoomState(
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(), acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized, joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized, knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (JoinRoomEvents) -> Unit = {} eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState( ) = JoinRoomState(
contentState = contentState, contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState, acceptDeclineInviteState = acceptDeclineInviteState,
joinAction = joinAction, joinAction = joinAction,
knockAction = knockAction, knockAction = knockAction,
cancelKnockAction = cancelKnockAction,
applicationName = "AppName", applicationName = "AppName",
eventSink = eventSink eventSink = eventSink
) )

View file

@ -7,31 +7,50 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
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.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.libraries.designsystem.atomic.atoms.PlaceholderAtom 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.RoomPreviewDescriptionAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom 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.atoms.RoomPreviewTitleAtom
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.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.molecules.RoomPreviewMembersCountMolecule
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@ -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.avatar.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton 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.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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.Button
import io.element.android.libraries.designsystem.theme.components.ButtonSize 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.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.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@ -64,17 +85,20 @@ fun JoinRoomView(
Box( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) { ) {
var knockMessage by rememberSaveable { mutableStateOf("") }
LightGradientBackground() LightGradientBackground()
HeaderFooterPage( HeaderFooterPage(
containerColor = Color.Transparent, containerColor = Color.Transparent,
paddingValues = PaddingValues(16.dp), paddingValues = PaddingValues(16.dp),
topBar = { topBar = {
JoinRoomTopBar(onBackClick = onBackClick) JoinRoomTopBar(contentState = state.contentState, onBackClick = onBackClick)
}, },
content = { content = {
JoinRoomContent( JoinRoomContent(
contentState = state.contentState, contentState = state.contentState,
applicationName = state.applicationName, applicationName = state.applicationName,
knockMessage = knockMessage,
onKnockMessageUpdate = { knockMessage = it },
) )
}, },
footer = { footer = {
@ -90,7 +114,10 @@ fun JoinRoomView(
state.eventSink(JoinRoomEvents.JoinRoom) state.eventSink(JoinRoomEvents.JoinRoom)
}, },
onKnockRoom = { onKnockRoom = {
state.eventSink(JoinRoomEvents.KnockRoom) state.eventSink(JoinRoomEvents.KnockRoom(knockMessage))
},
onCancelKnock = {
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
}, },
onRetry = { onRetry = {
state.eventSink(JoinRoomEvents.RetryFetchingContent) state.eventSink(JoinRoomEvents.RetryFetchingContent)
@ -103,12 +130,27 @@ fun JoinRoomView(
AsyncActionView( AsyncActionView(
async = state.joinAction, async = state.joinAction,
onSuccess = { onJoinSuccess() }, onSuccess = { onJoinSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearError) }, onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
) )
AsyncActionView( AsyncActionView(
async = state.knockAction, async = state.knockAction,
onSuccess = { onKnockSuccess() }, 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,63 +161,77 @@ private fun JoinRoomFooter(
onDeclineInvite: () -> Unit, onDeclineInvite: () -> Unit,
onJoinRoom: () -> Unit, onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit, onKnockRoom: () -> Unit,
onCancelKnock: () -> Unit,
onRetry: () -> Unit, onRetry: () -> Unit,
onGoBack: () -> Unit, onGoBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.contentState is ContentState.Failure) { Box(modifier = modifier.fillMaxWidth().padding(top = 8.dp)) {
Button( if (state.contentState is ContentState.Failure) {
text = stringResource(CommonStrings.action_retry), Button(
onClick = onRetry, text = stringResource(CommonStrings.action_retry),
modifier = modifier.fillMaxWidth(), onClick = onRetry,
size = ButtonSize.Large, modifier = Modifier.fillMaxWidth(),
) size = ButtonSize.Large,
} else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) { )
Button( } else if (state.contentState is ContentState.Loaded && state.contentState.roomType == RoomType.Space) {
text = stringResource(CommonStrings.action_go_back), Button(
onClick = onGoBack, text = stringResource(CommonStrings.action_go_back),
modifier = modifier.fillMaxWidth(), onClick = onGoBack,
size = ButtonSize.Large, modifier = Modifier.fillMaxWidth(),
) size = ButtonSize.Large,
} else { )
val joinAuthorisationStatus = state.joinAuthorisationStatus } else {
when (joinAuthorisationStatus) { val joinAuthorisationStatus = state.joinAuthorisationStatus
is JoinAuthorisationStatus.IsInvited -> { when (joinAuthorisationStatus) {
ButtonRowMolecule(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(20.dp)) { 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.CanKnock -> {
SuperButton(
onClick = onKnockRoom,
modifier = Modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_knock_action),
)
}
}
JoinAuthorisationStatus.IsKnocked -> {
OutlinedButton( OutlinedButton(
text = stringResource(CommonStrings.action_decline), text = stringResource(R.string.screen_join_room_cancel_knock_action),
onClick = onDeclineInvite, onClick = onCancelKnock,
modifier = Modifier.weight(1f), modifier = Modifier.fillMaxWidth(),
size = ButtonSize.LargeLowPadding, size = ButtonSize.Large,
)
Button(
text = stringResource(CommonStrings.action_accept),
onClick = onAcceptInvite,
modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
) )
} }
JoinAuthorisationStatus.Unknown -> Unit
} }
JoinAuthorisationStatus.CanJoin -> {
SuperButton(
onClick = onJoinRoom,
modifier = modifier.fillMaxWidth(),
buttonSize = ButtonSize.Large,
) {
Text(
text = stringResource(R.string.screen_join_room_join_action),
)
}
}
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 +240,219 @@ private fun JoinRoomFooter(
private fun JoinRoomContent( private fun JoinRoomContent(
contentState: ContentState, contentState: ContentState,
applicationName: String, applicationName: String,
knockMessage: String,
onKnockMessageUpdate: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (contentState) { Box(modifier = modifier) {
is ContentState.Loaded -> { when (contentState) {
RoomPreviewOrganism( is ContentState.Loaded -> {
modifier = modifier, when (contentState.joinAuthorisationStatus) {
avatar = { is JoinAuthorisationStatus.IsKnocked -> {
Avatar(contentState.avatarData(AvatarSize.RoomHeader)) IsKnockedLoadedContent()
}, }
title = { else -> {
if (contentState.name != null) { DefaultLoadedContent(
RoomPreviewTitleAtom( modifier = Modifier.verticalScroll(rememberScrollState()),
title = contentState.name, contentState = contentState,
applicationName = applicationName,
knockMessage = knockMessage,
onKnockMessageUpdate = onKnockMessageUpdate
) )
} 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,
)
}
}
},
memberCount = {
if (contentState.showMemberCount) {
RoomPreviewMembersCountMolecule(memberCount = contentState.numberOfMembers ?: 0)
} }
} }
) }
} is ContentState.UnknownRoom -> {
is ContentState.UnknownRoom -> { RoomPreviewOrganism(
RoomPreviewOrganism( avatar = {
modifier = modifier, PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
avatar = { },
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) title = {
}, RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview))
title = { },
RoomPreviewTitleAtom(stringResource(R.string.screen_join_room_title_no_preview)) subtitle = {
}, RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview))
subtitle = { },
RoomPreviewSubtitleAtom(stringResource(R.string.screen_join_room_subtitle_no_preview)) )
}, }
) is ContentState.Loading -> {
} RoomPreviewOrganism(
is ContentState.Loading -> { avatar = {
RoomPreviewOrganism( PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
modifier = modifier, },
avatar = { title = {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) PlaceholderAtom(width = 200.dp, height = 22.dp)
}, },
title = { subtitle = {
PlaceholderAtom(width = 200.dp, height = 22.dp) PlaceholderAtom(width = 140.dp, height = 20.dp)
}, },
subtitle = { )
PlaceholderAtom(width = 140.dp, height = 20.dp) }
}, is ContentState.Failure -> {
) RoomPreviewOrganism(
} avatar = {
is ContentState.Failure -> { PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp)
RoomPreviewOrganism( },
modifier = modifier, title = {
avatar = { when (contentState.roomIdOrAlias) {
PlaceholderAtom(width = AvatarSize.RoomHeader.dp, height = AvatarSize.RoomHeader.dp) is RoomIdOrAlias.Alias -> {
}, RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier)
title = { }
when (contentState.roomIdOrAlias) { is RoomIdOrAlias.Id -> {
is RoomIdOrAlias.Alias -> { PlaceholderAtom(width = 200.dp, height = 22.dp)
RoomPreviewTitleAtom(contentState.roomIdOrAlias.identifier) }
} }
is RoomIdOrAlias.Id -> { },
PlaceholderAtom(width = 200.dp, height = 22.dp) subtitle = {
} Text(
} text = stringResource(id = CommonStrings.error_unknown),
}, textAlign = TextAlign.Center,
subtitle = { color = MaterialTheme.colorScheme.error,
Text( )
text = stringResource(id = CommonStrings.error_unknown), },
textAlign = TextAlign.Center, )
color = MaterialTheme.colorScheme.error, }
)
},
)
} }
} }
} }
@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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun JoinRoomTopBar( private fun JoinRoomTopBar(
contentState: ContentState,
onBackClick: () -> Unit, onBackClick: () -> Unit,
) { ) {
TopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
BackButton(onClick = onBackClick) 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
)
}
}
},
) )
} }

View file

@ -19,5 +19,9 @@ interface KnockRoom {
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom { class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
override suspend fun invoke(roomId: RoomId) = client.knockRoom(roomId) override suspend fun invoke(roomId: RoomId): Result<Unit> {
return client
.knockRoom(roomId)
.map { }
}
} }

View file

@ -214,7 +214,7 @@ class JoinRoomPresenterTest {
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
state.eventSink(JoinRoomEvents.ClearError) state.eventSink(JoinRoomEvents.ClearActionStates)
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized) assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)

View file

@ -79,7 +79,7 @@ class JoinRoomViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_ok) rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearError) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
} }
@Test @Test
@ -93,7 +93,7 @@ class JoinRoomViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_ok) rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearError) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
} }
@Test @Test