knock requests : start implementing banner ui
This commit is contained in:
@@ -29,3 +29,17 @@ fun KnockRequest.getAvatarData(size: AvatarSize) = AvatarData(
|
||||
fun KnockRequest.getBestName(): String {
|
||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
||||
}
|
||||
|
||||
fun aKnockRequest(
|
||||
userId: UserId = UserId("@jacob_ross:example.com"),
|
||||
displayName: String? = "Jacob Ross",
|
||||
avatarUrl: String? = null,
|
||||
reason: String? = "Hi, I would like to get access to this room please.",
|
||||
formattedDate: String = "20 Nov 2024",
|
||||
) = KnockRequest(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
reason = reason,
|
||||
formattedDate = formattedDate,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.banner
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
import io.element.android.features.knockrequests.impl.getBestName
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
sealed interface KnockRequestsBannerState {
|
||||
data object Hidden : KnockRequestsBannerState
|
||||
data class Visible(
|
||||
val knockRequests: ImmutableList<KnockRequest>,
|
||||
val acceptAction: AsyncAction<Unit>,
|
||||
val canAccept: Boolean,
|
||||
) : KnockRequestsBannerState {
|
||||
|
||||
val subtitle = if (knockRequests.size == 1) {
|
||||
knockRequests.first().userId.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val reason = if (knockRequests.size == 1) {
|
||||
knockRequests.first().reason
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun formattedTitle(): String {
|
||||
return when (knockRequests.size) {
|
||||
0 -> ""
|
||||
1 -> stringResource(CommonStrings.screen_room_single_knock_request_title, knockRequests.first().getBestName())
|
||||
else -> {
|
||||
val firstRequest = knockRequests.first()
|
||||
val otherRequestsCount = knockRequests.size - 1
|
||||
pluralStringResource(
|
||||
id = CommonPlurals.screen_room_multiple_knock_requests_title,
|
||||
count = otherRequestsCount,
|
||||
firstRequest.getBestName(),
|
||||
otherRequestsCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.banner
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
import io.element.android.features.knockrequests.impl.aKnockRequest
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsBannerState> {
|
||||
override val values: Sequence<KnockRequestsBannerState>
|
||||
get() = sequenceOf(
|
||||
KnockRequestsBannerState.Hidden,
|
||||
aVisibleKnockRequestsBannerState(),
|
||||
aVisibleKnockRequestsBannerState(
|
||||
knockRequests = listOf(
|
||||
aKnockRequest(),
|
||||
aKnockRequest(displayName = "Alice")
|
||||
)
|
||||
),
|
||||
aVisibleKnockRequestsBannerState(
|
||||
knockRequests = listOf(
|
||||
aKnockRequest(),
|
||||
aKnockRequest(displayName = "Alice"),
|
||||
aKnockRequest(displayName = "Bob"),
|
||||
aKnockRequest(displayName = "Charlie")
|
||||
)
|
||||
),
|
||||
aVisibleKnockRequestsBannerState(
|
||||
canAccept = false
|
||||
),
|
||||
aVisibleKnockRequestsBannerState(
|
||||
acceptAction = AsyncAction.Loading
|
||||
),
|
||||
aVisibleKnockRequestsBannerState(
|
||||
acceptAction = AsyncAction.Failure(Throwable())
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aVisibleKnockRequestsBannerState(
|
||||
knockRequests: List<KnockRequest> = listOf(aKnockRequest()),
|
||||
acceptAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
canAccept: Boolean = true,
|
||||
) = KnockRequestsBannerState.Visible(
|
||||
knockRequests = knockRequests.toImmutableList(),
|
||||
acceptAction = acceptAction,
|
||||
canAccept = canAccept
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.banner
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
import io.element.android.features.knockrequests.impl.getAvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
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.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private const val MAX_AVATAR_COUNT = 3
|
||||
|
||||
@Composable
|
||||
fun KnockRequestsBannerView(
|
||||
state: KnockRequestsBannerState,
|
||||
onDismissClick: () -> Unit,
|
||||
onViewRequestsClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (state) {
|
||||
is KnockRequestsBannerState.Hidden -> Unit
|
||||
is KnockRequestsBannerState.Visible -> VisibleKnockRequestsBannerView(
|
||||
state = state,
|
||||
onDismissClick = onDismissClick,
|
||||
onViewRequestsClick = onViewRequestsClick,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VisibleKnockRequestsBannerView(
|
||||
state: KnockRequestsBannerState.Visible,
|
||||
onDismissClick: () -> Unit,
|
||||
onViewRequestsClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = ElementTheme.colors.bgCanvasDefault,
|
||||
shadowElevation = 24.dp
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp)
|
||||
) {
|
||||
Row {
|
||||
KnockRequestAvatarView(state.knockRequests)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = state.formattedTitle(),
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
if (state.subtitle != null) {
|
||||
Text(
|
||||
text = state.subtitle,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier.clickable(onClick = onDismissClick),
|
||||
imageVector = CompoundIcons.Close(),
|
||||
contentDescription = stringResource(CommonStrings.action_close)
|
||||
)
|
||||
}
|
||||
if (state.reason != null) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = state.reason,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
if (state.knockRequests.size > 1) {
|
||||
Button(
|
||||
text = "View all",
|
||||
onClick = onViewRequestsClick,
|
||||
size = ButtonSize.MediumLowPadding,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else {
|
||||
OutlinedButton(
|
||||
text = "View",
|
||||
onClick = onViewRequestsClick,
|
||||
size = ButtonSize.MediumLowPadding,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (state.canAccept) {
|
||||
Button(
|
||||
text = "Accept",
|
||||
onClick = {},
|
||||
size = ButtonSize.MediumLowPadding,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestAvatarView(
|
||||
knockRequests: ImmutableList<KnockRequest>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier) {
|
||||
when (knockRequests.size) {
|
||||
0 -> Unit
|
||||
1 -> Avatar(knockRequests.first().getAvatarData(AvatarSize.KnockRequestBanner))
|
||||
else -> KnockRequestAvatarListView(knockRequests)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestAvatarListView(
|
||||
knockRequests: ImmutableList<KnockRequest>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val avatarSize = AvatarSize.KnockRequestBanner.dp
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(-avatarSize / 2),
|
||||
) {
|
||||
knockRequests
|
||||
.take(MAX_AVATAR_COUNT)
|
||||
.forEachIndexed { index, knockRequest ->
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(size = avatarSize)
|
||||
.clip(CircleShape)
|
||||
.background(color = ElementTheme.colors.bgCanvasDefault)
|
||||
.zIndex(-index.toFloat()),
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier.padding(2.dp),
|
||||
avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@PreviewsDayNight
|
||||
internal fun KnockRequestsBannerViewPreview(@PreviewParameter(KnockRequestsBannerStateProvider::class) state: KnockRequestsBannerState) = ElementPreview {
|
||||
KnockRequestsBannerView(
|
||||
state = state,
|
||||
onDismissClick = {},
|
||||
onViewRequestsClick = {},
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
import io.element.android.features.knockrequests.impl.aKnockRequest
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
@@ -101,20 +102,6 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider<KnockReques
|
||||
)
|
||||
}
|
||||
|
||||
fun aKnockRequest(
|
||||
userId: UserId = UserId("@jacob_ross:example.com"),
|
||||
displayName: String? = "Jacob Ross",
|
||||
avatarUrl: String? = null,
|
||||
reason: String? = "Hi, I would like to get access to this room please.",
|
||||
formattedDate: String = "20 Nov 2024",
|
||||
) = KnockRequest(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
reason = reason,
|
||||
formattedDate = formattedDate,
|
||||
)
|
||||
|
||||
fun aKnockRequestsListState(
|
||||
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
|
||||
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,
|
||||
|
||||
@@ -56,4 +56,5 @@ enum class AvatarSize(val dp: Dp) {
|
||||
Suggestion(32.dp),
|
||||
|
||||
KnockRequestItem(52.dp),
|
||||
KnockRequestBanner(32.dp),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user