Skip to content

Commit 9dac27a

Browse files
authored
Merge pull request #4005 from element-hq/feature/fga/requests_to_join_banner
feat(knock) : Knock Requests Banner UI
2 parents d1cfad4 + 364a374 commit 9dac27a

File tree

42 files changed

+581
-47
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+581
-47
lines changed

features/knockrequests/api/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
plugins {
9-
id("io.element.android-library")
9+
id("io.element.android-compose-library")
1010
}
1111

1212
android {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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.api.banner
9+
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
13+
interface KnockRequestsBannerRenderer {
14+
@Composable
15+
fun View(modifier: Modifier, onViewRequestsClick: () -> Unit)
16+
}

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,29 @@
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.ui.Modifier
12+
import com.squareup.anvil.annotations.ContributesBinding
13+
import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer
14+
import io.element.android.libraries.di.RoomScope
15+
import javax.inject.Inject
16+
17+
@ContributesBinding(RoomScope::class)
18+
class DefaultKnockRequestsBannerRenderer @Inject constructor(
19+
private val presenter: KnockRequestsBannerPresenter,
20+
) : KnockRequestsBannerRenderer {
21+
@Composable
22+
override fun View(modifier: Modifier, onViewRequestsClick: () -> Unit) {
23+
val state = presenter.present()
24+
KnockRequestsBannerView(
25+
state = state,
26+
onViewRequestsClick = onViewRequestsClick,
27+
)
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
sealed interface KnockRequestsBannerEvents {
11+
data object AcceptSingleRequest : KnockRequestsBannerEvents
12+
data object Dismiss : KnockRequestsBannerEvents
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.getValue
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.setValue
15+
import io.element.android.libraries.architecture.AsyncAction
16+
import io.element.android.libraries.architecture.Presenter
17+
import kotlinx.collections.immutable.persistentListOf
18+
import javax.inject.Inject
19+
20+
class KnockRequestsBannerPresenter @Inject constructor() : Presenter<KnockRequestsBannerState> {
21+
@Composable
22+
override fun present(): KnockRequestsBannerState {
23+
var shouldShowBanner by remember { mutableStateOf(false) }
24+
25+
fun handleEvents(event: KnockRequestsBannerEvents) {
26+
when (event) {
27+
is KnockRequestsBannerEvents.AcceptSingleRequest -> Unit
28+
is KnockRequestsBannerEvents.Dismiss -> {
29+
shouldShowBanner = false
30+
}
31+
}
32+
}
33+
34+
return KnockRequestsBannerState(
35+
knockRequests = persistentListOf(),
36+
acceptAction = AsyncAction.Uninitialized,
37+
canAccept = false,
38+
isVisible = shouldShowBanner,
39+
eventSink = ::handleEvents,
40+
)
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.ui.res.pluralStringResource
12+
import androidx.compose.ui.res.stringResource
13+
import io.element.android.features.knockrequests.impl.KnockRequest
14+
import io.element.android.features.knockrequests.impl.R
15+
import io.element.android.features.knockrequests.impl.getBestName
16+
import io.element.android.libraries.architecture.AsyncAction
17+
import io.element.android.libraries.core.extensions.firstIfSingle
18+
import kotlinx.collections.immutable.ImmutableList
19+
20+
data class KnockRequestsBannerState(
21+
val isVisible: Boolean,
22+
val knockRequests: ImmutableList<KnockRequest>,
23+
val acceptAction: AsyncAction<Unit>,
24+
val canAccept: Boolean,
25+
val eventSink: (KnockRequestsBannerEvents) -> Unit,
26+
) {
27+
val subtitle = knockRequests.firstIfSingle()?.userId?.value
28+
val reason = knockRequests.firstIfSingle()?.reason
29+
30+
@Composable
31+
fun formattedTitle(): String {
32+
return when (knockRequests.size) {
33+
0 -> ""
34+
1 -> stringResource(R.string.screen_room_single_knock_request_title, knockRequests.first().getBestName())
35+
else -> {
36+
val firstRequest = knockRequests.first()
37+
val otherRequestsCount = knockRequests.size - 1
38+
pluralStringResource(
39+
id = R.plurals.screen_room_multiple_knock_requests_title,
40+
count = otherRequestsCount,
41+
firstRequest.getBestName(),
42+
otherRequestsCount
43+
)
44+
}
45+
}
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
aKnockRequestsBannerState(),
20+
aKnockRequestsBannerState(
21+
knockRequests = listOf(
22+
aKnockRequest(
23+
reason = "A very long reason that should probably be truncated, " +
24+
"but could be also expanded so you can see it over the lines, wow," +
25+
"very amazing reason, I know, right, I'm so good at writing reasons."
26+
)
27+
)
28+
),
29+
aKnockRequestsBannerState(
30+
knockRequests = listOf(
31+
aKnockRequest(),
32+
aKnockRequest(displayName = "Alice")
33+
)
34+
),
35+
aKnockRequestsBannerState(
36+
knockRequests = listOf(
37+
aKnockRequest(),
38+
aKnockRequest(displayName = "Alice"),
39+
aKnockRequest(displayName = "Bob"),
40+
aKnockRequest(displayName = "Charlie")
41+
)
42+
),
43+
aKnockRequestsBannerState(
44+
canAccept = false
45+
),
46+
aKnockRequestsBannerState(
47+
acceptAction = AsyncAction.Loading
48+
),
49+
aKnockRequestsBannerState(
50+
acceptAction = AsyncAction.Failure(Throwable("Failed to accept knock"))
51+
),
52+
)
53+
}
54+
55+
fun aKnockRequestsBannerState(
56+
knockRequests: List<KnockRequest> = listOf(aKnockRequest()),
57+
acceptAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
58+
canAccept: Boolean = true,
59+
isVisible: Boolean = true,
60+
eventSink: (KnockRequestsBannerEvents) -> Unit = {}
61+
) = KnockRequestsBannerState(
62+
knockRequests = knockRequests.toImmutableList(),
63+
acceptAction = acceptAction,
64+
canAccept = canAccept,
65+
isVisible = isVisible,
66+
eventSink = eventSink,
67+
)

0 commit comments

Comments
 (0)