Skip to content

Commit dfb5362

Browse files
committed
knock requests : branch the api in presenters
1 parent e9a1c30 commit dfb5362

File tree

19 files changed

+555
-207
lines changed

19 files changed

+555
-207
lines changed

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

Lines changed: 0 additions & 45 deletions
This file was deleted.

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

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,89 @@
88
package io.element.android.features.knockrequests.impl.banner
99

1010
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.MutableState
12+
import androidx.compose.runtime.collectAsState
13+
import androidx.compose.runtime.derivedStateOf
1114
import androidx.compose.runtime.getValue
1215
import androidx.compose.runtime.mutableStateOf
1316
import androidx.compose.runtime.remember
14-
import androidx.compose.runtime.setValue
15-
import io.element.android.libraries.architecture.AsyncAction
17+
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
18+
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
1619
import io.element.android.libraries.architecture.Presenter
17-
import kotlinx.collections.immutable.persistentListOf
20+
import io.element.android.libraries.core.coroutine.mapState
21+
import io.element.android.libraries.core.extensions.firstIfSingle
22+
import io.element.android.libraries.matrix.api.room.MatrixRoom
23+
import io.element.android.libraries.matrix.ui.room.canInviteAsState
24+
import kotlinx.collections.immutable.toImmutableList
25+
import kotlinx.coroutines.CoroutineScope
26+
import kotlinx.coroutines.delay
27+
import kotlinx.coroutines.launch
1828
import javax.inject.Inject
1929

20-
class KnockRequestsBannerPresenter @Inject constructor() : Presenter<KnockRequestsBannerState> {
30+
private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L
31+
32+
class KnockRequestsBannerPresenter @Inject constructor(
33+
private val room: MatrixRoom,
34+
private val knockRequestsService: KnockRequestsService,
35+
private val appCoroutineScope: CoroutineScope,
36+
) : Presenter<KnockRequestsBannerState> {
2137
@Composable
2238
override fun present(): KnockRequestsBannerState {
23-
var shouldShowBanner by remember { mutableStateOf(false) }
39+
val knockRequests by remember {
40+
knockRequestsService.knockRequestsFlow.mapState { knockRequests ->
41+
knockRequests.dataOrNull().orEmpty()
42+
.filter { !it.isSeen }
43+
.toImmutableList()
44+
}
45+
}.collectAsState()
46+
47+
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
48+
val canAccept by room.canInviteAsState(syncUpdateFlow.value)
49+
val showAcceptError = remember { mutableStateOf(false) }
50+
51+
val shouldShowBanner by remember {
52+
derivedStateOf {
53+
knockRequests.isNotEmpty()
54+
}
55+
}
2456

2557
fun handleEvents(event: KnockRequestsBannerEvents) {
2658
when (event) {
27-
is KnockRequestsBannerEvents.AcceptSingleRequest -> Unit
59+
is KnockRequestsBannerEvents.AcceptSingleRequest -> {
60+
appCoroutineScope.acceptSingleKnockRequest(
61+
knockRequests = knockRequests,
62+
displayAcceptError = showAcceptError,
63+
)
64+
}
2865
is KnockRequestsBannerEvents.Dismiss -> {
29-
shouldShowBanner = false
66+
appCoroutineScope.launch {
67+
knockRequestsService.markAllKnockRequestsAsSeen()
68+
}
3069
}
3170
}
3271
}
3372

3473
return KnockRequestsBannerState(
35-
knockRequests = persistentListOf(),
36-
acceptAction = AsyncAction.Uninitialized,
37-
canAccept = false,
74+
knockRequests = knockRequests,
75+
displayAcceptError = showAcceptError.value,
76+
canAccept = canAccept,
3877
isVisible = shouldShowBanner,
3978
eventSink = ::handleEvents,
4079
)
4180
}
81+
82+
private fun CoroutineScope.acceptSingleKnockRequest(
83+
knockRequests: List<KnockRequestPresentable>,
84+
displayAcceptError: MutableState<Boolean>,
85+
) = launch {
86+
val knockRequest = knockRequests.firstIfSingle()
87+
if (knockRequest != null) {
88+
knockRequestsService.acceptKnockRequest(knockRequest, optimistic = true)
89+
.onFailure {
90+
displayAcceptError.value = true
91+
delay(ACCEPT_ERROR_DISPLAY_DURATION)
92+
displayAcceptError.value = false
93+
}
94+
}
95+
}
4296
}

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,15 @@ package io.element.android.features.knockrequests.impl.banner
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.ui.res.pluralStringResource
1212
import androidx.compose.ui.res.stringResource
13-
import io.element.android.features.knockrequests.impl.KnockRequest
13+
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
1414
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
1715
import io.element.android.libraries.core.extensions.firstIfSingle
1816
import kotlinx.collections.immutable.ImmutableList
1917

2018
data class KnockRequestsBannerState(
2119
val isVisible: Boolean,
22-
val knockRequests: ImmutableList<KnockRequest>,
23-
val acceptAction: AsyncAction<Unit>,
20+
val knockRequests: ImmutableList<KnockRequestPresentable>,
21+
val displayAcceptError: Boolean,
2422
val canAccept: Boolean,
2523
val eventSink: (KnockRequestsBannerEvents) -> Unit,
2624
) {

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
package io.element.android.features.knockrequests.impl.banner
99

1010
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
11+
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
12+
import io.element.android.features.knockrequests.impl.data.aKnockRequest
1413
import kotlinx.collections.immutable.toImmutableList
1514

1615
class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsBannerState> {
@@ -44,10 +43,7 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsB
4443
canAccept = false
4544
),
4645
aKnockRequestsBannerState(
47-
acceptAction = AsyncAction.Loading
48-
),
49-
aKnockRequestsBannerState(
50-
acceptAction = AsyncAction.Failure(Throwable("Failed to accept knock"))
46+
displayAcceptError = true
5147
),
5248
aKnockRequestsBannerState(
5349
knockRequests = listOf(
@@ -60,14 +56,14 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsB
6056
}
6157

6258
fun aKnockRequestsBannerState(
63-
knockRequests: List<KnockRequest> = listOf(aKnockRequest()),
64-
acceptAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
59+
knockRequests: List<KnockRequestPresentable> = listOf(aKnockRequest()),
60+
displayAcceptError: Boolean = false,
6561
canAccept: Boolean = true,
6662
isVisible: Boolean = true,
6763
eventSink: (KnockRequestsBannerEvents) -> Unit = {}
6864
) = KnockRequestsBannerState(
6965
knockRequests = knockRequests.toImmutableList(),
70-
acceptAction = acceptAction,
66+
displayAcceptError = displayAcceptError,
7167
canAccept = canAccept,
7268
isVisible = isVisible,
7369
eventSink = eventSink,

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

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
2020
import androidx.compose.foundation.layout.height
2121
import androidx.compose.foundation.layout.padding
2222
import androidx.compose.foundation.layout.size
23+
import androidx.compose.foundation.layout.statusBarsPadding
2324
import androidx.compose.foundation.layout.width
2425
import androidx.compose.material3.MaterialTheme
2526
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.LaunchedEffect
28+
import androidx.compose.runtime.SideEffect
2629
import androidx.compose.ui.Modifier
2730
import androidx.compose.ui.draw.drawWithContent
2831
import androidx.compose.ui.geometry.Offset
@@ -37,9 +40,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
3740
import androidx.compose.ui.unit.dp
3841
import io.element.android.compound.theme.ElementTheme
3942
import io.element.android.compound.tokens.generated.CompoundIcons
40-
import io.element.android.features.knockrequests.impl.KnockRequest
4143
import io.element.android.features.knockrequests.impl.R
42-
import io.element.android.features.knockrequests.impl.getAvatarData
44+
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
45+
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
46+
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
47+
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
4348
import io.element.android.libraries.designsystem.components.avatar.Avatar
4449
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
4550
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -52,6 +57,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface
5257
import io.element.android.libraries.designsystem.theme.components.Text
5358
import io.element.android.libraries.ui.strings.CommonStrings
5459
import kotlinx.collections.immutable.ImmutableList
60+
import timber.log.Timber
5561

5662
private const val MAX_AVATAR_COUNT = 3
5763

@@ -61,22 +67,42 @@ fun KnockRequestsBannerView(
6167
onViewRequestsClick: () -> Unit,
6268
modifier: Modifier = Modifier,
6369
) {
64-
AnimatedVisibility(
65-
visible = state.isVisible,
66-
enter = expandVertically(),
67-
exit = shrinkVertically(),
68-
modifier = modifier,
69-
) {
70-
Surface(
71-
shape = MaterialTheme.shapes.small,
72-
color = ElementTheme.colors.bgCanvasDefaultLevel1,
73-
shadowElevation = 24.dp,
74-
modifier = Modifier.padding(16.dp),
70+
Box(modifier = modifier) {
71+
AnimatedVisibility(
72+
visible = state.isVisible,
73+
enter = expandVertically(),
74+
exit = shrinkVertically(),
7575
) {
76-
KnockRequestsBannerContent(
77-
state = state,
78-
onViewRequestsClick = onViewRequestsClick,
79-
)
76+
Surface(
77+
shape = MaterialTheme.shapes.small,
78+
color = ElementTheme.colors.bgCanvasDefaultLevel1,
79+
shadowElevation = 24.dp,
80+
modifier = Modifier.padding(16.dp),
81+
) {
82+
KnockRequestsBannerContent(
83+
state = state,
84+
onViewRequestsClick = onViewRequestsClick,
85+
)
86+
}
87+
}
88+
KnockRequestsAcceptErrorView(displayError = state.displayAcceptError)
89+
}
90+
}
91+
92+
@Composable
93+
private fun KnockRequestsAcceptErrorView(
94+
displayError: Boolean,
95+
modifier: Modifier = Modifier,
96+
) {
97+
val asyncIndicatorState = rememberAsyncIndicatorState()
98+
AsyncIndicatorHost(modifier = modifier.statusBarsPadding(), state = asyncIndicatorState)
99+
LaunchedEffect(displayError) {
100+
if (displayError) {
101+
asyncIndicatorState.enqueue {
102+
AsyncIndicator.Custom(text = stringResource(CommonStrings.error_unknown))
103+
}
104+
} else {
105+
asyncIndicatorState.clear()
80106
}
81107
}
82108
}
@@ -96,9 +122,9 @@ private fun KnockRequestsBannerContent(
96122
}
97123

98124
Column(
99-
modifier
100-
.fillMaxWidth()
101-
.padding(all = 16.dp)
125+
modifier
126+
.fillMaxWidth()
127+
.padding(all = 16.dp)
102128
) {
103129
Row {
104130
KnockRequestAvatarView(
@@ -122,13 +148,15 @@ private fun KnockRequestsBannerContent(
122148
)
123149
}
124150
}
151+
Spacer(modifier = Modifier.width(4.dp))
125152
Icon(
126153
modifier = Modifier.clickable(onClick = ::onDismissClick),
127154
imageVector = CompoundIcons.Close(),
128155
contentDescription = stringResource(CommonStrings.action_close)
129156
)
130157
}
131-
if (state.reason != null) {
158+
val reason = state.reason
159+
if (!reason.isNullOrEmpty()) {
132160
Spacer(modifier = Modifier.height(16.dp))
133161
Text(
134162
text = state.reason,
@@ -169,7 +197,7 @@ private fun KnockRequestsBannerContent(
169197

170198
@Composable
171199
private fun KnockRequestAvatarView(
172-
knockRequests: ImmutableList<KnockRequest>,
200+
knockRequests: ImmutableList<KnockRequestPresentable>,
173201
modifier: Modifier = Modifier,
174202
) {
175203
Box(modifier) {
@@ -183,7 +211,7 @@ private fun KnockRequestAvatarView(
183211

184212
@Composable
185213
private fun KnockRequestAvatarListView(
186-
knockRequests: ImmutableList<KnockRequest>,
214+
knockRequests: ImmutableList<KnockRequestPresentable>,
187215
modifier: Modifier = Modifier,
188216
) {
189217
val avatarSize = AvatarSize.KnockRequestBanner.dp
@@ -198,27 +226,27 @@ private fun KnockRequestAvatarListView(
198226
smallReversedList.forEachIndexed { index, knockRequest ->
199227
Avatar(
200228
modifier = Modifier
201-
.padding(start = avatarSize / 2 * (lastItemIndex - index))
202-
.graphicsLayer {
203-
compositingStrategy = CompositingStrategy.Offscreen
204-
}
205-
.drawWithContent {
206-
// Draw content and clear the pixels for the avatar on the left.
207-
drawContent()
208-
if (index < lastItemIndex) {
209-
drawCircle(
210-
color = Color.Black,
211-
center = Offset(
212-
x = 0f,
213-
y = size.height / 2,
214-
),
215-
radius = avatarSize.toPx() / 2,
216-
blendMode = BlendMode.Clear,
217-
)
229+
.padding(start = avatarSize / 2 * (lastItemIndex - index))
230+
.graphicsLayer {
231+
compositingStrategy = CompositingStrategy.Offscreen
232+
}
233+
.drawWithContent {
234+
// Draw content and clear the pixels for the avatar on the left.
235+
drawContent()
236+
if (index < lastItemIndex) {
237+
drawCircle(
238+
color = Color.Black,
239+
center = Offset(
240+
x = 0f,
241+
y = size.height / 2,
242+
),
243+
radius = avatarSize.toPx() / 2,
244+
blendMode = BlendMode.Clear,
245+
)
246+
}
218247
}
219-
}
220-
.size(size = avatarSize)
221-
.padding(2.dp),
248+
.size(size = avatarSize)
249+
.padding(2.dp),
222250
avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
223251
)
224252
}

0 commit comments

Comments
 (0)