Skip to content

Commit 84299fc

Browse files
committed
knock requests : start implementing banner ui
1 parent eae73ac commit 84299fc

File tree

6 files changed

+335
-14
lines changed

6 files changed

+335
-14
lines changed

features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/KnockRequest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ fun KnockRequest.getAvatarData(size: AvatarSize) = AvatarData(
2929
fun KnockRequest.getBestName(): String {
3030
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
3131
}
32+
33+
fun aKnockRequest(
34+
userId: UserId = UserId("@jacob_ross:example.com"),
35+
displayName: String? = "Jacob Ross",
36+
avatarUrl: String? = null,
37+
reason: String? = "Hi, I would like to get access to this room please.",
38+
formattedDate: String = "20 Nov 2024",
39+
) = KnockRequest(
40+
userId = userId,
41+
displayName = displayName,
42+
avatarUrl = avatarUrl,
43+
reason = reason,
44+
formattedDate = formattedDate,
45+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.knockrequests.impl.banner
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.Immutable
12+
import androidx.compose.ui.res.pluralStringResource
13+
import androidx.compose.ui.res.stringResource
14+
import io.element.android.features.knockrequests.impl.KnockRequest
15+
import io.element.android.features.knockrequests.impl.getBestName
16+
import io.element.android.libraries.architecture.AsyncAction
17+
import io.element.android.libraries.ui.strings.CommonPlurals
18+
import io.element.android.libraries.ui.strings.CommonStrings
19+
import kotlinx.collections.immutable.ImmutableList
20+
21+
@Immutable
22+
sealed interface KnockRequestsBannerState {
23+
data object Hidden : KnockRequestsBannerState
24+
data class Visible(
25+
val knockRequests: ImmutableList<KnockRequest>,
26+
val acceptAction: AsyncAction<Unit>,
27+
val canAccept: Boolean,
28+
) : KnockRequestsBannerState {
29+
30+
val subtitle = if (knockRequests.size == 1) {
31+
knockRequests.first().userId.value
32+
} else {
33+
null
34+
}
35+
36+
val reason = if (knockRequests.size == 1) {
37+
knockRequests.first().reason
38+
} else {
39+
null
40+
}
41+
42+
@Composable
43+
fun formattedTitle(): String {
44+
return when (knockRequests.size) {
45+
0 -> ""
46+
1 -> stringResource(CommonStrings.screen_room_single_knock_request_title, knockRequests.first().getBestName())
47+
else -> {
48+
val firstRequest = knockRequests.first()
49+
val otherRequestsCount = knockRequests.size - 1
50+
pluralStringResource(
51+
id = CommonPlurals.screen_room_multiple_knock_requests_title,
52+
count = otherRequestsCount,
53+
firstRequest.getBestName(),
54+
otherRequestsCount
55+
)
56+
}
57+
}
58+
}
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.knockrequests.impl.banner
9+
10+
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import io.element.android.features.knockrequests.impl.KnockRequest
12+
import io.element.android.features.knockrequests.impl.aKnockRequest
13+
import io.element.android.libraries.architecture.AsyncAction
14+
import kotlinx.collections.immutable.toImmutableList
15+
16+
class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsBannerState> {
17+
override val values: Sequence<KnockRequestsBannerState>
18+
get() = sequenceOf(
19+
KnockRequestsBannerState.Hidden,
20+
aVisibleKnockRequestsBannerState(),
21+
aVisibleKnockRequestsBannerState(
22+
knockRequests = listOf(
23+
aKnockRequest(),
24+
aKnockRequest(displayName = "Alice")
25+
)
26+
),
27+
aVisibleKnockRequestsBannerState(
28+
knockRequests = listOf(
29+
aKnockRequest(),
30+
aKnockRequest(displayName = "Alice"),
31+
aKnockRequest(displayName = "Bob"),
32+
aKnockRequest(displayName = "Charlie")
33+
)
34+
),
35+
aVisibleKnockRequestsBannerState(
36+
canAccept = false
37+
),
38+
aVisibleKnockRequestsBannerState(
39+
acceptAction = AsyncAction.Loading
40+
),
41+
aVisibleKnockRequestsBannerState(
42+
acceptAction = AsyncAction.Failure(Throwable())
43+
),
44+
)
45+
}
46+
47+
fun aVisibleKnockRequestsBannerState(
48+
knockRequests: List<KnockRequest> = listOf(aKnockRequest()),
49+
acceptAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
50+
canAccept: Boolean = true,
51+
) = KnockRequestsBannerState.Visible(
52+
knockRequests = knockRequests.toImmutableList(),
53+
acceptAction = acceptAction,
54+
canAccept = canAccept
55+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2024 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only
5+
* Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
package io.element.android.features.knockrequests.impl.banner
9+
10+
import androidx.compose.foundation.background
11+
import androidx.compose.foundation.clickable
12+
import androidx.compose.foundation.layout.Arrangement
13+
import androidx.compose.foundation.layout.Box
14+
import androidx.compose.foundation.layout.Column
15+
import androidx.compose.foundation.layout.Row
16+
import androidx.compose.foundation.layout.Spacer
17+
import androidx.compose.foundation.layout.fillMaxWidth
18+
import androidx.compose.foundation.layout.height
19+
import androidx.compose.foundation.layout.padding
20+
import androidx.compose.foundation.layout.size
21+
import androidx.compose.foundation.layout.width
22+
import androidx.compose.foundation.shape.CircleShape
23+
import androidx.compose.material3.MaterialTheme
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.draw.clip
28+
import androidx.compose.ui.res.stringResource
29+
import androidx.compose.ui.text.style.TextAlign
30+
import androidx.compose.ui.tooling.preview.PreviewParameter
31+
import androidx.compose.ui.unit.dp
32+
import androidx.compose.ui.zIndex
33+
import io.element.android.compound.theme.ElementTheme
34+
import io.element.android.compound.tokens.generated.CompoundIcons
35+
import io.element.android.features.knockrequests.impl.KnockRequest
36+
import io.element.android.features.knockrequests.impl.getAvatarData
37+
import io.element.android.libraries.designsystem.components.avatar.Avatar
38+
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
39+
import io.element.android.libraries.designsystem.preview.ElementPreview
40+
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
41+
import io.element.android.libraries.designsystem.theme.components.Button
42+
import io.element.android.libraries.designsystem.theme.components.ButtonSize
43+
import io.element.android.libraries.designsystem.theme.components.Icon
44+
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
45+
import io.element.android.libraries.designsystem.theme.components.Surface
46+
import io.element.android.libraries.designsystem.theme.components.Text
47+
import io.element.android.libraries.ui.strings.CommonStrings
48+
import kotlinx.collections.immutable.ImmutableList
49+
50+
private const val MAX_AVATAR_COUNT = 3
51+
52+
@Composable
53+
fun KnockRequestsBannerView(
54+
state: KnockRequestsBannerState,
55+
onDismissClick: () -> Unit,
56+
onViewRequestsClick: () -> Unit,
57+
modifier: Modifier = Modifier,
58+
) {
59+
when (state) {
60+
is KnockRequestsBannerState.Hidden -> Unit
61+
is KnockRequestsBannerState.Visible -> VisibleKnockRequestsBannerView(
62+
state = state,
63+
onDismissClick = onDismissClick,
64+
onViewRequestsClick = onViewRequestsClick,
65+
modifier = modifier
66+
)
67+
}
68+
}
69+
70+
@Composable
71+
private fun VisibleKnockRequestsBannerView(
72+
state: KnockRequestsBannerState.Visible,
73+
onDismissClick: () -> Unit,
74+
onViewRequestsClick: () -> Unit,
75+
modifier: Modifier = Modifier,
76+
) {
77+
Surface(
78+
modifier.fillMaxWidth(),
79+
shape = MaterialTheme.shapes.small,
80+
color = ElementTheme.colors.bgCanvasDefault,
81+
shadowElevation = 24.dp
82+
) {
83+
Column(
84+
Modifier
85+
.fillMaxWidth()
86+
.padding(all = 16.dp)
87+
) {
88+
Row {
89+
KnockRequestAvatarView(state.knockRequests)
90+
Spacer(modifier = Modifier.width(10.dp))
91+
Column(modifier = Modifier.weight(1f)) {
92+
Text(
93+
text = state.formattedTitle(),
94+
style = ElementTheme.typography.fontBodyMdMedium,
95+
color = MaterialTheme.colorScheme.primary,
96+
textAlign = TextAlign.Start,
97+
)
98+
if (state.subtitle != null) {
99+
Text(
100+
text = state.subtitle,
101+
style = ElementTheme.typography.fontBodySmRegular,
102+
color = MaterialTheme.colorScheme.secondary,
103+
textAlign = TextAlign.Start,
104+
)
105+
}
106+
}
107+
Icon(
108+
modifier = Modifier.clickable(onClick = onDismissClick),
109+
imageVector = CompoundIcons.Close(),
110+
contentDescription = stringResource(CommonStrings.action_close)
111+
)
112+
}
113+
if (state.reason != null) {
114+
Spacer(modifier = Modifier.height(16.dp))
115+
Text(
116+
text = state.reason,
117+
color = ElementTheme.colors.textPrimary,
118+
style = ElementTheme.typography.fontBodyMdRegular,
119+
)
120+
}
121+
Spacer(modifier = Modifier.height(16.dp))
122+
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
123+
if (state.knockRequests.size > 1) {
124+
Button(
125+
text = "View all",
126+
onClick = onViewRequestsClick,
127+
size = ButtonSize.MediumLowPadding,
128+
modifier = Modifier.weight(1f),
129+
)
130+
} else {
131+
OutlinedButton(
132+
text = "View",
133+
onClick = onViewRequestsClick,
134+
size = ButtonSize.MediumLowPadding,
135+
modifier = Modifier.weight(1f),
136+
)
137+
if (state.canAccept) {
138+
Button(
139+
text = "Accept",
140+
onClick = {},
141+
size = ButtonSize.MediumLowPadding,
142+
modifier = Modifier.weight(1f),
143+
)
144+
}
145+
}
146+
}
147+
}
148+
}
149+
}
150+
151+
@Composable
152+
private fun KnockRequestAvatarView(
153+
knockRequests: ImmutableList<KnockRequest>,
154+
modifier: Modifier = Modifier,
155+
) {
156+
Box(modifier) {
157+
when (knockRequests.size) {
158+
0 -> Unit
159+
1 -> Avatar(knockRequests.first().getAvatarData(AvatarSize.KnockRequestBanner))
160+
else -> KnockRequestAvatarListView(knockRequests)
161+
}
162+
}
163+
}
164+
165+
@Composable
166+
private fun KnockRequestAvatarListView(
167+
knockRequests: ImmutableList<KnockRequest>,
168+
modifier: Modifier = Modifier,
169+
) {
170+
val avatarSize = AvatarSize.KnockRequestBanner.dp
171+
Row(
172+
modifier = modifier,
173+
horizontalArrangement = Arrangement.spacedBy(-avatarSize / 2),
174+
) {
175+
knockRequests
176+
.take(MAX_AVATAR_COUNT)
177+
.forEachIndexed { index, knockRequest ->
178+
Box(
179+
contentAlignment = Alignment.Center,
180+
modifier = Modifier
181+
.size(size = avatarSize)
182+
.clip(CircleShape)
183+
.background(color = ElementTheme.colors.bgCanvasDefault)
184+
.zIndex(-index.toFloat()),
185+
) {
186+
Avatar(
187+
modifier = Modifier.padding(2.dp),
188+
avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
189+
)
190+
}
191+
}
192+
}
193+
}
194+
195+
@Composable
196+
@PreviewsDayNight
197+
internal fun KnockRequestsBannerViewPreview(@PreviewParameter(KnockRequestsBannerStateProvider::class) state: KnockRequestsBannerState) = ElementPreview {
198+
KnockRequestsBannerView(
199+
state = state,
200+
onDismissClick = {},
201+
onViewRequestsClick = {},
202+
modifier = Modifier.padding(16.dp)
203+
)
204+
}

features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package io.element.android.features.knockrequests.impl.list
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
1111
import io.element.android.features.knockrequests.impl.KnockRequest
12+
import io.element.android.features.knockrequests.impl.aKnockRequest
1213
import io.element.android.libraries.architecture.AsyncAction
1314
import io.element.android.libraries.architecture.AsyncData
1415
import io.element.android.libraries.matrix.api.core.UserId
@@ -101,20 +102,6 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider<KnockReques
101102
)
102103
}
103104

104-
fun aKnockRequest(
105-
userId: UserId = UserId("@jacob_ross:example.com"),
106-
displayName: String? = "Jacob Ross",
107-
avatarUrl: String? = null,
108-
reason: String? = "Hi, I would like to get access to this room please.",
109-
formattedDate: String = "20 Nov 2024",
110-
) = KnockRequest(
111-
userId = userId,
112-
displayName = displayName,
113-
avatarUrl = avatarUrl,
114-
reason = reason,
115-
formattedDate = formattedDate,
116-
)
117-
118105
fun aKnockRequestsListState(
119106
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
120107
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,

libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ enum class AvatarSize(val dp: Dp) {
5656
Suggestion(32.dp),
5757

5858
KnockRequestItem(52.dp),
59+
KnockRequestBanner(32.dp),
5960
}

0 commit comments

Comments
 (0)