knock requests : branch the api in presenters
This commit is contained in:
parent
741ae35aca
commit
8a73a9c158
19 changed files with 555 additions and 207 deletions
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
|
|
||||||
data class KnockRequest(
|
|
||||||
val userId: UserId,
|
|
||||||
val displayName: String?,
|
|
||||||
val avatarUrl: String?,
|
|
||||||
val reason: String?,
|
|
||||||
val formattedDate: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun KnockRequest.getAvatarData(size: AvatarSize) = AvatarData(
|
|
||||||
id = userId.value,
|
|
||||||
name = displayName,
|
|
||||||
url = avatarUrl,
|
|
||||||
size = size,
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
@ -8,35 +8,89 @@
|
||||||
package io.element.android.features.knockrequests.impl.banner
|
package io.element.android.features.knockrequests.impl.banner
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import io.element.android.libraries.core.coroutine.mapState
|
||||||
|
import io.element.android.libraries.core.extensions.firstIfSingle
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.ui.room.canInviteAsState
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KnockRequestsBannerPresenter @Inject constructor() : Presenter<KnockRequestsBannerState> {
|
private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L
|
||||||
|
|
||||||
|
class KnockRequestsBannerPresenter @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
private val knockRequestsService: KnockRequestsService,
|
||||||
|
private val appCoroutineScope: CoroutineScope,
|
||||||
|
) : Presenter<KnockRequestsBannerState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): KnockRequestsBannerState {
|
override fun present(): KnockRequestsBannerState {
|
||||||
var shouldShowBanner by remember { mutableStateOf(false) }
|
val knockRequests by remember {
|
||||||
|
knockRequestsService.knockRequestsFlow.mapState { knockRequests ->
|
||||||
|
knockRequests.dataOrNull().orEmpty()
|
||||||
|
.filter { !it.isSeen }
|
||||||
|
.toImmutableList()
|
||||||
|
}
|
||||||
|
}.collectAsState()
|
||||||
|
|
||||||
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
val canAccept by room.canInviteAsState(syncUpdateFlow.value)
|
||||||
|
val showAcceptError = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val shouldShowBanner by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
knockRequests.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(event: KnockRequestsBannerEvents) {
|
fun handleEvents(event: KnockRequestsBannerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is KnockRequestsBannerEvents.AcceptSingleRequest -> Unit
|
is KnockRequestsBannerEvents.AcceptSingleRequest -> {
|
||||||
|
appCoroutineScope.acceptSingleKnockRequest(
|
||||||
|
knockRequests = knockRequests,
|
||||||
|
displayAcceptError = showAcceptError,
|
||||||
|
)
|
||||||
|
}
|
||||||
is KnockRequestsBannerEvents.Dismiss -> {
|
is KnockRequestsBannerEvents.Dismiss -> {
|
||||||
shouldShowBanner = false
|
appCoroutineScope.launch {
|
||||||
|
knockRequestsService.markAllKnockRequestsAsSeen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return KnockRequestsBannerState(
|
return KnockRequestsBannerState(
|
||||||
knockRequests = persistentListOf(),
|
knockRequests = knockRequests,
|
||||||
acceptAction = AsyncAction.Uninitialized,
|
displayAcceptError = showAcceptError.value,
|
||||||
canAccept = false,
|
canAccept = canAccept,
|
||||||
isVisible = shouldShowBanner,
|
isVisible = shouldShowBanner,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.acceptSingleKnockRequest(
|
||||||
|
knockRequests: List<KnockRequestPresentable>,
|
||||||
|
displayAcceptError: MutableState<Boolean>,
|
||||||
|
) = launch {
|
||||||
|
val knockRequest = knockRequests.firstIfSingle()
|
||||||
|
if (knockRequest != null) {
|
||||||
|
knockRequestsService.acceptKnockRequest(knockRequest, optimistic = true)
|
||||||
|
.onFailure {
|
||||||
|
displayAcceptError.value = true
|
||||||
|
delay(ACCEPT_ERROR_DISPLAY_DURATION)
|
||||||
|
displayAcceptError.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,15 @@ package io.element.android.features.knockrequests.impl.banner
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
|
||||||
import io.element.android.features.knockrequests.impl.R
|
import io.element.android.features.knockrequests.impl.R
|
||||||
import io.element.android.features.knockrequests.impl.getBestName
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.core.extensions.firstIfSingle
|
import io.element.android.libraries.core.extensions.firstIfSingle
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class KnockRequestsBannerState(
|
data class KnockRequestsBannerState(
|
||||||
val isVisible: Boolean,
|
val isVisible: Boolean,
|
||||||
val knockRequests: ImmutableList<KnockRequest>,
|
val knockRequests: ImmutableList<KnockRequestPresentable>,
|
||||||
val acceptAction: AsyncAction<Unit>,
|
val displayAcceptError: Boolean,
|
||||||
val canAccept: Boolean,
|
val canAccept: Boolean,
|
||||||
val eventSink: (KnockRequestsBannerEvents) -> Unit,
|
val eventSink: (KnockRequestsBannerEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,8 @@
|
||||||
package io.element.android.features.knockrequests.impl.banner
|
package io.element.android.features.knockrequests.impl.banner
|
||||||
|
|
||||||
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.data.KnockRequestPresentable
|
||||||
import io.element.android.features.knockrequests.impl.aKnockRequest
|
import io.element.android.features.knockrequests.impl.data.aKnockRequest
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsBannerState> {
|
class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsBannerState> {
|
||||||
|
|
@ -44,10 +43,7 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsB
|
||||||
canAccept = false
|
canAccept = false
|
||||||
),
|
),
|
||||||
aKnockRequestsBannerState(
|
aKnockRequestsBannerState(
|
||||||
acceptAction = AsyncAction.Loading
|
displayAcceptError = true
|
||||||
),
|
|
||||||
aKnockRequestsBannerState(
|
|
||||||
acceptAction = AsyncAction.Failure(Throwable("Failed to accept knock"))
|
|
||||||
),
|
),
|
||||||
aKnockRequestsBannerState(
|
aKnockRequestsBannerState(
|
||||||
knockRequests = listOf(
|
knockRequests = listOf(
|
||||||
|
|
@ -60,14 +56,14 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsB
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aKnockRequestsBannerState(
|
fun aKnockRequestsBannerState(
|
||||||
knockRequests: List<KnockRequest> = listOf(aKnockRequest()),
|
knockRequests: List<KnockRequestPresentable> = listOf(aKnockRequest()),
|
||||||
acceptAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
displayAcceptError: Boolean = false,
|
||||||
canAccept: Boolean = true,
|
canAccept: Boolean = true,
|
||||||
isVisible: Boolean = true,
|
isVisible: Boolean = true,
|
||||||
eventSink: (KnockRequestsBannerEvents) -> Unit = {}
|
eventSink: (KnockRequestsBannerEvents) -> Unit = {}
|
||||||
) = KnockRequestsBannerState(
|
) = KnockRequestsBannerState(
|
||||||
knockRequests = knockRequests.toImmutableList(),
|
knockRequests = knockRequests.toImmutableList(),
|
||||||
acceptAction = acceptAction,
|
displayAcceptError = displayAcceptError,
|
||||||
canAccept = canAccept,
|
canAccept = canAccept,
|
||||||
isVisible = isVisible,
|
isVisible = isVisible,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,12 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
|
@ -37,9 +40,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
|
||||||
import io.element.android.features.knockrequests.impl.R
|
import io.element.android.features.knockrequests.impl.R
|
||||||
import io.element.android.features.knockrequests.impl.getAvatarData
|
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncIndicatorHost
|
||||||
|
import io.element.android.libraries.designsystem.components.async.rememberAsyncIndicatorState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
|
@ -52,6 +57,7 @@ import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
private const val MAX_AVATAR_COUNT = 3
|
private const val MAX_AVATAR_COUNT = 3
|
||||||
|
|
||||||
|
|
@ -61,22 +67,42 @@ fun KnockRequestsBannerView(
|
||||||
onViewRequestsClick: () -> Unit,
|
onViewRequestsClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
Box(modifier = modifier) {
|
||||||
visible = state.isVisible,
|
AnimatedVisibility(
|
||||||
enter = expandVertically(),
|
visible = state.isVisible,
|
||||||
exit = shrinkVertically(),
|
enter = expandVertically(),
|
||||||
modifier = modifier,
|
exit = shrinkVertically(),
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.small,
|
|
||||||
color = ElementTheme.colors.bgCanvasDefaultLevel1,
|
|
||||||
shadowElevation = 24.dp,
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
) {
|
) {
|
||||||
KnockRequestsBannerContent(
|
Surface(
|
||||||
state = state,
|
shape = MaterialTheme.shapes.small,
|
||||||
onViewRequestsClick = onViewRequestsClick,
|
color = ElementTheme.colors.bgCanvasDefaultLevel1,
|
||||||
)
|
shadowElevation = 24.dp,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
KnockRequestsBannerContent(
|
||||||
|
state = state,
|
||||||
|
onViewRequestsClick = onViewRequestsClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KnockRequestsAcceptErrorView(displayError = state.displayAcceptError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KnockRequestsAcceptErrorView(
|
||||||
|
displayError: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val asyncIndicatorState = rememberAsyncIndicatorState()
|
||||||
|
AsyncIndicatorHost(modifier = modifier.statusBarsPadding(), state = asyncIndicatorState)
|
||||||
|
LaunchedEffect(displayError) {
|
||||||
|
if (displayError) {
|
||||||
|
asyncIndicatorState.enqueue {
|
||||||
|
AsyncIndicator.Custom(text = stringResource(CommonStrings.error_unknown))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
asyncIndicatorState.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,9 +122,9 @@ private fun KnockRequestsBannerContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(all = 16.dp)
|
.padding(all = 16.dp)
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
KnockRequestAvatarView(
|
KnockRequestAvatarView(
|
||||||
|
|
@ -122,13 +148,15 @@ private fun KnockRequestsBannerContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.clickable(onClick = ::onDismissClick),
|
modifier = Modifier.clickable(onClick = ::onDismissClick),
|
||||||
imageVector = CompoundIcons.Close(),
|
imageVector = CompoundIcons.Close(),
|
||||||
contentDescription = stringResource(CommonStrings.action_close)
|
contentDescription = stringResource(CommonStrings.action_close)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.reason != null) {
|
val reason = state.reason
|
||||||
|
if (!reason.isNullOrEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = state.reason,
|
text = state.reason,
|
||||||
|
|
@ -169,7 +197,7 @@ private fun KnockRequestsBannerContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun KnockRequestAvatarView(
|
private fun KnockRequestAvatarView(
|
||||||
knockRequests: ImmutableList<KnockRequest>,
|
knockRequests: ImmutableList<KnockRequestPresentable>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
|
|
@ -183,7 +211,7 @@ private fun KnockRequestAvatarView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun KnockRequestAvatarListView(
|
private fun KnockRequestAvatarListView(
|
||||||
knockRequests: ImmutableList<KnockRequest>,
|
knockRequests: ImmutableList<KnockRequestPresentable>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val avatarSize = AvatarSize.KnockRequestBanner.dp
|
val avatarSize = AvatarSize.KnockRequestBanner.dp
|
||||||
|
|
@ -198,27 +226,27 @@ private fun KnockRequestAvatarListView(
|
||||||
smallReversedList.forEachIndexed { index, knockRequest ->
|
smallReversedList.forEachIndexed { index, knockRequest ->
|
||||||
Avatar(
|
Avatar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = avatarSize / 2 * (lastItemIndex - index))
|
.padding(start = avatarSize / 2 * (lastItemIndex - index))
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
compositingStrategy = CompositingStrategy.Offscreen
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
}
|
|
||||||
.drawWithContent {
|
|
||||||
// Draw content and clear the pixels for the avatar on the left.
|
|
||||||
drawContent()
|
|
||||||
if (index < lastItemIndex) {
|
|
||||||
drawCircle(
|
|
||||||
color = Color.Black,
|
|
||||||
center = Offset(
|
|
||||||
x = 0f,
|
|
||||||
y = size.height / 2,
|
|
||||||
),
|
|
||||||
radius = avatarSize.toPx() / 2,
|
|
||||||
blendMode = BlendMode.Clear,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
.drawWithContent {
|
||||||
.size(size = avatarSize)
|
// Draw content and clear the pixels for the avatar on the left.
|
||||||
.padding(2.dp),
|
drawContent()
|
||||||
|
if (index < lastItemIndex) {
|
||||||
|
drawCircle(
|
||||||
|
color = Color.Black,
|
||||||
|
center = Offset(
|
||||||
|
x = 0f,
|
||||||
|
y = size.height / 2,
|
||||||
|
),
|
||||||
|
radius = avatarSize.toPx() / 2,
|
||||||
|
blendMode = BlendMode.Clear,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.size(size = avatarSize)
|
||||||
|
.padding(2.dp),
|
||||||
avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
|
avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.data
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
fun aKnockRequest(
|
||||||
|
eventId: EventId = EventId("\$eventId"),
|
||||||
|
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",
|
||||||
|
) = object : KnockRequestPresentable {
|
||||||
|
override val eventId: EventId = eventId
|
||||||
|
override val userId: UserId = userId
|
||||||
|
override val displayName: String? = displayName
|
||||||
|
override val avatarUrl: String? = avatarUrl
|
||||||
|
override val reason: String? = reason
|
||||||
|
override val formattedDate: String? = formattedDate
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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.data
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
interface KnockRequestPresentable {
|
||||||
|
val eventId: EventId
|
||||||
|
val userId: UserId
|
||||||
|
val displayName: String?
|
||||||
|
val avatarUrl: String?
|
||||||
|
val reason: String?
|
||||||
|
val formattedDate: String?
|
||||||
|
|
||||||
|
fun getAvatarData(size: AvatarSize) = AvatarData(
|
||||||
|
id = userId.value,
|
||||||
|
name = displayName,
|
||||||
|
url = avatarUrl,
|
||||||
|
size = size,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getBestName(): String {
|
||||||
|
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.data
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
|
|
||||||
|
class KnockRequestWrapper(
|
||||||
|
private val knockRequest: KnockRequest,
|
||||||
|
dateFormatter: (Long?) -> String? = { null }
|
||||||
|
) : KnockRequestPresentable {
|
||||||
|
override val eventId: EventId = knockRequest.eventId
|
||||||
|
override val userId: UserId = knockRequest.userId
|
||||||
|
override val displayName: String? = knockRequest.displayName
|
||||||
|
override val avatarUrl: String? = knockRequest.avatarUrl
|
||||||
|
override val reason: String? = knockRequest.reason?.trim()
|
||||||
|
override val formattedDate: String? = dateFormatter(knockRequest.timestamp)
|
||||||
|
|
||||||
|
val isSeen: Boolean = knockRequest.isSeen
|
||||||
|
|
||||||
|
suspend fun accept(): Result<Unit> = knockRequest.accept()
|
||||||
|
|
||||||
|
suspend fun decline(reason: String?): Result<Unit> = knockRequest.decline(reason)
|
||||||
|
|
||||||
|
suspend fun declineAndBan(reason: String?): Result<Unit> = knockRequest.declineAndBan(reason)
|
||||||
|
|
||||||
|
suspend fun markAsSeen(): Result<Unit> = knockRequest.markAsSeen()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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.data
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
class KnockRequestsService @Inject constructor(room: MatrixRoom) {
|
||||||
|
|
||||||
|
// Keep track of the knock requests that have been handled, so we don't have to wait for sync to remove them.
|
||||||
|
private val handledKnockRequestIds = MutableStateFlow<Set<EventId>>(emptySet())
|
||||||
|
|
||||||
|
val knockRequestsFlow = combine(
|
||||||
|
room.wrappedKnockRequestsFlow(),
|
||||||
|
handledKnockRequestIds,
|
||||||
|
) { knockRequests, handledKnockIds ->
|
||||||
|
val presentableKnockRequests = knockRequests
|
||||||
|
.filter { it.eventId !in handledKnockIds }
|
||||||
|
.toImmutableList()
|
||||||
|
AsyncData.Success(presentableKnockRequests)
|
||||||
|
}.stateIn(room.roomCoroutineScope, SharingStarted.Lazily, AsyncData.Loading())
|
||||||
|
|
||||||
|
private fun knockRequestsList() = knockRequestsFlow.value.dataOrNull().orEmpty()
|
||||||
|
|
||||||
|
private fun getKnockRequestById(eventId: EventId): KnockRequestWrapper? {
|
||||||
|
return knockRequestsList().find { it.eventId == eventId }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a knock request.
|
||||||
|
* @param knockRequest The knock request to accept.
|
||||||
|
* @param optimistic If true, the request will be marked as handled before the server responds.
|
||||||
|
*/
|
||||||
|
suspend fun acceptKnockRequest(knockRequest: KnockRequestPresentable, optimistic: Boolean = false): Result<Unit> {
|
||||||
|
val wrapped = getKnockRequestById(knockRequest.eventId) ?: return knockRequestNotFoundResult()
|
||||||
|
return handleKnockRequest(wrapped, optimistic) { accept() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decline a knock request.
|
||||||
|
* @param knockRequest The knock request to decline.
|
||||||
|
* @param optimistic If true, the request will be marked as handled before the server responds.
|
||||||
|
*/
|
||||||
|
suspend fun declineKnockRequest(knockRequest: KnockRequestPresentable, optimistic: Boolean = false): Result<Unit> {
|
||||||
|
val wrapped = getKnockRequestById(knockRequest.eventId) ?: return knockRequestNotFoundResult()
|
||||||
|
return handleKnockRequest(wrapped, optimistic) { decline(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decline a knock request by banning the user.
|
||||||
|
* @param knockRequest The knock request to decline.
|
||||||
|
* @param optimistic If true, the request will be marked as handled before the server responds.
|
||||||
|
*/
|
||||||
|
suspend fun declineAndBanKnockRequest(knockRequest: KnockRequestPresentable, optimistic: Boolean = false): Result<Unit> {
|
||||||
|
val wrapped = getKnockRequestById(knockRequest.eventId) ?: return knockRequestNotFoundResult()
|
||||||
|
return handleKnockRequest(wrapped, optimistic) { declineAndBan(null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept all currently known knock requests.
|
||||||
|
* @param optimistic If true, the requests will be marked as handled before the server responds.
|
||||||
|
*/
|
||||||
|
suspend fun acceptAllKnockRequests(optimistic: Boolean = false): Result<Unit> = supervisorScope {
|
||||||
|
val results = knockRequestsList()
|
||||||
|
.map { knockRequest ->
|
||||||
|
async {
|
||||||
|
acceptKnockRequest(knockRequest, optimistic = optimistic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
if (results.all { it.isSuccess }) {
|
||||||
|
Result.success(Unit)
|
||||||
|
} else {
|
||||||
|
Result.failure(IllegalStateException("Failed to accept all knock requests"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all currently known knock requests as seen.
|
||||||
|
*/
|
||||||
|
suspend fun markAllKnockRequestsAsSeen() = supervisorScope {
|
||||||
|
knockRequestsList()
|
||||||
|
.map { knockRequest ->
|
||||||
|
async { knockRequest.markAsSeen() }
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleKnockRequest(
|
||||||
|
knockRequest: KnockRequestWrapper,
|
||||||
|
optimistic: Boolean,
|
||||||
|
action: suspend (KnockRequestWrapper.() -> Result<Unit>)
|
||||||
|
): Result<Unit> {
|
||||||
|
if (optimistic) {
|
||||||
|
handledKnockRequestIds.getAndUpdate { it + knockRequest.eventId }
|
||||||
|
}
|
||||||
|
return action(knockRequest)
|
||||||
|
.onFailure {
|
||||||
|
if (optimistic) {
|
||||||
|
handledKnockRequestIds.getAndUpdate { it - knockRequest.eventId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onSuccess {
|
||||||
|
if (!optimistic) {
|
||||||
|
handledKnockRequestIds.getAndUpdate { it + knockRequest.eventId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun knockRequestNotFoundResult() = Result.failure<Unit>(IllegalArgumentException("Knock request not found"))
|
||||||
|
|
||||||
|
private fun MatrixRoom.wrappedKnockRequestsFlow() = knockRequestsFlow.map { knockRequests ->
|
||||||
|
knockRequests.map { KnockRequestWrapper(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,14 @@
|
||||||
|
|
||||||
package io.element.android.features.knockrequests.impl.list
|
package io.element.android.features.knockrequests.impl.list
|
||||||
|
|
||||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
|
||||||
|
|
||||||
sealed interface KnockRequestsListEvents {
|
sealed interface KnockRequestsListEvents {
|
||||||
data class Accept(val knockRequest: KnockRequest) : KnockRequestsListEvents
|
data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
|
||||||
data class Decline(val knockRequest: KnockRequest) : KnockRequestsListEvents
|
data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
|
||||||
data class DeclineAndBan(val knockRequest: KnockRequest) : KnockRequestsListEvents
|
data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
|
||||||
data object AcceptAll : KnockRequestsListEvents
|
data object AcceptAll : KnockRequestsListEvents
|
||||||
data object DismissCurrentAction : KnockRequestsListEvents
|
data object ResetCurrentAction : KnockRequestsListEvents
|
||||||
|
data object RetryCurrentAction : KnockRequestsListEvents
|
||||||
|
data object ConfirmCurrentAction : KnockRequestsListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,70 +11,109 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.knockrequests.impl.data.KnockRequestsService
|
||||||
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.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.ui.room.canBanAsState
|
import io.element.android.libraries.matrix.ui.room.canBanAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.canInviteAsState
|
import io.element.android.libraries.matrix.ui.room.canInviteAsState
|
||||||
import io.element.android.libraries.matrix.ui.room.canKickAsState
|
import io.element.android.libraries.matrix.ui.room.canKickAsState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KnockRequestsListPresenter @Inject constructor(
|
class KnockRequestsListPresenter @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
private val knockRequestsService: KnockRequestsService,
|
||||||
) : Presenter<KnockRequestsListState> {
|
) : Presenter<KnockRequestsListState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): KnockRequestsListState {
|
override fun present(): KnockRequestsListState {
|
||||||
val currentAction = remember { mutableStateOf<KnockRequestsCurrentAction>(KnockRequestsCurrentAction.None) }
|
val asyncAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
var actionTarget by remember { mutableStateOf<KnockRequestsActionTarget>(KnockRequestsActionTarget.None) }
|
||||||
|
var targetActionConfirmed by remember { mutableStateOf(false) }
|
||||||
|
var retryCount by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
val canBan by room.canBanAsState(syncUpdateFlow.value)
|
val canBan by room.canBanAsState(syncUpdateFlow.value)
|
||||||
val canDecline by room.canKickAsState(syncUpdateFlow.value)
|
val canDecline by room.canKickAsState(syncUpdateFlow.value)
|
||||||
val canAccept by room.canInviteAsState(syncUpdateFlow.value)
|
val canAccept by room.canInviteAsState(syncUpdateFlow.value)
|
||||||
|
|
||||||
|
val knockRequests by knockRequestsService.knockRequestsFlow.collectAsState()
|
||||||
|
|
||||||
fun handleEvents(event: KnockRequestsListEvents) {
|
fun handleEvents(event: KnockRequestsListEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
KnockRequestsListEvents.AcceptAll -> {
|
KnockRequestsListEvents.AcceptAll -> {
|
||||||
currentAction.value = KnockRequestsCurrentAction.AcceptAll(AsyncAction.Uninitialized)
|
actionTarget = KnockRequestsActionTarget.AcceptAll
|
||||||
}
|
}
|
||||||
is KnockRequestsListEvents.Accept -> {
|
is KnockRequestsListEvents.Accept -> {
|
||||||
currentAction.value = KnockRequestsCurrentAction.Accept(event.knockRequest, AsyncAction.Uninitialized)
|
actionTarget = KnockRequestsActionTarget.Accept(event.knockRequest)
|
||||||
}
|
}
|
||||||
is KnockRequestsListEvents.Decline -> {
|
is KnockRequestsListEvents.Decline -> {
|
||||||
currentAction.value = KnockRequestsCurrentAction.Decline(event.knockRequest, AsyncAction.Uninitialized)
|
actionTarget = KnockRequestsActionTarget.Decline(event.knockRequest)
|
||||||
}
|
}
|
||||||
is KnockRequestsListEvents.DeclineAndBan -> {
|
is KnockRequestsListEvents.DeclineAndBan -> {
|
||||||
currentAction.value = KnockRequestsCurrentAction.DeclineAndBan(event.knockRequest, AsyncAction.Uninitialized)
|
actionTarget = KnockRequestsActionTarget.DeclineAndBan(event.knockRequest)
|
||||||
}
|
}
|
||||||
KnockRequestsListEvents.DismissCurrentAction -> {
|
KnockRequestsListEvents.ResetCurrentAction -> {
|
||||||
currentAction.value = KnockRequestsCurrentAction.None
|
actionTarget = KnockRequestsActionTarget.None
|
||||||
|
}
|
||||||
|
KnockRequestsListEvents.RetryCurrentAction -> {
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
|
KnockRequestsListEvents.ConfirmCurrentAction -> {
|
||||||
|
targetActionConfirmed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(currentAction) {
|
LaunchedEffect(actionTarget, targetActionConfirmed, retryCount) {
|
||||||
when (currentAction.value) {
|
when (val action = actionTarget) {
|
||||||
is KnockRequestsCurrentAction.Accept -> {
|
is KnockRequestsActionTarget.Accept -> {
|
||||||
// Accept the knock request
|
runUpdatingState(asyncAction) {
|
||||||
|
knockRequestsService.acceptKnockRequest(action.knockRequest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is KnockRequestsCurrentAction.Decline -> {
|
is KnockRequestsActionTarget.Decline -> {
|
||||||
// Decline the knock request
|
if (targetActionConfirmed) {
|
||||||
|
runUpdatingState(asyncAction) {
|
||||||
|
knockRequestsService.declineKnockRequest(action.knockRequest)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
asyncAction.value = AsyncAction.ConfirmingNoParams
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is KnockRequestsCurrentAction.DeclineAndBan -> {
|
is KnockRequestsActionTarget.DeclineAndBan -> {
|
||||||
// Decline and ban the user
|
if (targetActionConfirmed) {
|
||||||
|
runUpdatingState(asyncAction) {
|
||||||
|
knockRequestsService.declineAndBanKnockRequest(action.knockRequest)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
asyncAction.value = AsyncAction.ConfirmingNoParams
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is KnockRequestsCurrentAction.AcceptAll -> {
|
is KnockRequestsActionTarget.AcceptAll -> {
|
||||||
// Accept all knock requests
|
if (targetActionConfirmed) {
|
||||||
|
runUpdatingState(asyncAction) {
|
||||||
|
knockRequestsService.acceptAllKnockRequests()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
asyncAction.value = AsyncAction.ConfirmingNoParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KnockRequestsActionTarget.None -> {
|
||||||
|
targetActionConfirmed = false
|
||||||
|
asyncAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
KnockRequestsCurrentAction.None -> Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return KnockRequestsListState(
|
return KnockRequestsListState(
|
||||||
knockRequests = AsyncData.Success(persistentListOf()),
|
knockRequests = knockRequests,
|
||||||
currentAction = currentAction.value,
|
actionTarget = actionTarget,
|
||||||
|
asyncAction = asyncAction.value,
|
||||||
canAccept = canAccept,
|
canAccept = canAccept,
|
||||||
canDecline = canDecline,
|
canDecline = canDecline,
|
||||||
canBan = canBan,
|
canBan = canBan,
|
||||||
|
|
|
||||||
|
|
@ -8,27 +8,28 @@
|
||||||
package io.element.android.features.knockrequests.impl.list
|
package io.element.android.features.knockrequests.impl.list
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
|
||||||
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 kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class KnockRequestsListState(
|
data class KnockRequestsListState(
|
||||||
val knockRequests: AsyncData<ImmutableList<KnockRequest>>,
|
val knockRequests: AsyncData<ImmutableList<KnockRequestPresentable>>,
|
||||||
val currentAction: KnockRequestsCurrentAction,
|
val actionTarget: KnockRequestsActionTarget,
|
||||||
|
val asyncAction: AsyncAction<Unit>,
|
||||||
val canAccept: Boolean,
|
val canAccept: Boolean,
|
||||||
val canDecline: Boolean,
|
val canDecline: Boolean,
|
||||||
val canBan: Boolean,
|
val canBan: Boolean,
|
||||||
val eventSink: (KnockRequestsListEvents) -> Unit,
|
val eventSink: (KnockRequestsListEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val canAcceptAll = knockRequests is AsyncData.Success && knockRequests.data.size > 1
|
val canAcceptAll = canAccept && knockRequests is AsyncData.Success && knockRequests.data.size > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface KnockRequestsCurrentAction {
|
sealed interface KnockRequestsActionTarget {
|
||||||
data object None : KnockRequestsCurrentAction
|
data object None : KnockRequestsActionTarget
|
||||||
data class Accept(val knockRequest: KnockRequest, val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget
|
||||||
data class Decline(val knockRequest: KnockRequest, val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget
|
||||||
data class DeclineAndBan(val knockRequest: KnockRequest, val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget
|
||||||
data class AcceptAll(val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
data object AcceptAll : KnockRequestsActionTarget
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
package io.element.android.features.knockrequests.impl.list
|
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.data.KnockRequestPresentable
|
||||||
import io.element.android.features.knockrequests.impl.aKnockRequest
|
import io.element.android.features.knockrequests.impl.data.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
|
||||||
|
|
@ -64,7 +64,17 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider<KnockReques
|
||||||
aKnockRequest()
|
aKnockRequest()
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
currentAction = KnockRequestsCurrentAction.AcceptAll(AsyncAction.Loading),
|
actionTarget = KnockRequestsActionTarget.AcceptAll,
|
||||||
|
asyncAction = AsyncAction.ConfirmingNoParams,
|
||||||
|
),
|
||||||
|
aKnockRequestsListState(
|
||||||
|
knockRequests = AsyncData.Success(
|
||||||
|
persistentListOf(
|
||||||
|
aKnockRequest()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
actionTarget = KnockRequestsActionTarget.AcceptAll,
|
||||||
|
asyncAction = AsyncAction.Loading,
|
||||||
),
|
),
|
||||||
aKnockRequestsListState(
|
aKnockRequestsListState(
|
||||||
knockRequests = AsyncData.Success(
|
knockRequests = AsyncData.Success(
|
||||||
|
|
@ -73,6 +83,8 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider<KnockReques
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
canAccept = false,
|
canAccept = false,
|
||||||
|
actionTarget = KnockRequestsActionTarget.AcceptAll,
|
||||||
|
asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")),
|
||||||
),
|
),
|
||||||
aKnockRequestsListState(
|
aKnockRequestsListState(
|
||||||
knockRequests = AsyncData.Success(
|
knockRequests = AsyncData.Success(
|
||||||
|
|
@ -103,15 +115,17 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider<KnockReques
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aKnockRequestsListState(
|
fun aKnockRequestsListState(
|
||||||
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
|
knockRequests: AsyncData<ImmutableList<KnockRequestPresentable>> = AsyncData.Success(persistentListOf()),
|
||||||
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,
|
actionTarget: KnockRequestsActionTarget = KnockRequestsActionTarget.None,
|
||||||
|
asyncAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
canAccept: Boolean = true,
|
canAccept: Boolean = true,
|
||||||
canDecline: Boolean = true,
|
canDecline: Boolean = true,
|
||||||
canBan: Boolean = true,
|
canBan: Boolean = true,
|
||||||
eventSink: (KnockRequestsListEvents) -> Unit = {},
|
eventSink: (KnockRequestsListEvents) -> Unit = {},
|
||||||
) = KnockRequestsListState(
|
) = KnockRequestsListState(
|
||||||
knockRequests = knockRequests,
|
knockRequests = knockRequests,
|
||||||
currentAction = currentAction,
|
actionTarget = actionTarget,
|
||||||
|
asyncAction = asyncAction,
|
||||||
canAccept = canAccept,
|
canAccept = canAccept,
|
||||||
canDecline = canDecline,
|
canDecline = canDecline,
|
||||||
canBan = canBan,
|
canBan = canBan,
|
||||||
|
|
|
||||||
|
|
@ -46,23 +46,25 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
|
||||||
import io.element.android.features.knockrequests.impl.R
|
import io.element.android.features.knockrequests.impl.R
|
||||||
import io.element.android.features.knockrequests.impl.getAvatarData
|
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
|
||||||
import io.element.android.features.knockrequests.impl.getBestName
|
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.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.components.BigIcon
|
import io.element.android.libraries.designsystem.components.BigIcon
|
||||||
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.text.toDp
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
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.ButtonSize
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.OutlinedButton
|
||||||
|
|
@ -70,6 +72,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -100,14 +103,18 @@ private fun KnockRequestsListContent(
|
||||||
state: KnockRequestsListState,
|
state: KnockRequestsListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onAcceptClick(knockRequest: KnockRequest) {
|
fun onAcceptClick(knockRequest: KnockRequestPresentable) {
|
||||||
state.eventSink(KnockRequestsListEvents.Accept(knockRequest))
|
state.eventSink(KnockRequestsListEvents.Accept(knockRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeclineClick(knockRequest: KnockRequest) {
|
fun onDeclineClick(knockRequest: KnockRequestPresentable) {
|
||||||
state.eventSink(KnockRequestsListEvents.Decline(knockRequest))
|
state.eventSink(KnockRequestsListEvents.Decline(knockRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onBanClick(knockRequest: KnockRequestPresentable) {
|
||||||
|
state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequest))
|
||||||
|
}
|
||||||
|
|
||||||
var bottomPaddingInPixels by remember { mutableIntStateOf(0) }
|
var bottomPaddingInPixels by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
Box(modifier.fillMaxSize()) {
|
Box(modifier.fillMaxSize()) {
|
||||||
|
|
@ -124,16 +131,30 @@ private fun KnockRequestsListContent(
|
||||||
canBan = state.canBan,
|
canBan = state.canBan,
|
||||||
onAcceptClick = ::onAcceptClick,
|
onAcceptClick = ::onAcceptClick,
|
||||||
onDeclineClick = ::onDeclineClick,
|
onDeclineClick = ::onDeclineClick,
|
||||||
|
onBanClick = ::onBanClick,
|
||||||
contentPadding = PaddingValues(bottom = bottomPaddingInPixels.toDp()),
|
contentPadding = PaddingValues(bottom = bottomPaddingInPixels.toDp()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is AsyncData.Loading -> {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
color = ElementTheme.colors.iconPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
KnockRequestsActionsView(
|
KnockRequestsActionsView(
|
||||||
actions = state.currentAction,
|
actionTarget = state.actionTarget,
|
||||||
|
asyncAction = state.asyncAction,
|
||||||
|
onConfirm = {
|
||||||
|
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
|
||||||
|
},
|
||||||
|
onRetry = {
|
||||||
|
state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
|
||||||
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
state.eventSink(KnockRequestsListEvents.DismissCurrentAction)
|
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (state.canAcceptAll) {
|
if (state.canAcceptAll) {
|
||||||
|
|
@ -152,53 +173,45 @@ private fun KnockRequestsListContent(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun KnockRequestsActionsView(
|
private fun KnockRequestsActionsView(
|
||||||
actions: KnockRequestsCurrentAction,
|
actionTarget: KnockRequestsActionTarget,
|
||||||
|
asyncAction: AsyncAction<Unit>,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
onRetry: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
when (actions) {
|
AsyncActionView(
|
||||||
is KnockRequestsCurrentAction.AcceptAll -> {
|
async = asyncAction,
|
||||||
AsyncActionView(
|
onSuccess = { onDismiss() },
|
||||||
async = actions.async,
|
onErrorDismiss = onDismiss,
|
||||||
onSuccess = {},
|
confirmationDialog = {
|
||||||
onErrorDismiss = onDismiss,
|
ConfirmationDialog(
|
||||||
|
title = "Confirmation",
|
||||||
|
content = "Are you sure?",
|
||||||
|
onSubmitClick = onConfirm,
|
||||||
|
onDismiss = onDismiss,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
is KnockRequestsCurrentAction.Accept -> {
|
progressDialog = {
|
||||||
AsyncActionView(
|
ProgressDialog(
|
||||||
async = actions.async,
|
text = "Loading",
|
||||||
onSuccess = {},
|
|
||||||
onErrorDismiss = onDismiss,
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
is KnockRequestsCurrentAction.Decline -> {
|
onRetry = onRetry,
|
||||||
AsyncActionView(
|
)
|
||||||
async = actions.async,
|
|
||||||
onSuccess = {},
|
|
||||||
onErrorDismiss = onDismiss,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is KnockRequestsCurrentAction.DeclineAndBan -> {
|
|
||||||
AsyncActionView(
|
|
||||||
async = actions.async,
|
|
||||||
onSuccess = {},
|
|
||||||
onErrorDismiss = onDismiss,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
KnockRequestsCurrentAction.None -> Unit
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun KnockRequestsList(
|
private fun KnockRequestsList(
|
||||||
knockRequests: ImmutableList<KnockRequest>,
|
knockRequests: ImmutableList<KnockRequestPresentable>,
|
||||||
canAccept: Boolean,
|
canAccept: Boolean,
|
||||||
canDecline: Boolean,
|
canDecline: Boolean,
|
||||||
canBan: Boolean,
|
canBan: Boolean,
|
||||||
onAcceptClick: (KnockRequest) -> Unit,
|
onAcceptClick: (KnockRequestPresentable) -> Unit,
|
||||||
onDeclineClick: (KnockRequest) -> Unit,
|
onDeclineClick: (KnockRequestPresentable) -> Unit,
|
||||||
|
onBanClick: (KnockRequestPresentable) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
|
|
@ -214,6 +227,7 @@ private fun KnockRequestsList(
|
||||||
canDecline = canDecline,
|
canDecline = canDecline,
|
||||||
canAccept = canAccept,
|
canAccept = canAccept,
|
||||||
onDeclineClick = onDeclineClick,
|
onDeclineClick = onDeclineClick,
|
||||||
|
onBanClick = onBanClick,
|
||||||
)
|
)
|
||||||
if (index != knockRequests.size - 1) {
|
if (index != knockRequests.size - 1) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
@ -224,12 +238,13 @@ private fun KnockRequestsList(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun KnockRequestItem(
|
private fun KnockRequestItem(
|
||||||
knockRequest: KnockRequest,
|
knockRequest: KnockRequestPresentable,
|
||||||
canAccept: Boolean,
|
canAccept: Boolean,
|
||||||
canDecline: Boolean,
|
canDecline: Boolean,
|
||||||
canBan: Boolean,
|
canBan: Boolean,
|
||||||
onAcceptClick: (KnockRequest) -> Unit,
|
onAcceptClick: (KnockRequestPresentable) -> Unit,
|
||||||
onDeclineClick: (KnockRequest) -> Unit,
|
onDeclineClick: (KnockRequestPresentable) -> Unit,
|
||||||
|
onBanClick: (KnockRequestPresentable) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -252,10 +267,11 @@ private fun KnockRequestItem(
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
style = ElementTheme.typography.fontBodyLgMedium,
|
style = ElementTheme.typography.fontBodyLgMedium,
|
||||||
)
|
)
|
||||||
if (!knockRequest.formattedDate.isNullOrEmpty()) {
|
val formattedDate = knockRequest.formattedDate
|
||||||
|
if (!formattedDate.isNullOrEmpty()) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = knockRequest.formattedDate,
|
text = formattedDate,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
)
|
)
|
||||||
|
|
@ -272,7 +288,8 @@ private fun KnockRequestItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Reason
|
// Reason
|
||||||
if (!knockRequest.reason.isNullOrBlank()) {
|
val reason = knockRequest.reason
|
||||||
|
if (!reason.isNullOrBlank()) {
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
var isExpanded by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
|
var isExpanded by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
|
||||||
var isExpandable by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
|
var isExpandable by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
|
||||||
|
|
@ -283,7 +300,7 @@ private fun KnockRequestItem(
|
||||||
.clickable(enabled = isExpandable) { isExpanded = !isExpanded }
|
.clickable(enabled = isExpandable) { isExpanded = !isExpanded }
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = knockRequest.reason,
|
text = reason,
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
maxLines = if (isExpanded) Int.MAX_VALUE else 3,
|
maxLines = if (isExpanded) Int.MAX_VALUE else 3,
|
||||||
onTextLayout = { result ->
|
onTextLayout = { result ->
|
||||||
|
|
@ -336,7 +353,7 @@ private fun KnockRequestItem(
|
||||||
TextButton(
|
TextButton(
|
||||||
text = stringResource(R.string.screen_knock_requests_list_decline_and_ban_action_title),
|
text = stringResource(R.string.screen_knock_requests_list_decline_and_ban_action_title),
|
||||||
onClick = {
|
onClick = {
|
||||||
onAcceptClick(knockRequest)
|
onBanClick(knockRequest)
|
||||||
},
|
},
|
||||||
destructive = true,
|
destructive = true,
|
||||||
size = ButtonSize.Small,
|
size = ButtonSize.Small,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
|
@ -53,6 +54,8 @@ interface MatrixRoom : Closeable {
|
||||||
val activeMemberCount: Long
|
val activeMemberCount: Long
|
||||||
val joinedMemberCount: Long
|
val joinedMemberCount: Long
|
||||||
|
|
||||||
|
val roomCoroutineScope: CoroutineScope
|
||||||
|
|
||||||
val roomInfoFlow: Flow<MatrixRoomInfo>
|
val roomInfoFlow: Flow<MatrixRoomInfo>
|
||||||
val roomTypingMembersFlow: Flow<List<UserId>>
|
val roomTypingMembersFlow: Flow<List<UserId>>
|
||||||
val identityStateChangesFlow: Flow<List<IdentityStateChange>>
|
val identityStateChangesFlow: Flow<List<IdentityStateChange>>
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,17 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.room.knock
|
package io.element.android.libraries.matrix.api.room.knock
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
interface KnockRequest {
|
interface KnockRequest {
|
||||||
|
val eventId: EventId
|
||||||
val userId: UserId
|
val userId: UserId
|
||||||
val displayName: String?
|
val displayName: String?
|
||||||
val avatarUrl: String?
|
val avatarUrl: String?
|
||||||
val reason: String?
|
val reason: String?
|
||||||
val timestamp: Long?
|
val timestamp: Long?
|
||||||
|
val isSeen: Boolean
|
||||||
|
|
||||||
suspend fun accept(): Result<Unit>
|
suspend fun accept(): Result<Unit>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ import kotlinx.coroutines.withContext
|
||||||
import org.matrix.rustcomponents.sdk.DateDividerMode
|
import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||||
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
|
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
|
||||||
import org.matrix.rustcomponents.sdk.JoinRequest
|
import org.matrix.rustcomponents.sdk.JoinRequest
|
||||||
import org.matrix.rustcomponents.sdk.RequestsToJoinListener
|
import org.matrix.rustcomponents.sdk.JoinRequestsListener
|
||||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||||
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
import org.matrix.rustcomponents.sdk.RoomInfoListener
|
||||||
import org.matrix.rustcomponents.sdk.RoomListItem
|
import org.matrix.rustcomponents.sdk.RoomListItem
|
||||||
|
|
@ -162,7 +162,7 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val knockRequestsFlow: Flow<List<KnockRequest>> = mxCallbackFlow {
|
override val knockRequestsFlow: Flow<List<KnockRequest>> = mxCallbackFlow {
|
||||||
innerRoom.subscribeToJoinRequests(object : RequestsToJoinListener {
|
innerRoom.subscribeToJoinRequests(object : JoinRequestsListener {
|
||||||
override fun call(joinRequests: List<JoinRequest>) {
|
override fun call(joinRequests: List<JoinRequest>) {
|
||||||
val knockRequests = joinRequests.map { RustKnockRequest(it) }
|
val knockRequests = joinRequests.map { RustKnockRequest(it) }
|
||||||
channel.trySend(knockRequests)
|
channel.trySend(knockRequests)
|
||||||
|
|
@ -176,7 +176,7 @@ class RustMatrixRoom(
|
||||||
// ...except getMember methods as it could quickly fill the roomDispatcher...
|
// ...except getMember methods as it could quickly fill the roomDispatcher...
|
||||||
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
|
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
|
||||||
|
|
||||||
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
|
override val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
|
||||||
private val _syncUpdateFlow = MutableStateFlow(0L)
|
private val _syncUpdateFlow = MutableStateFlow(0L)
|
||||||
private val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher)
|
private val roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room.knock
|
package io.element.android.libraries.matrix.impl.room.knock
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
import io.element.android.libraries.matrix.api.room.knock.KnockRequest
|
||||||
import org.matrix.rustcomponents.sdk.JoinRequest
|
import org.matrix.rustcomponents.sdk.JoinRequest
|
||||||
|
|
@ -14,11 +15,13 @@ import org.matrix.rustcomponents.sdk.JoinRequest
|
||||||
class RustKnockRequest(
|
class RustKnockRequest(
|
||||||
private val inner: JoinRequest,
|
private val inner: JoinRequest,
|
||||||
) : KnockRequest {
|
) : KnockRequest {
|
||||||
|
override val eventId: EventId = EventId(inner.eventId)
|
||||||
override val userId: UserId = UserId(inner.userId)
|
override val userId: UserId = UserId(inner.userId)
|
||||||
override val displayName: String? = inner.displayName
|
override val displayName: String? = inner.displayName
|
||||||
override val avatarUrl: String? = inner.avatarUrl
|
override val avatarUrl: String? = inner.avatarUrl
|
||||||
override val reason: String? = inner.reason
|
override val reason: String? = inner.reason
|
||||||
override val timestamp: Long? = inner.timestamp?.toLong()
|
override val timestamp: Long? = inner.timestamp?.toLong()
|
||||||
|
override val isSeen: Boolean = inner.isSeen
|
||||||
|
|
||||||
override suspend fun accept(): Result<Unit> = runCatching {
|
override suspend fun accept(): Result<Unit> = runCatching {
|
||||||
inner.actions.accept()
|
inner.actions.accept()
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,14 @@ import io.element.android.libraries.matrix.test.notificationsettings.FakeNotific
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class FakeMatrixRoom(
|
class FakeMatrixRoom(
|
||||||
|
|
@ -73,6 +75,7 @@ class FakeMatrixRoom(
|
||||||
override val activeMemberCount: Long = 234L,
|
override val activeMemberCount: Long = 234L,
|
||||||
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
override val liveTimeline: Timeline = FakeTimeline(),
|
override val liveTimeline: Timeline = FakeTimeline(),
|
||||||
|
override val roomCoroutineScope: CoroutineScope = TestScope(),
|
||||||
private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
|
private var roomPermalinkResult: () -> Result<String> = { lambdaError() },
|
||||||
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
|
private var eventPermalinkResult: (EventId) -> Result<String> = { lambdaError() },
|
||||||
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
|
private val sendCallNotificationIfNeededResult: () -> Result<Unit> = { lambdaError() },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue