knock requests : start implementing banner ui
This commit is contained in:
parent
41ac352060
commit
b149acff26
6 changed files with 335 additions and 14 deletions
|
|
@ -29,3 +29,17 @@ fun KnockRequest.getAvatarData(size: AvatarSize) = AvatarData(
|
||||||
fun KnockRequest.getBestName(): String {
|
fun KnockRequest.getBestName(): String {
|
||||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
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 androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
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.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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(
|
fun aKnockRequestsListState(
|
||||||
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
|
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
|
||||||
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,
|
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,
|
||||||
|
|
|
||||||
|
|
@ -56,4 +56,5 @@ enum class AvatarSize(val dp: Dp) {
|
||||||
Suggestion(32.dp),
|
Suggestion(32.dp),
|
||||||
|
|
||||||
KnockRequestItem(52.dp),
|
KnockRequestItem(52.dp),
|
||||||
|
KnockRequestBanner(32.dp),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue